最近的一个项目采用Spring Boot构建,使用了log4j2记录日志;按照之前的习惯,通过-Dlog4j.configurationFile指定log4j2.xml路径,启动应用,但意外的发现日志配置竟然没有生效;
初步估计是Spring Boot对日志部分进行了某种修改,导致了这个问题,查看Spring Boot的jar包,发现spring.factories中有这么一段配置:
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
可以看到有个LoggingApplicationListener:
public class LoggingApplicationListener implements GenericApplicationListener{}
可以看到它会监听Spring的事件,关键逻辑如下:
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationStartedEvent) {
onApplicationStartedEvent((ApplicationStartedEvent) 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();
}
}
首先看看它的** onApplicationStartedEvent**方法:
private void onApplicationStartedEvent(ApplicationStartedEvent event) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
this.loggingSystem.beforeInitialize();
}
public void beforeInitialize() {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
super.beforeInitialize();
loggerContext.getConfiguration().addFilter(FILTER);
}
private LoggerContext getLoggerContext() {
return (LoggerContext) LogManager.getContext(false);
}
LogManager.getContext(false)实际上会调用Log4jContextFactory的getContext方法:
public LoggerContext getContext(final String fqcn, final ClassLoader loader, final Object externalContext,
final boolean currentContext, final URI configLocation, final String name) {
final LoggerContext ctx = selector.getContext(fqcn, loader, currentContext, configLocation);
if (externalContext != null && ctx.getExternalContext() == null) {
ctx.setExternalContext(externalContext);
}
if (name != null) {
ctx.setName(name);
}
if (ctx.getState() == LifeCycle.State.INITIALIZED) {
if (configLocation != null || name != null) {
ContextAnchor.THREAD_CONTEXT.set(ctx);
final Configuration config = ConfigurationFactory.getInstance().getConfiguration(name, configLocation);
LOGGER.debug("Starting LoggerContext[name={}] from configuration at {}", ctx.getName(), configLocation);
ctx.start(config);
ContextAnchor.THREAD_CONTEXT.remove();
} else {
ctx.start();
}
}
return ctx;
}
而ConfigurationFactory就会从log4j.configurationFile获取配置文件路径执行初始化;既然已经加载了-Dlog4j.configurationFile指定的配置文件,那为什么没最终生效呢?继续看下去:
private void onApplicationEnvironmentPreparedEvent(
ApplicationEnvironmentPreparedEvent event) {
if (this.loggingSystem == null) {
this.loggingSystem = LoggingSystem
.get(event.getSpringApplication().getClassLoader());
}
initialize(event.getEnvironment(), event.getSpringApplication().getClassLoader());
}
protected void initialize(ConfigurableEnvironment environment,
ClassLoader classLoader) {
new LoggingSystemProperties(environment).apply();
LogFile logFile = LogFile.get(environment);//读取logging.file和logging.path参数
if (logFile != null) {
logFile.applyToSystemProperties();//添加系统属性LOG_PATH、LOG_FILE
}
initializeEarlyLoggingLevel(environment); //如果在application.yml或application.properties中定义了debug或trace,则设置日志级别为DEBUG或TRACE
//关键方法,调用LoggingSystem的initialize方法下面单独说明
initializeSystem(environment, this.loggingSystem, logFile);
initializeFinalLoggingLevels(environment, this.loggingSystem);
registerShutdownHookIfNecessary(environment, this.loggingSystem);
}
Log4J2LoggingSystem的initialize方法逻辑如下:
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
LoggerContext loggerContext = getLoggerContext();
if (isAlreadyInitialized(loggerContext)) {
return;
}
loggerContext.getConfiguration().removeFilter(FILTER);
super.initialize(initializationContext, configLocation, logFile);
markAsInitialized(loggerContext);
}
继续看super.initialize(initializationContext, configLocation, logFile);实现:
public void initialize(LoggingInitializationContext initializationContext,
String configLocation, LogFile logFile) {
if (StringUtils.hasLength(configLocation)) {//如果通过logging.config指定了log4j2配置文件,则采用该配置文件
initializeWithSpecificConfig(initializationContext, configLocation, logFile);
return;
}
//采用默认配置
initializeWithConventions(initializationContext, logFile);
}
如果配置了logging.config属性,最终会通过如下代码重新配置log4j2:
protected void loadConfiguration(String location, LogFile logFile) {
Assert.notNull(location, "Location must not be null");
try {
LoggerContext ctx = getLoggerContext();
URL url = ResourceUtils.getURL(location);
ConfigurationSource source = getConfigurationSource(url);
//关键方法,采用新的配置文件重新配置log4j2
ctx.start(ConfigurationFactory.getInstance().getConfiguration(source));
}
catch (Exception ex) {
throw new IllegalStateException(
"Could not initialize Log4J2 logging from " + location, ex);
}
}
如果没有指定,会调用initializeWithConventions方法:
private void initializeWithConventions(
LoggingInitializationContext initializationContext, LogFile logFile) {
//根据支持的格式,在classpath下查找log4j2.yaml、log4j2.yml、log4j2.json、log4j2.jsn和log4j2.xml文件,如果找到则调用getLoggerContext().reconfigure()重现初始化log4j2,防止某些属性文件发生变化
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下查找log4j2-spring.yaml、log4j2-spring.yml、log4j2-spring.json、log4j2-spring.jsn和log4j2-spring.xml文件
config = getSpringInitializationConfig();
}
if (config != null) {//如果找到以spring结尾的配置文件,调用ctx.start重新配置log4j2
loadConfiguration(initializationContext, config, logFile);
return;
}
//如果前面的查找都失败了,则加载默认配置;如果定义了log日志文件路径,则加载log4j2-file.xml,否则加载log4j2.xml
loadDefaults(initializationContext, logFile);
}
在Spring boot jar包的org.springframework.boot.logging.log4j2包下面,有两个配置文件log4j2.xml和log4j2-file.xml,这是默认的log4j2配置文件,文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_PATTERN">%clr{%d{yyyy-MM-dd HH:mm:ss.SSS}}{faint} %clr{${LOG_LEVEL_PATTERN}} %clr{${sys:PID}}{magenta} %clr{---}{faint} %clr{[%15.15t]}{faint} %clr{%-40.40c{1.}}{cyan} %clr{:}{faint} %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${LOG_PATTERN}" />
</Console>
</Appenders>
<Loggers>
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.crsh.plugin" level="warn" />
<logger name="org.crsh.ssh" level="warn"/>
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration" level="warn"/>
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<logger name="org.thymeleaf" level="warn"/>
<Root level="info">
<AppenderRef ref="Console" />
</Root>
</Loggers>
</Configuration>
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Properties>
<Property name="PID">????</Property>
<Property name="LOG_EXCEPTION_CONVERSION_WORD">%xwEx</Property>
<Property name="LOG_LEVEL_PATTERN">%5p</Property>
<Property name="LOG_PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} ${LOG_LEVEL_PATTERN} ${sys:PID} --- [%t] %-40.40c{1.} : %m%n${sys:LOG_EXCEPTION_CONVERSION_WORD}</Property>
</Properties>
<Appenders>
<Console name="Console" target="SYSTEM_OUT" follow="true">
<PatternLayout pattern="${LOG_PATTERN}" />
</Console>
<RollingFile name="File" fileName="${sys:LOG_FILE}" filePattern="logs/$${date:yyyy-MM}/app-%d{yyyy-MM-dd-HH}-%i.log.gz">
<PatternLayout>
<Pattern>${LOG_PATTERN}</Pattern>
</PatternLayout>
<Policies>
<SizeBasedTriggeringPolicy size="10 MB" />
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<Logger name="org.apache.catalina.startup.DigesterFactory" level="error" />
<Logger name="org.apache.catalina.util.LifecycleBase" level="error" />
<Logger name="org.apache.coyote.http11.Http11NioProtocol" level="warn" />
<logger name="org.apache.sshd.common.util.SecurityUtils" level="warn"/>
<Logger name="org.apache.tomcat.util.net.NioSelectorPool" level="warn" />
<Logger name="org.crsh.plugin" level="warn" />
<logger name="org.crsh.ssh" level="warn"/>
<Logger name="org.eclipse.jetty.util.component.AbstractLifeCycle" level="error" />
<Logger name="org.hibernate.validator.internal.util.Version" level="warn" />
<logger name="org.springframework.boot.actuate.autoconfigure.CrshAutoConfiguration" level="warn"/>
<logger name="org.springframework.boot.actuate.endpoint.jmx" level="warn"/>
<logger name="org.thymeleaf" level="warn"/>
<Root level="info">
<AppenderRef ref="Console" />
<AppenderRef ref="File" />
</Root>
</Loggers>
</Configuration>
网友评论