前言
Log4j 日志框架我们经常会使用到,最近,我就遇到了一个与日志配置相关的问题。简单来说,就是在原来日志配置的基础上,指定类的日志打印到指定的日志文件中。
这样讲述可能不是那么好理解,且听我从需求来源讲起。
一、扩展配置的需求来源
我们的项目中使用的是 Log4j2 日志框架,日志配置log4j.yml
是这样的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| Configuration: status: warn Appenders: Console: name: Console target: SYSTEM_OUT RollingFile: - name: ROLLING_FILE Loggers: Root: level: info AppenderRef: - ref: Console - ref: ROLLING_FILE Logger: - name: com.myproject level: info
|
配置很简单,只是一个滚动日志文件和控制台的输出。现在来了这么一个需求:要把项目的 HTTP 接口访问日志单独打印到一个日志文件logs/access.log
中,这个功能由配置开关casslog.accessLogEnabled
决定是否开启。
说做就做,我立马把原来的log4j.yml
文件改成log4j_with_accesslog.yml
,并添加了访问日志的Appender
:ACCESS_LOG
,如下配置所示。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| Configuration: status: warn Appenders: Console: name: Console target: SYSTEM_OUT RollingFile: - name: ROLLING_FILE - name: ACCESS_LOG fileName: logs/access.log Loggers: Root: level: info AppenderRef: - ref: Console - ref: ROLLING_FILE Logger: - name: com.myproject level: info - name: com.myproject.commons.AccessLog level: trace additivity: false AppenderRef: - ref: Console - ref: ACCESS_LOG
|
上面配置注释中【新增的配置开始(1)】和【新增的配置开始(2)】就是添加的配置内容。功能开关是下面这样实现的,在项目启动时做判断。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import org.springframework.boot.logging.log4j2.Log4J2LoggingSystem;
public class MyProjectLoggingSystem extends Log4J2LoggingSystem {
static final boolean accessLogEnabled = Boolean.parseBoolean(System.getProperty("casslog.accessLogEnabled", "true"));
@Override protected String[] getStandardConfigLocations() { if (accessLogEnabled) { return new String[]{"casslog_with_accesslog.yml"}; } return new String[]{"casslog.yml"}; } }
|
这样功能就实现了,程序也确实可以运行。但是总感觉不够优雅,如果有上百个项目都要加上这个功能,这些项目的日志配置文件都要改,想想都崩溃。
二、看看开源项目 Nacos 的实现
使用过 Nacos 的朋友可能知道,Nacos 的配置模块与服务发现模块是两个功能,日志也是分开的。具体通过nacos-client.jar
中的nacos-log4j2.xml
就可以看出来。

