SpringBoot统一日志处理原理,Java面试题2020中高级

本文深入探讨了Spring Boot如何通过排除其他日志框架,利用兼容包实现日志系统统一。通过分析SLF4J的桥接原理,了解到其如何在运行时自动查找并加载日志实现。文章还提出了针对分布式日志的改造方案,即通过实现Logger接口,聚合Logger和LogBean,以达到在不改变第三方框架日志的基础上,实现分布式日志的无缝接入。
摘要由CSDN通过智能技术生成

原来这些日志兼容包的包名与原来的日志框架的包名完全一样,并且完全按照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的桥接过程,使得我们可以灵活的使用本地日志或者分布式日志呢?首先我们先看看我们需要实现的需求:

  1. logger和logbean结合,统一日志入口
  2. logbean降低代码侵入性
  3. 无缝替换第三方框架中的日志,根据需求加入到分布式日志中

想要实现这个功能,有以下两个思路实现:

1.我们通过自定义appender,基于logback的appender进行扩展,可以实现分别输出本地日志以及分布式日志,但是缺陷在于appender扩展性不高,很多参数信息获取不到,例如上下文信息等

2.我们通过实现Logger接口,用来将Logger和LogBean聚合在一起,从而实现LogBean集成到Logger中,同样此种方式的缺陷在于对于第三方框架日志,我们无能为力,无法直接替换使用,并且在使用的时候需要使用自定义的LogFactory

第一种思路我们可以看出来,局限性太高,灵活度不够,接下来我们尝试使用第二种方案,实现聚合LoggerLogBean,对外公开统一的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)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值