Java日志 - log4j1升级为log4j2,涉及commons-logging、slf4j、Jboss-logging日志组件(SSH项目)
1 前言
1. 1 问题起因
SSH框架中已集成log4j,采用log4j.preoerties配置文件的方式进行日志记录。因升级至log4j2导致spring相关日志输出、单元测试运行报错等情况。因此梳理了SSH框架中涉及到的日志工具:log4j1、log4j2、commons-logging、slf4j、jboss-logging 之间的关系,以及升级中如何解决出现的问题。
1.2 java各日志工具的解释
首先要对我们见到的日志工具分个类。
一种是负责具体的实现:jdk-logging、log4j1、log4j2、logback、jboss-logging;(可提供API编程,并且完成具体日志输出的)
另一种是仅提供接口:commons-logging、slf4j;(提供API编程的,或桥接其他日志工具进行日志输出的)
各个工具的解释如下:
- log4j1: 由Apache推出的最早的日志工具;
- jdk-logging:SUN公司,jdk自带的日志工具(我们不常用这个)
- commons-logging:也被称为 JCL(Java Common Logging),为了解决各个日志工具的适配问题,不做具体实现JCL,抽象出一个接口层,对每个日志实现都进行适配。Spring的日志输出也是依赖JCL
- slf4j:(即Simple Logging Facade for Java)是JCL的升级版。同样只是作为接口。
- logback:为了slf4j定制了一个实现的日志工具。
- log4j2:log4j2的升级版。
- jboss-logging:Hibernate集成的日志工具,因此我们在集成其他日志工具的时候要解决jboss-logging转由其他日志工具进行输出。
关于工具较为详细的介绍和关系可参考该篇文章三、四部分:java日志从入门到实战
1.3 java各日志工具及其jar包的关系
上述的日中工具的分类可以让我们可能得到以下几种可能的日志输出的组合:
- 直接采用单一具体实现的工具的API编程,并进行日志输出;
如直接用log4j2,只需要log4j2的jar包:
- log4j-api;(log4j2定义的API)
- log4j-core;(log4j2上述API的实现)
- 采用了只提供接口的日志工具API进行编程,转由具体实现的日志工具进行输出的;
如采用slf4j的API编程,使用log4j2进行日志输出;那我们除了slf4j和log4j2的jar包,还需要一个实现这两个工具桥接的包,如下:
- log4j-api;(log4j2定义的API)
- log4j-core;(log4j2上述API的实现)
- slf4j-api;(slf4j定义的API)
- log4j-slf4j-impl;(slf4j到log4j2的桥梁)
- 采用了某个具体实现的日志工具API进行编程,但想用另一个具体实现的日志工具进行输出;
如采用了log4j进行编程,最后用logback输出,需要jar包如下:
- log4j;(log4j定义的API和实现)
- slf4j-api;(slf4j定义的API)
- log4j-over-slf4j;(log4j1到slf4j的桥梁)
- logback-core;(logback的实现)
- logback-classic;(slf4j到logback的桥梁)
请注意区分各个工具之间的关系及jar包的关系,若存在冲突的组合关系是会无法成功集成的。
更详细和完整的工具间集成的jar包关系、集成原理以及冲突的组合说明,可以参考一下这篇文章:
java 日志总结:logging、log4j1、log4j2、logback、commons-logging、slf4j 的关系
2 SSH项目日志升级前日志工具使用情况
目前的SSH框架中涉及到的日志工具包括:log4j1、JCL、slf4j、jboss-logging。
- log4j1是最终实现日志输出,采用log4j.properties文件配置日志输出规则。并且项目中程序编写直接使用log4j进行编程(引入log4j-1.2.15.jar),实现程序日志的输出。因此升级需要考虑修改程序代码。
配置文件不详细介绍,使用方式如下:
import org.apache.log4j.Logger;
public class SystemAction {
private final Logger log = LoggerFactory.getLogger(SystemAction.class);
public void valid() {
log.info("效验log4j日志输出");
}
}
但除了程序中直接采用log4j中进行日志输出外,SSH框架中还涉及其他日志工具的依赖,包括了:
- Spring默认使用的是JCL,JCL会自己去找具体实现的日志工具,如log4j;或者是桥接其他日志接口。项目中通过桥接slf4j(引入jcl-over-slf4j-1.7.5.jar),最后slf4j交由log4j进行具体日志实现,输出日志(引入slf-log4j12-1.7.5.jar)。因此升级需要考虑保留该工具集成方式。
- Hibernate默认使用了jboss-logging(默认引入jboss-logging-3.1.0.GA.jar),最终交由log4j实现日志输出。因此要考虑log4j配置及实现jar包删除对jboss的影响,是否可以支持log4j2。
3 SSH项目日志升级中的问题解决(log4j升级至log4j2)
此次升级中需要解决程序代码中log4j1代码的替换,日志工具的替换。以及其他未预知到的问题。包括如下:
3.1 log4j2替换log4j1
替换包括:删除原log4j1配置及jar包,新增log4j2配置文件及jar包;因为项目原来直接采用log4j进行编程,因此还需修改对应的代码,具体内容如下:
- 删除log4j1依赖及配置文件:log4j.properties、log4j-1.2.15.jar
- 增加log4j2相应的jar包:log4j-api-2.11.0.jar、log4j-core-2.11.0.jar
- 配置log4j2.xml
更为详细的配置说明可参考:log4j2.xml 配置文件详解
<?xml version="1.0" encoding="UTF-8"?>
<!-- log4j2配置xml -->
<Configuration Status="INFO">
<Properties>
<!--自定义一些常量,之后使用${变量名}引用-->
<Property name="logFilePath">${sys:catalina.home}/projectName_log</Property>
<Property name="archiveLogFilePath">${logFilePath}/archiveLog</Property>
</Properties>
<Appenders>
<!-- 输出到控制台的日志 -->
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%F:%L) - %m%n"/>
</Console>
<!-- 输出到每日日志文件 -->
<RollingFile name="DailyInfoLog" fileName="${logFilePath}/info.log"
filePattern="${archiveLogFilePath}/info-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%F:%L) - %msg%n"/>
<DefaultRolloverStrategy max="20"/>
<!-- 使用 ThresholdFilter,实现日志分级输出,大前提是Root里的日志级别 -->
<!-- 匹配高于INFO级的日志都输出,不匹配的拒绝 -->
<ThresholdFilter level="INFO" onMatch="ACCEPT" onMismatch="DENY"/>
<Policies>
<TimeBasedTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<!-- 日志打印级别info以上,控制台与日志文件均输出 -->
<Root level="INFO">
<appender-ref ref="Console"/>
<appender-ref ref="DailyInfoLog"/>
</Root>
</Loggers>
</Configuration>
- 修改日志输出代码:
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SystemAction {
private final Logger log = LogManager.getLogger(SystemAction.class);
public void valid() {
log.info("效验log4j2日志输出");
}
}
已集成slf4j建议改成使用slf4j的API。
3.2 JCL、slf4j、log4j2的集成 (解决spring日志的输出问题)
原项目中已经集成JCL与slf4j,因此相关的jar包保留不变。但要增加log4j2与slf4j的桥梁,否则找不到对应具体的实现的日志工具,spring相关的日志信息不会被log4j2输出。
- 保留 jcl-over-slf4j-1.7.5.jar,实现JCL与slf4j的集成。
- 保留 slf4j相关的jar包:slf4j-api-1.7.5.jar
- 删除掉原本slf4j与log4j1的桥梁包:slf4j12-1.7.5.jar,不删除则会和其他桥梁冲突,slf4j不知道要将日志实现交给谁来处理。
- 增加 log4j-slf4j-impl-2.11.0.jar,实现slf4j与log4j2的集成,将日志具体实现交给log4j2
已经集成了slf4j,建议采用slf4j提供的API进行编程,而不是采取使用log4j2的API。采用slf4j,后续具体实现日志的工具有进行升级更换,如换成logback,则不需要再改程序代码,只需要修改相应桥梁的jar包即可,采用slf4j编写如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class SystemAction {
private final Logger log = LoggerFactory.getLogger(SystemAction.class);
public void valid() {
log.info("效验log4j2日志输出");
}
}
3.3 jboss-logging、log4j2的集成
项目原jboss-logging的jar包版本为:boss-logging-3.1.0.GA.jar,该版本没有提供log4j2的支持,因此在升级log4j2升级后,将有如下几个问题:
- 若保留了log4j1的配置文件(log4j.properties),jboss-logging会自己找到该配置文件,根据配置输出日志。(从运行结果上看是如此,未找到相关资料验证)
- 若删除log4j.properties后,Jenkins单元测试结果异常,大量单元测试会抛出以下异常:
Caused by: java.lang.NoSuchFieldError: TRACE
at org.jboss.logging.Log4jLogger.translate(Log4jLogger.java:64) ~[jboss-logging-3.1.0.GA.jar:3.1.0.GA]
at org.jboss.logging.Log4jLogger.isEnabled(Log4jLogger.java:39) ~[jboss-logging-3.1.0.GA.jar:3.1.0.GA]
at org.jboss.logging.Logger.isTraceEnabled(Logger.java:98) ~[jboss-logging-3.1.0.GA.jar:3.1.0.GA]
...
以上两个问题的解决,一是需要删除掉原本log4j1的配置文件,同时也要升级jboss-logging jar包为:jboss-logging-3.3.0.FIANL.jar。该版本支持log4j2,会找log4j2.xml文件进行日志输出。这样就解决了通过log4j1输出日志和大量单元测试的异常。
jboss-logging与其他日志工具的集成资料少,只通过改造尝试的结果来推断问题原因及解决办法,未作深入研究
3.4 其他问题
- sys:catalina.home配置无效:运行单元测试与项目在Tomcat中启动不同,单元测试运行找不到catalina.home路径,因此日志文件无法输出到配置中的路径,导致如下异常:
ERROR Unable to create file ${sys:catalina.home}/projectName_log/info.log java.io.IOException: 文件名、目录名或卷标语法不正确。
at java.io.WinNTFileSystem.canonicalize0(Native Method)
at java.io.WinNTFileSystem.canonicalize(WinNTFileSystem.java:428)
at java.io.File.getCanonicalPath(File.java:618)
...
解决办法则是添加jvm参数,指定catalina.home:
-Dcatalina.home=D:/JunitLog/${proj_name}
Jenkins 构建自动执行单元测试(使用的是Ant编译的),可在配置文件中的中增加一个jvm参数标签,指定catalina.home ,如下:
<!-- proj_name是其他地方定义的项目名 -->
<property name="jvm_arg_catalinaHome" value="-Dcatalina.home=D:/JunitLog/${proj_name}" description="jvm参数设置catalina.home输出日志" />
<jvmarg value="${jvm_arg_catalinaHome}"/>