首页 > 其他分享 >SpringBoot 3.0日志系统设计LoggingSystem详解

SpringBoot 3.0日志系统设计LoggingSystem详解

时间:2023-08-14 22:31:45浏览次数:51  
标签:logging SpringBoot LoggingSystem 3.0 org 日志 logFile logback

0 前言

SpringBoot对日志的配置和加载进行了封装,让我们可以很方便地使用一些日志框架,只需要定义对应日志框架的配置文件,如LogBack、Log4j、Log4j2等,代码内部便可以直接使用。

如我们在resources目录下定义了一个logback xml文件,文件内容是logback相关配置,然后就可以直接在代码在使用Logger记录日志啦:

SpringBoot 3.0日志系统设计LoggingSystem详解_配置文件

SpringBoot对日志功能的封装:

SpringBoot 3.0日志系统设计LoggingSystem详解_配置文件_02

1 LoggingSystem内部结构

1.1 SpringBoot3.0默认支持的日志类型

  • JDK内置的Log(JavaLoggingSystem)
  • Log4j2(Log4J2LoggingSystem)
  • Logback(LogbackLoggingSystem)

LoggingSystem是个抽象类,内部

1.2 API

  1. beforeInitialize方法:日志系统初始化之前需要处理的事情。抽象方法,不同的日志架构进行不同的处理
  2. initialize方法:初始化日志系统。默认不进行任何处理,需子类进行初始化工作
  3. cleanUp方法:日志系统的清除工作。默认不进行任何处理,需子类进行清除工作
  4. getShutdownHandler方法:返回一个Runnable用于当jvm退出的时候处理日志系统关闭后需要进行的操作,默认返回null,也就是什么都不做
  5. setLogLevel方法:抽象方法,用于设置对应logger的级别

1.3 AbstractLoggingSystem抽象类

继承LoggingSystem抽象类进行扩展,实现beforeInitialize方法,但内部无任何处理。重点在initialize方法

重写initialize

@Override
 public void initialize(LoggingInitializationContext initializationContext,
     String configLocation, LogFile logFile) {
   // 如传递了日志配置文件,则使用指定文件
   if (StringUtils.hasLength(configLocation)) {
     initializeWithSpecificConfig(initializationContext, configLocation, logFile);
     return;
   }
   // 没传递日志配置文件,使用约定方式
   initializeWithConventions(initializationContext, logFile);
 }
① 指定日志文件
private void initializeWithSpecificConfig(
     LoggingInitializationContext initializationContext, String configLocation,
     LogFile logFile) {
   // 处理日志配置文件中的占位符
   configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
   loadConfiguration(initializationContext, configLocation, logFile);
 }
② 约定配置文件

 
 private void initializeWithConventions(
     LoggingInitializationContext initializationContext, LogFile logFile) {
   // 获取自初始化的日志配置文件,该方法会使用getStandardConfigLocations抽象方法得到的文件数组
   // 然后进行遍历,如果文件存在,返回对应的文件目录。注意这里的文件指的是classpath下的文件
   String config = getSelfInitializationConfig();
   // 如果找到对应的日志配置文件并且logFile为null(logFile为null表示只有console会输出)
   if (config != null && logFile == null) {
     // 调用reinitialize方法重新初始化
     // 默认的reinitialize方法不做任何处理,logback,log4j和log4j2覆盖了这个方法,会进行处理
     reinitialize(initializationContext);
     return;
   }
   // 如果没有找到对应的日志配置文件
   if (config == null) {
     // 获取日志配置文件
     // 该方法与getSelfInitializationConfig方法的区别在于getStandardConfigLocations方法得到的文件数组内部遍历的逻辑
     // getSelfInitializationConfig方法直接遍历并判断classpath下是否存在对应的文件
     // getSpringInitializationConfig方法遍历后判断的文件名会在后缀前加上 "-spring" 字符串
     // 比如查找logback.xml文件,getSelfInitializationConfig会直接查找classpath下是否存在logback.xml文件,而getSpringInitializationConfig方法会判断classpath下是否存在logback-spring.xml文件
     config = getSpringInitializationConfig();
   }
   // 如找到对应日志配置文件
   if (config != null) {
     // 调用抽象方法,子类实现
     loadConfiguration(initializationContext, config, logFile);
     return;
   }
   // 还没找到日志配置文件,调用抽象方法加载
   loadDefaults(initializationContext, logFile);
 }
 
 protected abstract String[] getStandardConfigLocations();
 
 protected abstract void loadConfiguration(
     LoggingInitializationContext initializationContext, String location,
     LogFile logFile);
 
 protected abstract void loadDefaults(
     LoggingInitializationContext initializationContext, LogFile logFile);

