深入浅出:Java日志系统的演变

Logging System

Log4j

较早出现的比较成功的日志系统是 Log4j.
Log4j 开创的日志系统模型(Logger/Appender/Level)行之有效, 并一直延用至今.

JUL

JDK1.4 是第一个自带日志系统的 JDK, 简称(JUL).
JUL 并没有明显的优势来战胜 Log4j, 反而造成了标准的混乱 —— 采用不同日志系统的应用程序无法和谐共存.

Logback
是较新的日志系统.
它是 Log4j 的作者吸取多年的经验教训以后重新做出的一套系统. 它的使用更方便, 功能更强, 而且性能也更高.
Logback 不能单独使用, 必须配合日志框架 SLF4J 来使用.

Logging Framework

JCL (Jakarta Commons Logging)

这是目前最流行的一个日志框架, 由 Apache Jakarta 社区提供.
Spring 框架、许多老应用都依赖于 JCL.

SLF4J

这是一个最新的日志框架, 由 Log4j 的作者推出.
SLF4J 提供了新的 API, 特别用来配合 Logback 的新功能. 但 SLF4J 同样兼容 Log4j.
(全称是 Simple Loging Facade For Java) 是一个为 Java 程序提供日志输出的统一接口, 并不是一个具体的日志实现方案, 就好像我们经常使用的 JDBC 一样,
只是一种规则而已. 因此单独的 slf4j 是不能工作的, 它必须搭配其他具体的日志实现方案, 比如 apache 的 org.apache.log4j.Logger, jdk 自带的
java.util.logging.Logger 等等.

其中对与 jar 包:

  1. slf4j-log4j12-x.x.x.jar 是使用 org.apache.log4j.Logger 提供的驱动
  2. slf4j-jdk14-x.x.x.jar 是使用 java.util.logging 提供的驱动
  3. slf4j-simple-x.x.x.jar 直接绑定 System.err
  4. slf4j-jcl-x.x.x.jar 是使用 commons-logging 提供的驱动
  5. logback-classic-x.x.x.jar 是使用 logback 提供的驱动

Commons-logging+log4j

经典的一个日志实现方案. 出现在各种框架里. 如 spring 、webx 、ibatis 等等. 直接使用 log4j 即可满足我们的日志方案. 但是一般为了避免直接依赖具体的日志实现,
一般都是结合 commons-logging 来实现. 常见代码如下:

1
2
3
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
private static Log logger = LogFactory.getLog(CommonsLoggingTest.class);

代码上, 没有依赖任何的 log4j 内部的类. 那么 log4j 是如何被装载的?
Log 是一个接口声明. LogFactory 的内部会去装载具体的日志系统, 并获得实现该 Log 接口的实现类. 而内部有一个 Log4JLogger 实现类对 Log 接口同时内部提供了对
log4j logger 的代理. LogFactory 内部装载日志系统流程:

  1. 首先, 寻找 org.apache.commons.logging.LogFactory 属性配置
  2. 否则, 利用 JDK1.3 开始提供的 service 发现机制, 会扫描 classpah 下的 META-INF/services/org.apache.commons.logging.LogFactory 文件,
    若找到则装载里面的配置, 使用里面的配置.
  3. 否则, 从 Classpath 里寻找 commons-logging.properties , 找到则根据里面的配置加载.
  4. 否则, 使用默认的配置: 如果能找到 Log4j 则默认使用 log4j 实现, 如果没有则使用 JDK14Logger 实现, 再没有则使用 commons-logging 内部提供的
    SimpleLog 实现.

从上述加载流程来看, 如果没有做任何配置, 只要引入了 log4j 并在 classpath 配置了 log4j.xml , 则 commons-logging 就会使 log4j 使用正常,
而代码里不需要依赖任何 log4j 的代码.

Commons-logging+log4j+slf4j

如果在原有 commons-logging 系统里, 如果要迁移到 slf4j, 使用 slf4j 替换 commons-logging , 也是可以做到的. 原理使用到了上述 commons-logging
加载的第二点. 需要引入 org.slf4j.jcl-over-slf4j-1.5.6.jar . 这个 jar 包提供了一个桥接, 让底层实现是基于 slf4j . 原理是在该 jar 包里存放了配置
META-INF/services/org.apache.commons.logging.LogFactory =org.apache.commons.logging.impl.SLF4JLogFactory,
而 commons-logging 在初始化的时候会找到这个 serviceId , 并把它作为 LogFactory .

