写在开篇

本文主要介绍了log4j、xml方式的配置以及插件的使用。

sl4j与log4j

关系

slf4j不是具体的日志解决方案,而是一种适配器的实现方式,为我们提供一个一致的API,开发者只需要关注slf4j的api接口,而不用关心具体日志是由log4j、log4j2还是logback等日志框架实现的。

log4j1
log4j1

适配过程

上面讲到sl4j是适配层,那么它是怎么适配到日志框架的呢?我们一起通过源码来看一下。

1
2
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 {
// 调用slf4j的api,首先我们需要获它的logger
private static final Logger LOGGER = LoggerFactory.getLogger(LogDemo.class);

public static void main(String[] args) {
LOGGER.info("log4j2");
}
}

LoggerFactory类中获取logger的方法。

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
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);
}

// 获取ILoggerFactory
public static ILoggerFactory getILoggerFactory() {
// 双重校验锁创建单例的LoggerFactory
if (INITIALIZATION_STATE == UNINITIALIZED) {
Class var0 = LoggerFactory.class;
synchronized(LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 初始化ILoggerFactory的核心方法
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。

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
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()) {
// 找出绑定的日志的path,即StaticLoggerBinder.class文件
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
// 我们只需要一个实现的日志框架,如果有多个要上报
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}

// 实例化StaticLoggerBinder
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError var2) {
// ...
} catch (Exception var4) {
// ...
}
}

// 找到所有的StaticLoggerBinder
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet();

try {
// 获取LoggerFactory,即slf4j-api的类加载器
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration paths;
if (loggerFactoryClassLoader == null) {
// 说明是由Bootstrap Classloader加载的,则转为App Classloader去加载
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
// 用slf4j的Classloader去加载
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
log4j2

从类加载器的用法可以看出org/slf4j/impl/StaticLoggerBinder.class要和slf4j-api.jar包在同一个类加载器中,一般来说即要求放在同一路径下比较稳妥。

我们来浅窥下log4j2中对于StaticLoggerBinder的具体实现。

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
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配置来介绍。

1
2
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主要有ConsoleFileRollingFileAsyncRouting

  • 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方法,处理日志

在使用场景上,可以应用至将分布式服务的单机上日志输出到统一的机器上。

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
@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);
}
}
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
<?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方法,处理日志

在使用场景上,可以替换日志中的敏感信息。

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
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 = PatternLayout.newBuilder().withPattern(pattern).build();
}

@Override
public String toSerializable(LogEvent event) {
// 调用原本的 toSerializable 方法,获取到原本要输出的日志
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);
}
}

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
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">
<!--<PatternLayout>-->
<!-- <pattern>${PATTERN}</pattern>-->
<!--</PatternLayout>-->
<!--将原先的日志输出替换为自定义的日志输出appender插件-->
<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">
<!--<PatternLayout-->
<!--pattern="[${MODULE_NAME}] %d{yyyy-MM-dd HH:mm:ss SSS} [%p] [c=%c{1}] [%thread] %m%n"/>-->
<!--将原先的日志输出替换为自定义的日志输出appender插件-->
<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的简单应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Plugin(name = "threadName", category = StrLookup.CATEGORY)
public class ThreadName implements StrLookup {
// 插件的功能即根据key值获取相应结果,这里我们直接返回线程名字
@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();
}
}
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
<?xml version="1.0" encoding="UTF-8"?>
<Configuration name="log-demo-config" status="error" monitorInterval="10">
<Appenders>
<!-- 默认保留Console,用于控制台日志输出 -->
<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