Springboot的Log日志系统详解 您所在的位置:网站首页 jboss日志配置详解 Springboot的Log日志系统详解

Springboot的Log日志系统详解

2023-03-24 03:38| 来源: 网络整理| 查看: 265

Springboot的Log系统分为两个启动阶段:LoggingApplicationListener启动之前和LoggingApplicationListener成功加载。

1. LoggingApplicationListener启动之前

此时,业务定义的log尚未加载,所以起作用的是Springboot系统内部定义的log。 我们可以从main方法跟进去,发现在SpringApplication中定义了log

private static final Log logger = LogFactory.getLog(SpringApplication.class);

这就是系统内部log的启动。 我们看看这个Log和LogFactory

import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory;

是不是很眼熟,怎么会是apache的package?! 有一个熟悉的commons-logging.jar出现在我们脑海里! 但是,当我们再看看这个Log和LogFactory所在jar:

spring-jcl 竟然不是commons-logging.jar! Spring够鸡贼的!用自己的jar包替换了apache的commons-logging.jar包! 这样就导致了我们的项目中如果出现了spring-jcl.jar时,就不能再使用commons-logging.jar了! 既然Spring这样做,那就有这么做的道理了,我们来看看这个jar主要做了什么工作吧。 我们先看看LogFactory.getLog(...)做了啥:

public abstract class LogFactory { /** * Convenience method to return a named logger. * @param clazz containing Class from which a log name will be derived */ public static Log getLog(Class clazz) { return getLog(clazz.getName()); } /** * Convenience method to return a named logger. * @param name logical name of the Log instance to be returned */ public static Log getLog(String name) { return LogAdapter.createLog(name); }

它就做了一件事,调用LogAdapter创建Log 。 我们接着往下看:

public static Log createLog(String name) { switch (logApi) { case LOG4J: return Log4jAdapter.createLog(name); case SLF4J_LAL: return Slf4jAdapter.createLocationAwareLog(name); case SLF4J: return Slf4jAdapter.createLog(name); default: // Defensively use lazy-initializing adapter class here as well since the // java.logging module is not present by default on JDK 9. We are requiring // its presence if neither Log4j nor SLF4J is available; however, in the // case of Log4j or SLF4J, we are trying to prevent early initialization // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly // trying to parse the bytecode for all the cases of this switch clause. return JavaUtilAdapter.createLog(name); } }

而logApi又是怎么来的呢

private static final LogApi logApi; static { if (isPresent(LOG4J_SPI)) { if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) { // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI; // however, we still prefer Log4j over the plain SLF4J API since // the latter does not have location awareness support. logApi = LogApi.SLF4J_LAL; } else { // Use Log4j 2.x directly, including location awareness support logApi = LogApi.LOG4J; } } else if (isPresent(SLF4J_SPI)) { // Full SLF4J SPI including location awareness support logApi = LogApi.SLF4J_LAL; } else if (isPresent(SLF4J_API)) { // Minimal SLF4J API without location awareness support logApi = LogApi.SLF4J; } else { // java.util.logging as default logApi = LogApi.JUL; } }

这里又出现了4个常量:

private static final String LOG4J_SPI = "org.apache.logging.log4j.spi.ExtendedLogger"; private static final String LOG4J_SLF4J_PROVIDER = "org.apache.logging.slf4j.SLF4JProvider"; private static final String SLF4J_SPI = "org.slf4j.spi.LocationAwareLogger"; private static final String SLF4J_API = "org.slf4j.Logger";

其中,LOG4J_SPI 代表log4j日志系统;其余3个都代表logback日志系统。 接着往下看:

private static boolean isPresent(String className) { try { Class.forName(className, false, LogAdapter.class.getClassLoader()); return true; } catch (ClassNotFoundException ex) { return false; } }

这个方法就是判定对应的log实现是否存在于classpath中。 通过上面几段代码的分析,我们可以得出如下结论: 1.1. Spring先查找org.apache.logging.log4j.spi.ExtendedLogger; 1.2. 如果ExtendedLogger存在,那么继续查找org.apache.logging.slf4j.SLF4JProvider和org.slf4j.spi.LocationAwareLogger; 1.3. 如果SLF4JProvider和LocationAwareLogger都存在,那么就启用SLF4J_LAL日志系统; 1.4. 如果SLF4JProvider和LocationAwareLogger有一个不存在,就启用LOG4J日志系统; 1.5. 如果ExtendedLogger不存在,就查找org.slf4j.spi.LocationAwareLogger; 1.6. 如果LocationAwareLogger存在,就启用SLF4J_LAL日志系统; 1.7. 如果LocationAwareLogger不存在,就继续查找org.slf4j.Logger; 1.8. 如果org.slf4j.Logger存在,就启用SLF4J日志系统; 1.9. 如果以上都不存在,就启用JUL日志系统。

接着往下看 LOG4J日志系统

private static class Log4jAdapter { public static Log createLog(String name) { return new Log4jLog(name); } }

SLF4J_LAL和SLF4J日志系统

private static class Slf4jAdapter { public static Log createLocationAwareLog(String name) { Logger logger = LoggerFactory.getLogger(name); return (logger instanceof LocationAwareLogger ? new Slf4jLocationAwareLog((LocationAwareLogger) logger) : new Slf4jLog(logger)); } public static Log createLog(String name) { return new Slf4jLog(LoggerFactory.getLogger(name)); } }

JUL日志系统

private static class JavaUtilAdapter { public static Log createLog(String name) { return new JavaUtilLog(name); } }

接下来我们不再一一分析了,我们主要看一下SLF4J_LAL日志系统。

SLF4J_LAL日志系统 import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.spi.LocationAwareLogger;

这里是启用了原生态的logback的日志系统,所以这里会启用logback.xml配置文件,因此,我们最好把logback的配置文件命名为logback.xml,而非logback-spring.xml,或者其他,否者的话,这段启动过程中的日志可能无法正常输出。 这样,Springboot的内部日志系统就启动起来了。

2. LoggingApplicationListener加载业务日志系统

这里的逻辑就比较简单了,其核心代码如下:

private void onApplicationStartingEvent(ApplicationStartingEvent event) { this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader()); this.loggingSystem.beforeInitialize(); } private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) { if (this.loggingSystem == null) { this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader()); } initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader()); }

也就是从系统中查找LoggingSystem,然后用找到的loggingSystem初始化系统。 接下来的核心代码如下

初始化业务日志系统

继续

初始化业务日志系统 其中, public static final String CONFIG_PROPERTY = "logging.config";

也就是我们定义的log系统的配置文件:

logging: #config: classpath:logback-spring.xml config: classpath:logback.xml file.path: ${LOGGING_PATH} register-shutdown-hook: false

接下来我们看看LoggingSystem文件,其核心代码如下

LoggingSystem核心代码

其中,SYSTEMS内容如下

private static final Map SYSTEMS; static { Map 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); }