以LogbackLoggingSystem.java为例,看具体的

1.4 初始化过程

根据AbstractLoggingSystem 使用logback日志库时,会查找classpath下是否存在这些文件:

  • logback-test.groovy
  • logback-test.xml
  • logback.groovy
  • logback.xml
  • logback-test-spring.groovy
  • logback-test-spring.xml
  • logback-spring.groovy
  • logback-spring.xml
@Override
 protected String[] getStandardConfigLocations() {
   return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
       "logback.xml" };
 }
@Override
 protected void loadConfiguration(LoggingInitializationContext initializationContext,
     String location, LogFile logFile) {
   // 调用父类Slf4JLoggingSystem的方法
   super.loadConfiguration(initializationContext, location, logFile);
   // 获取slf4j内部的LoggerContext
   LoggerContext loggerContext = getLoggerContext();
   // logback环境的一些配置配置处理
   stopAndReset(loggerContext);
   try {
     configureByResourceUrl(initializationContext, loggerContext,
         ResourceUtils.getURL(location));
   }
   catch (Exception ex) {
     throw new IllegalStateException(
         "Could not initialize Logback logging from " + location, ex);
   }
   List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
   StringBuilder errors = new StringBuilder();
   for (Status status : statuses) {
     if (status.getLevel() == Status.ERROR) {
       errors.append(errors.length() > 0 ? "\n" : "");
       errors.append(status.toString());
     }
   }
   if (errors.length() > 0) {
     throw new IllegalStateException(
         "Logback configuration error " + "detected: \n" + errors);
   }
 }
 
 // 没找到日志配置文件的话使用loadDefaults方法加载
 @Override
 protected void loadDefaults(LoggingInitializationContext initializationContext,
     LogFile logFile) {
   // 获取slf4j内部的LoggerContext
   LoggerContext context = getLoggerContext();
   stopAndReset(context);
   LogbackConfigurator configurator = new LogbackConfigurator(context);
   context.putProperty("LOG_LEVEL_PATTERN",
       initializationContext.getEnvironment().resolvePlaceholders(
           "${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
   // 构造默认的console Appender。如果logFile不为空,还会构造file Appender
   new DefaultLogbackConfiguration(initializationContext, logFile)
       .apply(configurator);
   context.setPackagingDataEnabled(true);
 }
 
 // logback的清除工作
 @Override
 public void cleanUp() {
   super.cleanUp();
   getLoggerContext().getStatusManager().clear();
 }
 
 // 动态设置logger的level
 @Override
 public void setLogLevel(String loggerName, LogLevel level) {
   getLogger(loggerName).setLevel(LEVELS.get(level));
 }
 
 // 清除后的一些工作
 // ShutdownHandler会调用LoggerContext的stop方法
 @Override
 public Runnable getShutdownHandler() {
   return new ShutdownHandler();
 }

2 LoggingSystem的初始化

LoggingApplicationListener是ApplicationListener接口的实现类,会被springboot使用工厂加载机制加载:

// spring-boot-version.jar/META-INF/spring.factories
 # Application Listeners
 org.springframework.context.ApplicationListener=\
 org.springframework.boot.builder.ParentContextCloserApplicationListener,\
 org.springframework.boot.context.FileEncodingApplicationListener,\
 org.springframework.boot.context.config.AnsiOutputApplicationListener,\
 org.springframework.boot.context.config.ConfigFileApplicationListener,\
 org.springframework.boot.context.config.DelegatingApplicationListener,\
 org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
 org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
 org.springframework.boot.logging.LoggingApplicationListener
 
 // SpringApplication.class
 private void initialize(Object[] sources) {
   if (sources != null && sources.length > 0) {
     this.sources.addAll(Arrays.asList(sources));
   }
   this.webEnvironment = deduceWebEnvironment();
   setInitializers((Collection) getSpringFactoriesInstances(
       ApplicationContextInitializer.class));
   // 使用工厂加载机制找到这些Listener
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   this.mainApplicationClass = deduceMainApplicationClass();
 }
 
 // LoggingApplicationListener.class
 @Override
 public void onApplicationEvent(ApplicationEvent event) {
   // SpringApplication的run方法执行的时候触发该事件
   if (event instanceof ApplicationStartedEvent) {
     // onApplicationStartedEvent方法内部会先得到LoggingSystem,然后调用beforeInitialize方法
     onApplicationStartedEvent((ApplicationStartedEvent) event);
   }
   // 环境信息准备好,ApplicationContext创建之前触发该事件
   else if (event instanceof ApplicationEnvironmentPreparedEvent) {
     // onApplicationEnvironmentPreparedEvent方法内部会做一下几个事情
     // 1. 读取配置文件中"logging."开头的配置,比如logging.pattern.level, logging.pattern.console等设置到系统属性中
     // 2. 构造一个LogFile(LogFile是对日志对外输出文件的封装),使用LogFile的静态方法get构造,会使用配置文件中logging.file和logging.path配置构造
     // 3. 判断配置中是否配置了debug并为true,如果是,设置level的DEBUG,然后继续查看是否配置了trace并为true,如果是,设置level的TRACE
     // 4. 构造LoggingInitializationContext,查看是否配置了logging.config,如有配置,调用LoggingSystem的initialize方法并带上该参数,否则调用initialize方法并且configLocation为null
     // 5. 设置一些比如org.springframework.boot、org.springframework、org.apache.tomcat、org.apache.catalina、org.eclipse.jetty、org.hibernate.tool.hbm2ddl、org.hibernate.SQL这些包的log level,跟第3步的level一样
     // 6. 查看是否配置了logging.register-shutdown-hook,如配置并设置为true,使用addShutdownHook的addShutdownHook方法加入LoggingSystem的getShutdownHandler
     onApplicationEnvironmentPreparedEvent(
         (ApplicationEnvironmentPreparedEvent) event);
   }
   // Spring容器创建好,并进行了部分操作之后触发该事件
   else if (event instanceof ApplicationPreparedEvent) {
     // onApplicationPreparedEvent方法内部会把LoggingSystem注册到BeanFactory中(前期是BeanFactory中不存在name为springBootLoggingSystem的实例)
     onApplicationPreparedEvent((ApplicationPreparedEvent) event);
   }
   // Spring容器关闭的时候触发该事件
   else if (event instanceof ContextClosedEvent && ((ContextClosedEvent) event)
       .getApplicationContext().getParent() == null) {
     // onContextClosedEvent方法内部调用LoggingSystem的cleanUp方法进行清除工作
     onContextClosedEvent();
   }
 }
 
 private void onApplicationStartedEvent(ApplicationStartedEvent event) {
   // 一开始先使用LoggingSystem的静态方法get获取LoggingSystem
   // 静态方法get会从下面那段static代码块中得到的Map中进行遍历
   // 如果对应的key(key是某个类的全名)在classloader中存在,那么会构造该key对应的value对应的LoggingSystem
   this.loggingSystem = LoggingSystem
       .get(event.getSpringApplication().getClassLoader());
   this.loggingSystem.beforeInitialize();
 }
 
 static {
   Map<String, String> systems = new LinkedHashMap<String, String>();
   systems.put("ch.qos.logback.core.Appender",
       "org.springframework.boot.logging.logback.LogbackLoggingSystem");
   systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory",
       "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
   systems.put("org.apache.log4j.PropertyConfigurator",
       "org.springframework.boot.logging.log4j.Log4JLoggingSystem");
   systems.put("java.util.logging.LogManager",
       "org.springframework.boot.logging.java.JavaLoggingSystem");
   SYSTEMS = Collections.unmodifiableMap(systems);
 }
 
 private void onApplicationEnvironmentPreparedEvent(
     ApplicationEnvironmentPreparedEvent event) {
   if (this.loggingSystem == null) {
     this.loggingSystem = LoggingSystem
         .get(event.getSpringApplication().getClassLoader());
   }
   initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
 }
 
 private void onApplicationPreparedEvent(ApplicationPreparedEvent event) {
   ConfigurableListableBeanFactory beanFactory = event.getApplicationContext()
       .getBeanFactory();
   if (!beanFactory.containsBean(LOGGING_SYSTEM_BEAN_NAME)) {
     beanFactory.registerSingleton(LOGGING_SYSTEM_BEAN_NAME, this.loggingSystem);
   }
 }
 
 private void onContextClosedEvent() {
   if (this.loggingSystem != null) {
     this.loggingSystem.cleanUp();
   }
 }

spring-boot-starter模块内部会引用spring-boot-starter-logging模块,这个starter-logging模块内部会引入logback相关的依赖。这一依赖会导致LoggingSystem的静态方法get获取LoggingSystem的时候会得到LogbackLoggingSystem。

因此默认情况下,springboot程序基本都是使用logback作为默认日志。

前提都是以LogbackLoggingSystem作为日志系统。

项目无任何日志配置

执行到AbstractLoggingSystem#initialize时,日志配置文件为null:

SpringBoot 3.0日志系统设计LoggingSystem详解_配置文件_03

最后只能调loadDefaults进行加载,LogbackLoggingSystem#loadDefaults方法,由于logFile为null,所以最终只构造个ConsoleAppender。

所以项目没有任何日志配置时,默认就是在控制台打印了项目启动信息。

项目无任何logback的配置,只有yaml中配置logging.file和logging.path

logging.file和logging.path的配置在LogFile这个日志文件类中生效。

比如yaml配置如下(只定义了logging.file):

logging:
   file: /tmp/temp.log

这配置导致调用initialize方法时候logFile存在,这样不止有ConsoleAppender,还有个FileAppender,这个FileAppender对应的文件就是LogFile文件,即 /tmp/temp.log日志文件。

比如yaml配置如下(只定义了logging.path):

logging:
   path: /tmp

这时FileAppender对应file是/tmp/spring.log文件。

// LogFile.class
 @Override
 public String toString() {
   // 如果配置了logging.file,直接使用该文件
   if (StringUtils.hasLength(this.file)) {
     return this.file;
   }
   // 否则使用logging.path目录,在该目录下创建spring.log日志文件
   String path = this.path;
   if (!path.endsWith("/")) {
     path = path + "/";
   }
   return StringUtils.applyRelativePath(path, "spring.log");
 }

所以我们如果配置了logging.path和logging.file,那么生效的只有logging.file配置。

resources下有logback.xml配置(相当于classpath下存在logback.xml文件)

LogbackLoggingSystem中的getStandardConfigLocations方法返回以下文件:

  1. logback-test.groovy或者logback-test-spring.groovy
  2. logback-test.xml或者logback-test-spring.xml
  3. logback.groovy或者logback-spring.groovy
  4. logback.xml或者logback-spring.xml

在resources目录下定义logback-spring.xml文件,内容如下:

<?xml versinotallow="1.0" encoding="UTF-8"?>
 <configuration>
     <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
         <encoder>
             <!--<pattern>%d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger[%line] -- %msg%n</pattern>-->
             <pattern>%d{YYYY-MM-dd} [%thread] %-5level %logger[%line] -- %msg%n</pattern>
         </encoder>
     </appender>
 
     <root level="INFO">
         <appender-ref ref="STDOUT"/>
     </root>
 </configuration>

这时logging.file配置失效,这是因为没有调用loadDefaults方法(loadDefaults方法内部会把LogFile构造成FileAppender),而是调用了loadConfiguration方法,该方法会根据logback.xml文件中的配置去构造Appender。

resources下有my-logback.xml配置

由于LogbackLoggingSystem中没有对my-logback.xml路径的解析,所有不会被识别,但是可以在yaml中配置logging.config配置:

logging:
   config: classpath:my-logback.xml

这样配置就能识别my-logback.xml文件。

3 其它

SpringBoot内部的NoOpLoggingSystem,这个日志系统内部什么都不做,构造过程:

public static LoggingSystem get(ClassLoader classLoader) {
   // SYSTEM_PROPERTY静态变量是LoggingSystem的类全名
   String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
   if (StringUtils.hasLength(loggingSystem)) {
     if (NONE.equals(loggingSystem)) { // None静态变量是值是none
       return new NoOpLoggingSystem();
     }
     return get(classLoader, loggingSystem);
   }
   for (Map.Entry<String, String> entry : SYSTEMS.entrySet()) {
     if (ClassUtils.isPresent(entry.getKey(), classLoader)) {
       return get(classLoader, entry.getValue());
     }
   }
   throw new IllegalStateException("No suitable logging system located");
 }

所以程序启动的时候加上 -Dorg.springframework.boot.logging.LoggingSystem=none就可构造NoOpLoggingSystem这个日志系统。

标签:logging,SpringBoot,LoggingSystem,3.0,org,日志,logFile,logback
From: https://blog.51cto.com/JavaEdge/7082430

相关文章

  • SpringBoot 启动流程追踪(第二篇)
    上一篇文章分析了除refresh方法外的流程,并着重分析了load方法,这篇文章就主要分析refresh方法,可以说refresh方法是springboot启动流程最重要的一环,没有之一。try{ //Allowspost-processingofthebeanfactoryincontextsubclasses. postProcessBeanFactory(bea......
  • 还在手动更改SpringBoot的环境yml配置文件?老鸟带你可视化配置
    问题说明:在SpringBoot开发时、SpringBoot的特性:‘约定大于配置’,我们只需要在**application.yml**配置当前的环境变量属与那个文件比如测试环境‘application-test.yml’,我们需要手动指定application.yml中配置profiles:active:test我们总会设置一些配置文件我们需要手动......
  • Springboot中实现适配器模式
    当在SpringBoot中实现适配器模式时,可以按照以下步骤进行详细的实现:1.首先,定义一个目标接口(TargetInterface):publicinterfaceTarget{voidrequest();}目标接口定义了适配器需要实现的方法。2.创建一个适配器类(AdapterClass),实现目标接口,并适配一个已有的类或接......
  • 2023.08.12 codeforces round 892 div2
    年轻人的第三场div2(已完成:ABCDE)rank:1265solved:4ratingchange:+276newrating:1323A.UnitedWeStand题意:给定一个数列a,问是否能分成两个非空的数列b和c,使得c中任意一个数不是b中任意一个数的因子;若x是y的因子则有x<=y;因此不妨将数列的最大值放入c,把剩下的数放入b;注意数列中......
  • SpringBoot——整合WebSocket(基于STOMP协议)
    参考链接Spring官文:https://docs.spring.io/spring-framework/docs/6.0.0-SNAPSHOT/reference/html/web.html#websocket-stomp-benefits前端页面:https://github.com/callicoder/spring-boot-websocket-chat-demoSTOMP定义STOMP中文为“面向消息的简单文本协议”,STOM......
  • springboot整合nacos和dubbo
    0.源码源码:gitee1.版本java:1.8.0_281nacos:2.1.22.创建项目创建一个简单的springboot或者maven项目,或者代码库(gitee/github/其他代码库管理平台)创建一个空白的拉下来,最后只保留一个pom.xml即可.2.1根项目依赖版本控制参考:版本说明其中有一句话Spring......
  • Spring batch document 2.1.8(supported by spring core 3.0)
    http://static.springsource.org/spring-batch/reference/html-single/index.html#configuringAJob SpringBatch-ReferenceDocumentationAuthorsLucasWard,DaveSyer,ThomasRisberg,RobertKasanicky,DanGarrette,WayneLund......
  • SpringBoot有几种获取Request对象的方法?
    HttpServletRequest简称Request,它是一个ServletAPI提供的对象,用于获取客户端发起的HTTP请求信息。例如:获取请求参数、获取请求头、获取Session会话信息、获取请求的IP地址等信息。那么问题来了,在SpringBoot中,获取Request对象的方法有哪些?常见的获取Request对......
  • springboot开启prometheus可采集的指标配置
    1、引包<!--实现对Actuator的自动化配置--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-actuator</artifactId></dependency>......
  • SpringBoot实现大文件上传
    ​ 对于大文件的处理,无论是用户端还是服务端,如果一次性进行读取发送、接收都是不可取,很容易导致内存问题。所以对于大文件上传,采用切块分段上传,从上传的效率来看,利用多线程并发上传能够达到最大效率。 本文是基于springboot+vue实现的文件上传,本文主要介绍服务端实现文件......