官网日志文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging
本文以2.2.0为例来说明下,首先还是打开spring-boot-starter-logging.jar包,不出所料,空空如也:

然后打开autoconfigure的jar包,找到spring.factories,搜索logging:

打开ConditionEvaluationReportLoggingListener,这里也没有配置使用哪一个log框架啊,没办法,只好看一下它的依赖:

依赖了logback,同时还依赖了jul-to-slf4j和log4j-to-slf4j,我们知道logback是springboot默认使用的日志框架,那么后面这两个jar又是干啥的的?
我们先来看第一个问题:日志框架是啥时候加载的
我们打开spring-boot-2.2.0.jar,这里面也有一个spring.factories:

就是通过这个ApplicationListener来加载的,我们跟一下代码:
从main函数入口开始:
public static void main(String[] args) {SpringApplication.run(DemoApplication.class, args);}public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {return run(new Class<?>[] { primarySource }, args);}public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {return new SpringApplication(primarySources).run(args);}
看下SpringApplication的构造函数:
public SpringApplication(Class<?>... primarySources) {this(null, primarySources);}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 = WebApplicationType.deduceFromClasspath();setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));this.mainApplicationClass = deduceMainApplicationClass();}public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {this.listeners = new ArrayList<>(listeners);}
这里面有一个setListeners(),这里就是读取的spring.factories里面所有的ApplicationListener,然后保存到listeners成员变量里面去备用。
继续看下run()方法:
public ConfigurableApplicationContext run(String... args) {//这里是获取了一个SpringApplicationRunListenerSpringApplicationRunListeners listeners = getRunListeners(args);listeners.starting();。。。return context;}
首先看下getRunListeners(args):
private SpringApplicationRunListeners getRunListeners(String[] args) {Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };return new SpringApplicationRunListeners(logger,getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));}
还是读取的spring.factories:

默认值配置了唯一的一个EventPublishingRunListener,很显然是来做事件发布的,因为此时Spring环境还没有构建出来,Spring的那一套事件机制还无法使用,SpringBoot只好自己又搞了一个。这里拿到了EventPublishingRunListener以后,然后又封装进了SpringApplicationRunListeners里面,同时还传进去一个log:
class SpringApplicationRunListeners {private final Log log;private final List<SpringApplicationRunListener> listeners;SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {this.log = log;this.listeners = new ArrayList<>(listeners);}
注意这里的logger此时还是org.apache.commons.logging这个包下面的log。SpringBoot在启动阶段的所有的事件都是通过这个SpringApplicationRunListeners来进行发布的,我们随便找一个事件看一下:
public class SpringApplicationRunListeners {void starting() {for (SpringApplicationRunListener listener : this.listeners) {listener.starting();}}}
其实就是把事件转发给了初始化时候放进去的EventPublishingRunListener,看下EventPublishingRunListener#starting:
@Overridepublic void starting() {this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));}
首先看下initialMulticaster是什么玩意,看下构造函数:
public EventPublishingRunListener(SpringApplication application, String[] args) {this.application = application;this.args = args;//这个的类型和Spring里面的那个Multicaster是一样的this.initialMulticaster = new SimpleApplicationEventMulticaster();//这里是一开始就从spring.factories拿到的ApplicationListenerfor (ApplicationListener<?> listener : application.getListeners()) {this.initialMulticaster.addApplicationListener(listener);}}
这里的initialMulticaster实际上就是使用的Spring里面的那个,然后把一开始从spring.factories中拿到的所有的ApplicationListener注册到了initialMulticaster里面,显然这里面也包括了我们今天要说的主角LoggingApplicationListener。当initialMulticaster发布事件的时候,就可以根据事件的类型回调不同的ApplicationListener,看下LoggingApplicationListener所接收的事件:
private static final Class<?>[] EVENT_TYPES = {ApplicationStartingEvent.class,ApplicationEnvironmentPreparedEvent.class,ApplicationPreparedEvent.class,ContextClosedEvent.class,ApplicationFailedEvent.class };
因此,当SpringBoot发出以上几个事件的时候,是可以回调到LoggingApplicationListener里面的,我们看下事件的回调处理:
@Overridepublic void onApplicationEvent(ApplicationEvent event) {if (event instanceof ApplicationStartingEvent) {onApplicationStartingEvent((ApplicationStartingEvent) event);}else if (event instanceof ApplicationEnvironmentPreparedEvent) {onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);}else if (event instanceof ApplicationPreparedEvent) {onApplicationPreparedEvent((ApplicationPreparedEvent) event);}else if (event instanceof ContextClosedEvent&& ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {onContextClosedEvent();}else if (event instanceof ApplicationFailedEvent) {onApplicationFailedEvent();}}
先是第一个事件就是ApplicationStartingEvent事件:
private void onApplicationStartingEvent(ApplicationStartingEvent event) {this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());this.loggingSystem.beforeInitialize();}
这里才真正开始加载日志框架,继续看下LoggingSystem#get:
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"));}
实际上是遍历SYSTEMS里面的entry,判断key代表的class是否存在,如果存在就把value代表的那个LoggingSystem给加载了,看下SYSTEMS里面都有啥:
private static final Map<String, String> SYSTEMS;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);}
这里默认添加了3个日志框架,依次是logback、log4j2和jdk的log,因为spring-boot-starter-logging默认依赖了logback,因此,logback会被初始化使用。
我们以LogbackLoggingSystem为例,看下它使用的是哪一个配置文件。当LoggingApplicationListener接收到第二个事件ApplicationEnvironmentPreparedEvent事件的时候:
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {if (this.loggingSystem == null) {this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());}initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());}
看下具体的initialize():
protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {new LoggingSystemProperties(environment).apply();this.logFile = LogFile.get(environment);if (this.logFile != null) {this.logFile.applyToSystemProperties();}this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);initializeEarlyLoggingLevel(environment);//看下这个initializeSystem(environment, this.loggingSystem, this.logFile);initializeFinalLoggingLevels(environment, this.loggingSystem);registerShutdownHookIfNecessary(environment, this.loggingSystem);}
initializeSystem():
private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);String logConfig = environment.getProperty(CONFIG_PROPERTY);if (ignoreLogConfig(logConfig)) {//如果没有手动配置system.initialize(initializationContext, null, logFile);}else {try {ResourceUtils.getURL(logConfig).openStream().close();system.initialize(initializationContext, logConfig, logFile);}catch (Exception ex) {// NOTE: We can't use the logger here to report the problemSystem.err.println("Logging system failed to initialize using configuration from '" + logConfig + "'");ex.printStackTrace(System.err);throw new IllegalStateException(ex);}}}
LogbackLoggingSystem#initialize():
AbstractLoggingSystem#initialize():
@Overridepublic void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {if (StringUtils.hasLength(configLocation)) {initializeWithSpecificConfig(initializationContext, configLocation, logFile);return;}initializeWithConventions(initializationContext, logFile);}
默认走initializeWithConventions():
private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {//配置文件查找:首先找"logback.xml"String config = getSelfInitializationConfig();if (config != null && logFile == null) {reinitialize(initializationContext);return;}//然后加后缀查找,就是"logback-spring.xml"if (config == null) {config = getSpringInitializationConfig();}if (config != null) {loadConfiguration(initializationContext, config, logFile);return;}//最后使用yml中的配置项loadDefaults(initializationContext, logFile);}
看下getSelfInitializationConfig():
protected String getSelfInitializationConfig() {//getStandardConfigLocations被子类重写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;}
LogbackLoggingSystem#getStandardConfigLocations():
public class LogbackLoggingSystem extends Slf4JLoggingSystem {@Overrideprotected String[] getStandardConfigLocations() {return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };}}
首先是查找classpath下的那几个文件,如果找不到,继续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;}
这里实际上是给配置文件添加了-spring的后缀,也就是继续查找logback-spring.xml。如果还找不到,看下loadDefaults():
@Overrideprotected 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);}
默认的配置实际上也是写在了源码里面:

以上就是完整的日志框架加载过程了。
现在再回头看下第二个问题:jul-to-slf4j和log4j-to-slf4j是干啥用的
这个比较说来话长了,java里面的日志框架很多,比较有名的:jdk自带的jul、jcl、log4j、logback等等,后来为了统一这些乱七八糟的日志出来了一个slf4j,所以现在大家一般都是使用slf4j的api来打印日志。
slf4j绑定
slf4j仅仅定义了接口,因此,需要绑定到具体的日志框架才可以打印日志出来,具体如何来做呢,引用一张slf4j官网上的图片:

具体的说可以这样:
- 1.仅仅使用slf4j-api,日志是打到了/dev/null里面,因此啥也打印不出来
- 2.slf4j-api + logback-classic:使用的是logback,因为logback本身直接实现了slf4j的api
- 3.slf4j-api + slf4j-log4j + log4j:最终是使用log4j,因为log4j本身并没有实现slf4j的接口,所以中间用slf4j-log4j桥接了一下子。
- 4.slf4j-api + slf4j-jdk + jul:最终是使用jul,中间用slf4j-jdk桥接了一下。
- 5.slf4j-api + slf4j-simple:slf4j的一个简单实现,只能把日志打印到System.err中。
- 6.slf4j-api + slf4j-nop:跟只用slf4j-api一样,啥也不打印输出。
- 7.slf4j-api + slf4j-jcl + jcl: 最终是使用jcl。
重定向
很多时候,我们的项目依赖了某一个jar,依赖包里面可能并没有使用slf4j打印日志,而是使用的log4j或者jcl打印日志,而我们的项目本身又想用slf4j,能不能把依赖包里面的日志打印重定向成我们的slf4j呢?,slf4j对这种情况也做了处理,在不修改依赖包里面的代码的情况下可以这样:

上图中说:
- 1.把jcl的jar包删掉,换上jcl-over-slf4j;log4j的jar删掉,换成log4j-over-slf4j;添加上jul-to-slf4j;然后再添加上slf4j-api 和 logback就可以在不修改打日志的代码的情况下,最终都使用logback打印日志。
- 2.把jcl的jar包删掉,换上jcl-over-slf4j;添加上jul-to-slf4j;然后再添加上slf4j-api 和slf4j-log4j 和 log4j,最终就是使用log4j打印日志。
- 3.把jcl的jar包删掉,换上jcl-over-slf4j;log4j的jar删掉,换成log4j-over-slf4j;然后再添加上slf4j-api 和 slf4j-jdk + jul,最终就是使用jul打印日志。
以上也可以看出来,jcl-over-slf4j.jar和slf4j-jcl.jar不能共存的,log4j-over-slf4j.jar和slf4j-log4j12不能共存,jul-to-slf4j和slf4j-jdk14.jar不能共存。
回到文章开头提到的那两个依赖:jul-to-slf4j和log4j-to-slf4j(log4j2)就是用来把log重定向到slf4j的。
总结一下
- 1.SpringBoot启动的时候会读取spring-boot-2.2.0.jar里面的spring.factories,拿到所有的ApplicationListener(有很多个,其中包括了LoggingApplicationListener)和SpringApplicationRunListener(只有一个,EventPublishingRunListener,它里面会使用了Spring的SimpleApplicationEventMulticaster做事件发布)。
- 2.SpringBoot启动过程中会发出很多事件,LoggingApplicationListener在就收到ApplicationStartingEvent事件的时候,开始加载日志框架。
- 3.SpringBoot内置了对logback、log4j2和jdk 日志的支持,因为默认有logback的依赖,因此默认是使用logback打印日志。
- 4.SpringBoot同时还添加了jul-to-slf4j和log4j-to-slf4j,把依赖包中使用jul和log4j2打印的日志重定向使用slf4j做日志输出。
参考文档:
http://www.slf4j.org/manual.html
http://www.slf4j.org/legacy.html