public static final String SYSTEM_PROPERTY = LoggingSystem.class.getName();

也就是SYSTEM_PROPERTY是LoggingSystem具体实现类的名称:

private static LoggingSystem get(ClassLoader classLoader, String loggingSystemClass) { try { Class systemClass = ClassUtils.forName(loggingSystemClass, classLoader); Constructor constructor = systemClass.getDeclaredConstructor(ClassLoader.class); constructor.setAccessible(true); return (LoggingSystem) constructor.newInstance(classLoader); } catch (Exception ex) { throw new IllegalStateException(ex); } }

或许没有定义其他实现类,那么会从系统定义的SYSTEM中去查找第一个存在的LoggingSystem类

return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader)) .map((entry) -> get(classLoader, entry.getValue())).findFirst() .orElseThrow(() -> new IllegalStateException("No suitable logging system located"));

我们仍以logback为例说说具体实现。 系统定义了logback的LoggingSystem实现类(这也是Springboot的默认实现) LogbackLoggingSystem。 关于配置文件的加载核心代码块(AbstractLoggingSystem):

@Override public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) { if (StringUtils.hasLength(configLocation)) { initializeWithSpecificConfig(initializationContext, configLocation, logFile); return; } initializeWithConventions(initializationContext, logFile); }

这里,configLocation就是logging.config定义的配置文件,如果该文件存在,那么就直接去初始化logback,这里的配置文件名没有特定要求,只要是logback的.groovy或者.xml配置即可。但是为了与系统内部日志配置保持一致,建议用logback.xml。 如果我们没有定义logging.config配置文件,那么就去找系统默认配置文件,查找的核心代码如下:

