深入剖析Tomcat(七) 日志记录器

在看原书第六章之前,一直觉得Tomcat记日志的架构可能是个“有点东西”的东西。在看了第六章之后呢,额… 就这?不甘心的我又翻了翻logback与新版tomcat的源码,额…,日志架构原来也没那么神秘。本篇文章先过一遍原书内容,然后再说一下我看了其他日志框架后的见解。

《深入剖析Tomcat》这本书中的代码是基于Tomcat4的,它的日志架构非常简单,对日志记录器只定义了一个接口 org.apache.catalina.Logger ,只要实现了这个接口的类都可以作为日志记录器,Tomcat4内部提供了三种实现 SystemOutLogger、SystemErrLogger、FileLogger 他们都在 org.apache.catalina.logger 包下。基于日志记录器的共性,Tomcat4提供了LoggerBase这个抽象类,上面三种日志记录器都继承了LoggerBase类。

日志记录器的UML图

Logger接口

Logger接口定义了五种日志级别:FATAL、ERROR、WARNING、INFORMATION、DEBUG,并提供了getVerbosity()与setVerbosity()来获取与修改日志级别。

Logger接口定义了好几种log方法来记录日志,其中后两个方法允许传参日志级别,只有当前日志记录器的日志级别数字大于等于传参的话,才会记录日志。

LoggerBase类

这个抽象类编写了日志记录器的一些公共代码。它实现了Logger接口,并将其中大多数方法都实现了,仅仅保留了 void log(String message); 方法作为抽象方法,其他重载的log方法最终都会调用此方法,具体对日志怎么处理就看子类如何实现此方法了。

