一、搭建log4j日志框架环境并输出日志格式
1.1导入log4j依赖
导入apache下的log4j的jar包
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
导入测试包,用于测试日志输出
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
</dependency>
1.2 配置log4j.properties文件
在实际生产开发中,由于硬编码方式需要修改源码,不便于维护,故统一使用配置文件的方式来管理日志。这里由于只做测试使用,故在配置文件中仅配置控制台打印输出即可
#配置根节点:第一个参数为info,即默认打印输出级别,第二个参数为console,即默认控制台打印输出
log4j.rootLogger=info,console
#配置appender输出方式:控制台方式输出
log4j.appender.console=org.apache.log4j.ConsoleAppender
#配置输出格式:默认系统输出方式,此处不适用默认格式,使用下述的自定义格式
#log4j.appender.console.layout=org.apache.log4j.SimpleLayout
#配置输出格式:设置为自定义输出方式
log4j.appender.console.layout=org.apache.log4j.PatternLayout
#自定义输出格式
log4j.appender.console.layout.conversionPattern= [%-6p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
自定义格式中个占位符的含义
%m 输出代码中指定的日志信息
%p 输出优先级,及 DEBUG、INFO 等
%n 换行符(Windows平台的换行符为 "\n",Unix 平台为 "\n")
%r 输出自应用启动到输出该 log 信息耗费的毫秒数
%c 输出打印语句所属的类的全名
%t 输出产生该日志的线程全名
%d 输出服务器当前时间,默认为 ISO8601,也可以指定格式,如:%d{yyyy年MM月dd日 HH:mm:ss.SSS}
%l 输出日志时间发生的位置,包括类名、线程、及在代码中的行数。如:Test.main(Test.java:10)
%F 输出日志消息产生时所在的文件名称
%L 输出代码中的行号
%% 输出一个 "%" 字符
[%p]%r %c%t%d{yyyy-MM-dd HH:mm:ss:SSS} %m%n
可以在 % 与字符之间加上修饰符来控制最小宽度、最大宽度和文本的对其方式
[%10p]:[]中必须有10个字符,由空格来进行补齐,信息右对齐
[%-10p]:[]中必须有10个字符,由空格来进行补齐,信息左对齐,应用较广泛
1.3测试(未写出完整方法)
//注意:此处的Logger类是org.apache(log4j框架)下的Logger,而非Java原生的日志Logger对象(JUL框架)
Logger logger= LogManager.getLogger(SLF4JTest.class);
//默认info级别打印输出
logger.info("info信息输出");
1.4控制台结果打印
[INFO ]0 com.zm.slf4j.SLF4JTestmain2022-01-14 11:49:01:633 info信息输出
从结果可看出,此日志打印的格式是根据配置文件中自定义日志格式进行输出
二、搭建slf4j+logback日志框架环境并输出日志格式
2.1导入slf4j和logback的依赖
注意:导入时需先将log4j的包注释掉(原因:既然要在不改变源码的情况下改变日志输出的门面,则说明不能使用原来的日志框架,故将其注释掉,以便能很好的显示改变之后的效果)
<!--slf4j 核心依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<!--logback适配器依赖导入-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.10</version>
</dependency>
2.2导入桥接器的依赖
桥接器解决的是项目中日志的重构问题,当前系统中存在之前的日志API,可以通过桥接转换到slf4j的实现
<!--slf4j 桥接器-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.25</version>
</dependency>
2.3 测试1.3的源码并打印结果到控制台
11:52:46.958 [main] INFO com.zm.slf4j.SLF4JTest - info信息输出
可以看出,此时控制台打印的日志格式并不是上述配置文件中自定义的格式,而是logback日志框架默认的日志打印格式
2.4 补充
当去掉log4j的jar包依赖而又未导入桥接器、slf4j、logback的jar包之前,源码代码效果
导入包的效果
注意:此时无需重新导入包,因为导入识别的肯定是非log4j jar包下的Logger对象,无需管理,只需将桥接器、slf4j、logback的jar包导入之后,报错即可消失,由此便实现了在不改变任何一处源码的情况下,改变了日志框架。
三、底层源码实现逻辑
那么,为什么slf4j下的桥接器能起到这种效果呢,还是看看源码来一探究竟吧!
1.首先,log4j下的日志打印时需要调用LogManager来实例化logger记录器对象
即调用LogManager的getLogger方法进行实例化
Logger logger= LogManager.getLogger(SLF4JTest.class);
2.接着进入getLogger方法
可以看出该方法返回了Log4jLoggerFactory类下的getLogger方法
3.进入Log4jLoggerFactory对象
找到该类中的getLogger方法
public static Logger getLogger(String name) {
Logger instance = (Logger)log4jLoggers.get(name);
if (instance != null) {
return instance;
} else {
Logger newInstance = new Logger(name);
Logger oldInstance = (Logger)log4jLoggers.putIfAbsent(name, newInstance);
return oldInstance == null ? newInstance : oldInstance;
}
}
由于此时找不到log4j的jar包,无法对其进行初始化,也就是instance==NULL,则源码走else
新建logger对象
4.进入Logger对象
找到该类中如下的构造方法, 因为无法调用本类中的构造器进行初始化,故只能调用父类的构造器
protected Logger(String name) {
super(name);
}
5.进入super方法即Category类
Category(String name) {
this.name = name;
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
找到父类的构造方法,可以看到如下的核心代码是slf4jLogger
this.slf4jLogger = LoggerFactory.getLogger(name);
即slf4j包下,故LoggerFactory是来自于org.slf4j,所以可以看出,实际上在实例话logger时,是调用的slf4j包下的日志工厂进行实例化,而并非log4j包下的日志工厂。
四、总结
在使用桥接器之后,原有的基于log4j日志框架的源码完全无需修改,只需要在依赖中进行简单的注释导入即可。
桥接器和适配器不能同时导入依赖
桥接器如果配置在适配器的上方,则运行报错,不同同时出现
桥接器如果配置在适配器的下方,则不会执行桥接器,没有任何的意义
注:
适配器作用:在slf4j下集成log4j框架时需要用到,与上述的logback集成依赖属于同一类包