官网日志文档:https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-logging

    本文以2.2.0为例来说明下,首先还是打开spring-boot-starter-logging.jar包,不出所料,空空如也:

    SpringBoot日志源码解析 - 图1

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

    SpringBoot日志源码解析 - 图2

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

    SpringBoot日志源码解析 - 图3

    依赖了logback,同时还依赖了jul-to-slf4j和log4j-to-slf4j,我们知道logback是springboot默认使用的日志框架,那么后面这两个jar又是干啥的的?

    我们先来看第一个问题:日志框架是啥时候加载的
    我们打开spring-boot-2.2.0.jar,这里面也有一个spring.factories:

    SpringBoot日志源码解析 - 图4

    就是通过这个ApplicationListener来加载的,我们跟一下代码:
    从main函数入口开始:

    1. public static void main(String[] args) {
    2. SpringApplication.run(DemoApplication.class, args);
    3. }
    4. public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    5. return run(new Class<?>[] { primarySource }, args);
    6. }
    7. public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    8. return new SpringApplication(primarySources).run(args);
    9. }

    看下SpringApplication的构造函数:

    1. public SpringApplication(Class<?>... primarySources) {
    2. this(null, primarySources);
    3. }
    4. public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    5. this.resourceLoader = resourceLoader;
    6. Assert.notNull(primarySources, "PrimarySources must not be null");
    7. this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    8. this.webApplicationType = WebApplicationType.deduceFromClasspath();
    9. setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    10. setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    11. this.mainApplicationClass = deduceMainApplicationClass();
    12. }
    13. public void setListeners(Collection<? extends ApplicationListener<?>> listeners) {
    14. this.listeners = new ArrayList<>(listeners);
    15. }

    这里面有一个setListeners(),这里就是读取的spring.factories里面所有的ApplicationListener,然后保存到listeners成员变量里面去备用。
    继续看下run()方法:

    1. public ConfigurableApplicationContext run(String... args) {
    2. //这里是获取了一个SpringApplicationRunListener
    3. SpringApplicationRunListeners listeners = getRunListeners(args);
    4. listeners.starting();
    5. 。。。
    6. return context;
    7. }

    首先看下getRunListeners(args):

    1. private SpringApplicationRunListeners getRunListeners(String[] args) {
    2. Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
    3. return new SpringApplicationRunListeners(logger,
    4. getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
    5. }

    还是读取的spring.factories:

    SpringBoot日志源码解析 - 图5

    默认值配置了唯一的一个EventPublishingRunListener,很显然是来做事件发布的,因为此时Spring环境还没有构建出来,Spring的那一套事件机制还无法使用,SpringBoot只好自己又搞了一个。这里拿到了EventPublishingRunListener以后,然后又封装进了SpringApplicationRunListeners里面,同时还传进去一个log:

    1. class SpringApplicationRunListeners {
    2. private final Log log;
    3. private final List<SpringApplicationRunListener> listeners;
    4. SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
    5. this.log = log;
    6. this.listeners = new ArrayList<>(listeners);
    7. }

    注意这里的logger此时还是org.apache.commons.logging这个包下面的log。SpringBoot在启动阶段的所有的事件都是通过这个SpringApplicationRunListeners来进行发布的,我们随便找一个事件看一下:

    1. public class SpringApplicationRunListeners {
    2. void starting() {
    3. for (SpringApplicationRunListener listener : this.listeners) {
    4. listener.starting();
    5. }
    6. }
    7. }

    其实就是把事件转发给了初始化时候放进去的EventPublishingRunListener,看下EventPublishingRunListener#starting:

    1. @Override
    2. public void starting() {
    3. this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
    4. }

    首先看下initialMulticaster是什么玩意,看下构造函数:

    1. public EventPublishingRunListener(SpringApplication application, String[] args) {
    2. this.application = application;
    3. this.args = args;
    4. //这个的类型和Spring里面的那个Multicaster是一样的
    5. this.initialMulticaster = new SimpleApplicationEventMulticaster();
    6. //这里是一开始就从spring.factories拿到的ApplicationListener
    7. for (ApplicationListener<?> listener : application.getListeners()) {
    8. this.initialMulticaster.addApplicationListener(listener);
    9. }
    10. }

    这里的initialMulticaster实际上就是使用的Spring里面的那个,然后把一开始从spring.factories中拿到的所有的ApplicationListener注册到了initialMulticaster里面,显然这里面也包括了我们今天要说的主角LoggingApplicationListener。当initialMulticaster发布事件的时候,就可以根据事件的类型回调不同的ApplicationListener,看下LoggingApplicationListener所接收的事件:

    1. private static final Class<?>[] EVENT_TYPES = {
    2. ApplicationStartingEvent.class,
    3. ApplicationEnvironmentPreparedEvent.class,
    4. ApplicationPreparedEvent.class,
    5. ContextClosedEvent.class,
    6. ApplicationFailedEvent.class };

    因此,当SpringBoot发出以上几个事件的时候,是可以回调到LoggingApplicationListener里面的,我们看下事件的回调处理:

    1. @Override
    2. public void onApplicationEvent(ApplicationEvent event) {
    3. if (event instanceof ApplicationStartingEvent) {
    4. onApplicationStartingEvent((ApplicationStartingEvent) event);
    5. }
    6. else if (event instanceof ApplicationEnvironmentPreparedEvent) {
    7. onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
    8. }
    9. else if (event instanceof ApplicationPreparedEvent) {
    10. onApplicationPreparedEvent((ApplicationPreparedEvent) event);
    11. }
    12. else if (event instanceof ContextClosedEvent
    13. && ((ContextClosedEvent) event).getApplicationContext().getParent() == null) {
    14. onContextClosedEvent();
    15. }
    16. else if (event instanceof ApplicationFailedEvent) {
    17. onApplicationFailedEvent();
    18. }
    19. }

    先是第一个事件就是ApplicationStartingEvent事件:

    1. private void onApplicationStartingEvent(ApplicationStartingEvent event) {
    2. this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    3. this.loggingSystem.beforeInitialize();
    4. }

    这里才真正开始加载日志框架,继续看下LoggingSystem#get:

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

    实际上是遍历SYSTEMS里面的entry,判断key代表的class是否存在,如果存在就把value代表的那个LoggingSystem给加载了,看下SYSTEMS里面都有啥:

    1. private static final Map<String, String> SYSTEMS;
    2. static {
    3. Map<String, String> systems = new LinkedHashMap<>();
    4. systems.put("ch.qos.logback.core.Appender", "org.springframework.boot.logging.logback.LogbackLoggingSystem");
    5. systems.put("org.apache.logging.log4j.core.impl.Log4jContextFactory","org.springframework.boot.logging.log4j2.Log4J2LoggingSystem");
    6. systems.put("java.util.logging.LogManager", "org.springframework.boot.logging.java.JavaLoggingSystem");
    7. SYSTEMS = Collections.unmodifiableMap(systems);
    8. }

    这里默认添加了3个日志框架,依次是logback、log4j2和jdk的log,因为spring-boot-starter-logging默认依赖了logback,因此,logback会被初始化使用。

    我们以LogbackLoggingSystem为例,看下它使用的是哪一个配置文件。当LoggingApplicationListener接收到第二个事件ApplicationEnvironmentPreparedEvent事件的时候:

    1. private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
    2. if (this.loggingSystem == null) {
    3. this.loggingSystem = LoggingSystem.get(event.getSpringApplication().getClassLoader());
    4. }
    5. initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
    6. }

    看下具体的initialize():

    1. protected void initialize(ConfigurableEnvironment environment, ClassLoader classLoader) {
    2. new LoggingSystemProperties(environment).apply();
    3. this.logFile = LogFile.get(environment);
    4. if (this.logFile != null) {
    5. this.logFile.applyToSystemProperties();
    6. }
    7. this.loggerGroups = new LoggerGroups(DEFAULT_GROUP_LOGGERS);
    8. initializeEarlyLoggingLevel(environment);
    9. //看下这个
    10. initializeSystem(environment, this.loggingSystem, this.logFile);
    11. initializeFinalLoggingLevels(environment, this.loggingSystem);
    12. registerShutdownHookIfNecessary(environment, this.loggingSystem);
    13. }

    initializeSystem():

    1. private void initializeSystem(ConfigurableEnvironment environment, LoggingSystem system, LogFile logFile) {
    2. LoggingInitializationContext initializationContext = new LoggingInitializationContext(environment);
    3. String logConfig = environment.getProperty(CONFIG_PROPERTY);
    4. if (ignoreLogConfig(logConfig)) {
    5. //如果没有手动配置
    6. system.initialize(initializationContext, null, logFile);
    7. }
    8. else {
    9. try {
    10. ResourceUtils.getURL(logConfig).openStream().close();
    11. system.initialize(initializationContext, logConfig, logFile);
    12. }
    13. catch (Exception ex) {
    14. // NOTE: We can't use the logger here to report the problem
    15. System.err.println("Logging system failed to initialize using configuration from '" + logConfig + "'");
    16. ex.printStackTrace(System.err);
    17. throw new IllegalStateException(ex);
    18. }
    19. }
    20. }

    LogbackLoggingSystem#initialize():

    AbstractLoggingSystem#initialize():

    1. @Override
    2. public void initialize(LoggingInitializationContext initializationContext, String configLocation, LogFile logFile) {
    3. if (StringUtils.hasLength(configLocation)) {
    4. initializeWithSpecificConfig(initializationContext, configLocation, logFile);
    5. return;
    6. }
    7. initializeWithConventions(initializationContext, logFile);
    8. }

    默认走initializeWithConventions():

    1. private void initializeWithConventions(LoggingInitializationContext initializationContext, LogFile logFile) {
    2. //配置文件查找:首先找"logback.xml"
    3. String config = getSelfInitializationConfig();
    4. if (config != null && logFile == null) {
    5. reinitialize(initializationContext);
    6. return;
    7. }
    8. //然后加后缀查找,就是"logback-spring.xml"
    9. if (config == null) {
    10. config = getSpringInitializationConfig();
    11. }
    12. if (config != null) {
    13. loadConfiguration(initializationContext, config, logFile);
    14. return;
    15. }
    16. //最后使用yml中的配置项
    17. loadDefaults(initializationContext, logFile);
    18. }

    看下getSelfInitializationConfig():

    1. protected String getSelfInitializationConfig() {
    2. //getStandardConfigLocations被子类重写
    3. return findConfig(getStandardConfigLocations());
    4. }
    5. private String findConfig(String[] locations) {
    6. for (String location : locations) {
    7. ClassPathResource resource = new ClassPathResource(location, this.classLoader);
    8. if (resource.exists()) {
    9. return "classpath:" + location;
    10. }
    11. }
    12. return null;
    13. }

    LogbackLoggingSystem#getStandardConfigLocations():

    1. public class LogbackLoggingSystem extends Slf4JLoggingSystem {
    2. @Override
    3. protected String[] getStandardConfigLocations() {
    4. return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy", "logback.xml" };
    5. }
    6. }

    首先是查找classpath下的那几个文件,如果找不到,继续getSpringInitializationConfig():

    1. protected String getSpringInitializationConfig() {
    2. return findConfig(getSpringConfigLocations());
    3. }
    4. protected String[] getSpringConfigLocations() {
    5. String[] locations = getStandardConfigLocations();
    6. for (int i = 0; i < locations.length; i++) {
    7. String extension = StringUtils.getFilenameExtension(locations[i]);
    8. locations[i] = locations[i].substring(0, locations[i].length() - extension.length() - 1) + "-spring."
    9. + extension;
    10. }
    11. return locations;
    12. }

    这里实际上是给配置文件添加了-spring的后缀,也就是继续查找logback-spring.xml。如果还找不到,看下loadDefaults():

    1. @Override
    2. protected void loadDefaults(LoggingInitializationContext initializationContext, LogFile logFile) {
    3. LoggerContext context = getLoggerContext();
    4. stopAndReset(context);
    5. boolean debug = Boolean.getBoolean("logback.debug");
    6. if (debug) {
    7. StatusListenerConfigHelper.addOnConsoleListenerInstance(context, new OnConsoleStatusListener());
    8. }
    9. LogbackConfigurator configurator = debug ? new DebugLogbackConfigurator(context)
    10. : new LogbackConfigurator(context);
    11. Environment environment = initializationContext.getEnvironment();
    12. context.putProperty(LoggingSystemProperties.LOG_LEVEL_PATTERN,
    13. environment.resolvePlaceholders("${logging.pattern.level:${LOG_LEVEL_PATTERN:%5p}}"));
    14. context.putProperty(LoggingSystemProperties.LOG_DATEFORMAT_PATTERN, environment.resolvePlaceholders(
    15. "${logging.pattern.dateformat:${LOG_DATEFORMAT_PATTERN:yyyy-MM-dd HH:mm:ss.SSS}}"));
    16. context.putProperty(LoggingSystemProperties.ROLLING_FILE_NAME_PATTERN, environment
    17. .resolvePlaceholders("${logging.pattern.rolling-file-name:${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz}"));
    18. new DefaultLogbackConfiguration(initializationContext, logFile).apply(configurator);
    19. context.setPackagingDataEnabled(true);
    20. }

    默认的配置实际上也是写在了源码里面:

    SpringBoot日志源码解析 - 图6

    以上就是完整的日志框架加载过程了。

    现在再回头看下第二个问题:jul-to-slf4j和log4j-to-slf4j是干啥用的

    这个比较说来话长了,java里面的日志框架很多,比较有名的:jdk自带的jul、jcl、log4j、logback等等,后来为了统一这些乱七八糟的日志出来了一个slf4j,所以现在大家一般都是使用slf4j的api来打印日志。

    slf4j绑定

    slf4j仅仅定义了接口,因此,需要绑定到具体的日志框架才可以打印日志出来,具体如何来做呢,引用一张slf4j官网上的图片:

    SpringBoot日志源码解析 - 图7

    具体的说可以这样:

    • 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对这种情况也做了处理,在不修改依赖包里面的代码的情况下可以这样:

    SpringBoot日志源码解析 - 图8

    上图中说:

    • 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