public abstract class LoggerBase implements Logger {//关联的容器protected Container container = null;protected int debug = 0;// 该日志记录器的描述protected static final String info = "org.apache.catalina.logger.LoggerBase/1.0";// 属性值变化监听 辅助类,用于监听属性值的变化,并通知与此组件绑定的监听器protected PropertyChangeSupport support = new PropertyChangeSupport(this);// 日记级别protected int verbosity = ERROR;// =================== getter  setter 方法 ==================public Container getContainer() {return (container);}public void setContainer(Container container) {Container oldContainer = this.container;this.container = container;// 属性值发生了变化,通知监听器support.firePropertyChange("container", oldContainer, this.container);}public int getDebug() {return (this.debug);}public void setDebug(int debug) {this.debug = debug;}public String getInfo() {return (info);}public int getVerbosity() {return (this.verbosity);}public void setVerbosity(int verbosity) {this.verbosity = verbosity;}public void setVerbosityLevel(String verbosity) {if ("FATAL".equalsIgnoreCase(verbosity)) this.verbosity = FATAL;else if ("ERROR".equalsIgnoreCase(verbosity)) this.verbosity = ERROR;else if ("WARNING".equalsIgnoreCase(verbosity)) this.verbosity = WARNING;else if ("INFORMATION".equalsIgnoreCase(verbosity)) this.verbosity = INFORMATION;else if ("DEBUG".equalsIgnoreCase(verbosity)) this.verbosity = DEBUG;}// ================== Public Methods ====================// 为此组件添加一个属性监听器public void addPropertyChangeListener(PropertyChangeListener listener) {support.addPropertyChangeListener(listener);}// 记录日志public abstract void log(String msg);public void log(Exception exception, String msg) {log(msg, exception);}public void log(String msg, Throwable throwable) {CharArrayWriter buf = new CharArrayWriter();PrintWriter writer = new PrintWriter(buf);writer.println(msg);throwable.printStackTrace(writer);Throwable rootCause = null;if (throwable instanceof LifecycleException) rootCause = ((LifecycleException) throwable).getThrowable();else if (throwable instanceof ServletException) rootCause = ((ServletException) throwable).getRootCause();if (rootCause != null) {writer.println("----- Root Cause -----");rootCause.printStackTrace(writer);}log(buf.toString());}// 只有当该组件的日志级别数字大于参数的话,才记日志。也就是说过滤掉低级别的日志public void log(String message, int verbosity) {if (this.verbosity >= verbosity) {log(message);} }public void log(String message, Throwable throwable, int verbosity) {if (this.verbosity >= verbosity) {log(message, throwable);}}// 移除一个属性监听器public void removePropertyChangeListener(PropertyChangeListener listener) {support.removePropertyChangeListener(listener);}}

接下来的几个日志记录器的类就是对log方法的具体实现了,有的是将日志直接打印到控制台上,有的是写入到文件中。

SystemOutLogger类

这个类的实现就是调用 System.out.println 方法,将日志打印到控制台上。

package org.apache.catalina.logger;public class SystemOutLogger extends LoggerBase {// 该日志记录器的描述protected static final String info = "org.apache.catalina.logger.SystemOutLogger/1.0";public void log(String msg) {System.out.println(msg);}}

SystemErrLogger类

这个类的实现就是调用 System.err.println 方法,将日志打印到控制台上。看清了这个是 err 不是 out 了。

package org.apache.catalina.logger;public class SystemErrLogger extends LoggerBase {// 该日志记录器的描述protected static final String info = "org.apache.catalina.logger.SystemErrLogger/1.0";public void log(String msg) {System.err.println(msg);}}

FileLogger类

这个类是将日志记录到文件中,相比于前两个类来说,要复杂一点。本类实现了Lifecycle接口,但是相关方法的实现并没有什么内容,所以下面的代码展示省略了这些方法。

该类定义了日志文件的 所在文件夹、文件名前后缀,并可以设置是否记录日志时间等。日志文件按照日期来区分,当日期发生改变时,会改变 date 及 writer 的属性值,将其切换到新的日志文件上。

public class FileLogger extends LoggerBase implements Lifecycle {// 当前正打开的日志文件对应的日期字符串,如果没有打开的日志文件则为空字符串private String date = "";// 日志文件的所在文件夹private String directory = "logs";// 该日志记录器的描述protected static final String info = "org.apache.catalina.logger.FileLogger/1.0";// 生命周期事件 辅助类protected LifecycleSupport lifecycle = new LifecycleSupport(this);// 日志文件的文件名前缀private String prefix = "catalina.";private StringManager sm = StringManager.getManager(Constants.Package);// 组件是否启动了private boolean started = false;// 日志文件的文件名后缀private String suffix = ".log";// 记录的消息中是否应该有日期/时间戳?private boolean timestamp = false;// 当前正在记日志的PrintWriter,切换日志文件时 该对象也会切换private PrintWriter writer = null;// ------------- getter setter 方法省略 ---------// -------------   生命周期相关方法省略   --------/*** 将日志消息写入文件*/public void log(String msg) {// 构建时间戳对象Timestamp ts = new Timestamp(System.currentTimeMillis());String tsString = ts.toString().substring(0, 19);String tsDate = tsString.substring(0, 10);// 如果date发生了变化,则切换日志文件if (!date.equals(tsDate)) {synchronized (this) {if (!date.equals(tsDate)) {// 关闭当前日志文件close();date = tsDate;// 打开新的日志文件open();}}}// 记录日志, 如果需要的话,将时间也加入日志中if (writer != null) {if (timestamp) {writer.println(tsString + " " + msg);} else {writer.println(msg);}}}/*** 关闭当前正打开的文件*/private void close() {if (writer == null) return;writer.flush();writer.close();writer = null;date = "";}/*** 根据date属性,打开新的日志文件*/private void open() {// 如果需要的,先创建logs文件夹File dir = new File(directory);if (!dir.isAbsolute()) {dir = new File(System.getProperty("catalina.base"), directory);}dir.mkdirs();// 打开当前需要写入日志的文件try {String pathname = dir.getAbsolutePath() + File.separator + prefix + date + suffix;writer = new PrintWriter(new FileWriter(pathname, true), true);} catch (IOException e) {writer = null;}}}

SImpleContext类

本章的应用程序代码沿用第六章的代码,为了使用一下日志框架,仅将SimpleContext类做了点改造,嵌入了日志记录器,并在start与stop方法中打印了日志。

下面是该类的部分代码

public class SimpleContext implements Context, Pipeline, Lifecycle {public SimpleContext() {pipeline.setBasic(new SimpleContextValve());}protected HashMap children = new HashMap();private Loader loader = null;private Logger logger = null;protected LifecycleSupport lifecycle = new LifecycleSupport(this);private SimplePipeline pipeline = new SimplePipeline(this);private HashMap servletMappings = new HashMap();protected Mapper mapper = null;protected HashMap mappers = new HashMap();private Container parent = null;protected boolean started = false;public Logger getLogger() {return logger;}public void setLogger(Logger logger) {this.logger = logger;}public synchronized void start() throws LifecycleException {log("starting Context");if (started)throw new LifecycleException("SimpleContext has already started");// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(BEFORE_START_EVENT, null);started = true;try {// Start our subordinate components, if anyif ((loader != null) && (loader instanceof Lifecycle))((Lifecycle) loader).start();// Start our child containers, if anyContainer children[] = findChildren();for (int i = 0; i < children.length; i++) {if (children[i] instanceof Lifecycle)((Lifecycle) children[i]).start();}// Start the Valves in our pipeline (including the basic),// if anyif (pipeline instanceof Lifecycle)((Lifecycle) pipeline).start();// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(START_EVENT, null);}catch (Exception e) {e.printStackTrace();}// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(AFTER_START_EVENT, null);log("Context started");}public void stop() throws LifecycleException {log("stopping Context");if (!started)throw new LifecycleException("SimpleContext has not been started");// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(BEFORE_STOP_EVENT, null);lifecycle.fireLifecycleEvent(STOP_EVENT, null);started = false;try {// Stop the Valves in our pipeline (including the basic), if anyif (pipeline instanceof Lifecycle) {((Lifecycle) pipeline).stop();}// Stop our child containers, if anyContainer children[] = findChildren();for (int i = 0; i < children.length; i++) {if (children[i] instanceof Lifecycle)((Lifecycle) children[i]).stop();}if ((loader != null) && (loader instanceof Lifecycle)) {((Lifecycle) loader).stop();}}catch (Exception e) {e.printStackTrace();}// Notify our interested LifecycleListenerslifecycle.fireLifecycleEvent(AFTER_STOP_EVENT, null);log("Context stopped");}private void log(String message) {Logger logger = this.getLogger();if (logger!=null)logger.log(message);}
}

Bootstrap类

启动类的话,相比于上一章的类,就是多创建了一个日志记录器(Logger)并将其绑定到context容器上。

public final class Bootstrap {public static void main(String[] args) {Connector connector = new HttpConnector();Wrapper wrapper1 = new SimpleWrapper();wrapper1.setName("Primitive");wrapper1.setServletClass("PrimitiveServlet");Wrapper wrapper2 = new SimpleWrapper();wrapper2.setName("Modern");wrapper2.setServletClass("ModernServlet");Loader loader = new SimpleLoader();Context context = new SimpleContext();context.addChild(wrapper1);context.addChild(wrapper2);Mapper mapper = new SimpleContextMapper();mapper.setProtocol("http");LifecycleListener listener = new SimpleContextLifecycleListener();((Lifecycle) context).addLifecycleListener(listener);context.addMapper(mapper);context.setLoader(loader);// context.addServletMapping(pattern, name);context.addServletMapping("/Primitive", "Primitive");context.addServletMapping("/Modern", "Modern");// ------ add logger --------System.setProperty("catalina.base", System.getProperty("user.dir"));FileLogger logger = new FileLogger();logger.setPrefix("FileLog_");logger.setSuffix(".txt");logger.setTimestamp(true);logger.setDirectory("webroot");context.setLogger(logger);//---------------------------connector.setContainer(context);try {connector.initialize();((Lifecycle) connector).start();((Lifecycle) context).start();// make the application wait until we press a key.System.in.read();((Lifecycle) context).stop();}catch (Exception e) {e.printStackTrace();}}
}

将Bootstrap启动,并向控制台输入内容使其关闭。

控制台日志

SimpleContextLifecycleListener's event before_start
Starting SimpleLoader
Starting Wrapper Primitive
Starting Wrapper Modern
SimpleContextLifecycleListener's event start
Starting context.
SimpleContextLifecycleListener's event after_start关闭应用
SimpleContextLifecycleListener's event before_stop
SimpleContextLifecycleListener's event stop
Starting context.
Stopping wrapper Primitive
Stopping wrapper Modern
SimpleContextLifecycleListener's event after_stopProcess finished with exit code 0

在webroot目录下多了一个日志文件

文件内容为

2024-05-07 17:03:26 HttpConnector Opening server socket on all host IP addresses
2024-05-07 17:03:26 HttpConnector[8080] Starting background thread
2024-05-07 17:03:26 HttpProcessor[8080][0] Starting background thread
2024-05-07 17:03:26 HttpProcessor[8080][1] Starting background thread
2024-05-07 17:03:26 HttpProcessor[8080][2] Starting background thread
2024-05-07 17:03:26 HttpProcessor[8080][3] Starting background thread
2024-05-07 17:03:26 HttpProcessor[8080][4] Starting background thread
2024-05-07 17:03:26 starting Context
2024-05-07 17:03:26 Context started
2024-05-07 17:03:31 stopping Context
2024-05-07 17:03:31 Context stopped

好,Tomcat4的日志架构就是这样,是不是很简单?另外,有一点需要注意,FileLogger在将日志写入文件的时候,整个IO操作是同步的而不是异步的,这意味着打印日志这个步骤,多多少少会增加程序运行的耗时。 那么很自然的我们就会发出疑问:那日志框架发展到现在,应该支持异步了吧? 我翻看了一下Tomcat9 juli包下的日志架构与我们目前常用的logback,他们在默认情况下仍然使用的是同步IO,但是使用FileChannel等技术做了优化,减少了耗时;但是它们也都提供了异步IO的日志记录器实现。

 

上图中的这两个类就是异步记日志的日志记录器,他们的实现方式也很相似,通过一个线程内部类的方式启动一个独立线程;同时外部类拥有一个队列属性,来存放写日志的事件;用户线程负责往队列里放日志事件,独立线程就不停地从队列里取日志事件出来将其写入到文件中。

下面是logback框架下AsyncAppenderBase类的部分代码

public class AsyncAppenderBase<E> extends UnsynchronizedAppenderBase<E> implements AppenderAttachable<E> {AppenderAttachableImpl<E> aai = new AppenderAttachableImpl<E>();BlockingQueue<E> blockingQueue;public static final int DEFAULT_QUEUE_SIZE = 256;int queueSize = DEFAULT_QUEUE_SIZE;int appenderCount = 0;Worker worker = new Worker();@Overridepublic void start() {if (isStarted())return;if (appenderCount == 0) {addError("No attached appenders found.");return;}if (queueSize < 1) {addError("Invalid queue size [" + queueSize + "]");return;}blockingQueue = new ArrayBlockingQueue<E>(queueSize);if (discardingThreshold == UNDEFINED)discardingThreshold = queueSize / 5;addInfo("Setting discardingThreshold to " + discardingThreshold);worker.setDaemon(true);worker.setName("AsyncAppender-Worker-" + getName());// make sure this instance is marked as "started" before staring the worker Threadsuper.start();worker.start();}@Overridepublic void stop() {if (!isStarted())return;// mark this appender as stopped so that Worker can also processPriorToRemoval if it is invoking// aii.appendLoopOnAppenders// and sub-appenders consume the interruptionsuper.stop();// interrupt the worker thread so that it can terminate. Note that the interruption can be consumed// by sub-appendersworker.interrupt();InterruptUtil interruptUtil = new InterruptUtil(context);try {interruptUtil.maskInterruptFlag();worker.join(maxFlushTime);// check to see if the thread ended and if not add a warning messageif (worker.isAlive()) {addWarn("Max queue flush timeout (" + maxFlushTime + " ms) exceeded. Approximately " + blockingQueue.size()+ " queued events were possibly discarded.");} else {addInfo("Queue flush finished successfully within timeout.");}} catch (InterruptedException e) {int remaining = blockingQueue.size();addError("Failed to join worker thread. " + remaining + " queued events may be discarded.", e);} finally {interruptUtil.unmaskInterruptFlag();}}@Overrideprotected void append(E eventObject) {if (isQueueBelowDiscardingThreshold() && isDiscardable(eventObject)) {return;}preprocess(eventObject);put(eventObject);}private void put(E eventObject) {if (neverBlock) {blockingQueue.offer(eventObject);} else {putUninterruptibly(eventObject);}}private void putUninterruptibly(E eventObject) {boolean interrupted = false;try {while (true) {try {blockingQueue.put(eventObject);break;} catch (InterruptedException e) {interrupted = true;}}} finally {if (interrupted) {Thread.currentThread().interrupt();}}}public void addAppender(Appender<E> newAppender) {if (appenderCount == 0) {appenderCount++;addInfo("Attaching appender named [" + newAppender.getName() + "] to AsyncAppender.");aai.addAppender(newAppender);} else {addWarn("One and only one appender may be attached to AsyncAppender.");addWarn("Ignoring additional appender named [" + newAppender.getName() + "]");}}class Worker extends Thread {public void run() {AsyncAppenderBase<E> parent = AsyncAppenderBase.this;AppenderAttachableImpl<E> aai = parent.aai;// loop while the parent is startedwhile (parent.isStarted()) {try {E e = parent.blockingQueue.take();aai.appendLoopOnAppenders(e);} catch (InterruptedException ie) {break;}}addInfo("Worker thread will flush remaining events before exiting. ");for (E e : parent.blockingQueue) {aai.appendLoopOnAppenders(e);parent.blockingQueue.remove(e);}aai.detachAndStopAllAppenders();}}
}

AsyncAppender类继承了AsyncAppenderBase,由上述代码可知,一个AsyncAppender对象只会创建一个Worker线程。而且根据我搜到资料显示,如果应用程序使用了logback框架的话,整个程序中也只会创建一个AsyncAppender对象,即单实例。

这里我对单worker线程的理解是

  1. 在某一时段内,记日志的IO操作针对的是同一个日志文件,如果多线程同时争抢这个文件资源的话,不加锁会导致写入的内容混乱、损坏,加锁的话又丢失了并发的意义。
  2. 应用程序的日志应该是有顺序的,多线程同时消费日志消息的话,由于操作系统调度和线程执行顺序的不确定性,可能导致数据的写入顺序错乱,影响我们分析日志。

同理,使用单例AsyncAppender对象也是类似原因。但是具体是否有多实例这个情况,这里就暂时不深究了。

通过本章的内容,我们了解了Tomcat是怎么记日志的,它默认采用同步IO的形式来记日志,当然有有异步IO供你选择。下一章,我们来研究下Tomcat中载入器的实现方式,看看它是怎么自定义类加载机制的。

源码分享

https://gitee.com/huo-ming-lu/HowTomcatWorks

原书中代码没有bug,我仅仅格式化了一下代码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://xiahunao.cn/news/3018076.html

如若内容造成侵权/违法违规/事实不符,请联系瞎胡闹网进行投诉反馈,一经查实,立即删除!

相关文章

CSS选择器(基本+复合+伪类)

目录 CSS选择器 基本选择器 标签选择器&#xff1a;使用标签名作为选择器->选中同名标签设置样式 类选择器&#xff1a;给类选择器定义一个名字.类名&#xff0c;并给标签添加class"类名" id选择器&#xff1a;跟类选择器非常相似&#xff0c;给id选择器定义…

Vitis HLS 学习笔记--理解串流Stream(1)

目录 1. 介绍 2. 示例 2.1 代码解析 2.2 串流数据类型 2.3 综合报告 3. 总结 1. 介绍 在Vitis HLS中&#xff0c;hls::stream是一个用于在C/C中进行高级合成的关键数据结构。它类似于C标准库中的std::stream&#xff0c;但是专门设计用于硬件描述语言&#xff08;如Veri…

windows vscode设置扩展和缓存目录

vscode的扩展和缓存占了很大的空间&#xff0c;而且默认在C盘&#xff0c;很烦。。。 修改vscode快捷方式的目标处&#xff1a;"C:\Users\Nv9\AppData\Local\Programs\Microsoft VS Code\Code.exe" --extensions-dir "D:\Program Cache\VScode\extensions"…

基于参数化建模的3D产品组态实现

我们最近为荷兰设计师家具制造商 KILO 发布了基于网络的 3D 配置器的第一个生产版本。我们使用了 Salsita 3D 配置器&#xff0c;这是一个内部 SDK&#xff0c;使新的 3D 配置器的实施变得轻而易举。虽然它给我们带来了巨大帮助&#xff0c;但我们仍然面临一些有趣的挑战。 NSD…

泰迪智能科技中职大数据实验室建设(职业院校大数据实验室建设指南)

职校大数据实验室是职校校园文化建设的重要部分&#xff0c;大数据实训室的建设方案应涵盖多个方面&#xff0c;包括硬件设施的配备、软件环境的搭建、课程资源的开发、师资力量的培养以及实践教学体系的完善等。 打造特色&#xff0c;对接生产 社会经济与产业的…

欧盟委员会发布《数据法》指南

文章目录 前言一、B to B和B to C的数据共享二、企业间数据共享三、不公平合同条款四、企业对政府的数据共享五、数据处理服务之间的切换六、关于第三国政府非法访问数据七、关于可互操作性八、关于《数据法》的执行前言 4月21日,欧盟委员会在其官方网站发布了《数据法》(Th…

论文阅读-THE GENERALIZATION GAP IN OFFLINE REINFORCEMENT LEARNING(ICLR 2024)

1.Motivation 本文希望比较online RL、offline RL、序列决策和BC等方法的泛化能力(对于不同的初始状态、transition functions、reward functions&#xff0c;现阶段offline RL训练的方式都是在同一个环境下的数据集进行训练)。实验发现offline的算法相较于online算法对新环境…

【Ping32】-企业级加密软件,让核心机密更安全!

Ping32&#xff0c;作为一款企业级加密软件&#xff0c;以其卓越的性能和强大的功能&#xff0c;致力于保护企业的核心机密安全。在当今这个信息化时代&#xff0c;企业的机密信息往往成为不法分子觊觎的目标&#xff0c;因此&#xff0c;如何确保核心机密的安全成为每个企业都…

【零基础】system generator①设置卡解析

1.在matlab中我们输入的是双精度浮点型数据&#xff0c;经过gateway后变成定点型。十六位十四个小数位&#xff0c;整个数据有十六位&#xff0c;其中十四位给了小数 2.fixed-point定点型&#xff1b;signed有符号&#xff1b;2’s comp补码 3.量化误差 truncate&#xff0c;舍…

在QEMU上运行OpenSBI+Linux+Rootfs

在QEMU上运行OpenSBILinuxRootfs 1 编译QEMU2 安装交叉编译工具3 编译OpenSBI4 编译Linux5 创建根文件系统5.1 编译busybox5.2 创建目录结构5.3 制作文件系统镜像5.3.1 创建 ext2 文件5.3.2 将目录结构拷贝进 ext2 文件5.3.3 取消挂载 6 运行OpenSBILinuxRootfs 本文所使用的版…

重生奇迹mu套装大全

1.战士 汉斯的皮套装&#xff1a;冰之指环,皮护腿,皮盔,皮护手,皮靴,皮铠,流星槌 汉斯的青铜套装&#xff1a;青铜护腿,青铜靴,青铜铠 汉斯的翡翠套装&#xff1a;雷之项链,翡翠护腿,翡翠盔,翡翠铠,远古之盾 汉斯的黄金套装&#xff1a;火之项链,黄金护腿,黄金护手,黄金靴,…

跟TED演讲学英文:What we‘re learning from online education by Daphne Koller

What we’re learning from online education Link: https://www.ted.com/talks/daphne_koller_what_we_re_learning_from_online_education Speaker: Daphne Koller Date: June 2012 文章目录 What were learning from online educationIntroductionVocabularyTranscriptSum…

15【PS作图】像素画地图绘制

绘制视角 绘制地图的时候&#xff0c;有的人会习惯把要绘制的 房子、车子、围栏 小物件先画好&#xff0c;然后安放在地图上 但这样绘制出的各种物件之间&#xff0c;会缺乏凝聚力 既然物品都是人构造出的&#xff0c;不如以人的视角去一步步丰富地图&#xff1b; 比如下图…

[Algorithm][多源BFS][矩阵][飞地的数量][地图中的最高点][地图分析] + 多源BFS原理讲解 详细讲解

目录 0.原理讲解1.矩阵1.题目链接2.算法原理详解3.代码实现 2.飞地的数量1.题目链接2.算法原理详解3.代码实现 3.地图中的最高点1.题目链接2.算法原理详解3.代码实现 4.地图分析1.题目链接2.算法原理详解3.代码实现 0.原理讲解 注意&#xff1a;只要是用**BFS解决的最短路径问题…

springboot(3.2.5)初步集成MinIO(8.5.9)开发记录

springboot初步集成MinIO开发记录 说明一&#xff1a;引入maven依赖二&#xff1a;手动注入minioClient三&#xff1a;创建service类四&#xff1a;测试打印连接信息五&#xff1a;时区转化工具类六&#xff1a;常用操作演示 说明 这里只是作者开发的记录&#xff0c;已备将来…

论文| Visual place recognition: A survey from deep learning perspective

2021-Visual place recognition: A survey from deep learning perspective

c++笔记——概述运算符重载——解析运算符重载的难点

前言:运算符重载是面向对象的一个重要的知识点。我们都知道内置类型可以进行一般的运算符的运算。但是如果是一个自定义类型&#xff0c; 这些运算符就无法使用了。那么为了解决这个问题&#xff0c; 我们的祖师爷就在c中添加了运算符重载的概念。 本篇主要通过实例的实现来讲述…

Facebook革命:数字社交的全新篇章

随着互联网的不断普及和科技的飞速发展&#xff0c;社交媒体已经成为现代社会不可或缺的一部分。在众多社交媒体平台中&#xff0c;Facebook以其广泛的用户群体和强大的功能而备受瞩目。然而&#xff0c;Facebook并非止步于现状&#xff0c;而是正在掀起一场数字社交的革命&…

Github 2024-05-07 开源项目日报 Tp10

根据Github Trendings的统计,今日(2024-05-07统计)共有10个项目上榜。根据开发语言中项目的数量,汇总情况如下: 开发语言项目数量TypeScript项目4Jupyter Notebook项目2Python项目1Batchfile项目1非开发语言项目1Java项目1HTML项目1C#项目1从零开始构建你喜爱的技术 创建周期…

MySQL 高级 - 第七章 | 索引的数据结构

目录 一、为什么使用索引二、什么是索引2.1 索引的概述2.2 索引的优缺点 三、InnoDB 中索引的推演3.1 InnoDB 页简介3.2 没有索引的查找3.3 设计索引3.3.1 一个简单的索引设计方案3.3.2 InnoDB 中索引方案① 迭代 1 次&#xff1a;目录项记录的页② 迭代 2 次&#xff1a;多个目…