原来这些日志兼容包的包名与原来的日志框架的包名完全一样,并且完全按照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实现
private LoggerContext loggerContext;
public CustomLoggerFactory(LoggerContext loggerContext) {
this.loggerContext = loggerContext;
}
//返回CustomLogger
@Override
public Logger getLogger(String name) {
ch.qos.logback.classic.Logger logger = loggerContext.getLogger(name);
return CustomLogger.getLogger(logger);
}
public LoggerContext getLoggerContext() {
return loggerContext;
}
@Override
public ILoggerFactory getLoggerFactory() {
if (!initialized) {
return defaultLoggerContext;
}
if (contextSelectorBinder.getContextSelector() == null) {
throw new IllegalStateException(
"contextSelector cannot be null. See also " + NULL_CS_URL);
### 最后
> 由于细节内容实在太多了,为了不影响文章的观赏性,只截出了一部分知识点大致的介绍一下,每个小节点里面都有更细化的内容!
![](https://img-blog.csdnimg.cn/img_convert/536670449d83818b4dd6a3499f338731.png)
**需要这份文档的朋友可以帮忙点个赞还有小编准备的一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》**
##### **[CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】](https://codechina.csdn.net/m0_60958482/java-p7)**
需要这份文档的朋友可以帮忙点个赞还有小编准备的一份Java进阶学习路线图(Xmind)以及来年金三银四必备的一份《Java面试必备指南》**
##### **[CodeChina开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频】](https://codechina.csdn.net/m0_60958482/java-p7)**
![](https://img-blog.csdnimg.cn/img_convert/2bc22a0a1dd125e183f8a9ca0a141829.png)