这里写自定义目录标题
- 修复漏洞,去掉Log4j,引入logback
- 1 背景
- 2 困难点
- 3 组件介绍
- 3.1 代码结构
- 3.2 关键类说明
- 3.2.1 ClassOfCallerConverter.java
- 3.2.2 MethodOfCallerConverter.java
- 3.2.3 LineOfCallerConverter.java
- 3.2.4 LogbackConfigLoader.java
- 3.2.5 AbstractLogbackInitListener.java
- 3.2.6 DefaultLogbackInitListener.java
- 3.2.7 Logger.java
- 3.2.8 logback.xml
- 3.2.9 log4j-1.2.17.jar
- 3.2.10 logback-core-1.2.3.jar、logback-classic-1.2.3.jar
- 4 如何使用
- 5 特别注意
- 6 附录:logback取代log4j的理由
修复漏洞,去掉Log4j,引入logback
1 背景
由于log4j版本低于1.2.17的有安全漏洞问题,如果直接升级log4j到2.X版本存在困难点。因此本方案直接将log4j替换成logback,理由见附录。
如果只是使用组件进行业务系统的集成,可以只看4 如何使用
一节
2 困难点
-
log4j-1.2.17升级到log4j-2.x版本的问题
问题:(1)基本的Logger类的路径变了,影响全局的业务类。
方案:需要新建Logger类桥接问题:(2)配置文件不能复用,需要按照新规则重新配置
方案:按照新的规则重新配置,且资料较少,需要学习成本问题:(3)组件jar和三方jar引用的低版本log4j,直接升级三方jar启动会报错
方案:需要保留低版本的log4j,可能会和高版本的log4j冲突,因此该问题比较麻烦结论:基于上述3个问题,将log4j直接转换成主流日志框架logback。
-
升级成logback存在的问题
问题:(1)考虑到尽可能的不影响业务,新建一个Logger类桥接,但存在的问题是,日志输出打印的类、方法、行号均是Logger类的相关信息,不能体现是具体哪个业务类打印出来的。
方案:既然logback支持pattern配置日志的格式,那内部是有地方取类、方法名、行号的,重写对应的逻辑即可。问题:(2)与jboss有冲突问题,具体请转至
第5节
3 组件介绍
考虑到对业务系统侵入小,尽量让业务上少改动,并且其他项目组有类似的应用场景,考虑复用,因此以组件方式进行开发。
3.1 代码结构
bdc-component-log4j2logback
│ pom.xml
└─src
├─main
│ ├─java
│ │ ├─ch
│ │ │ └─qos
│ │ │ └─logback
│ │ │ └─classic
│ │ │ └─pattern
│ │ │ ClassOfCallerConverter.java
│ │ │ LineOfCallerConverter.java
│ │ │ MethodOfCallerConverter.java
│ │ ├─com
│ │ │ └─xxxx
│ │ │ └─bdc
│ │ │ └─component
│ │ │ └─log4j2logback
│ │ │ │ LogbackConfigLoader.java
│ │ │ ├─bridge
│ │ │ │ JdbcLogger.java
│ │ │ └─listener
│ │ │ AbstractLogbackInitListener.java
│ │ │ DefaultLogbackInitListener.java
│ │ └─org
│ │ ├─apache
│ │ │ └─log4j
│ │ │ Logger.java
│ │ └─slf4j
│ │ └─override
│ │ LoggerFactory.java
│ └─resources
│ ├─lib
│ │ log4j-1.2.17.jar
│ │ logback-classic-1.2.3.jar
│ │ logback-core-1.2.3.jar
│ │ slf4j-api-1.6.0.jar
│ └─sample
│ logback.xml
└─test
└─java
3.2 关键类说明
3.2.1 ClassOfCallerConverter.java
功能说明:logback.xml中pattern将业务类的全路径名打印出来,重新实现该类的逻辑可以打印出真实的业务类,而不是Logger这个类。如果不重新实现该类,打印出来的业务类都是Logger,体现不出来日志输出的真实的业务类。
核心代码:
@Override
protected String getFullyQualifiedName(ILoggingEvent event) {
StackTraceElement[] cda = event.getCallerData();
if (cda != null) {
for (int i = 0; i < cda.length; i++) {
StackTraceElement element = cda[i];
if (element != null) {
String className = element.getClassName();
if ("org.apache.log4j.Logger".equals(className)) {
// 如果是新建的桥接类,直接取下一个className,这样才能真实的获取到打印日志的业务类
continue;
}
return className;
}
}
}
return CallerData.NA;
}
3.2.2 MethodOfCallerConverter.java
功能说明:logback.xml中pattern将业务类的方法名打印出来,重新实现该类的逻辑可以打印出真实的业务类的方法名,而不是Logger这个类的方法。如果不重新实现该类,打印出来的方法名都是Logger类中方法,体现不出来日志输出的真实的业务类的方法名。
核心代码:
@Override
public String convert(ILoggingEvent le) {
StackTraceElement[] cda = le.getCallerData();
if (cda != null) {
for (int i = 0; i < cda.length; i++) {
StackTraceElement element = cda[i];
if (element != null) {
String className = element.getClassName();
if ("org.apache.log4j.Logger".equals(className)) {
// 如果是新建的桥接类,直接取下一个类的方法名,这样才能真实的获取到打印日志的业务类的方法名
continue;
}
return element.getMethodName();
}
}
}
return CallerData.NA;
}
3.2.3 LineOfCallerConverter.java
功能说明:logback.xml中pattern将业务类的具体行号打印出来,重新实现该类的逻辑可以打印出真实的业务类的具体行号,而不是Logger这个类的行号。如果不重新实现该类,打印出来的行号都是Logger类中打印日志的行号,体现不出来日志输出的真实的业务类的具体行号。
核心代码:
@Override
public String convert(ILoggingEvent le) {
StackTraceElement[] cda = le.getCallerData();
if (cda != null) {
for (int i = 0; i < cda.length; i++) {
StackTraceElement element = cda[i];
if (element != null) {
String className = element.getClassName();
if ("org.apache.log4j.Logger".equals(className)) {
// 如果是新建的桥接类,直接取下一个类的方法名,这样才能真实的获取到打印日志的业务类的方法名
continue;
}
return Integer.toString(element.getLineNumber());
}
}
}
return CallerData.NA;
}
3.2.4 LogbackConfigLoader.java
功能说明:手动的加载logback.xml配置文件,为日志打印做好前提准备。
核心代码:
public static void load(ILoggerFactory factory,String externalConfigFileLocation) throws IOException, JoranException {
LoggerContext lc = (LoggerContext) factory;
File externalConfigFile = new File(externalConfigFileLocation);
if (!externalConfigFile.exists()) {
throw new IOException("Logback External Config File Parameter does not reference a file that exists");
} else {
if (!externalConfigFile.isFile()) {
throw new IOException("Logback External Config File Parameter exists, but does not reference a file");
} else {
if (!externalConfigFile.canRead()) {
throw new IOException("Logback External Config File exists and is a file, but cannot be read.");
} else {
JoranConfigurator configurator = new JoranConfigurator();
configurator.setContext(lc);
lc.reset();
configurator.doConfigure(externalConfigFileLocation);
StatusPrinter.printInCaseOfErrorsOrWarnings(lc);
}
}
}
}
3.2.5 AbstractLogbackInitListener.java
功能说明:该类为抽象类,不可直接实例化,该类调用LogbackConfigLoader类来加载logback.xml配置,logback.xml配置文件所在的路径通过抽象方法getLogbackAbsolutePath()获取,该抽象方法在具体的业务系统要集成的时候写一个类来实现这个抽象类,将自己系统中的配置文件所在的路径返回即可。
核心代码:
public abstract class AbstractLogbackInitListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent servletContextEvent) {
// 获得配置文件
String configFile = getLogbackAbsolutePath();
System.out.println("获得日志配置文件为:" + configFile);
try {
LogbackConfigLoader.load(getLoggerContext(), configFile);
} catch (IOException e) {
System.err.println("加载logback配置文件失败");
e.printStackTrace();
} catch (JoranException e) {
System.err.println("加载logback配置文件失败");
e.printStackTrace();
}
// 加载XML配置文件(每半小时扫描一次配置文件,如果有更新,自动加载)
System.out.println("初始化日志组件成功");
}
/**
* 获取logback.xml的绝对路径
*
* @return logback.xml所在的绝对路径,包括文件名称
*/
public abstract String getLogbackAbsolutePath();
/**
* 返回ILoggerFactory的实现类LoggerContext
* <p>
* 有两种方式:
* 方式一:org.slf4j.LoggerFactory.getILoggerFactory()
* 方式二:org.slf4j.override.LoggerFactory.getILoggerFactory()
* <p>
* 区别:如果不修改webapp/WEB-INF/jboss-deployment-structure.xml,前者和jboss有冲突
* 建议:使用方式二
*
* @return ILoggerFactory的实现类,LoggerContext
*/
public ILoggerFactory getLoggerContext() {
return LoggerFactory.getILoggerFactory();
}
@Override
public void contextDestroyed(ServletContextEvent servletContextEvent) {
}
}
3.2.6 DefaultLogbackInitListener.java
功能说明:该类是提供的一个AbstractLogbackInitListener抽象类的默认继承类,不具有实际意义(不同系统路径不同),仅提供参考。
核心代码:
public class DefaultLogbackInitListener extends AbstractLogbackInitListener {
@Override
public String getLogbackAbsolutePath() {
// 日志配置路径
String logPath = "config/uspc/log";
// 日志配置文件
String logFile = "logback.xml";
// 获得配置文件
StringBuilder logbackFile = new StringBuilder(System.getProperty("user.dir"));
logbackFile = logbackFile.append(File.separator).append(logPath).append(File.separator).append(logFile);
return logbackFile.toString();
}
}
3.2.7 Logger.java
功能说明:业务系统中原来使用的log4j,使用的Logger的路径正好是org.apache.log4j.Logger,为了不改动业务代码,因此新建一个同路径同名类来做桥接,这里面的日志打印方法均是调用的logback日志框架的方法,因此日志输出也会按照logback.xml的配置进行输出。
注意:可能这里会有疑问,log4j包有个org.apache.log4j.Logger类,该组件包也有一个org.apache.log4j.Logger类,会不会冲突呢,这是需要了解JVM类的加载机制。一般情况下,不同jar包,相同类(路径和类名都相同)的加载顺序是按照jar包的字母排序加载的,比如该组件命名为
bdc-component-log4j2logback-1.0.0.RELEASE.jar
,默认的log4j的包为log4j-1.2.17.jar
,按照字母排序,组件jar包中的Logger先加载,而log4j中的Logger类将不会被JVM加载,因此不会冲突。可通过增加-XX:+TraceClassLoading
JVM参数来观察类的加载顺序。
核心代码:
package org.apache.log4j;
import com.xxxx.bdc.component.log4j2logback.bridge.JdbcLogger;
import org.slf4j.Marker;
import org.slf4j.override.LoggerFactory;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author zhuxiaolong@xxxx.com
* @project BDC
* @package org.apache.log4j
* @date 2020/6/16 11:11
**/
public class Logger implements org.slf4j.Logger, JdbcLogger {
private String name;
private static org.slf4j.Logger logger;
private static final Logger self = new Logger();
public static Logger getLogger(Class cls) {
// 特别注意:
// 这里必须用org.slf4j.override.LoggerFactory类来获得Logger,
// 否则通过org.slf4j.LoggerFactory来获取,会取到jboss对org.slf4j.Logger接口的实现类
// slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar\org\slf4j\impl\Slf4jLogger.class,导致日志不能按照logback.xml输出
// 如果服务容器不是jboss,使用两者都可以
// 如果从jboss层面或者jboss-deployment-structure.xml中排除了对slf4j的依赖,使用两者都可以
// 业务系统中使用哪个LoggerFactory也需要按照如上要求来
logger = LoggerFactory.getILoggerFactory().getLogger(getClassName());
self.name = cls.getName();
return self;
}
public static Logger getLogger(String name) {
self.name = name;
return self;
}
private static String getClassName() {
return new SecurityManager() {
public String getClassName() {
return getClassContext()[3].getName();
}
}.getClassName();
}
3.2.8 logback.xml
功能说明:日志输出的核心配置,组件提供默认的配置
核心代码:
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<!--此处的logRootDir的值是tomcat/bin或者jboss/bin目录下-->
<property name="logRootDir" value="log"/>
<property name="businessModule" value="uspc"/>
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoder 默认配置为PatternLayoutEncoder -->
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%level] [%class.%method:%line]- %m %n</pattern>
</encoder>
</appender>
<appender name="SQL_DEBUG" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${logRootDir}/${businessModule}/sql/debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- Rollover everyday. If file exceeds 1GB within a day, then file is archived with index starting from 0 -->
<fileNamePattern>${logRootDir}/${businessModule}/sql/debug.log-%d{yyyyMMdd}-%i</fileNamePattern>
<!-- Each file should be at most 50MB -->
<maxFileSize>50MB</maxFileSize>
<!-- Keep maximum 30 days worth of archive files, deleting older ones -->
<maxHistory>30</maxHistory>
<!-- Total size of all archived files is at most 5GB -->
<totalSizeCap>5GB</totalSizeCap>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] [%level] [%class.%method:%line]- %m %n</pattern>
<!--配置伪true,实时输出日志到文件中,如果需要提升性能,将其改为false,启用缓冲区-->
<immediateFlush>true</immediateFlush>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
....省略
</configuration>
3.2.9 log4j-1.2.17.jar
功能说明:按照目前的了解,日志输出虽然是采用的logback日志框架输出,但log4j的包还不能移除,有些三方jar比如freemarker、velocity等依赖了低版本的log4j,移除后会报错,如果一定要移除,需要自己单独新增缺少的类来桥接,很麻烦,因此建议不移除。
3.2.10 logback-core-1.2.3.jar、logback-classic-1.2.3.jar
功能说明:日志框架,日志输出的核心代码实现均在这两个类
4 如何使用
4.1 业务系统集成
将日志组件集成到业务系统侵入比较小,主要有3处改动的地方
4.1.1 组件jar依赖
需要将组件jar放到项目的lib依赖库中
4.1.2 增加一个类继承AbstractLogbackInitListener
业务系统的logback.xml在哪里,由具体的业务系统决定,因此要通过组件加载到logback.xml,就需要集成AbstractLogbackInitListener抽象类,实现抽象方法
/**
* 获取logback.xml的绝对路径
*
* @return logback.xml所在的绝对路径,包括文件名称
*/
public abstract String getLogbackAbsolutePath();
方法返回值为logback.xml的绝对路径
4.1.3 在web.xml中增加监听器配置
AbstractLogbackInitListener抽象类实现了ServletContextListener接口,因此在web.xml中增加<listener></listener>
的配置,可以在项目启动的时候就通过组件来讲logback.xml加载并初始化,便于打印出启动及以后的日志。listener的加载顺序是按照listener的配置顺序加载,因此将该类配置在第一个位置首先加载。这里假设新建的类为LogbackInitListener,样例配置如下
<listener>
<listener-class>com.xxxx.sims.rtplt.component.listener.LogbackInitListener</listener-class>
</listener>
另外,如果没有web.xml,是springboot工程,更加的灵活,只要在项目启动的时候主动调用下contextInitialized方法即可。
4.1.4 修改jboss-deployment-structure.xml(可选)
修改配置文件webapp/WEB-INF/jboss-deployment-structure.xml
增加:
<module name="org.slf4j.impl"/>
<module name="org.slf4j"/>
<module name="org.jboss.logmanager"/>
到配置文件中
目的:如果项目是通过jboss部署,会和jboss自身依赖的slf4j冲突(与tomcat无冲突),增加改配置可防止冲突,此步骤可选,因为已经从组件上来解决了与jboss的冲突。但如果系统中有主动使用org.slf4j.LoggerFactory类来获取日志打印类,该配置必须,是否会获取到jboss对org.slf4j.Logger的实现(slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar\org\slf4j\impl\Slf4jLogger.class),导致打印不出日志。
<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure>
<deployment>
<!-- Exclusions allow you to prevent the server from automatically adding some dependencies -->
<exclusions>
<module name="org.slf4j.impl"/>
<module name="org.slf4j"/>
<module name="org.jboss.logmanager"/>
<module name="org.apache.log4j"/>
<module name="org.apache.commons.logging"/>
</exclusions>
</deployment>
</jboss-deployment-structure>
4.2 测试
项目启动便可测试,看日志是否按照logback.xml的配置进行输出。
5 特别注意
5.1 集成问题
5.1.1 类型强制转换失败问题(已修复)
在前期未发现与jboss有冲突的时候,集成了该jar包之后,war包在jboss中启动会报错:java.lang.ClassCastException: org.slf4j.impl.Slf4jLoggerFactory cannot be cast to ch.qos.logback.classic.LoggerContext
原因:org.slf4j.LoggerFactory.getILoggerFactory()获取到的接口实现类为jboss中的实现类,因为jboss对slf4j相关的实现类先加载,因此在此时调用LoggerFactory.getILoggerFactory()获取到的是jboss的实现,强转成LoggerContext会报错,而logback对slf4j的实现通过LoggerFactory.getILoggerFactory()获取的正是LoggerContext类。
方案一:
从jboss本身入手,排除掉slf4j相关jar的依赖,可能存在潜在问题,毕竟动了jboss
优点:可彻底解决业务系统和jboss的slf4j的任何冲突问题
弊端:可能会影响jboss
- 去掉slf4j-api-1.7.2-redhat-1.jar的依赖,注释掉如下依赖
/opt/xxxx/product/bdc/eapjboss/modules/system/layers/base/org/slf4j/main/slf4j-api-1.7.2-redhat-1.jar
<module xmlns="urn:jboss:module:1.1" name="org.slf4j">
<resources>
<!--<resource-root path="slf4j-api-1.7.2-redhat-1.jar"/>-->
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.slf4j.impl"/>
</dependencies>
</module>
- 去掉slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar的依赖,注释掉如下配置
/opt/xxxx/product/bdc/eapjboss/modules/system/layers/base/org/slf4j/impl/main/slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar
<module xmlns="urn:jboss:module:1.1" name="org.slf4j.impl">
<properties>
<property name="jboss.api" value="private"/>
</properties>
<resources>
<!--<resource-root path="slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar"/>-->
<!-- Insert resources here -->
</resources>
<dependencies>
<module name="org.slf4j"/>
<module name="org.jboss.logmanager"/>
</dependencies>
</module>
方案二:
从应用着手解决冲突
修改webapp\WEB-INF\jboss-deployment-structure.xml
优点:也可以有效解决与jboss的冲突
弊端:每个业务系统都将增加该配置,如果不是传统的war服务,暂时可能没有该配置。
增加依赖排除配置:
<module name="org.slf4j.impl"/>
<module name="org.slf4j"/>
<module name="org.jboss.logmanager"/>
<?xml version='1.0' encoding='UTF-8'?>
<jboss-deployment-structure>
<deployment>
<!-- Exclusions allow you to prevent the server from automatically adding some dependencies -->
<exclusions>
<module name="org.slf4j.impl"/>
<module name="org.slf4j"/>
<module name="org.jboss.logmanager"/>
<module name="org.apache.log4j" />
<module name="org.apache.commons.logging" />
</exclusions>
</deployment>
</jboss-deployment-structure>
5.1.2 日志不打印问题(已修复)
在修复前,桥接类Logger用的是org.slf4j.LoggerFactory.getLogger(),这样获得的是jboss对org.slf4j.Logger的实现(slf4j-jboss-logmanager-1.0.2.GA-redhat-1.jar\org\slf4j\impl\Slf4jLogger.class),那就不能按照logback.xml的配置进行日志输出,修复后是通过重写了LoggerFactory类,改了包路径,目的是为了桥接类里面能得到logback对slf4j的实现。
5.2 如何使用LoggerFactory(重要)
-
如果容器用的jboss,webapp/WEB-INF/jboss-deployment-structure.xml配置排除了slf4j的依赖,那么随便用org.slf4j.LoggerFactory或者org.slf4j.override.LoggerFactory(组件重新实现)都可以。
-
如果容器是jboss,但webapp/WEB-INF/jboss-deployment-structure.xml未排除slf4j的依赖,那使用org.slf4j.override.LoggerFactory才会打印出日志,用org.slf4j.LoggerFactory不会打印出日志
-
如果容器不是jboss,则没有限制,使用两者都可以。
6 附录:logback取代log4j的理由
- 更快的实现:Logback的内核重写了,在一些关键执行路径上性能提升10倍以上。而且logback不仅性能提升了,初始化内存加载也更小了。
- 非常充分的测试:Logback经过了几年,数不清小时的测试。Logback的测试完全不同级别的。
- logback-classic非常自然实现了SLF4j:Logback-classic实现了SLF4j。在使用SLF4j中,你都感觉不到logback-classic。而且因为logback-classic非常自然地实现了slf4j , 所 以切换到log4j或者其他,非常容易,只需要提供成另一个jar包就OK,根本不需要去动那些通过SLF4JAPI实现的代码。
- 非常充分的文档 官方网站有两百多页的文档。
- 自动重新加载配置文件,当配置文件修改了,Logback-classic能自动重新加载配置文件。扫描过程快且安全,它并不需要另外创建一个扫描线程。这个技术充分保证了应用程序能跑得很欢在JEE环境里面。
- Lilith是log事件的观察者,和log4j的chainsaw类似。而lilith还能处理大数量的log数据 。
- 谨慎的模式和非常友好的恢复,在谨慎模式下,多个FileAppender实例跑在多个JVM下,能 够安全地写道同一个日志文件。RollingFileAppender会有些限制。Logback的FileAppender和它的子类包括 RollingFileAppender能够非常友好地从I/O异常中恢复。
- 配置文件可以处理不同的情况,开发人员经常需要判断不同的Logback配置文件在不同的环境下(开发,测试,生产)。而这些配置文件仅仅只有一些很小的不同,可以通过,和来实现,这样一个配置文件就可以适应多个环境。
- Filters(过滤器)有些时候,需要诊断一个问题,需要打出日志。在log4j,只有降低日志级别,不过这样会打出大量的日志,会影响应用性能。在Logback,你可以继续 保持那个日志级别而除掉某种特殊情况,如alice这个用户登录,她的日志将打在DEBUG级别而其他用户可以继续打在WARN级别。要实现这个功能只需加4行XML配置。可以参考MDCFIlter 。
- SiftingAppender(一个非常多功能的Appender):它可以用来分割日志文件根据任何一个给定的运行参数。如,SiftingAppender能够区别日志事件跟进用户的Session,然后每个用户会有一个日志文件。 11. 自动压缩已经打出来的log:RollingFileAppender在产生新文件的时候,会自动压缩已经打出来的日志文件。压缩是个异步过程,所以甚至对于大的日志文件,在压缩过程中应用不会受任何影响。
- 堆栈树带有包版本:Logback在打出堆栈树日志时,会带上包的数据。
- 自动去除旧的日志文件:通过设置TimeBasedRollingPolicy或者SizeAndTimeBasedFNATP的maxHistory属性,你可以控制已经产生日志文件的最大数量。如果设置maxHistory 12,那那些log文件超过12个月的都会被自动移除。