private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) { String config = getSelfInitializationConfig(); if (config != null && logFile == null) { // self initialization has occurred, reinitialize in case of property changes reinitialize(initializationContext); return; } if (config == null) { config = getSpringInitializationConfig(); } if (config != null) { loadConfiguration(initializationContext, config, logFile); return; } loadDefaults(initializationContext, logFile); }

首先是通过String config = getSelfInitializationConfig()来加载:

protected String getSelfInitializationConfig() { return findConfig(getStandardConfigLocations()); } private String findConfig(String[] locations) { for (String location : locations) { ClassPathResource resource = new ClassPathResource(location, this.classLoader); if (resource.exists()) { return "classpath:" + location; } } return null; } protected abstract String[] getStandardConfigLocations();

在LogbackLoggingSystem的实现是

@Override protected String[] getStandardConfigLocations() { return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" }; }

也就是说这些命名的配置文件都可以被加载。其中,logback.xml还可以与前面说的Springboot内部log共用。 如果这些文件都不存在,那么会执行config = getSpringInitializationConfig()去继续查找配置文件:

protected String getSpringInitializationConfig() { return findConfig(getSpringConfigLocations()); } protected String[] getSpringConfigLocations() { String[] locations = getStandardConfigLocations(); for (int i = 0; i < locations.length; i++) { String extension = StringUtils.getFilenameExtension(locations[i]); locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring." + extension; } return locations; }

也就是说,对"logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml"这些文件名处理后再查找:"logback-test-spring.groovy", "logback-test-spring.xml", "logback-spring.groovy", "logback-spring.xml",这也是为什么网上很多教程要求大家配置logback-spring.xml文件名的原因。 如果这些处理后的文件还不存在,那就继续执行loadDefaults(initializationContext, logFile)去初始化logback配置:

protected abstract void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile);

在LogbackLoggingSystem中实现如下:

@Override protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) { LoggerContext context = getLoggerContext(); stopAndReset(context); boolean debug = Boolean.getBoolean("logback.debug"); if (debug) { StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener()); } LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context) : new LogbackConfigurator(context); Environment environment = initializationContext.getEnvironment(); context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN, environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}")); context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders( "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}")); context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}")); new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator); context.setPackagingDataEnabled(true); }

这些执行完毕之后,LoggingApplicationListener也起动起来了,同事还把前面定义的内部log系统更新为刚加载的log日志系统。 经过上面这些步骤,Springboot的日志系统算是正式启动起来了。

3. 通过上面的分析,我们可以得出以下注意事项:

3.1 Springboot系统中不允许引入commons-logging.jar,但是我们在使用日志系统的时候,最好使用Log logger = LogFactory.getLog(SpringApplication.class)来定义,org.apache.commons.logging.Log和org.apache.commons.logging.LogFactory便于兼容多种日志系统; 3.2 log4j-api.jar、log4j-to-slf4j.jar、slf4j-api.jar可以共存,但是优先级是log4j > logback; 3.3 log4j只能启用2.x; 3.4 Logback的配置文件命名最好使用logback.xml; 3.5 最好明确定义出logging.config: classpath:logback.xml; 3.6 可以通过继承LoggingSystem来定义自己的LoggingSystem,通过设置System.setProperty(全类名)进行加载; 3.7 LoggingSystem加载完毕后,系统注册了3个单例bean:springBootLoggingSystem=LoggingSystem实例,springBootLogFile=LogFile实例,对应于logging.file.path配置,springBootLoggerGroups=LoggerGroups日志分组实例。 3.8 由于我们修订的logback配置文件的名称为logback.xml,这样会导致系统未使用spring的方式加载logback,所以在logback.xml中的属性配置就不能再使用springProperty,而直接使用logback的标签property即可。

4. 如何用log4j替换logback?

有时候,我们需要使用log4j而不是logback,那我们该如何做呢? 4.1 根据上面的分析,我们清楚地知道,在classpath中存在log4j-api.jar并且不存在slf4j-api.jar时,第一阶段就会启用log4j日志系统,但是只能是启用2.x版; 4.2 在第二阶段,加载LoggingSystem时有如下代码

public static LoggingSystem get(ClassLoader classLoader) { String loggingSystem = System.getProperty(SYSTEM_PROPERTY); if (StringUtils.hasLength(loggingSystem)) { if (NONE.equals(loggingSystem)) { return new NoOpLoggingSystem(); } return get(classLoader, loggingSystem); } return SYSTEMS.entrySet().stream().filter((entry) -> ClassUtils.isPresent(entry.getKey(), classLoader)) .map((entry) -> get(classLoader, entry.getValue())).findFirst() .orElseThrow(() -> new IllegalStateException("No suitable logging system located")); }

其中,SYSTEM_PROPERTY=LoggingSystem.class.getName(),只要loggingSystem存在,系统就会加载该LoggingSystem,而不会再去classpath去查找其他信息了。而log4j对应的LoggingSystem是org.springframework.boot.logging.log4j2.Log4J2LoggingSystem,因此,我们只需要在main启动的时候,添加对应的属性值即可:

public static void main(String[] args) throws Exception { System.setProperty("org.springframework.boot.logging.LoggingSystem", "org.springframework.boot.logging.log4j2.Log4J2LoggingSystem"); configureApplication(new SpringApplicationBuilder()).run(args); }

4.3 注意配置log的配置文件:logging.config: classpath:log4j.properties,另外注意引入log4j的依赖jar。 这样,Springboot的默认logback日志系统就会被log4j日志系统所替代。



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有