Java 日志库是最能体现 Java 库在进化中的渊源关系的,在理解时重点理解日志框架本身和日志门面,以及比较好的时间等。要关注其历史渊源和设计(比如桥接),而具体在使用时查询接口即可,否则会陷入 JUL(Java Util Log),JCL(Commons Logging),Log4j,SLF4J,Logback,Log4j2 傻傻分不清楚的境地。
1. 日志库简介
理解日志库可以从下面三个角度去理解:
- 最重要的一点是:区分日志系统和日志门面;
- 其次是日志库的使用,包含配置与 API 使用;配置侧重于日志系统的配置,API 使用侧重于日志门面;
- 最后是选型,改造和最佳实践等。
2. 日志库之日志系统
2.1 java.util.logging(JUC)
JDK1.4 开始,通过 java.util.logging 提供日志功能,虽然是官方自带的 log lib,JUL 的使用却不广泛。主要原因:
- JUL 从 JDK1.4 才开始加入(2002 年),当时各种第三方 log lib 以及被广泛使用了;
- JUL 早期存在性能问题,到 JDK1.5 才有了不错的进步,但现在和 Logback/Log4j2 相比还是有所不如;
- JUL 的功能不如 Logback/Log4j2 等完善,比如 Output Handler 就没有 Log’back/Log4j2 的丰富,有时候需要自己来集成定制,又比如默认没有从 ClassPath 里加载配置文件的功能。
2.2 Log4j
Log4j 是 apache 的一个开源项目,创始人 Ceki Gulcu。Log4j 应该说是 Java 领域资格最老,应用最广的日志工具。Log4j 是高度可配置的,并可通过在运行时的外部文件配置。它根据记录的优先级别,并提供机制,以指示记录信息到许多的目的地,注入:数据库,文件,控制台,UNIX 系统日志等。
Log4j 中有三个主要组成部分:
- loggers:负责捕获记录信息。
- appenders:负责发布日志信息,以不同的首选目的地。
- layouts:负责格式化不同风格的日志信息。
官网地址:Apache Log4j :: Apache Log4j
Log4j 的短板在于性能,在 Logback 和 Log4j2 出来之后,Log4j 的使用也减少了。
2.3 Logback
Logback 是由 log4j 创始人 Ceki Gulcu 设计的又一个开源日志组件,是作为 Log4j 的继承者来开发的,提供了性能更好的实现,异步 logger,Filter 等更多的特性。
logback 当前分成三个模块:logback-core、logback-classic 和 logback-access。
- logback-core:是其它两个模块的基础模块。
- logback-classic:是 log4j 的一个改良版本。此外 logback-classic 完整实现 SLF4J API 使你可以很方便地更换成其它日志系统如 log4j 或 JDK14 Logging。
- logback-access:访问模块与 Servlet 容器集成提供通过 Http 来访问日志的功能。
官网地址:Logback Home
2.4 Log4j2
维护 Log4j 的人为了性能又高出了 Log4j2。
Log4j2 和 Log4j1.x 并不兼容,设计上很大程度上模仿了 SLF4J/Logback,性能上也获得了很大的提升。
Log4j2 也做了 Facade/Implementation 分离的设计,分成了 log4j-api 和 log4j-core。
官网地址:Apache Log4j :: Apache Log4j
2.5 Log4j vs Logback vs Log4j2
从性能上 Log4j2 要强,但从生态上 Logback + SLF4J 优先。
初步对比
logback 和 log4j2 都宣称自己是 log4j 的后代,一个是出于同一个作者,另一个则是在名字上根正苗红。
比较 log4j2 和 logback:
- log4j2 比 logback 更新:log4j2 的 GA 版在 2014 年底才推出,比 logback 晚了好几年,这期间 log4j2 确实吸收了 slf4j 和 logback 的一些优点(比如日志模板),同时应用了不少的新技术;
- 由于采用了更先进的锁机制和 LMAX Disruptor 库,log4j2 的性能优于 logback,特别是在多线程环境下和使用异步日志的环境下;
- 二者都支持 Filter(应该说是 log4j2 借鉴了 logback 的 Filter),能够实现灵活的日志记录规则(例如仅对一部分用户记录 debug 级别的日志);
- 二者都支持对配置文件的动态更新;
- 二者都能够适配 slf4j,logback 与 slf4j 的适配应该会更好一些,毕竟省掉了一层适配库;
- logback 能够自动压缩/删除旧日志;
- logback 提供了对日志的 HTTP 访问功能;
- log4j2 实现了"无垃圾"和"低垃圾"模式。简单地说,log4j2 在记录日志时,能够重用对象(如 String 等),尽可能避免实例化新的临时对象,减少因日志记录产生的垃圾对象,减少垃圾回收带来的性能下降;
- log4j2 和 logback 各有所长, 总体来说,如果对性能要求比较高的话,log4j2 相对还是较优的选择。
性能对比
附上 log4j2 与 logback 性能对比的 benchmark,这份 benchmark 是 Apache Logging 出的,仅供参考。
同步写文件日志的 benchmark:
异步写日志的 benchmark:
当然,这些 benchmark 都是在日志 Pattern 中不包含 Location 信息(如日志代码行号,调用者信息,Class 名/源码文件名等)时测定的,如果输出 Location 信息的话,性能谁也拯救不了:
3. 日志库之日志门面
3.1 common-logging
common-logging 是 apache 的一个开源项目。也称 Jakarta Commons Logging,缩写 JCL。
common-logging 的功能是提供日志功能的 API 接口,本身并不提供日志的具体实现(当然,common-logging 内部有一个 Simple logger 的简单实现,但是功能很弱,直接忽略),而是在运行时动态绑定日志实现组件来工作(如 log4j、java.util.logging)。
官网地址:Apache Commons Logging – Overview
3.2 slf4j
全称为 Simple Logging Facade for Java,即 java 简单日志门面。
作者也是 Ceki Gulcu!
类似于 Common-Logging,slf4j 是对不同日志框架提供的一个 API 封装,可以在部署的时候不修改配置即可接入一种日志实现方案。但是,slf4j 在编译时静态绑定真正的 Log 库。使用 SLF4J 时,如果你需要使用某一种日志实现,那么你必须选择正确的 SLF4J 的 jar 包的集合(各种桥接包)。
官网地址:SLF4J
3.3 common-logging vs slf4j
slf4j 库类似于 Apache Common-Logging。但是,它在编译时静态绑定真正的日志库。这点似乎很麻烦,其实也不过是导入桥接 jar 包而已。
sjf4j 一大亮点是提供了更方便的日志记录方式:
不需要使用 logger.isDebugEnabled() 来解决日志因为字符拼接产生的性能问题。slf4j 的方式是使用 {} 作为字符串替换符,形式如下:
logger.debug("id:{},name:{}", id, name);
4. 日志库使用方案
使用日志解决方案基本可分为三步:
- 引入 jar 包
- 配置
- 使用 API
常见的各种日志解决方案的第 2 步和第 3 步基本一样,实施上的差别主要在第 1 步,也就是使用不同的库。
4.1 日志库 jar 包
这里首选推荐使用 slf4j + logback 的组合。
如果你习惯了 common-logging,可以选择 common-logging + log4j。
强烈建议不要直接使用日志实现组件(logback、log4j、java.util.logging),理由前面也说过,就是无法灵活替换日志库。
还有一种情况:你的老项目使用了 common-logging,或是直接使用日志实现组件。如果修改老的代码,工作量太大,需要兼容处理。在下文,都将看到各种应对方法。
slf4j 直接绑定日志组件
- slfj + loback
添加依赖到 pom.xml 即可。
logback-classic-1.0.13 jar 会自动将 slf4j-api-1.7.21.jar 和 logabck-core-1.0.13.jar 也添加到你的项目中。
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.0.13</version>
</dependency>
- slf4j + log4j
添加依赖到 pom.xml 中即可。
slf4j-log4j12-1.7.21.jar 会自动将 slf4j-api-1.7.21.jar 和 log4j-1.2.17.jar 也添加到你的项目中。
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-log4j12</artifactId><version>1.7.21</version>
</dependency>
- slf4j + java.util.logging
添加依赖到 pom.xml 中即可。
slf4j-jdk14-1.7.21.jar 会自动将 slf4j-api-1.7.21.jar 也添加到你的项目中。
<dependency><groupId>org.slf4j</groupId><artifactId>slf4j-jdk14</artifactId><version>1.7.21</version>
</dependency>
slf4j 兼容非 slf4j 日志组件
在介绍解决方案前,先提一个概念 一一 桥接
- 什么是桥接?
假如你正在开发应用程序所调用的组件当中已经使用了 common-logging,这时你需要 jcl-over-slf4j.jar 把日志信息输出重定向到 slf4j-api,slf4j-api 再去调用 slf4j 实际依赖的日志组件。这个过程称为桥接。下图是官方的 slf4j 桥接策略图:
从图中应该可以看出,无论你的老项目中使用的是 common-logging 或是直接使用 log4j、java.util.logging,都可以使用对应的桥接 jar 包来解决兼容问题。
- slf4j 兼容 common-logging
<dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>1.7.12</version>
</dependency>
- slf4j 兼容 log4j
<dependency><groupId>org.slf4j</groupId><artifactId>log4j-over-slf4j</artifactId><version>1.7.12</version>
</dependency>
- slf4j 兼容 java.util.logging
<dependency><groupId>org.slf4j</groupId><artifactId>jul-to-slf4j</artifactId><version>1.7.12</version>
</dependency>
- spring 集成 slf4j
做 java web 开发,基本离不开 spring 框架。很遗憾,spring 使用的日志解决方案是 common-logging + log4j。
所以,你需要一个桥接 jar 包:logback-ext-spring。
<dependency><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId><version>1.1.3</version>
</dependency>
<dependency><groupId>org.logback-extensions</groupId><artifactId>logback-ext-spring</artifactId><version>0.1.2</version>
</dependency>
<dependency><groupId>org.slf4j</groupId><artifactId>jcl-over-slf4j</artifactId><version>1.7.12</version>
</dependency>
common-logging 绑定日志组件
- common-logging + log4j
<dependency><groupId>commons-logging</groupId><artifactId>commons-logging</artifactId><version>1.2</version>
</dependency>
<dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version>
</dependency>
4.2 日志库配置 - 针对于日志框架
log4j2 配置
log4j2 基本配置形式如下:
<?xml version="1.0" encoding="UTF-8"?>
<Configuration><Properties><Property name="name1">value</Property><Property name="name2" value="value2"/></Properties><Filter type="type" ... /><Appenders><Appender type="type" name="name"><Filter type="type" ... /></Appender>...</Appenders><Loggers><Logger name="name1"><Filter type="type" ... /></Logger><Root level="level"><AppenderRef ref="name"/></Root></Loggers>
</Configuration>
配置示例:
<?xml version="1.0" encoding="UTF-8" ?>
<Configuration><Properties><Property name="filename">target/test.log</Property></Properties><!-- 配置全局过滤器,只记录 trace 及以上级别的日志 --><Filte type="ThresholdFilter" level="trace"/><!-- 定义日志输出方式 --><Appenders><!-- 控制台输出 --><Appender type="Console" name="STDOUT"><!-- 日志格式 --><Layout type="PatternLayout" pattern="%m MDC%X%n"/><!-- 过滤器:拒绝标记为 FLOW 的日志,接收标记为 EXCEPTION 的日志 --><Filters><Filter type="MarkerFilter" marker="FLOW" onMatch="DENY" onMismaatch="NEUTRAL"/><Filter type="MarkerFilter" marker="EXCEPTION" onMatch="DENY" onMismaatch="ACCEPT"/></Filters></Appender><Appender type="Console" name="FLOW"><Layout type="PatternLayout" pattern="%C{1}.%M %m %ex%n"/><Filters><Filter type="MarkerFilter" marker="FLOW" onMatch="ACCEPT" onMismaatch="NEUTRAL"/><Filter type="MarkerFilter" marker="EXCEPTION" onMatch="ACCEPT" onMismaatch="DENY"/></Filters></Appender><!-- 文件输出 --><Appender type="File" name="File" fileName="${filename}"><!-- 日志格式 --><Layout type="PatternLayout"><Pattern>%d %p $C{1.} [%t] %m%n</Pattern></Layout></Appender></Appenders><!-- 定义日志记录器 --><Loggers><!-- 名为 org.apache.logging.log4j.test1 的日志记录器 --><Logger name="org.apache.logging.log4j.test1" level="DEBUG" additivity="FALSE"><!-- 过滤器:只有当 ThreadContext 中包含键值对 "test=123" 时才记录日志 --><Filter type="ThreadContextMapFilter"><KeyValuePair key="test" value="123"/></Filter></Logger><!-- 名为 org.apache.logging.log4j.test2 的日志记录器 --><Logger name="org.apache.logging.log4j.test2" level="DEBUG" additivity="FALSE"> <!--5--><!-- 引用名为 file 的 Appender --><AppenderRef ref="file"/></Logger><!-- 根日志记录器,所有未指定记录器的日志都会到这里 --><Root level="trace"><!-- 引用名为 STDOUT 的 Appender --><AppenderRef ref="STDOUT"/></Root></Loggers>
</Configuration>
Logback 配置
<?xml version="1.0" encoding="UTF-8" ?>
<!-- logback 中一共有 5 种有效级别,分别是 TRACE、DEBUG、INFO、WARN、ERROR,优先级依次从低到高 -->
<configuration scan="true" scanPeiriod="30 seconds" debug="false"><property name="DIR_NAME" value="spring"/><!-- 将记录日志打印到控制台 --><appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><!-- RollingFilterAppender begin --><appender name="ALL" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/all.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>30MB</maxFileSize></trggeringPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><appender name="ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/error.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>10MB</maxFileSize></trggeringPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>ERROR</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><appender name="WARN" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/warn.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>10MB</maxFileSize></trggeringPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>WARN</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><appender name="INFO" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/info.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>10MB</maxFileSize></trggeringPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>INFO</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><appender name="DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/debug.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>10MB</maxFileSize></trggeringPolicy><filter class="ch.qos.logback.classic.filter.LevelFilter"><level>DEBUG</level><onMatch>ACCEPT</onMatch><onMismatch>DENY</onMismatch></filter><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><appender name="SPRING" class="ch.qos.logback.core.rolling.RollingFileAppender"><!-- 根据时间来制定滚动策略 --><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><fileNamePattern>${user.dir}/logs/${DIR_NAME}/springframework.%d{yyyy-MM-dd}.log</fileNamePattern><maxHistory>30</maxHistory></rollingPolicy><!-- 根据文件大小来制定滚动策略 --><trggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"><maxFileSize>10MB</maxFileSize></trggeringPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%-5p] %c{36}.%M - %m%n</pattern></encoder></appender><!-- RollingFilterAppender end--><!-- logger begin --><!-- 本项目的日志记录,分级打印 --><logger name="io.zhanbo.log" level="TRACE" additivity="false"><appender-ref ref="STDOUT"/><appender-ref ref="ERROR"/><appender-ref ref="WARN"/><appender-ref ref="INFO"/><appender-ref ref="DEBUG"/><appender-ref ref="TRACE"/></logger><!-- SPRING 框架日志 --><logger name="org.springframework" level="WARN" additivity="false"><appender-ref ref="SPRING"/></logger><logger level="TRACE"><appender-ref ref="ALL"/></logger><!-- logger end -->
</configuration>
log4j 配置
完整的 log4j.xml 参考示例:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE log4j:configuration SYSTEM "log4j.dtd"><log4j:configuration xmlns:log4j='http://jakarta.apache.org/log4j/'><appender name="STDOUT" class="org.apache.log4j.ConsoleAppender"><layout class="org.apache.log4j.PatternLayout"><param name="ConversionPattern"value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/></layout><!--过滤器设置输出的级别--><filter class="org.apache.log4j.varia.LevelRangeFilter"><param name="levelMin" value="debug"/><param name="levelMax" value="fatal"/><param name="AcceptOnMatch" value="true"/></filter></appender><appender name="ALL" class="org.apache.log4j.DailyRollingFileAppender"><param name="File" value="${user.dir}/logs/spring-common/jcl/all"/><param name="Append" value="true"/><!-- 每天重新生成日志文件 --><param name="DatePattern" value="'-'yyyy-MM-dd'.log'"/><!-- 每小时重新生成日志文件 --><!--<param name="DatePattern" value="'-'yyyy-MM-dd-HH'.log'"/>--><layout class="org.apache.log4j.PatternLayout"><param name="ConversionPattern"value="%d{yyyy-MM-dd HH:mm:ss,SSS\} [%-5p] [%t] %c{36\}.%M - %m%n"/></layout></appender><!-- 指定logger的设置,additivity指示是否遵循缺省的继承机制--><logger name="io.zhanbo.log" additivity="false"><level value="error"/><appender-ref ref="STDOUT"/><appender-ref ref="ALL"/></logger><!-- 根logger的设置--><root><level value="warn"/><appender-ref ref="STDOUT"/></root>
</log4j:configuration>
4.3 日志库 API - 针对于日志门面
slf4j 用法
使用 slf4j 的 API 很简单。使用LoggerFactory 初始化一个 Logger 实例,然后调用 Logger 对应的打印等级函数就行了。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;public class Test {private static final Logger logger = LoggerFactory.getLogger(Test.class);public static void main(String[] args) {String msg = "print log, current level: {}";logger.trace(msg, "trace");logger.debug(msg, "debug");logger.info(msg, "info");logger.warn(msg, "warn");logger.error(msg, "error");}
}
common-logging 用法
common-logging 用法和 slf4j 几乎一样,但是支持的打印等级多了一个更高的级别:fatal。
此外,common-logging 不支持 {} 替换参数,你只能选择拼接字符串这种方式了。
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;public class Test {private static final Log log = LogFactory.getLog(Test.class);public static void main(String[] args) {String msg = "print log, current level:";log.trace(msg + "trace");log.debug(msg + "debug");log.info(msg + "info");log.warn(msg + "warn");log.error(msg + "error");log.fatal(msg + "fatal");}
}
5. 日志库选型与改造
5.1 对 Java 日志组件选型的建议
slf4j 已经成为了 Java 日志组件的明星选手,可以完美代替 JCL,使用 JCL 桥接库也能完美兼容一切使用 JCL 作为日志门面的类库,现在的新系统已经没有不使用 slf4j 作为日志 API 的理由了。
日志记录服务方面,log4j 在功能上输于 logback 和 log4j2,在性能方面 log4j2 则全面超越 log4j 和 logback。所以新系统应该在 logback 和 log4j2 中做出选择,对于性能有很高要求的系统,应优先考虑 log4j2。
5.2 对日志架构使用比较好的实践
总是使用 Log Facade,而不是具体 Log Implementation
正如之前所说的,使用 Log Facade 可以方便的切换具体的日志实现。而且,如果依赖多个项目,使用了不同的 Log Facade,还可以方便的通过 Adapter 转接到同一个实现上。如果依赖项目使用了多个不同的日志实现,就麻烦的多了。
具体来说,现在推荐使用 Log4j-API 或者 SLF4J,不推荐继续使用 JCL。
只添加一个 Log Implementation 依赖
毫无疑问,项目中应该只使用一个具体的 Log Implementation,建议使用 Logback 或者 Log4j2。 如果有依赖的项目中,使用的 Log Facade 不支持直接使用当前的 Log Implementation,就添加合适的桥接器依赖。具体的桥接关系可以看上一节的图。
具体的日志实现依赖应该设置为 optional 和使用 runtime scope
在项目中,Log Implementation 的依赖强烈建议设置为 runtime scope,并且设置为 optional。例如项目中使用了 SLF4J 作为 Log Facade,然后想使用 Log4j2 作为 Implementation,那么使用 maven 添加依赖的时候这样设置:
<dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>${log4j.version}</version><scope>runtime</scope><optional>true</optional>
</dependency>
<dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j-impl</artifactId><version>${log4j.version}</version><scope>runtime</scope><optional>true</optional>
</dependency>
设为 optional,依赖不会传递,这样如果你是个 lib 项目,然后别的项目使用了你这个 lib,不会被引入不想要的 Log Implementation 依赖;
Scope 设置为 runtime,是为了防止开发人员在项目中直接使用 Log Implementation 中的类,而不使用 Log Facade 中的类。
如果有必要,排除依赖的第三方库中的 Log Impementation 依赖
这是很常见的一个问题,第三方库的开发者未必会把具体的日志实现或者桥接器的依赖设置为 optional,然后你的项目继承了这些依赖 一一 具体的日志实现未必是你想使用的,比如他依赖的 Log4j,你想使用 Logback,这时就很尴尬。另外,如果不同的第三方依赖使用了不同的桥接器和 Log 实现,也极容易形成环。
这种情况下,推荐的处理方法,是使用 exclude 来排除所有的这些 Log 实现和桥接器的依赖,只保留第三方库里面对 Log Facade 的依赖。
比如阿里的 JStorm 就没有很好的处理这个问题,依赖 jstorm 会引入对 Logback 和 log4j-over-slf4j 的依赖,如果你想在自己的项目中使用 Log4j 或者其它 Log 实现的话,就需要加上 excludes:
<dependency><groupId>com.alibaba.jstorm</groupId><artifactId>jstorm-core</artifactId><version>2.1.1</version><exclusions><exclusion><groupId>org.slf4j</groupId><artifactId>log4j-over-slf4j</artifactId></exclusion><exclusion><groupId>ch.qos.logback</groupId><artifactId>logback-classic</artifactId></exclusion></exclusions>
</dependency>
避免为不会输出的 log 付出代价
Log 库都可以灵活的设置输出界面,所以每一条程序中的 log,都是有可能不会被输出的。这时候要注意不要额外的付出代价。
先看两个有问题的写法:
logger.debug("start process request, url: " + url);
logger.debug("receive request: {}", toJson(request));
第一条是直接做了字符串拼接,所以即使日志级别高于 debug 也会做一个字符串连接操作;第二条虽然用了 SLF4J/Log4j2 中的懒求值方式来避免不必要的字符串拼接开销,但是 toJson() 这个函数却是都会被调用并且开销很大。
推荐的写法如下:
logger.debug("start process request, url:{}", url); // SLF4J/LOG4J2
logger.debug("receive request: {}", () -> toJson(request)); // LOG4J2
logger.debug(() -> "receive request: " + toJson(request)); // LOG4J2
if (logger.isDebugEnabled()) { // SLF4J/LOG4J2logger.debug("receive request: " + toJson(request));
}
日志格式中最好不要使用行号,函数名等字段
原因是:为了获取语句所在的函数名,或者行号,log 库的实现都是获取当前的 stacktrace,然后分析取出这些信息,而获取 stacktrace 的代价是很昂贵的。如果有很多的日志输出,就会占用大量的 CPU。在没有特殊需要的情况下,建议不要在日志中输出这些字段。
最后,log 中不要输出稀奇古怪的字符!
部分开发人员为了方便看到自己的 log,会在 log 语句中加上醒目的前缀,比如:
logger.debug("========================start process request=============");
虽然对于自己来说是方便了,但是如果所有人都这样来做的话,那 log 输出就没法看了!正确的做法是使用 grep 来看自己只关心的日志。
5.3 对现有系统日志架构的改造建议
如果现有系统使用 JCL 作为日志门面,又确实面临着 JCL 的 ClassLoader 机制带来的问题,完全可以引入 slf4j 并通过桥接库将 JCL api 输出的日志桥接至 slf4j,再通过适配库配置现有的日志输出服务(如 log4j),如下图:
这样做不需要任何代码级的改造,就可以解决 JCL 的 ClassLoader 带来的问题,但没有办法享受日志模板等 slf4j 的 api 带来的优点。不过之后在现有系统上开发的新功能就可以使用 slf4j 的 api 了,老代码也可以分批进行改造。
如果现有系统使用 JCL 作为日志门面,又头疼 JCL 不支持 logback 和 log4j2 等新的日志服务,也可以通过桥接库以 slf4j 代替 JCL,但同样无法直接享受 slf4j api 的优点。
如果想要使用 slf4j 的 api,那么就不得不进行代码改造了,当然改造也可以参考1中提到的方法逐步进行。
如果现有系统面临着 log4j 的性能问题,可以使用 Apache Logging 提供的 log4j 到 log4j2 的桥接库 log4j-1.2-api,把通过 log4j api 输出的日志桥接至 log4j2.这样可以最快地使用上 log4j2 的先进性能,但组件中缺失了 slf4j,对后续进行日志架构改造的灵活性有影响。另一种办法是先把 log4j 桥街知 slf4j,再使用 slf4j 到 log4j2 的适配库。这样做稍微麻烦了一点,但可以逐步将系统中的日志输出标准化为使用 slf4j 的 api,为后面的工作打好基础。