解決spring-boot使用logback的大坑

最近在寫一個logback的kafka appender,無意中發現spring-boot在使用logback時的一個坑

用ConsoleAppender.java來舉例,假設在logback.xml中使用瞭該appender,那麼這個類的相關的初始化方法都會調兩次,如start()方法

打斷點進行debug,第一次進入start()方法如下:

可以看到所有的調用鏈(除瞭自己代碼的方法)都是logback或者slf4j相關的比較正常

當跳過該斷點時又會進入以此這個方法,看下調用鏈:

可以看到這次的初始化是由spring-boot發起的,所以這樣logback初始化一次,然後spring-boot初始化一次,一共兩次

我們現在可以將spring-boot的初始化去掉

debug代碼可以發現LoggingApplicationListener.java這個監聽器主要是用來初始化spring-boot的日志系統,現在目的將該listener在啟動之前去掉

spring-boot的啟動代碼為:

new SpringApplicationBuilder(Launcher.class).application().run(args);

在SpringApplicationBuilder.java的構造方法打斷點進行跟蹤,

進入SpringAppication.java會發現該類中的代碼:

private void initialize(Object[] sources) {
   if (sources != null && sources.length > 0) {
      this.sources.addAll(Arrays.asList(sources));
   }
   this.webEnvironment = deduceWebEnvironment();
   setInitializers((Collection) getSpringFactoriesInstances(
         ApplicationContextInitializer.class));
   setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
   this.mainApplicationClass = deduceMainApplicationClass();
}

第八行應該就是註冊監聽器的地方瞭,繼續往下跟蹤,進入以下方法:

private <T> Collection<? extends T> getSpringFactoriesInstances(Class<T> type,
      Class<?>[] parameterTypes, Object... args) {
   ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
   // Use names and ensure unique to protect against duplicates
   Set<String> names = new LinkedHashSet<String>(
         SpringFactoriesLoader.loadFactoryNames(type, classLoader));
   List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
         classLoader, args, names);
   AnnotationAwareOrderComparator.sort(instances);
   return instances;
}

繼續進入loadFactoryNames()方法,核心就在這裡瞭

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
   String factoryClassName = factoryClass.getName();
   try {
      Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
      List<String> result = new ArrayList<String>();
      while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
         String factoryClassNames = properties.getProperty(factoryClassName);
         result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
      }
      return result;
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() +
            "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex);
   }
}

FACTORIES_RESOURCE_LOCATION這個常量的值為META-INF/spring.factories,

打開該文件可以發現:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.context.web.ServerPortInfoApplicationContextInitializer
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener,\
org.springframework.boot.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.logging.LoggingApplicationListener
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor
# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer
# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

ApplicationListener應該就是我們需要修改的地方瞭,去掉org.springframework.boot.logging.LoggingApplicationListener就可以瞭,我們可以在代碼裡面覆蓋一份這塊代碼從而實現去掉這行,但是實際得再跑一遍,發現還是一樣初始化兩次

問題出在

Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
      ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));

這塊代碼是將所有的META-INF/spring.factories都讀取過來瞭然後進行合並,所以說哦這個META-INF/spring.factories隻能增加內容,但是不能去掉某些內容,沒辦法瞭隻能在代碼初始化瞭所有的listener之後再將listener去掉,

具體代碼如下(啟動spring-boot的main方法中):

SpringApplicationBuilder builder = new SpringApplicationBuilder(Launcher.class);
Set<ApplicationListener<?>> listeners = builder.application().getListeners();
for (Iterator<ApplicationListener<?>> it = listeners.iterator(); it.hasNext();) {
    ApplicationListener<?> listener = it.next();
    if (listener instanceof LoggingApplicationListener) {
        it.remove();
    }
}
builder.application().setListeners(listeners);
builder.run(args);

PS:其實log初始化兩次並無傷大雅,關鍵是遇到瞭問題總是想解決下或者瞭解下原理

以上為個人經驗,希望能給大傢一個參考,也希望大傢多多支持WalkonNet。

推薦閱讀: