總所周知,spring boot對各類日誌組件進行了集成,使用起來非常便捷,讓我們需要定義對應日誌框架的配置文件,比如LogBack、Log4j2等,代碼內部便可以直接使用。話不多說,接下來讓我們來領略spring這塊的奧祕吧。
目錄
logback-spring.xml的spring環境及變量
spring如何集成日誌組件
猜想肯定是spring應用啓動前已完成log組件的初始化工作?沒錯,spring boot中通過事件驅動,主要是藉助了ApplicationStartingEvent
(啓動)以及ApplicationEnvironmentPreparedEvent
(配置環境準備)來完成的。
入口在spring的SPI文件,spring-boot-2.1.3.RELEASE.jar/META-INF/spring.factories文件內容:
# 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.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
重點關注LoggingApplicationListener
,而ApplicationListener
想必就不陌生了,它的的初始化及觸發點在於spring boot的start-class SpringApplication#run
//源於package org.springframework.boot.context.logging;
public class LoggingApplicationListener implements GenericApplicationListener {
@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();
}
}
private void onApplicationStartingEvent(ApplicationStartingEvent event) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());//實例化loggingSystem
this.loggingSystem.beforeInitialize();//loggingSystem初始化操作的前置處理
}
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
}
initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());//loggingSystem初始化操作
}
private void onContextClosedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
private void onApplicationFailedEvent() {
if (this.loggingSystem != null) {
this.loggingSystem.cleanUp();
}
}
protected void initialize(ConfigurableEnvironment environment,
ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
LogFile logFile = LogFile.get(environment);//這個後面講解
if (logFile != null) {
logFile.applyToSystemProperties();
}
initializeEarlyLoggingLevel(environment);
initializeSystem(environment, this.loggingSystem, logFile);
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
}
//源於package org.springframework.boot;
public class SpringApplication {
public ConfigurableApplicationContext run(String... args) {//這個不陌生吧,spring boot的入口
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();//觸發ApplicationStartingEvent事件
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(
args);
ConfigurableEnvironment environment = prepareEnvironment(listeners,
applicationArguments);//觸發ApplicationPreparedEvent事件
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(
SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments,
printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass)
.logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
}
到這來你基本上已經可以一覽它的原貌,而這裏面的細節(功能特性)會一一展開。
LoggingSystem
LoggingSystem
是spring通用日誌組件的抽象,它支持4種類型的日誌:
-
Log:JavaLoggingSystem
-
Log4j:Log4JLoggingSystem
-
Log4j2:Log4J2LoggingSystem
-
Logback:LogbackLoggingSystem
LoggingSystem是個抽象類,內部有這幾個方法:
-
beforeInitialize,日誌系統初始化之前需要處理的事情。抽象方法,不同的日誌架構進行不同的處理
-
initialize,初始化日誌系統
-
cleanUp,日誌系統的清除工作
-
getShutdownHandler,返回一個Runnable用於當jvm退出的時候處理日誌系統關閉後需要進行的操作,默認返回null,也就是什麼都不做
-
setLogLevel,抽象方法,用於設置對應logger的級別
//源於package org.springframework.boot.logging;
public abstract class AbstractLoggingSystem extends LoggingSystem {
@Override
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {// 如果傳遞了日誌配置文件
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
//加載各種默認配置文件
initializeWithConventions(initializationContext, logFile);
}
private void initializeWithSpecificConfig(
LoggingInitializationContext initializationContext, String configLocation,
LogFile logFile) {
// 處理日誌配置文件中的佔位符
configLocation = SystemPropertyUtils.resolvePlaceholders(configLocation);
loadConfiguration(initializationContext, configLocation, logFile);
}
private void initializeWithConventions(
LoggingInitializationContext initializationContext, LogFile logFile) {
//加載classpath下的默認的配置文件
String config = getSelfInitializationConfig();
if (config != null && logFile == null) {
// self initialization has occurred, reinitialize in case of property changes
reinitialize(initializationContext);
return;
}
if (config == null) {//加載classpath下的默認的配置文件(僅包含spring)
config = getSpringInitializationConfig();
}
if (config != null) {
loadConfiguration(initializationContext, config, logFile);
return;
}
// 還是沒找到日誌配置文件的話,調用loadDefaults抽象方法加載,讓子類實現
loadDefaults(initializationContext, logFile);
}
protected String getSelfInitializationConfig() {
return findConfig(getStandardConfigLocations());
}
protected String getSpringInitializationConfig() {
return findConfig(getSpringConfigLocations());
}
//findConfig有個特點就是找到第一個存在的立即返回
private String findConfig(String[] locations) {
for (String location : locations) {
ClassPathResource resource = new ClassPathResource(location,
this.classLoader);
if (resource.exists()) {
return "classpath:" + location;
}
}
return null;
}
//將默認的配置文件中替換成-spring的配置文件
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;
}
protected abstract String[] getStandardConfigLocations();
}
//源自package org.springframework.boot.logging;
public abstract class Slf4JLoggingSystem extends AbstractLoggingSystem {
}
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
@Override
protected String[] getStandardConfigLocations() {
return new String[] { "logback-test.groovy", "logback-test.xml", "logback.groovy",
"logback.xml" };
}
}
LogFile是什麼
在spring yml配置中提供了logging.file和logging.path的配置,而它正是作用於LogFile。
//源自package org.springframework.boot.logging;
public class LogFile {
LogFile(String file, String path) {
Assert.isTrue(StringUtils.hasLength(file) || StringUtils.hasLength(path),
"File or Path must not be empty");
this.file = file;
this.path = path;
}
@Override
public String toString() {
if (StringUtils.hasLength(this.file)) {
return this.file;
}
String path = this.path;
if (!path.endsWith("/")) {
path = path + "/";
}
return StringUtils.applyRelativePath(path, "spring.log");
}
public static LogFile get(PropertyResolver propertyResolver) {
String file = propertyResolver.getProperty(FILE_PROPERTY);
String path = propertyResolver.getProperty(PATH_PROPERTY);
if (StringUtils.hasLength(file) || StringUtils.hasLength(path)) {
return new LogFile(file, path);
}
return null;
}
}
這個配置導致了調用initialize方法的時候logFile存在,這樣不止有ConsoleAppender,還有一個FileAppender,這個FileAppender對應的文件就是LogFile文件,也就是logging.file配置的日誌文件。
從上面代碼實現可以看出,我們如果配置了logging.path和logging.file,那麼生效的只有logging.file配置。
其實個人覺得LogFile沒啥用,你會脫落開源日誌組件的控制。
LoggingSystem的實例化
LoggingSystem
被實例化那個,這個很多人講的不太對,默認它取得順序是LogbackLoggingSystem
>Log4J2LoggingSystem
>JavaLoggingSystem
,如果類存在就選擇。
//源自package org.springframework.boot.logging;
public abstract class LoggingSystem {
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);
}
public static LoggingSystem get(ClassLoader classLoader) {
String loggingSystem = System.getProperty(SYSTEM_PROPERTY);
if (StringUtils.hasLength(loggingSystem)) {//你也通過org.springframework.boot.logging.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"));//取第一個
}
}
logback.xml的加載優先級
其實從上面的代碼AbstractLoggingSystem#initializeWithConventions
中已經可以看出,它默認的加載順序爲:
-
logback-test.groovy
-
logback-test.xml
-
logback.groovy
-
logback.xml
-
logback-test-spring.groovy
-
logback-test-spring.xml
-
logback-spring.groovy
-
logback-spring.xml
logback-spring.xml的spring環境及變量
很多使用logback-spring.xml的同學大多比較在意可以使用<springProperty/>和<springProfile/>
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="false">
<property name="LOG_HOME" value="/log"/>
<conversionRule conversionWord="ipandhostname" converterClass="com.yonghui.logback.IpConvert"/><!--
1. 文件的命名和加載順序
logback.xml早於application.yml加載,logback-spring.xml晚於application.yml加載
如果logback配置需要使用application.yml中的屬性,需要命名爲logback-spring.xml
2. logback使用application.yml中的屬性
使用springProperty纔可使用application.yml中的值 可以設置默認值
-->
<springProperty scope="context" name="projectName" source="spring.project.name"/>
<springProperty scope="context" name="appName" source="spring.application.name"/>
<springProperty scope="context" name="appDev" source="spring.profiles.active"/>
<springProperty scope="context" name="kafkaTopic" source="logback.kafka.topic"/>
<springProperty scope="context" name="kafkaServers" source="logback.kafka.servers"/><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.core.encoder.LayoutWrappingEncoder">
<layout class="org.apache.skywalking.apm.toolkit.log.logback.v1.x.TraceIdPatternLogbackLayout">
<!-- 時間|環境 |項目名稱 |應用名稱|錯誤級別|ip|hostname|[%thread]| %logger{50}| %msg%n -->
<pattern>%d{yyyy-MM-dd HH:mm:ss SSS}|${appDev}|${projectName}|${appName}|%-5level|%ipandhostname|[%thread]|%logger{50}|%tid|%msg%n
</pattern>
</layout>
</encoder>
</appender>
<logger name="org.apache.kafka.clients.NetworkClient" level="error"/>
<logger name="c.c.f.apollo.internals.RemoteConfigLongPollService" level="error"/><springProfile name="SIT">
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
</root>
</springProfile><springProfile name="UAT">
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ASYNC"/>
</root>
</springProfile><springProfile name="PRO">
<root level="info">
<appender-ref ref="ASYNC"/>
</root>
</springProfile>
</configuration>
它的底層實現其實也不復雜, 如果配置文件是xml,解析時藉助日誌組件的攔截(解析spring環境信息)
//源自package org.springframework.boot.logging.logback;
public class LogbackLoggingSystem extends Slf4JLoggingSystem {
//AbstractLoggingSystem#initializeWithConventions中被調用(config找到第一件事就是幹這個)
@Override
protected void loadConfiguration(LoggingInitializationContext initializationContext,
String location, LogFile logFile) {
super.loadConfiguration(initializationContext, location, logFile);
LoggerContext loggerContext = getLoggerContext();
stopAndReset(loggerContext);
try {
configureByResourceUrl(initializationContext, loggerContext,
ResourceUtils.getURL(location));//重點關注這個
}
catch (Exception ex) {
throw new IllegalStateException(
"Could not initialize Logback logging from " + location, ex);
}
List<Status> statuses = loggerContext.getStatusManager().getCopyOfStatusList();
StringBuilder errors = new StringBuilder();
for (Status status : statuses) {
if (status.getLevel() == Status.ERROR) {
errors.append((errors.length() > 0) ? String.format("%n") : "");
errors.append(status.toString());
}
}
if (errors.length() > 0) {
throw new IllegalStateException(
String.format("Logback configuration error detected: %n%s", errors));
}
}
private void configureByResourceUrl(
LoggingInitializationContext initializationContext,
LoggerContext loggerContext, URL url) throws JoranException {
if (url.toString().endsWith("xml")) {//如果是xml,加載spring環境
JoranConfigurator configurator = new SpringBootJoranConfigurator(
initializationContext);
configurator.setContext(loggerContext);
configurator.doConfigure(url);//觸發的GenericConfigurator.doConfigure(通過攔截器解析xml部分spring配置)
}
else {
new ContextInitializer(loggerContext).configureByResource(url);
}
}
}
//源自package org.springframework.boot.logging.logback;
class SpringBootJoranConfigurator extends JoranConfigurator {
private LoggingInitializationContext initializationContext;
SpringBootJoranConfigurator(LoggingInitializationContext initializationContext) {
this.initializationContext = initializationContext;
}
@Override
public void addInstanceRules(RuleStore rs) {
super.addInstanceRules(rs);
Environment environment = this.initializationContext.getEnvironment();
rs.addRule(new ElementSelector("configuration/springProperty"),
new SpringPropertyAction(environment));
rs.addRule(new ElementSelector("*/springProfile"),
new SpringProfileAction(environment));
rs.addRule(new ElementSelector("*/springProfile/*"), new NOPAction());
}
}
至此,你可能再也不會糾結於spring中日誌系統何時被加載,何時被卸載。