本文以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
歡迎掃碼檢視更多文章: