解決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。
推薦閱讀:
- Spring中的spring.factories文件用法(Spring如何加載第三方Bean)
- Spring Boot 項目啟動失敗的解決方案
- springboot自動配置原理以及spring.factories文件的作用詳解
- SpringBoot自動配置深入探究實現原理
- Spring Boot 分層打包 Docker 鏡像實踐及分析(推薦)