文章目录
日志门面
日志门面概述
门面模式(外观模式)
- 日志门面
- 日志门面概述
- 门面模式(外观模式)
- 日志门面
- 常见的日志框架及日志门面
- JCL简介
- SLF4J
- SLF4J简介
- SLF4J桥接技术
- SLF4J特点
- SLF4J集成日志实现(jul,log4j,logback等)
- SLF4J的执行原理
- 通过slf4j日志门面无缝将log4j替换为logback
- 我们先谈一谈GoF23种设计模式其中之一。
- 门面模式(Facade Pattern),也称之为外观模式,其核心为:
外部与一个子系统的通信必须通过一个统一的外观对象进行,使得子系统更易于使用
。 - 外观模式主要是体现了Java中的一种好的封装性。更简单的说,就是对外提供的接口要尽可能的简单。
- 前面介绍的几种日志框架,每一种日志框架都有自己单独的API,要使用对应的框架就要使用其对应的API,这就大大的增加应用程序代码对于日志框架的耦合性。
- 为了解决这个问题,就是在日志框架和应用程序之间架设一个沟通的桥梁,对于应用程序来说,无论底层的日志框架如何变,都不需要有任何感知。只要门面服务做的足够好,随意换另外一个日志框架,应用程序不需要修改任意一行代码,就可以直接上线。
- 常见的日志实现:JUL、log4j、logback、log4j2
- 常见的日志门面 :JCL、slf4j
- 出现顺序 :log4j -->JUL–>JCL–> slf4j --> logback --> log4j2
- 全称为Jakarta Commons Logging,是Apache提供的一个
通用日志API
。 - 用户可以自由选择第三方的日志组件作为具体实现,像log4j,或者jdk自带的jul, common-logging会通过
动态查找
的机制,在程序运行时自动找出真正使用的日志库。 - 当然,common-logging内部有一个Simple logger的简单实现,但是功能很弱。所以使用common-logging,通常都是配合着log4j以及其他日志框架来使用。
- 使用它的好处就是,代码依赖是common-logging而非log4j的API, 避免了和具体的日志API直接耦合,在有必要时,可以更改日志实现的第三方库。
- JCL 有两个基本的抽象类:
- Log:日志记录器
- LogFactory:日志工厂(负责创建Log实例)
使用JCL日志门面, 如果什么第三方日志框架都没有导入, 就默认采用JUL日志(Java自带); 如果在pom文件中导入了log4j的jar包, 就会使用log4j作为日志实现; 因为JCL内部对各种日志进行了优先级排序. 第一位的就是Log4j
@Test
public void test01(){
/*
我们暂时没有导入第三方的日志框架,例如log4j
默认的情况下,会使用JUL日志框架做日志的记录操作
JCL使用原则:
如果有log4j,优先使用log4j
如果没有任何第三方日志框架的时候,我们使用的就是JUL
*/
Log log = LogFactory.getLog(JCLTest01.class);
log.info("info信息");
}
@Test
public void test02(){
/*
导入log4j依赖,继续测试原有程序
总结:
我们上一个案例,使用的是JUL,但是在集成了log4j环境后,使用的又是log4j
通过测试观察,虽然日志框架发生了变化,但是代码完全没有改变
日志门面技术的好处:
门面技术是面向接口的开发,不再依赖具体的实现类,减少代码的耦合性
可以根据实际需求,灵活的切换日志框架
统一的API,方便开发者学习和使用
统一的配置管理便于项目日志的维护工作
查看源码:
Log接口的4个实现类
JDk13
JDK14 正常java.util.logging
Log4j 我们集成的log4j
Simple JCL自带实现类
(1)查看Jdk14Logger证明里面使用的是JUL日志框架
(2)查看Log4JLogger证明里面使用的是Log4j日志框架
(3)观察LogFactory,看看如何加载的Logger对象
这是一个抽象类,无法实例化
需要观察其实现类LogFactoryImpl
(4)观察LogFactoryImpl
真正加载日志实现使用的就是这个实现类LogFactoryImpl
(5)进入getLog
进入getInstance
找到instance = this.newInstance(name);,继续进入
找到instance = this.discoverLogImplementation(name); 表示发现一个日志的实现
for(int i = 0; i < classesToDiscover.length && result == null; ++i) {
result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
}
遍历我们拥有的日志实现框架
遍历的是一个数组,这个数组是按照
log4j
jdk14
jdk13
SimpleLogger
的顺序依次遍历
表示的是,第一个要遍历的就是log4j,如果有log4j则执行该日志框架
如果没有,则遍历出来第二个,使用jdk14的JUL日志框架
以此类推
result = this.createLogFromClass(classesToDiscover[i], logCategory, true);
表示帮我们创建Logger对象
在这个方法中,我们看到了
c = Class.forName(logAdapterClassName, true, currentCL);
是取得该类型的反射类型对象
使用反射的形式帮我们创建logger对象
constructor = c.getConstructor(this.logConstructorSignature);
*/
Log log = LogFactory.getLog(JCLTest01.class);
log.info("info信息");
}
SLF4J
SLF4J简介
- 简单日志门面(Simple Logging Facade For Java) SLF4J主要是为了给Java日志访问提供一套标准、规范的API框架,其主要意义在于提供接口,具体的实现可以交由其他日志框架,例如log4j和logback等。
- 当然slf4j自己也提供了功能较为简单的实现,但是一般很少用到。对于一般的Java项目而言,日志框架会选择slf4j-api作为门面,配上具体的实现框架(log4j、logback等),中间使用桥接器完成桥接。
- 所以我们可以得出 SLF4J最重要的两个功能就是对于
日志框架的绑定
以及日志框架的桥接
。 官方网站: https://www.slf4j.org/
- 通常,我们依赖的某些组件(Log4j、JUL)依赖于SLF4J以外的日志API; (因为Log4j、JUL比SLF4j出现的还早, 肯定没有遵循SLF4J日志门面的API)。
- 如果这些组件将来不会切换到SLF4J的规则。为了处理这种情况,
SLF4J附带了几个桥接模块,这些模块会将对log4j,JCL和java.util.logging API的调用重定向为行为,就好像是对SLF4J API进行的操作一样
。
org.slf4j
slf4j-api
1.7.25
@Test
public void test01(){
/*
入门案例
SLF4J对日志的级别划分
trace、debug、info、warn、error五个级别
trace:日志追踪信息
debug:日志详细信息
info:日志的关键信息 默认打印级别
warn:日志警告信息
error:日志错误信息
在没有任何其他日志实现框架集成的基础之上
slf4j使用的就是自带的框架slf4j-simple
slf4j-simple也必须以单独依赖的形式导入进来
org.slf4j
slf4j-simple
1.7.25
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
logger.trace("trace信息");
logger.debug("debug信息");
logger.info("info信息");
logger.warn("warn信息");
logger.error("error信息");
}
@Test
public void test02(){
/*
我们输出动态的信息时
也可以使用占位符的形式来代替字符串的拼接
我们有些时候输出的日志信息,需要我们搭配动态的数据
有可能是信息,有可能是数据库表中的数据
总之我们这样做最大的好处就是能够让日志打印变得更加灵活
如果是通过拼接字符串的形式,不仅麻烦,而且更重要的是可读性查
我们的日志打印是支持以替代符的形式做日志信息拼接的
一般情况下,几乎所有的日志实现产品,都会提供这种基础功能
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
String name = "zs";
int age = 23;
//logger.info("学生信息-姓名:"+name+";年龄:"+age);
//logger.info("学生信息-姓名:{},年龄:{}",new Object[]{name,age});
logger.info("学生信息-姓名:{},年龄:{}",name,age);
}
@Test
public void test03(){
/*
日志对于异常信息的处理
一般情况下,我们在开发中的异常信息,都是记录在控制台上(我们开发环境的一种日志打印方式)
我们会根据异常信息提取出有用的线索,来调试bug
但是在真实生产环境中(项目上线),对于服务器或者是系统相关的问题
在控制台上其实也会提供相应的异常或者错误信息的输出
但是这种错误输出方式(输出的时间,位置,格式...)都是服务器系统默认的
我们可以通过日志技术,选择将异常以日志打印的方式,进行输出查看
输出的时间,位置(控制台,文件),格式,完全由我们自己去进行定义
*/
//System.out.println(123);
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
try {
Class.forName("aaa");
} catch (ClassNotFoundException e) {
//打印栈追踪信息
//e.printStackTrace();
logger.info("XXX类中的XXX方法出现了异常,请及时关注信息");
//e是引用类型对象,不能根前面的{}做有效的字符串拼接
//logger.info("具体错误是:{}",e);
//我们不用加{},直接后面加上异常对象e即可
logger.info("具体错误是:",e);
}
}
SLF4J集成日志实现(jul,log4j,logback等)
@Test
public void test04(){
/*
集成其他日志实现之前
观察官网图
SLF4J日志门面,共有3种情况对日志实现进行绑定
1.在没有绑定任何日志实现的基础之上,日志是不能够绑定实现任何功能的
值得大家注意的是,通过我们刚刚的演示,slf4j-simple是slf4j官方提供的
使用的时候,也是需要导入依赖,自动绑定到slf4j门面上
如果不导入,slf4j 核心依赖是不提供任何实现的
2.logback和simple(包括nop)(图中蓝色部分)
都是slf4j门面时间线后面提供的日志实现,所以API完全遵循slf4j进行的设计
那么我们只需要导入想要使用的日志实现依赖,即可与slf4j无缝衔接
值得一提的是nop虽然也划分到实现中了,但是他是指不实现日志记录(后续课程)
3.log4j和JUL (图中青色部分)
都是slf4j门面时间线前面的日志实现,所以API不遵循slf4j进行设计
通过适配桥接的技术,完成的与日志门面的衔接
试着将logback日志框架集成进来
测试1:
在原有slf4j-simple日志实现的基础上,又集成了logback
通过测试,日志是打印出来了 java.lang.ClassNotFoundException: aaa
通过这一句可以发现SLF4J: Actual binding is of type [org.slf4j.impl.SimpleLoggerFactory]
虽然集成了logback,但是我们现在使用的仍然是slf4j-simple
事实上只要出现了这个提示
SLF4J: Class path contains multiple SLF4J bindings. // 表示有slf4j有多个日志实现
在slf4j环境下,证明同时出现了多个日志实现
如果先导入logback依赖,后导入slf4j-simple依赖
那么默认使用的就是logback依赖
注意: 如果有多个日志实现的话,默认使用先导入的实现
测试:
将slf4j-simple注释掉
只留下logback,那么slf4j门面使用的就是logback日志实现
值得一提的是,这一次没有多余的提示信息
所以在实际应用的时候,我们一般情况下,仅仅只是做一种日志实现的集成就可以了
ch.qos.logback
logback-classic
1.2.3
通过这个集成测试,我们会发现虽然底层的日志实现变了,但是源代码完全没有改变
这就是日志门面给我们带来最大的好处
在底层真实记录日志的时候,不需要应用去做任何的了解
应用只需要去记slf4j的API就可以了
值得一提的是,我们虽然底层使用的是log4j做的打印,但是从当前代码使用来看
我们其实使用的仍然是slf4j日志门面,至于日志是log4j打印的(或者是logback打印的)
都是由slf4j进行操作的,我们不用操心
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
try {
Class.forName("aaa");
} catch (ClassNotFoundException e) {
logger.info("具体错误是:",e);
}
}
@Test
public void test05(){
/*
使用slf4j-nop
表示不记录日志
在我们使用slf4j-nop的时候
首先还是需要导入实现依赖
这个实现依赖,根据我们之前所总结出来的日志日志实现种类的第二种
与logback和simple是属于一类的
通过集成依赖的顺序而定
所以如果想要让nop发挥效果,禁止所有日志的打印
那么就必须要将slf4j-nop的依赖放在所有日志实现依赖的上方
org.slf4j
slf4j-nop
1.7.25
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
try {
Class.forName("aaa");
} catch (ClassNotFoundException e) {
logger.info("具体错误是:",e);
}
}
@Test
public void test06(){
/*
接下来我们来绑定log4j
由于log4j是在slf4j之前出品的日志框架实现
所以并没有遵循slf4j的API规范
(之前集成的logback,是slf4j之后出品的日志框架实现
logback就是按照slf4j的标准指定的API,所以我们导入依赖就能用)
如果想要使用,需要绑定一个适配器
叫做slf4j-log4j12
再导入log4j的实现
org.slf4j
slf4j-log4j12
1.7.25
导入log4j依赖
log4j
log4j
1.2.17
测试:
log4j:WARN No appenders could be found for logger (com.bjpowernode.slf4j.test01.SLF4JTest01).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
虽然日志信息没有打印出来,那么根据警告信息可以得出:
使用了log4j日志实现框架
提示appender没有加载,需要在执行日志之前做相应的加载工作(初始化)
我们可以将log4j的配置文件导入使用
测试结果为log4j的日志打印,而且格式和级别完全是遵循log4j的配置文件进行的输出
*//*
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
logger.trace("trace信息");
logger.debug("debug信息");
logger.info("info信息");
logger.warn("warn信息");
logger.error("error信息");
}
@Test
public void test07(){
/*
接下来我们来适配JDK14
与上一个测试log4j导入适配器一样
JUL也是slf4j之前出品的日志实现框架
所以也需要相应的适配器: slf4j-jdk14
org.slf4j
slf4j-jdk14
1.7.25
适配器导入之后,JUL日志实现是不用导入依赖的
因为JUL,是JDK内置的
从测试结果来看,是JUL的日志打印,默认是info级别日志的输出
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
logger.trace("trace信息");
logger.debug("debug信息");
logger.info("info信息");
logger.warn("warn信息");
logger.error("error信息");
}
SLF4J的执行原理
@Test
public void test08(){
/*
绑定多个日志实现,会出现警告信息
通过源码来看看其原理(看看slf4j的执行原理)
进入到getLogger
看到Logger logger = getLogger(clazz.getName());
进入重载的getLogger
ILoggerFactory iLoggerFactory = getILoggerFactory(); 用来取得Logger工厂实现的方法
进入getILoggerFactory()
看到以双重检查锁的方式去做判断
执行performInitialization(); 工厂的初始化方法
进入performInitialization()
bind()就是用来绑定具体日志实现的方法
进入bind()
看到Set集合Set staticLoggerBinderPathSet = null;
因为当前有可能会有N多个日志框架的实现
看到staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
进入findPossibleStaticLoggerBinderPathSet()
看到创建了一个有序不可重复的集合对象
LinkedHashSet staticLoggerBinderPathSet = new LinkedHashSet();
声明了枚举类的路径,经过if else判断,以获取系统中都有哪些日志实现
看到Enumeration paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
我们主要观察常量STATIC_LOGGER_BINDER_PATH
通过常量我们会找到类StaticLoggerBinder
这个类是以静态的方式绑定Logger实现的类
来自slf4j-JDK14的适配器
进入StaticLoggerBinder
看到new JDK14LoggerFactory();
进入JDK14LoggerFactory类的无参构造方法
看到java.util.logging.Logger.getLogger("");
使用的就是jul的Logger
接着观察findPossibleStaticLoggerBinderPathSet
看到以下代码,表示如果还有其他的日志实现
while(paths.hasMoreElements()) {
URL path = (URL)paths.nextElement();
将路径添加进入
staticLoggerBinderPathSet.add(path);
}
回到bind方法
表示对于绑定多实现的处理
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
如果出现多日志实现的情况
则会打印
Util.report("Class path contains multiple SLF4J bindings.");
总结:
在真实生产环境中,slf4j只绑定一个日志实现框架就可以了
绑定多个,默认使用导入依赖的第一个,而且会产生没有必要的警告信息
*/
Logger logger = LoggerFactory.getLogger(SLF4JTest01.class);
logger.trace("trace信息");
logger.debug("debug信息");
logger.info("info信息");
logger.warn("warn信息");
logger.error("error信息");
}
通过slf4j日志门面无缝将log4j替换为logback
不修改任何源代码; 扔使用log4j的API
@Test
public void test09(){
/*
需求:
假设我们项目一直以来使用的是log4j日志框架
但是随着技术和需求的更新换代
log4j已然不能够满足我们系统的需求
我们现在就需要将系统中的日志实现重构为 slf4j+logback的组合
在不触碰java源代码的情况下,将这个问题给解决掉
首先将所有关于其他日志实现和门面依赖全部去除
仅仅只留下log4j的依赖
测试的过程中,只能使用log4j相关的组件
此时需要将日志替换为slf4j+logback
我们既然不用log4j了,就将log4j去除
将slf4j日志门面和logback的日志实现依赖加入进来
这样做,没有了log4j环境的支持,编译报错
这个时候就需要使用桥接器来做这个需求了
桥接器解决的是项目中日志的重构问题,当前系统中存在之前的日志API,可以通过桥接转换到slf4j的实现
桥接器的使用步骤:
1.去除之前旧的日志框架依赖
log4j
log4j
1.2.17
2.添加slf4j提供的桥接组件
log4j相关的桥接器
org.slf4j
log4j-over-slf4j
1.7.25
桥接器加入后,代码编译就不报错了
测试:
日志信息输出
输出格式为logback
证明了现在使用的确实是slf4j门面+logback实现
在重构之后,就会为我们造成这样一种假象
使用的明明是log4j包下的日志组件资源
但是真正日志的实现,却是使用slf4j门面+logback实现
这就是桥接器给我们带来的效果
注意:
在桥接器加入之后,适配器就没有必要加入了
桥接器和适配器不能同时导入依赖
桥接器如果配置在适配器的上方,则运行报错,不同同时出现
桥接器如果配置在适配器的下方,则不会执行桥接器,没有任何的意义
*/
Logger logger = LogManager.getLogger(SLF4JTest01.class);
logger.info("info信息");
}
@Test
public void test10(){
/*
在配置了桥接器之后,底层就是使用slf4j实现的日志
分析其中原理
通过getLogger
进入Log4jLoggerFactory
Logger newInstance = new Logger(name); 新建logger对象
进入构造方法
protected Logger(String name) {
super(name);
}
点击进入父类的构造方法
Category(String name) {
this.name = name;
this.slf4jLogger = LoggerFactory.getLogger(name);
if (this.slf4jLogger instanceof LocationAwareLogger) {
this.locationAwareLogger = (LocationAwareLogger)this.slf4jLogger;
}
}
在这个Category构造方法中,核心代码
this.slf4jLogger = LoggerFactory.getLogger(name);
LoggerFactory来自于org.slf4j
*/
Logger logger = LogManager.getLogger(SLF4JTest01.class);
}