写在开篇
本文主要介绍了log4j、xml方式的配置以及插件的使用。
sl4j与log4j
关系
slf4j不是具体的日志解决方案,而是一种适配器的实现方式,为我们提供一个一致的API,开发者只需要关注slf4j的api接口,而不用关心具体日志是由log4j、log4j2还是logback等日志框架实现的。
                
                log4j1
            
适配过程
上面讲到sl4j是适配层,那么它是怎么适配到日志框架的呢?我们一起通过源码来看一下。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 
 | package com.demo.log;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 public class LogDemo {
 
 private static final Logger LOGGER = LoggerFactory.getLogger(LogDemo.class);
 
 public static void main(String[] args) {
 LOGGER.info("log4j2");
 }
 }
 
 | 
LoggerFactory类中获取logger的方法。
| 12
 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
 38
 
 | public static Logger getLogger(Class<?> clazz) {Logger logger = getLogger(clazz.getName());
 
 return logger;
 }
 
 public static Logger getLogger(String name) {
 ILoggerFactory iLoggerFactory = getILoggerFactory();
 return iLoggerFactory.getLogger(name);
 }
 
 
 public static ILoggerFactory getILoggerFactory() {
 
 if (INITIALIZATION_STATE == UNINITIALIZED) {
 Class var0 = LoggerFactory.class;
 synchronized(LoggerFactory.class) {
 if (INITIALIZATION_STATE == UNINITIALIZED) {
 INITIALIZATION_STATE = ONGOING_INITIALIZATION;
 
 performInitialization();
 }
 }
 }
 
 switch (INITIALIZATION_STATE) {
 case ONGOING_INITIALIZATION:
 return SUBST_FACTORY;
 case FAILED_INITIALIZATION:
 throw new IllegalStateException("org.slf4j.LoggerFactory in failed state.");
 case SUCCESSFUL_INITIALIZATION:
 return StaticLoggerBinder.getSingleton().getLoggerFactory();
 case NOP_FALLBACK_INITIALIZATION:
 return NOP_FALLBACK_FACTORY;
 default:
 throw new IllegalStateException("Unreachable code");
 }
 }
 
 | 
初始化ILoggerFactory。
| 12
 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
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 
 | private static final void performInitialization() {bind();
 if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
 versionSanityCheck();
 }
 }
 
 private static final void bind() {
 String msg;
 try {
 Set<URL> staticLoggerBinderPathSet = null;
 if (!isAndroid()) {
 
 staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
 
 reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
 }
 
 
 StaticLoggerBinder.getSingleton();
 INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
 reportActualBinding(staticLoggerBinderPathSet);
 fixSubstituteLoggers();
 replayEvents();
 SUBST_FACTORY.clear();
 } catch (NoClassDefFoundError var2) {
 
 } catch (Exception var4) {
 
 }
 }
 
 
 static Set<URL> findPossibleStaticLoggerBinderPathSet() {
 Set<URL> staticLoggerBinderPathSet = new LinkedHashSet();
 
 try {
 
 ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
 Enumeration paths;
 if (loggerFactoryClassLoader == null) {
 
 paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
 } else {
 
 paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
 }
 
 while(paths.hasMoreElements()) {
 URL path = (URL)paths.nextElement();
 staticLoggerBinderPathSet.add(path);
 }
 } catch (IOException var4) {
 Util.report("Error getting resources from path", var4);
 }
 
 return staticLoggerBinderPathSet;
 }
 
 | 
看到这我们已经可以知道,各个日志框架是通过实现org/slf4j/impl/StaticLoggerBinder来对sl4j进行适配的。下图是log4j2实现的StaticLoggerBinder。
                
                log4j2
            
