0 前言
SpringBoot对日志的配置和加载进行了封装,让我们可以很方便地使用一些日志框架,只需要定义对应日志框架的配置文件,如LogBack、Log4j、Log4j2等,代码内部便可以直接使用。
如我们在resources目录下定义了一个logback xml文件,文件内容是logback相关配置,然后就可以直接在代码在使用Logger记录日志啦:
SpringBoot对日志功能的封装:
1 LoggingSystem内部结构
1.1 SpringBoot3.0默认支持的日志类型
- JDK内置的Log(JavaLoggingSystem)
- Log4j2(Log4J2LoggingSyststem)
- Logback(LogbackLoggingSystem)
LoggingSystem是个抽象类,内部
1.2 API
- beforeInitialize:日志系统初始化之前需要处理的事。抽象方法
- initialize:初始化日志系统。默认不进行任何处理,需子类初始化
- cleanUp:日志系统的清除工作。默认不进行任何处理,需子类清除
- getShutdownHandler:返回一个Runnable,用于当JVM退出时处理日志系统关闭后需要进行的操作,默认null
- 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 使用工厂加载机制加载。
2.1 spring.factories
spring-boot-3.0.0.jar/META-INF/spring.factories:
注意到LoggingApplicationListener,和 SpringBoot 启动流程关联点:
2.2 SpringApplication.java
@SuppressWarnings({ "unchecked", "rawtypes" }) public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) { this.resourceLoader = resourceLoader; Assert.notNull(primarySources, "PrimarySources must not be null"); this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources)); this.webApplicationType = deduceWebApplicationType(); setInitializers((Collection) getSpringFactoriesInstances( ApplicationContextInitializer.class)); // 使用工厂加载机制找到这些Listener setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); this.mainApplicationClass = deduceMainApplicationClass(); }
2.3 LoggingApplicationListener.java
SpringApplication#run执行时触发该事件
@Override public void onApplicationEvent(ApplicationEvent event) { if (event instanceof ApplicationStartedEvent) { // 先得到LoggingSystem,再调用beforeInitialize onApplicationStartedEvent((ApplicationStartedEvent) event); } ... } private void onApplicationStartedEvent(ApplicationStartedEvent event) { // 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<>(); 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("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem"); SYSTEMS = Collections.unmodifiableMap(systems); }
spring-boot-starter模块内部会引用spring-boot-starter-logging模块,这starter-logging模块内部会引入logback相关依赖。这依赖会导致LoggingSystem的静态方法get获取LoggingSystem时得到LogbackLoggingSystem。
因此springboot程序使用logback作默认日志。前提都是以LogbackLoggingSystem作为日志系统。
2.4 FAQ
① 项目无任何日志配置
执行到AbstractLoggingSystem#initialize时,日志配置文件为null:
最后只能调loadDefaults进行加载,LogbackLoggingSystem#loadDefaults方法,由于logFile为null,所以最终只构造个ConsoleAppender。
所以项目没有任何日志配置时,默认就是在控制台打印了项目启动信息。
② 项目无任何logback配置,只有yaml中配置logging.file/path
logging.file和logging.path的配置在LogFile这个日志文件类中生效。
比如yaml配置如下(只定义了logging.file):
logging: file: /tmp/temp.log
这时FileAppender对应file是/tmp/spring.log文件。
LogFile.java
@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/file,生效的只有logging.file配置。
③ resources下有logback.xml配置
相当于classpath下存在logback.xml文件。LogbackLoggingSystem#getStandardConfigLocations返回如下:
- logback-test.groovy或logback-test-spring.groovy
- logback-test.xml或logback-test-spring.xml
- logback.groovy或logback-spring.groovy
- logback.xml或logback-spring.xml
在resources目录下定义logback-spring.xml文件,内容如下:
<?xml version="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 NoOpLoggingSystem
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。