从图中我们可以看出来,官方的方案是针对不同的日志框架,开发了一套适配兼容的框架与之对应,使用这些兼容jar来替代原来的日志框架即可,例如log4j日志框架,与之对应的就是log4j-over-slf4j.jar,并且常见的日志框架,slf4j团队都实现了一套与之对应的基于slf4j的兼容框架,关系如下:
日志框架 | slf4j兼容框架 |
---|---|
log4j | log4j-over-slf4j |
commons logging | jcl-over-slf4j |
java.util.logging | jui-to-slf4j |
SpringBoot如何处理日志关系
在使用SpringBoot的时候,我们会发现官方默认使用的是spring‐boot‐starter‐logging
这个starter来引入日志系统的,我们展开该依赖的依赖图,如下:
可以看到spring‐boot‐starter‐logging这个starter中,引入了四个日志实例的依赖,分别是logback和我们前面提到的日志兼容jar的依赖,并且最终引入了slf4j的日志门面的依赖,实现了统一日志处理。但是为什么兼容jar引入后就能解决日志输出的问题呢?难道兼容包有什么神奇的黑科技吗?其实不然,我们随便展开其中的几个兼容日志jar的包名,如图:
原来这些日志兼容包的包名与原来的日志框架的包名完全一样,并且完全按照slf4j的方式实现了一套和以前一样的API,这样依赖这些日志框架的开源框架在运行的时候查找对应包名下的class也不会报错,但熟悉java类加载机制的都知道,两个jar的包名以及使用的class都一样的话,加载会出现异常,我们进入spring‐boot‐starter‐logging的pom依赖中一探究竟,最后在maven依赖中发现了端倪,如Spring框架使用的是commons-logging,而在spring-boot-starter-logging中,将spring的日志依赖排除,如下:
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring‐core</artifactId>
<exclusions>
<exclusion>
<groupId>commons‐logging</groupId>
<artifactId>commons‐logging</artifactId>
</exclusion>
</exclusions>
</dependency>
这样spring框架在运行时使用的时候,使用的就是兼容jar中的日志实例了,SpringBoot成功的完成了一次日志系统统一的偷天换日操作。
slf4j的桥接原理
通过查看SpringBoot的日志处理,我们可以大致总结如下几步操作:
1、将系统中其他日志框架先排除出去;
2、用中间包来替换原有的日志框架;
3、我们导入slf4j其他的实现
通过以上的操作,即可完成日志系统的统一,但是我们开始有了新的疑惑,slf4j是怎么做到的自动查找对应的实现日志,并且完成了日志的正常打印操作的呢?这个就要涉及到slf4j的桥接原理,我们先来看看slf4j源码中关于日志调用相关的代码:
//slf4j日志调用过程相关的代码
//根据名称获取日志实例
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
//获取日志实例工厂并且完成日志实例的查找与初始化操作
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
//查找实现类
performInitialization();
}
...
return StaticLoggerBinder.getSingleton().getLoggerFactory();
...
}
可以看到整个过程中是通过StaticLoggerBinder.getSingleton() 来进行初始化日志工厂操作,而StaticLoggerBinder
这个类是从哪来的呢?我们发现StaticLoggerBinder
类并不存在于slf4j的jar中,而是通过查找org/slf4j/impl/StaticLoggerBinder.class类的路径来发现具体的实现类,代码如下:
//设置默认的查找日志实例的StaticLoggerBinder路径
private static String STATIC_LOGGER_BINDER_PATH = "org/slf4j/impl/StaticLoggerBinder.class";
private static Set findPossibleStaticLoggerBinderPathSet() {
.......
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
......
}
这个时候我们就该思考一个问题,如果我们同时存在了多个StaticLoggerBinder
时会加载哪一个呢?熟悉java类加载机制可知,类加载器会按照一定的顺序逐个扫描jar包目录并且加载出来,所以先被类加载器扫描的StaticLoggerBinder
会优先被加载,具体的加载顺序如下:
1.$java_home/lib 目录下的java核心api
2.$java_home/lib/ext 目录下的java扩展jar包
3.java -classpath/-Djava.class.path所指的目录下的类与jar包
4.$CATALINA_HOME/common目录下按照文件夹的顺序从上往下依次加载
5.$CATALINA_HOME/server目录下按照文件夹的顺序从上往下依次加载
6.$CATALINA_BASE/shared目录下按照文件夹的顺序从上往下依次加载
7.项目/WEB-INF/classes下的class文件
8.项目/WEB-INF/lib下的jar文件
根据slf4j桥接原理改造logger
我们都知道平时使用slf4j输出日志的时候往往获取Logger
实例来进行日志打印,但是Logger
仅仅支持本地日志,不支持分布式环境的日志,而在slfj中有LogBean
实例,可以支持分布式日志,包含了链路相关信息,那么我们是否可以改造slf4j的桥接过程,使得我们可以灵活的使用本地日志或者分布式日志呢?首先我们先看看我们需要实现的需求:
- logger和logbean结合,统一日志入口
- logbean降低代码侵入性
- 无缝替换第三方框架中的日志,根据需求加入到分布式日志中
想要实现这个功能,有以下两个思路实现:
1.我们通过自定义appender,基于logback的appender进行扩展,可以实现分别输出本地日志以及分布式日志,但是缺陷在于appender扩展性不高,很多参数信息获取不到,例如上下文信息等
2.我们通过实现Logger接口,用来将Logger和LogBean聚合在一起,从而实现LogBean集成到Logger中,同样此种方式的缺陷在于对于第三方框架日志,我们无能为力,无法直接替换使用,并且在使用的时候需要使用自定义的LogFactory
第一种思路我们可以看出来,局限性太高,灵活度不够,接下来我们尝试使用第二种方案,实现聚合Logger和LogBean,对外公开统一的api进行日志输出使用:
public class CustomLogger implements LocationAwareLogger {
private Logger logger;
//提供getLogger方法获取logger
public static LoggerFacade getLogger(Class clazz) {
LoggerFacade loggerFacade = new LoggerFacade();
loggerFacade.logger = LoggerFactory.getLogger(clazz);
return loggerFacade;
}
...
//打印本地日志的同时 输出到logbean中
@Override
public void warn(String msg) {
logger.warn(msg);
appendExtra(msg, Level.WARN);
}
......
public void appendExtra(String str, Level level) {
String date = DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss");
//获取上下文,通过上下文判断,如果存在则获取分布式环境的LogBean实例
ThreadContext threadContext = ContextContainer.retrieveServiceContext();
if (threadContext != null) {
LogBean logBean = threadContext.getLogBean();
if (logBean != null) {
logBean.getInner().getExtra().add(date + " " + level.toString() + " " + simpleName(getName()) + " -" +
" " + str);
}
}
}
}
接下来我们可以替换slf4j的实现,修改为我们自定义的CustomerLogger
,内部调用logback的日志本地输出,而通过前面桥接原理可以知道,slf4j具体桥接获取实例的过程是通过LoggerFactory
来获取,那么我们来尝试修改LoggerFactory
的代码实现替换为CustomerLogger
实例:
public class CustomLoggerFactory implements ILoggerFactory {
private static CustomLoggerFactory customLoggerFactory;
public static CustomLoggerFactory getInstance(LoggerContext loggerContext) {
if (customLoggerFactory == null) {
customLoggerFactory = new CustomLoggerFactory(loggerContext);
}
return customLoggerFactory;
}
//logback的LoggerFactory实现
# 资料分享
这是我从某优质机构弄来的一些资料,内容我认为确实称得上优质二字,**如需领取,请点赞这篇文章,关注我然后[点击这里即可免费领取](https://gitee.com/vip204888/java-p7)**
**首先分享一份学习大纲,内容较多,涵盖了互联网行业所有的流行以及核心技术,以截图形式分享:**
(亿级流量性能调优实战+一线大厂分布式实战+架构师筑基必备技能+设计思想开源框架解读+性能直线提升架构技术+高效存储让项目性能起飞+分布式扩展到微服务架构.........实在是太多了)
![](https://img-blog.csdnimg.cn/img_convert/a792073bb5945eb2c2c176db172d1445.png)
**其次分享一些技术知识,以截图形式分享一部分:**
Tomcat架构解析:
![](https://img-blog.csdnimg.cn/img_convert/f7c6ab16b1f6a6277d9b97ffa88bce09.png)
算法训练+高分宝典:
![](https://img-blog.csdnimg.cn/img_convert/d4c72090e0c712e8e78700fd3d8f943e.png)
Spring Cloud+Docker微服务实战:
![](https://img-blog.csdnimg.cn/img_convert/e5d9d5f2214e544316daf9f1a0aefcdf.png)
**最后分享一波面试资料:**
> 切莫死记硬背,小心面试官直接让你出门右拐
1000道互联网Java面试题:
![](https://img-blog.csdnimg.cn/img_convert/64b7e62ce4ae7272ca437ad30ea027f6.png)
Java高级架构面试知识整理:
![](https://img-blog.csdnimg.cn/img_convert/7b27a79ff2dc621cdc216048eaedf580.png)
中...(img-pfh4zbCw-1628686030385)]
**最后分享一波面试资料:**
> 切莫死记硬背,小心面试官直接让你出门右拐
1000道互联网Java面试题:
[外链图片转存中...(img-2EBTkz0U-1628686030385)]
Java高级架构面试知识整理:
[外链图片转存中...(img-uLlbW79X-1628686030386)]