完成桥接后, 那么那么简单日志门面 SLF4J 内部又是如何来装载合适的 log 呢?

原理是 SLF4J 会在编译时会绑定 import org.slf4j.impl.StaticLoggerBinder; 该类里面实现对具体日志方案的绑定接入. 任何一种基于 slf4j
的实现都要有一个这个类. 如:
org.slf4j.slf4j-log4j12-1.5.6: 提供对 log4j 的一种适配实现.
Org.slf4j.slf4j-simple-1.5.6: 是一种 simple 实现, 会将 log 直接打到控制台.
……

那么这个地方就要注意了: 如果有任意两个实现 slf4j 的包同时出现, 那就有可能酿就悲剧, 你可能会发现日志不见了、或都打到控制台了. 原因是这两个 jar
包里都有各自的 org.slf4j.impl.StaticLoggerBinder , 编译时候绑定的是哪个是不确定的. 这个地方要特别注意!!出现过几次因为这个导致日志错乱的问题.

简单使用 log4j

Maven 依赖

1
2
3
4
5
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.9</version>
</dependency>

log4j.properties 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
### set log levels ###
log4j.rootLogger = debug , stdout

### 输出到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%n

### 输出到日志文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG ## 输出 DEBUG 级别以上的日志
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%n

### 保存异常信息到单独文件 ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = logs/error.log ## 异常日志文件名
log4j.appender.D.Append = true
log4j.appender.D.Threshold = ERROR ## 只输出 ERROR 级别以上的日志!!!
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [%t:%r] - [%p] %m%n
1
2
3
4
5
6
public static void main(String[] args) throws Exception {
Logger logger = Logger.getLogger(UserDaoTest.class);
logger.debug("开始");
example2();
logger.debug("结束");
}

简单使用 log4j2

Maven 依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.6.2</version>
</dependency>

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
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
<?xml version="1.0" encoding="UTF-8"?>

<!--
status : 这个用于设置 log4j2 自身内部的信息输出, 可以不设置, 当设置成 trace 时, 会看到 log4j2 内部各种详细输出
monitorInterval : Log4j 能够自动检测修改配置文件和重新配置本身, 设置间隔秒数.
-->
<Configuration status="WARN" monitorInterval="600">

<Properties>
<!-- 配置日志文件输出目录 -->
<Property name="LOG_HOME">/Users/hanhan.zhang/logs</Property>
</Properties>

<Appenders>

<!-- 这个输出控制台的配置 -->
<Console name="Console" target="SYSTEM_OUT">
<!-- 控制台只输出 level 及以上级别的信息 (onMatch), 其他的直接拒绝 (onMismatch) -->
<ThresholdFilter level="trace" onMatch="ACCEPT" onMismatch="DENY"/>
<!-- 输出日志的格式 -->
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
</Console>

<!-- 设置日志格式并配置日志压缩格式 (service.log. 年份.gz) -->
<RollingRandomAccessFile name="service_appender"
immediateFlush="false" fileName="${LOG_HOME}/service.log"
filePattern="${LOG_HOME}/service.log.%d{yyyy-MM-dd}.log.gz">
<!--
%d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间
%p : 日志输出格式
%c : logger 的名称
%m : 日志内容, 即 logger.info("message")
%n : 换行符
%C : Java 类名
%L : 日志输出所在行数
%M : 日志输出所在方法名
hostName : 本地机器名
hostAddress : 本地 ip 地址
-->
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36} %L %M -- %msg%xEx%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
</RollingRandomAccessFile>


<!-- DEBUG 日志格式 -->
<RollingRandomAccessFile name="service_debug_appender"
immediateFlush="false" fileName="${LOG_HOME}/service.log"
filePattern="${LOG_HOME}/service.log.%d{yyyy-MM-dd}.debug.gz">
<PatternLayout>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %class{36} %L %M -- %msg%xEx%n</pattern>
</PatternLayout>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true" />
</Policies>
</RollingRandomAccessFile>
</Appenders>

