spring-boot-starter-logging源码解析

37 篇文章 5 订阅

本文以2.2.0为例来说明下,首先还是打开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) {    
    //这里是获取了一个SpringApplicationRunListener
    SpringApplicationRunListeners 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:

@Override
public 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拿到的ApplicationListener
  for (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里面的,我们看下事件的回调处理:

@Override
public 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 problem
      System.err.println("Logging system failed to initialize using configuration from '" + logConfig + "'");
      ex.printStackTrace(System.err);
      throw new IllegalStateException(ex);
    }
  }
}

LogbackLoggingSystem#initialize():

AbstractLoggingSystem#initialize():


@Override
public 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 {
  @Override
  protected 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():


@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);
}

默认的配置实际上也是写在了源码里面:
在这里插入图片描述
以上就是完整的日志框架加载过程了。

现在再回头看下第二个问题: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

欢迎扫码查看更多文章:
在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值