从类加载器的用法可以看出org/slf4j/impl/StaticLoggerBinder.class要和slf4j-api.jar包在同一个类加载器中,一般来说即要求放在同一路径下比较稳妥。
我们来浅窥下log4j2中对于StaticLoggerBinder的具体实现。
| 12
 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
 
 | package org.slf4j.impl;
 import org.apache.logging.slf4j.Log4jLoggerFactory;
 import org.slf4j.ILoggerFactory;
 import org.slf4j.spi.LoggerFactoryBinder;
 
 public final class StaticLoggerBinder implements LoggerFactoryBinder {
 public static String REQUESTED_API_VERSION = "1.6";
 private static final String LOGGER_FACTORY_CLASS_STR = Log4jLoggerFactory.class.getName();
 private static final StaticLoggerBinder SINGLETON = new StaticLoggerBinder();
 private final ILoggerFactory loggerFactory = new Log4jLoggerFactory();
 
 private StaticLoggerBinder() {
 }
 
 public static StaticLoggerBinder getSingleton() {
 return SINGLETON;
 }
 
 public ILoggerFactory getLoggerFactory() {
 return this.loggerFactory;
 }
 
 public String getLoggerFactoryClassStr() {
 return LOGGER_FACTORY_CLASS_STR;
 }
 }
 
 | 
我们只需要关心,log4j2是通过Log4jLoggerFactory继承了ILoggerFactory、以及Log4jLogger继承了Logger,来实现适配到slf4j的即可。
log4j2配置
关于日志我们一开始不免会关心两个问题:
1、开发时日志是怎么输出到控制台,生产环境的日志是怎么输出的磁盘文件中的?
2、日志输出的格式为什么是这样的?
那我们就需要介绍下log4j2的配置了。
log4j2.xml
log4j2支持xml、json、yaml、properties四种配置方式,不过本文将通过大家常用的xml方式来介绍。
我们通过这个简单的xml配置来介绍。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 
 | <Configuration name="ConfigTest" status="ERROR" monitorInterval="5"><Appenders>
 <SystemPropertyArbiter propertyName="env" propertyValue="dev">
 <Console name="Out">
 <PatternLayout pattern="%m%n"/>
 </Console>
 </SystemPropertyArbiter>
 <SystemPropertyArbiter propertyName="env" propertyValue="prod">
 <List name="Out">
 </List>
 </SystemPropertyArbiter>
 </Appenders>
 
 <Loggers>
 <Logger name="org.apache.test" level="trace" additivity="false">
 <AppenderRef ref="Out"/>
 </Logger>
 <Root level="error">
 <AppenderRef ref="Out"/>
 </Root>
 </Loggers>
 </Configuration>
 
 | 
可以看到,同体上来说,主要Configuration构成分为两个部分。
Appender
通过appender指定一个日志的输出方式,目前支持的Appender主要有Console、File、RollingFile、Async、Routing等 
- Console
- 将日志打印到控制台
- name 指定Appender的名字
- target SYSTEM_OUT或SYSTEM_ERR
- PatternLayout pattern指定输出格式,不设置默认为:%m%n
 
- File
- 将日志打印到文件
- name 指定Appender的名字
- filename 指定输出日志的目的文件带全路径的文件名
- PatternLayout pattern指定输出格式,不设置默认为:%m%n
 
- RollingFile
- 将日志打印到文件,文件可以滚动保存
- name 指定Appender的名字
- filename 指定输出日志的目的文件带全路径的文件名
- filepattern 指定新建日志文件的名称格式
- filePermissions 指定日志文件权限
- PatternLayout pattern指定输出格式,不设置默认为:%m%n
- Policies 指定滚动日志的策略(支持基于时间、指定文件大小等滚动策略)
 
- Routing
- 指定日志路由,可以指定规则与Appender进行绑定
- name 指定Appender的名字
- pattern 根据所有注册的Lookups进行评估并将结果用于选择路由
 
Logger
指定logger与appeder进行关联,将logger中的日志输出到appender,由appender实现日志的控制台输出或者文件记录。 
- Root
- 用来指定项目的根日志
- level 日志输出级别
- AppenderRef 用来指定该日志输出到哪个Appender
 
- Logger
- 自定义的子日志 
- level 日志输出级别
- name 用来指定该Logger所适用的类或者类所在的包全路径,继承自Root节点
- additivity 日志是否在父Logger中输出,如果为false,只在自定义的Appender中进行输出
- AppenderRef 用来指定该日志输出到哪个Appender,如果没有指定,就会默认继承自Root
 
log4j2插件
在我们的自定义插件上需要使用@Plugin表明此时一个log4j2的插件,定义插件的名称和属性。
| 1
 | @Plugin(name = "MyAppender", category = "Core", elementType = "layout", printObject = true)
 | 