<Loggers>
<!-- 配置日志的根节点 -->
<root level="debug">
<appender-ref ref="Console"/>
</root>

<!-- 第三方日志系统 -->
<logger name="org.springframework.core" level="info"/>
<logger name="org.springframework.beans" level="info"/>
<logger name="org.springframework.context" level="info"/>
<logger name="org.springframework.web" level="info"/>
<logger name="org.jboss.netty" level="warn"/>
<logger name="org.apache.http" level="warn"/>

<!-- 日志实例 (info), 其中'service-log'继承 root, 但是 root 将日志输出控制台, 而'service-log'将日志输出到文件, 通过属性'additivity="false"'将'service-log'的
的日志不再输出到控制台 -->
<logger name="service_log" level="info" includeLocation="true" additivity="true">
<appender-ref ref="service_appender"/>
</logger>

<!-- 日志实例 (debug) -->
<logger name="service_log" level="debug" includeLocation="true" additivity="false">
<appender-ref ref="service_debug_appender"/>
</logger>

</Loggers>

</Configuration>
1
2
3
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
private static Logger logger_ = LogManager.getLogger(DateUtils2Joda.class);

简单使用 slf4j

pom
slf4j-simple 中包含了 slf4j-api

1
private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(HelloAspect.class);
1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.7</version>
</dependency>

log4j + slf4j 配置

  1. slf4j-api:Simple Logging Facade for Java-api, 为 Java 提供的简单日志 Facade. Facade: 门面, 更底层一点说就是接口. slf4j 入口就是众多接口的集合,
    他不负责具体的日志实现, 只在编译时负责寻找合适的日志系统进行绑定.
  2. slf4j-log4j12: 链接 slf4j-api 和 log4j 中间的适配器. 它实现了 slf4j-api 中 StaticLoggerBinder 接口, 从而使得在编译时绑定的是
    slf4j-log4j12 的 getSingleton() 方法.
  3. log4j: 这个是具体的日志系统. 通过 slf4j-log4j12 初始化 Log4j, 达到最终日志的输出.

pom
如果没有更高版本的 slf4j-api 和 log4j 要求, 则只添加第一条依赖就可以, 因为 slf4j-log4j12 依赖会包含 slf4j-api 和 log4j 依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.22</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>

log4j.properties (log4j.xml)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
log4j.rootLogger=debug,consoleAppender,fileAppender
log4j.category.ETTAppLogger=debug, ettAppLogFile
log4j.appender.consoleAppender=org.apache.log4j.ConsoleAppender
log4j.appender.consoleAppender.Threshold=TRACE
log4j.appender.consoleAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.consoleAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS} ->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.fileAppender=org.apache.log4j.DailyRollingFileAppender
log4j.appender.fileAppender.File=c:/temp/nstd/debug1.log
log4j.appender.fileAppender.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.fileAppender.Threshold=TRACE
log4j.appender.fileAppender.Encoding=BIG5
log4j.appender.fileAppender.layout=org.apache.log4j.PatternLayout
log4j.appender.fileAppender.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n
log4j.appender.ettAppLogFile=org.apache.log4j.DailyRollingFileAppender
log4j.appender.ettAppLogFile.File=c:/temp/nstd/ettdebug.log
log4j.appender.ettAppLogFile.DatePattern='_'yyyy-MM-dd'.log'
log4j.appender.ettAppLogFile.Threshold=DEBUG
log4j.appender.ettAppLogFile.layout=org.apache.log4j.PatternLayout
log4j.appender.ettAppLogFile.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss SSS}-->[%t]--[%-5p]--[%c{1}]--%m%n

slf4j 工作原理

slf4j-api 作为 slf4j 的接口类, 使用在程序代码中, 这个包提供了一个 Logger 类和 LoggerFactory 类, Logger 类用来打日志, LoggerFactory 类用来获取
Logger;slf4j-log4j 是连接 slf4j 和 log4j 的桥梁, 怎么连接的呢?我们看看 slf4j 的 LoggerFactory 类的 getLogger 函数的源码:

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
/**
* Return a logger named according to the name parameter using the statically
* bound {@link ILoggerFactory} instance.
*
* @param name
* The name of the logger.
* @return logger
*/
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

