天天看點

spring-boot-starter-logging源碼解析

本文以2.2.0為例來說明下,首先還是打開jar包,不出所料,空空如也:

spring-boot-starter-logging源碼解析

然後打開autoconfigure的jar包,找到spring.factories,搜尋logging:

spring-boot-starter-logging源碼解析

打開ConditionEvaluationReportLoggingListener,這裡也沒有配置使用哪一個log架構啊,沒辦法,隻好看一下它的依賴:

spring-boot-starter-logging源碼解析

依賴了logback,同時還依賴了jul-to-slf4j和log4j-to-slf4j,我們知道logback是springboot預設使用的日志架構,那麼後面這兩個jar又是幹啥的的?

我們先來看第一個問題:日志架構是啥時候加載的

我們打開spring-boot-2.2.0.jar,這裡面也有一個spring.factories:

spring-boot-starter-logging源碼解析

就是通過這個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:

spring-boot-starter-logging源碼解析

預設值配置了唯一的一個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);
}
           

預設的配置實際上也是寫在了源碼裡面:

spring-boot-starter-logging源碼解析

以上就是完整的日志架構加載過程了。

現在再回頭看下第二個問題:jul-to-slf4j和log4j-to-slf4j是幹啥用的

這個比較說來話長了,java裡面的日志架構很多,比較有名的:jdk自帶的jul、jcl、log4j、logback等等,後來為了統一這些亂七八糟的日志出來了一個slf4j,是以現在大家一般都是使用slf4j的api來列印日志。

slf4j綁定

slf4j僅僅定義了接口,是以,需要綁定到具體的日志架構才可以列印日志出來,具體如何來做呢,引用一張slf4j官網上的圖檔:

spring-boot-starter-logging源碼解析

具體的說可以這樣:

  • 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對這種情況也做了處理,在不修改依賴包裡面的代碼的情況下可以這樣:

spring-boot-starter-logging源碼解析

上圖中說:

  • 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

歡迎掃碼檢視更多文章:

spring-boot-starter-logging源碼解析