注意本文 Nacos 源码版本是nacos-client 1.4.1
。
nacos-log4j2.xml
我做了精简,内容如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| <Configuration status="WARN"> <Appenders> <RollingFile name="CONFIG_LOG_FILE" fileName="${sys:JM.LOG.PATH}/nacos/config.log" filePattern="${sys:JM.LOG.PATH}/nacos/config.log.%d{yyyy-MM-dd}.%i"> </RollingFile> <RollingFile name="NAMING_LOG_FILE" fileName="${sys:JM.LOG.PATH}/nacos/naming.log" filePattern="${sys:JM.LOG.PATH}/nacos/naming.log.%d{yyyy-MM-dd}.%i"> </RollingFile> </Appenders> <Loggers> <Logger name="com.alibaba.nacos.client.config" level="${sys:com.alibaba.nacos.config.log.level:-info}" additivity="false"> <AppenderRef ref="CONFIG_LOG_FILE"/> </Logger> <Logger name="com.alibaba.nacos.client.naming" level="${sys:com.alibaba.nacos.naming.log.level:-info}" additivity="false"> <AppenderRef ref="NAMING_LOG_FILE"/> </Logger> </Loggers> </Configuration>
|
通过以上日志配置可以看到,Nacos 将包名为com.alibaba.nacos.client.config
的类的日志输出到${sys:JM.LOG.PATH}/nacos/config.log
文件中,将包名为com.alibaba.nacos.client.naming
的类的日志输出到${sys:JM.LOG.PATH}/nacos/naming.log
文件中。${sys:JM.LOG.PATH}
默认配置的路径就是用户目录。
接下来,我们看看 Nacos 是如何将日志配置加载进应用程序的。(实现代码请自行赏析)
1 2 3 4 5 6 7 8 9
| import static org.slf4j.LoggerFactory.getLogger;
public class LogUtils { public static final Logger NAMING_LOGGER; static { NacosLogging.getInstance().loadConfiguration(); NAMING_LOGGER = getLogger("com.alibaba.nacos.client.naming"); } }
|
1 2 3 4 5 6 7 8 9
| public class NacosLogging { private AbstractNacosLogging nacosLogging; public void loadConfiguration() { try { nacosLogging.loadConfiguration(); } } }
|
1 2 3
| public abstract class AbstractNacosLogging { public abstract void loadConfiguration(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class Log4J2NacosLogging extends AbstractNacosLogging { private final String location = getLocation("classpath:nacos-log4j2.xml"); @Override public void loadConfiguration() { final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); final Configuration contextConfiguration = loggerContext.getConfiguration(); Configuration configuration = loadConfiguration(loggerContext, location); configuration.start(); Map<String, Appender> appenders = configuration.getAppenders(); for (Appender appender : appenders.values()) { contextConfiguration.addAppender(appender); } Map<String, LoggerConfig> loggers = configuration.getLoggers(); for (String name : loggers.keySet()) { if (name.startsWith(NACOS_LOGGER_PREFIX)) { contextConfiguration.addLogger(name, loggers.get(name)); } } loggerContext.updateLoggers(); } }
|
总结来说,就是先将扩展配置(即nacos-log4j2.xml
)转化成LoggerConfig
对象;然后将LoggerConfig
实例添加到应用的日志配置上下文contextConfiguration
中;最后更新应用的Loggers
。
三、即学即用
我们就把扩展日志当成一个对象,比如这里的「访问日志」,Nacos 中的「配置模块日志」都可以称为扩展日志。我们先来编写扩展日志的抽象AbstractLogExtend
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| @Slf4j public abstract class AbstractLogExtend { public void loadConfiguration() { final LoggerContext loggerContext = (LoggerContext) LogManager.getContext(false); final Configuration contextConfiguration = loggerContext.getConfiguration();
Configuration configurationExtend = loadConfiguration(loggerContext); configurationExtend.start();
Map<String, Appender> appenders = configurationExtend.getAppenders(); for (Appender appender : appenders.values()) { addAppender(contextConfiguration, appender); } Map<String, LoggerConfig> loggersExtend = configurationExtend.getLoggers(); loggersExtend.forEach((loggerName, loggerConfig) -> addLogger(contextConfiguration, loggerName, loggerConfig) );
loggerContext.updateLoggers(); } private Configuration loadConfiguration(LoggerContext loggerContext) { try { URL url = ResourceUtils.getResourceUrl(logConfig()); ConfigurationSource source = getConfigurationSource(url); return ConfigurationFactory.getInstance().getConfiguration(loggerContext, source); } catch (Exception e) { throw new IllegalStateException("Could not initialize Log4J2 logging from " + logConfig(), e); } }
public abstract String logConfig(); }
|
AbstractLogExtend
定义了两个方法,分别是:
- loadConfiguration():加载扩展日志配置;
- logConfig():扩展日志配置文件的路径;
然后我们把这些扩展日志加载进应用中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class LogExtendInitializer { private final List<AbstractLogExtend> cassLogExtends; @PostConstruct public void init() { cassLogExtends.forEach(cassLogExtend -> { try { cassLogExtend.loadConfiguration(); } }); } }
|
到这里,基础类代码写好了。下面我们回到文章开头的需求,来看看如何实现。
首先配置访问日志accesslog-log4j.xml
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| <Configuration status="WARN"> <Appenders> <RollingFile name="ACCESS_LOG" fileName="logs/access.log" filePattern="logs/$${date:yyyy-MM}/access-%d{yyyy-MM-dd}-%i.log.gz"> </RollingFile> </Appenders>
<Loggers> <Root level="INFO"/> <Logger name="com.myproject.commons.AccessLog" level="trace" additivity="false"> <AppenderRef ref="Console"/> <AppenderRef ref="ACCESS_LOG"/> </Logger> </Loggers> </Configuration>
|
我这里将accesslog-log4j.xml
放在了类包下。

接着就是配置accesslog-log4j.xml
的文件的路径,这里我把「访问日志」定义成了对象AccessLogConfigExtend
。
1 2 3 4 5 6 7 8
| public class AccessLogConfigExtend extends AbstractLogExtend {
@Override public String logConfig() { return "classpath:com/github/open/casslog/accesslog/accesslog-log4j.xml"; }
}
|
这样访问日志就配置好了,也可以将访问日志封装成基础jar
包供其他项目使用,这样其他项目就不需要重复配置了。
对于配置开关,可以使用@Conditional
来实现,具体如下。
1 2 3 4 5 6 7 8 9 10
| @Configuration @ConditionalOnProperty(value = "casslog.accessLogEnabled") public class AccessLogAutoConfiguration {
@Bean public AccessLogConfigExtend accessLogConfigExtend() { return new AccessLogConfigExtend(); }
}
|
这样实现,确实优雅了很多!
小结
本案例是我之前在做日志组件实现的一个功能,源码放在了我的 Github 上:https://github.com/studeyang/casslog