/**
* Return a logger named corresponding to the class passed as parameter, using
* the statically bound {@link ILoggerFactory} instance.
*
* @param clazz
* the returned logger will be named after clazz
* @return logger
*/
public static Logger getLogger(Class clazz) {
return getLogger(clazz.getName());
}

/**
* Return the {@link ILoggerFactory} instance in use.
*
* <p>
* ILoggerFactory instance is bound with this class at compile time.
*
* @return the ILoggerFactory instance in use
*/
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITILIZATION;
performInitialization();

}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITILIZATION:
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITILIZATION:
return NOP_FALLBACK_FACTORY;
case FAILED_INITILIZATION:
throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
case ONGOING_INITILIZATION:
// support re-entrant behavior.
// See also http://bugzilla.slf4j.org/show_bug.cgi?id=106
return TEMP_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}

查找到现在, 我们发现 LoggerFactory.getLogger() 首先获取一个 ILoggerFactory 接口, 然后使用该接口获取具体的 Logger. 获取 ILoggerFactory
的时候用到了一个 StaticLoggerBinder 类, 仔细研究我们会发现 StaticLoggerBinder 这个类并不是 slf4j-api 这个包中的类, 而是 slf4j-log4j 包中的类,
这个类就是一个中间类, 它用来将抽象的 slf4j 变成具体的 log4j, 也就是说具体要使用什么样的日志实现方案, 就得靠这个 StaticLoggerBinder 类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* The ILoggerFactory instance returned by the {@link #getLoggerFactory}
* method should always be the same object
*/
private final ILoggerFactory loggerFactory;

private StaticLoggerBinder() {
loggerFactory = new Log4jLoggerFactory();
try {
Level level = Level.TRACE;
} catch (NoSuchFieldError nsfe) {
Util.report("This version of SLF4J requires log4j version 1.2.12 or later. See also http://www.slf4j.org/codes.html#log4j_version");
}
}

public ILoggerFactory getLoggerFactory() {
return loggerFactory;
}

public String getLoggerFactoryClassStr() {
return loggerFactoryClassStr;
}

可以看到 slf4j-log4j 中的 StaticLoggerBinder 类创建的 ILoggerFactory 其实是一个 org.slf4j.impl.Log4jLoggerFactory, 这个类的 getLogger
函数代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* (non-Javadoc)
*
* @see org.slf4j.ILoggerFactory#getLogger(java.lang.String)
*/
public Logger getLogger(String name) {
Logger slf4jLogger = null;
// protect against concurrent access of loggerMap
synchronized (this) {
slf4jLogger = (Logger) loggerMap.get(name);
if (slf4jLogger == null) {
org.apache.log4j.Logger log4jLogger;
if(name.equalsIgnoreCase(Logger.ROOT_LOGGER_NAME)) {
log4jLogger = LogManager.getRootLogger();
} else {
log4jLogger = LogManager.getLogger(name);
}
slf4jLogger = new Log4jLoggerAdapter(log4jLogger);
loggerMap.put(name, slf4jLogger);
}
}
return slf4jLogger;
}

就在其中创建了真正的 org.apache.log4j.Logger, 也就是我们需要的具体的日志实现方案的 Logger 类. 就这样, 整个绑定过程就完成了.

log4j2 + slf4j 配置

Log4j 1.x 在高并发情况下出现死锁导致 cpu 使用率异常飙升, 而 Log4j2.0 基于 LMAX Disruptor 的异步日志在多线程环境下性能会远远优于 Log4j 1.x 和
logback(官方数据是 10 倍以上), 这里分享 slf4j + Log4j2 的使用方法.

pom 配置

删除以往依赖 Log4j1.x 的依赖项, 比如 slf4j-log4j12、log4j 等包.
可以到项目的根目录, 执行: mvn dependency:tree > tree.log , 查看之后 cat tree.log | grep log4j 查找.

1
2
3
4
5
6
7
8
9
10
<exclusions>
<exclusion>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</exclusion>
<exclusion>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
</exclusion>
</exclusions>

然后在工程的 pom.xml 新增以下 log4j2 的依赖关系:

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
<!-- Logging use log4j2-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.13</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.13</version>
<scope>runtime</scope>
</dependency>

<!-- 核心 log4j2jar 包 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.4.1</version>
</dependency>
<!-- 用于与 slf4j 保持桥接 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<version>2.4.1</version>
</dependency>
<!--web 工程需要包含 log4j-web, 非 web 工程不需要 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-web</artifactId>
<version>2.4.1</version>
<scope>runtime</scope>
</dependency>

<!-- 需要使用 log4j2 的 AsyncLogger 需要包含 disruptor-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.2.0</version>
</dependency>

web.xml
web 工程的 web.xml 文件中添加(Servlet3.0 不需要):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!--log4j2-->
<!-- 对于 log4j2, Servlet2.5 以前的版本需要 -->
<listener>
<listener-class>org.apache.logging.log4j.web.Log4jServletContextListener</listener-class>
</listener>
<filter>
<filter-name>log4jServletFilter</filter-name>
<filter-class>org.apache.logging.log4j.web.Log4jServletFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>log4jServletFilter</filter-name>
<url-pattern>/*</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
<dispatcher>INCLUDE</dispatcher>
<dispatcher>ERROR</dispatcher>
</filter-mapping>
<context-param>
<param-name>log4jConfiguration</param-name>
<param-value>/WEB-INF/classes/log4j2.xml</param-value>
</context-param>

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
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
<?xml version="1.0" encoding="UTF-8"?>

<Configuration status="off" monitorInterval="1800">

<properties>
<property name="LOG_HOME">/opt/logs/gct/shoppromo/logs</property>
<property name="ERROR_LOG_FILE_NAME">error</property>
</properties>


<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d %-5p (%F:%L) - %m%n" />
</Console>

<RollingRandomAccessFile name="ErrorLog"
fileName="${LOG_HOME}/${ERROR_LOG_FILE_NAME}.log"
filePattern="${LOG_HOME}/${ERROR_LOG_FILE_NAME}.log.%d{yyyy-MM-dd}.gz">
<PatternLayout
pattern="%d %-5p (%F:%L) - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="20"/>
</RollingRandomAccessFile>

</Appenders>

<Loggers>
<!-- 3rdparty Loggers -->
<logger name="org.springframework.core" level="info">
</logger>
<logger name="org.springframework.beans" level="info">
</logger>
<logger name="org.springframework.context" level="info">
</logger>
<logger name="org.springframework.web" level="info">
</logger>

<logger name="com.meituan.gct.shop.promo" level="error" includeLocation="true" additivity="false">
<appender-ref ref="ErrorLog"/>
<appender-ref ref="Console"/>
</logger>

<root level="info" includeLocation="true">
<appender-ref ref="Console"/>
</root>
</Loggers>
</Configuration>

logback + slf4j 配置

Logback 分为三个模块: logback-core, logback-classic, logback-access

  1. logback-core 是核心;
  2. logback-classic 改善了 log4j, 且自身实现了 SLF4J API, 所以即使用 Logback 你仍然可以使用其他的日志实现, 如原始的 Log4J, java.util.logging
    等;
  3. logback-access 让你方便的访问日志信息, 如通过 http 的方式

pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.24</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.11</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.11</version>
<type>jar</type>
</dependency>

配置文件

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
<?xml version="1.0" encoding="UTF-8" ?>
<configuration>
<!-- 控制台输出日志 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{mm:ss} %-5level %logger{36} >>> %msg%n</pattern>
</encoder>
</appender>
<!-- 每天生成一个日志文件, 保存 30 天的日志文件. -->
<appender name="DayFile" class="ch.qos.logback.core.rolling.RollingFileAppender">
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>logs/logFile.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>30</maxHistory>
</rollingPolicy>
<encoder>
<pattern>%-4relative [%thread] %-5level %logger{35} - %msg%n</pattern>
</encoder>
</appender>
<!-- 指定 logger name 为包名或类全名 指定级别 additivity 设置是否传递到 root logger -->
<logger name="slf4j" level="INFO" additivity="false">
<appender-ref ref="STDOUT"/>
<appender-ref ref="DayFile"/>
</logger>
<!--slf4j2 包下的类在 ERROR 级别时候传递到 root logger 中 -->
<logger name="slf4j2" level="ERROR" />
<!-- 根 logger 控制 -->
<root level="WARN">
<appender-ref ref="STDOUT" />
</root>
</configuration>

log4j 转 logback 配置

pom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!-- logback 日志配置开始 -->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-access</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.7</version>
</dependency>
<!-- logback 日志配置结束 -->

删除 src 下的 log4j.properties 文件, 在 src 下创建 logback.xml 配置文件

Spring Framework 依赖 SLF4J

总结

slf4j 桥接到具体日志

可以看到 slf4j 与具体日志框架结合的方案有很多种. 当然, 每种方案的最上层(绿色的应用层)都是统一的, 它们向下都是直接调用 slf4j 提供的
API(浅蓝色的抽象 API 层), 依赖 slf4j-api.jar. 然后 slf4j API 向下再怎么做就非常自由了, 几乎可以使用所有的具体日志框架. 注意图中的第二层是浅蓝色的,
看左下角的图例可知这代表抽象日志 API, 也就是说它们不是具体实现. 如果像左边第一种方案那样下层没有跟任何具体日志框架实现相结合,
那么日志是无法输出来的(这里不确定是否可能会默认输出到标准输出).
图中第三层明显就不如第一、二层那么整齐划一了, 因为这里已经开始涉及到了具体的日志框架.
首先看第三层中间的两个湖蓝色块, 这是适配层, 也就是桥接器. 左边的 slf4j-log4j12.jar 桥接器看名字就知道是 slf4j 到 log4j 的桥接器, 同样, 右边的
slf4j-jdk14.jar 就是 slf4j 到 Java 原生日志实现的桥接器了. 它们的下一层分别是对应的日志框架实现, log4j 的实现代码是 log4j.jar, 而 jul
实现代码已经包含在了 JVM runtime 中, 不需要单独的 jar 包.
再看第三层其余的三个深蓝色块. 它们三个也是具体的日志框架实现, 但是却不需要桥接器, 因为它们本身就已经直接实现了 slf4j API. slf4j-simple.jar 和
slf4j-nop.jar 这两个不用多说, 看名字就知道一个是 slf4j 的简单实现, 一个是 slf4j 的空实现, 平时用处也不大. 而 logback 之所以也实现了 slf4j
API, 据说是因为 logback 和 slf4j 出自同一人之手, 这人同时也是 log4j 的作者.
第三层所有的灰色 jar 包都带有红框, 这表示它们都直接实现了 slf4j API, 只是湖蓝色的桥接器对 slf4j API 的实现并不是直接输出日志, 而是转去调用别的日志框架的
API.

其他日志框架转调回 slf4j

上图展示了目前为止能安全地从别的日志框架 API 转调回 slf4j 的所有三种情形.
以左上角第一种情形为例, 当 slf4j 底层桥接到 logback 框架的时候, 上层允许桥接回 slf4j 的日志框架 API 有 log4j 和 jul. jcl 虽然不是什么日志框架的具体实现,
但是它的 API 仍然是能够被转调回 slf4j 的. 要想实现转调, 方法就是图上列出的用特定的桥接器 jar 替换掉原有的日志框架 jar. 需要注意的是这里不包含
logback API 到 slf4j API 的转调, 因为 logback 本来就是 slf4j API 的实现.
看完三种情形以后, 会发现几乎所有其他日志框架的 API, 包括 jcl 的 API, 都能够随意的转调回 slf4j. 但是有一个唯一的限制就是转调回 slf4j 的日志框架不能跟
slf4j 当前桥接到的日志框架相同. 这个限制就是为了防止 A-to-B.jar 跟 B-to-A.jar 同时出现在类路径中, 从而导致 A 和 B 一直不停地互相递归调用,
最后堆栈溢出. 目前这个限制并不是通过技术保证的, 仅仅靠开发者自己保证, 这也是为什么 slf4j 官网上要强调所有合理的方式只有上图的三种情形.
到这里, 在开始所展示的那个异常的原理基本已经清楚了. 此外, 通过上图还可以看出可能会出现类似异常的组合不仅仅是 log4j-over-slf4j 和
slf4j-log4j12, slf4j 官网还指出了另外一对: jcl-over-slf4j.jar 和 slf4j-jcl.jar