log4j为我们提供了Core、Converters、KeyProviders、Lookups、TypeConverters、Developer Notes等几种插件方式,我们下面将选取几个有代表性的为大家详细说明。
Core
Core插件是指那些由配置文件中的元素直接表示的插件,例如Appender、Layout、Logger或Filter。
在介绍core插件之前,先需要了解以下三个注解。
- @PluginFactory用于提供所有选项作为方法参数的静态工厂方法,即可以将xml中配置属性传递进方法中
- @PluginAttribute 插件的属性
- @PluginElement 插件的子元素
自定义appender插件
支持自定义appender:即指定日志输出目的地。
1、需要用@PluginFactory声明createAppender方法,创建一个Appender,
2、继承AbstractAppender,实现append方法,处理日志
在使用场景上,可以应用至将分布式服务的单机上日志输出到统一的机器上。
| 12
 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
 
 | @Log4j2@Plugin(name = "TestLogAppender", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
 public class TestLogAppender extends AbstractAppender {
 
 public TestLogAppender(String name, Filter filter, Layout<? extends Serializable> layout, boolean ignoreExceptions) {
 super(name, filter, layout, ignoreExceptions);
 }
 
 @Override
 public void append(LogEvent logEvent) {
 if (logEvent == null) {
 return;
 }
 final byte[] bytes = getLayout().toByteArray(logEvent);
 String log = new String(bytes);
 rpcService.doLog(log);
 }
 
 
 @PluginFactory
 public static TestLogAppender createAppender(
 @PluginAttribute("name") String name,
 @PluginElement("Filter") final Filter filter,
 @PluginElement("Layout") Layout<? extends Serializable> layout,
 @PluginAttribute("ignoreExceptions") boolean ignoreExceptions) {
 if (StringUtils.isBlank(name)) {
 log.error("No name provided for TestLogAppender");
 return null;
 }
 if (layout == null) {
 layout = PatternLayout.createDefaultLayout();
 }
 return new TestLogAppender(name, filter, layout, ignoreExceptions);
 }
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 
 | <?xml version="1.0" encoding="UTF-8"?>
 <Configuration status="WARN" monitorInterval="3600">
 <Appenders>
 
 <Console name="console" target="SYSTEM_OUT">
 <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
 
 <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread]] %-5level %logger{36} %F:%L - %msg %ex%n"/>
 </Console>>
 
 <TestLogAppender name="testLogAppender" append="true" >
 <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
 <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS}[%-5level]%m[%C.%M:%L]" />
 </TestLogAppender>
 </Appenders>
 
 <Loggers>
 
 <root level="info">
 <appender-ref ref="console"/>
 <appender-ref ref="testLogAppender" />
 </root>
 </Loggers>
 </Configuration>
 
 | 
自定义layout插件
支持自定义layout:即负责对输出日志格式化。
1、需要用@PluginFactory声明createAppender方法,创建一个Appender
2、继承AbstractAppender,实现toSerializable方法,处理日志
在使用场景上,可以替换日志中的敏感信息。
| 12
 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
 38
 39
 40
 41
 42
 
 | @Plugin(name = "Log4jEncodeLayout", category = Node.CATEGORY, elementType = Layout.ELEMENT_TYPE, printObject = true)public class Log4jEncodeLayout extends AbstractStringLayout {
 
 
 
 
 private final static Pattern PHONE_PATTERN = Pattern.compile("(?<![0-9a-zA-Z])1[345789]\d{9}(?![0-9a-zA-Z])");
 
 private PatternLayout patternLayout;
 
 protected Log4jEncodeLayout(Charset charset, String pattern) {
 
 super(charset);
 
 patternLayout = PatternLayout.newBuilder().withPattern(pattern).build();
 }
 
 @Override
 public String toSerializable(LogEvent event) {
 
 String message = patternLayout.toSerializable(event);
 
 Matcher match = PHONE_PATTERN.matcher(message);
 
 StringBuffer sb = new StringBuffer();
 while (match.find()) {
 match.appendReplacement(sb, "***");
 }
 match.appendTail(sb);
 
 
 return sb.toString();
 }
 
 
 @PluginFactory
 public static Layout createLayout(
 @PluginAttribute(value = "pattern") final String pattern,
 @PluginAttribute(value = "charset") final Charset charset) {
 return new Log4jEncodeLayout(charset, pattern);
 }
 }
 
 | 
| 12
 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
 38
 39
 40
 41
 42
 43
 44
 45
 46
 
 | <?xml version="1.0" encoding="UTF-8"?><Configuration status="INFO" name="XMLConfigTest" packages="org.apache.logging.log4j.test,com.zyx.demo">
 <Properties>
 <Property name="PATTERN">
 %d{yyyy-MM-dd HH:mm:ss SSS} [%p] [c=%c{1}] [%thread] %m%n
 </Property>
 <property name="MODULE_NAME">log4j2-demo</property>
 <property name="LOG_HOME">/data</property>
 </Properties>
 
 <Appenders>
 <Console name="STDOUT">
 
 
 
 
 <Log4jEncodeLayout pattern="${PATTERN}" charset="UTF-8"/>
 </Console>
 <RollingFile name="ROLLINGFILE" fileName="${LOG_HOME}/${MODULE_NAME}.log"
 filePattern="${LOG_HOME}/log/${MODULE_NAME}-%d{yyyy-MM-dd}-%i.log.gz">
 
 
 
 <Log4jEncodeLayout pattern="[${MODULE_NAME}] %d{yyyy-MM-dd HH:mm:ss SSS} [%p] [c=%c{1}] [%thread] %m%n" charset="UTF-8"/>
 <Policies>
 <TimeBasedTriggeringPolicy modulate="true"
 interval="1" />
 <SizeBasedTriggeringPolicy size="100MB"/>
 <CronTriggeringPolicy schedule="0 0 * * * ?"/>
 </Policies>
 <DefaultRolloverStrategy max="100">
 <Delete basePath="${LOG_HOME}" maxDepth="3">
 <IfFileName glob="*/${MODULE_NAME}-*.log.gz"/>
 <IfLastModified age="30d"/>
 </Delete>
 </DefaultRolloverStrategy>
 </RollingFile>
 </Appenders>
 
 <Loggers>
 <Root level="INFO">
 <AppenderRef ref="STDOUT"/>
 <AppenderRef ref="ROLLINGFILE"/>
 </Root>
 </Loggers>
 </Configuration>
 
 | 
Lookups
Lookups自定义插件支持对属性的key进行查找功能。而且自定义操作过程也很简单,只需要实现StrLookup,实现lookup方法。
在下面这个不同线程打印日志例子中我们看到Lookups的简单应用。
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 
 | @Plugin(name = "threadName", category = StrLookup.CATEGORY)public class ThreadName implements StrLookup {
 
 @Override
 public String lookup(String key) {
 return Thread.currentThread().getName();
 }
 
 @Override
 public String lookup(LogEvent event, String key) {
 return event.getThreadName() == null ? Thread.currentThread().getName()
 : event.getThreadName();
 }
 }
 
 | 
| 12
 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
 
 | <?xml version="1.0" encoding="UTF-8"?><Configuration name="log-demo-config" status="error" monitorInterval="10">
 <Appenders>
 
 <Console name="Console" target="SYSTEM_OUT">
 <PatternLayout pattern="[%d{yyyy-MM-dd;HH:mm:ss.SSS Z}] [%-5p] [%t] [%c] %m%n"></PatternLayout>
 </Console>
 <Routing name="Routing">
 <Routes pattern="$${threadName:threadName}">
 <Route>
 <RollingFile name="RollingFile-${threadName:threadName}"
 fileName="export\log\thread-${threadName:threadName}.log"
 filePattern="export\log\thread-${threadName:threadName}-%i.log">
 <PatternLayout pattern="[%d{yyyy-MM-dd HH:mm:ss,SSS Z}] [%-5p] [%t] [%c %L] %m%n"/>
 <Policies>
 <SizeBasedTriggeringPolicy size="10 MB" />
 </Policies>
 <DefaultRolloverStrategy max="10"/>
 </RollingFile>
 </Route>
 </Routes>
 </Routing>
 </Appenders>
 <Loggers>
 <Logger name="com.demo.log" level="INFO" additivity="false">
 <AppenderRef ref="Routing"></AppenderRef>
 </Logger>
 </Loggers>
 </Configuration>
 
 | 
巨人的肩膀:
https://logging.apache.org/log4j/2.x/
https://blog.csdn.net/numb_zl/category_11244831.html
https://blog.csdn.net/huangjinjin520/article/details/120600251
https://blog.csdn.net/zyx1260168395/article/details/126539475
         
        
    
    
        
    Last updated: