首页 > 其他分享 >SpringBoot日志框架分析

SpringBoot日志框架分析

时间:2023-01-19 15:44:29浏览次数:40  
标签:SpringBoot 框架 INITIALIZATION SLF4J slf4j 日志 logback

本文简介

  1. 第一部分,介绍spring-jcl适配各种日志框架的方式
  2. 第二部分,介绍slf4j适配各种日志框架的方式
  3. 第三部分,介绍下logback框架的使用及原理
 

一、spring-jcl分析

说明

Spring5.x开始自己实现了日志框架的适配器,就叫spring-jcl模块 ,该模块是对输出的日志框架进行适配,是从Apache的commons-logging改造而来。spring-jcl默认绑定的日志框架是JUL(全称Java util Logging,是java原生的日志框架)。

准备

本文依赖的Spring版本号:5.2.9.RELEASE(Spring5.X版本源码基本一致) 搜索打开LogAdapter类,maven依赖org.springframework:spring-jcl(如下图所示)  

源码分析

LogAdapter类 LogAdapter类定义的常量如下,看一眼就意识到,这是根据类的全限定名来查找各个日志框架的。
private static final String LOG4J_SPI= "org.apache.logging.log4j.spi.ExtendedLogger";

private static final String LOG4J_SLF4J_PROVIDER= "org.apache.logging.slf4j.SLF4JProvider";

private static final String SLF4J_SPI= "org.slf4j.spi.LocationAwareLogger";

private static final String SLF4J_API= "org.slf4j.Logger";
  直接看下LogAdapter类的静态代码块,保留原有的英文注释,我加上了中文的逻辑说明,这段代码完整展示了spring-jcl日志框架的加载顺序
static {
   // 优先log4j
   if (isPresent(LOG4J_SPI)) {
      // 如果存在log4j-to-slf4j和slf4j,优先选择slf4j
      //(这里slf4j最终会绑定log4j-to-slf4j,本质上使用的还是log4j)
      if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
         // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
         // however, we still prefer Log4j over the plain SLF4J API since
         // the latter does not have location awareness support.
         logApi = LogApi.SLF4J_LAL;
      }
      // 否则直接使用log4j
      else {
         // Use Log4j 2.x directly, including location awareness support
         logApi = LogApi.LOG4J;
      }
   }
   // 存在Full SLF4J SPI
   else if (isPresent(SLF4J_SPI)) {
      // Full SLF4J SPI including location awareness support
      logApi = LogApi.SLF4J_LAL;
   }
   // 存在Minimal SLF4J API
   else if (isPresent(SLF4J_API)) {
      // Minimal SLF4J API without location awareness support
      logApi = LogApi.SLF4J;
   }
   // 没有导入日志框架,默认使用JUL
   else {
      // java.util.logging as default
      logApi = LogApi.JUL;
   }
}
  其中isPresent()方法源码如下,就是通过类加载器加载对应的日志框架类,返回是否导入了对应的日志框架。
private static boolean isPresent(String className) {
   try {
      Class.forName(className, false, LogAdapter.class.getClassLoader());
      return true;
   }
   catch (ClassNotFoundException ex) {
      return false;
   }
}
  LogAdapter是怎么被调用的呢? 顺着日志框架的使用方式即可看到,我依次贴下代码
// spring-jcl使用标准代码
import org.apache.commons.logging.LogFactory;
LogFactory.getLog(AdvanceEverydayApplication.class).info("hello world!");
// 截取LogFactory的getlog方法,
// 可以看到,最终就是调用了LogAdapter的createLog方法
public abstract class LogFactory {

   public static Log getLog(Class<?> clazz) {
      return getLog(clazz.getName());
   }
   
   public static Log getLog(String name) {
       return LogAdapter.createLog(name);
    }
}
// 截取LogAdapter的createLog方法
// 可以看到,这里就是根据logApi这个属性的值,调用不同框架的适配器,分别创建不同框架的实例
public static Log createLog(String name) {
   switch (logApi) {
      case LOG4J:
         return Log4jAdapter.createLog(name);
      case SLF4J_LAL:
         return Slf4jAdapter.createLocationAwareLog(name);
      case SLF4J:
         return Slf4jAdapter.createLog(name);
      default:
         // Defensively use lazy-initializing adapter class here as well since the
         // java.logging module is not present by default on JDK 9. We are requiring
         // its presence if neither Log4j nor SLF4J is available; however, in the
         // case of Log4j or SLF4J, we are trying to prevent early initialization
         // of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
         // trying to parse the bytecode for all the cases of this switch clause.
         return JavaUtilAdapter.createLog(name);
   }
}
这个logApi就是最开始的那个静态代码块,确定导入了哪个日志框架

总结

spring-jcl是Spring自己的日志框架适配器,在什么依赖都未导入的情况下,默认使用JUL。引入了对应的日志框架依赖后,会自动使用对应的框架。 同时引入多个日志框架,spring-jcl的优先加载顺序是:log4j-to-slf4j > log4j > full slf4j > slf4j > jul 注意:
  • 这里的slf4j只是日志门面(facade模式),它必须绑定具体的日志框架才能工作(下一节详细分析)
  • full slf4j指的是SLF4J including location awareness support,对应org.slf4j.spi.LocationAwareLogger类,它可以提取发生的位置信息(例如方法名、行号等),必须配合桥接器(brigdes)使用
  • slf4j指的是标准的SLF4J接口,对应org.slf4j.Logger类

二、slf4j分析

概述

上面介绍了spring自带的日志适配器框架spring-jcl,但实际项目中大家更喜欢使用SLF4J,它不依赖于spring,架构设计更合理,可以兼容未来的日志框架。 SLF4J(Simple Logging Façade for Java)日志框架,是各种日志框架的简单门面(simple facade)或抽象接口,允许用户部署时选择具体的日志实现。

准备

slf4j与spring-jcl的使用方法是不同的(logger与log)
//slf4j api
LoggerFactory.getLogger(AdvanceEverydayApplication.class)
        .info("hello world!");

//spring-jcl api
LogFactory.getLog(AdvanceEverydayApplication.class)
        .info("hello world!");
  slf4j的优点
  • 采用静态绑定,简单,健壮,并且避免了jcl的类加载器问题
  • 增强了参数化日志,提升性能
  • 可以支持未来的日志系统
  使用slf4j的步骤
  1. 添加slf4j-api的依赖
  2. 绑定具体的日志实现框架
    1. 绑定已经实现了slf4j的日志框架,直接添加对应依赖
    2. (或者)绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
  3. 使用slf4j的API在项目中进行统一的日志记录
(注意第2点,slf4j与spring-jcl不同,它只绑定一种日志框架,引入多种框架有可能会报错)  

源码分析

下面源码是基于slf4j-api-1.7.30版本,依赖如下图所示   我们直接从LoggerFactory.getLogger()方法开始,一直到slf4j的绑定实现为止
// 只关注下面第3行代码即可
public static Logger getLogger(Class<?> clazz) {
    Logger logger = getLogger(clazz.getName());
    // 如果系统属性-Dslf4j.detectLoggerNameMismatch=true,进行检查
    if (DETECT_LOGGER_NAME_MISMATCH) {
        Class<?> autoComputedCallingClass = Util.getCallingClass();
        if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
            Util.report(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
                            autoComputedCallingClass.getName()));
            Util.report("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
        }
    }
    return logger;
}
// 关注第3行,怎样创建的ILoggerFactory?
public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    // 利用工厂对象获取Logger对象
    return iLoggerFactory.getLogger(name);
}
// 重点关注第8行即可,performInitialization()是只执行一次的初始化
// 常量代表了LoggerFactory的初始化状态,给出了中文注释
public static ILoggerFactory getILoggerFactory() {
    if (INITIALIZATION_STATE == UNINITIALIZED) {
        synchronized (LoggerFactory.class) {
            if (INITIALIZATION_STATE == UNINITIALIZED) {
                INITIALIZATION_STATE = ONGOING_INITIALIZATION;
                performInitialization();
            }
        }
    }
    switch (INITIALIZATION_STATE) {
    case SUCCESSFUL_INITIALIZATION:
        // 初始化成功,存在StaticLoggerBinder,返回具体的工厂
        return StaticLoggerBinder.getSingleton().getLoggerFactory();
    case NOP_FALLBACK_INITIALIZATION:
        // 没有找到日志框架,返回一个无操作的工厂
        return NOP_FALLBACK_FACTORY;
    case FAILED_INITIALIZATION:
        // 日志框架初始化失败,需要抛出失败的原因
        throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
    case ONGOING_INITIALIZATION:
        // support re-entrant behavior.
        // See also http://jira.qos.ch/browse/SLF4J-97
        return SUBST_FACTORY;
    }
    throw new IllegalStateException("Unreachable code");
}
// 第3行bind,就是大家常说的slf4j静态绑定
private final static void performInitialization() {
    bind();
    if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
        // 判断是否匹配
        versionSanityCheck();
    }
}
// 代码很长,重点关注第13行,catch里面是slf4j-api的版本兼容提示
private final static void bind() {
    try {
        Set<URL> staticLoggerBinderPathSet = null;
        // skip check under android, see also
        // http://jira.qos.ch/browse/SLF4J-328
        if (!isAndroid()) {
            // 如果有多个日志框架,这里进行检测并提示
            staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
            reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
        }
        // the next line does the binding
        StaticLoggerBinder.getSingleton();
        // 设置为成功初始化状态
        INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
        // 报告实际绑定的日志框架
        reportActualBinding(staticLoggerBinderPathSet);
    } catch (NoClassDefFoundError ncde) {
        // StaticLoggerBinder这个类不存在,设置状态并打印提示
        String msg = ncde.getMessage();
        if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
            INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
            Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
            Util.report("Defaulting to no-operation (NOP) logger implementation");
            Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
        } else {
            failedBinding(ncde);
            throw ncde;
        }
    } catch (java.lang.NoSuchMethodError nsme) {
        // StaticLoggerBinder.getSingleton()这个方法不存在,设置状态并打印提示
        String msg = nsme.getMessage();
        if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
            INITIALIZATION_STATE = FAILED_INITIALIZATION;
            Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
            Util.report("Your binding is version 1.5.5 or earlier.");
            Util.report("Upgrade your binding to version 1.6.x.");
        }
        throw nsme;
    } catch (Exception e) {
        failedBinding(e);
        throw new IllegalStateException("Unexpected initialization failure", e);
    } finally {
        postBindCleanUp();
    }
}
这一句就是关键代码:StaticLoggerBinder.getSingleton() StaticLoggerBinder这个类指定了包路径: import org.slf4j.impl.StaticLoggerBinder; 但是这个类并不在slf4j-api的包内,它是由各个日志框架提供的。 例如,logback-classic包里自带这个类,单例模式,直接适配slf4j   如果一个日志框架不带这个类怎么办? 自己写一个(

标签:SpringBoot,框架,INITIALIZATION,SLF4J,slf4j,日志,logback
From: https://www.cnblogs.com/sonicLi/p/17061639.html

相关文章

  • springboot整合es
    pom.xml增加依赖(注意版本要和springboot匹配)<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-st......
  • springboot 热更 2023.3
    热更使用devtools或者alt+shit+f9ideaFile|Settings|Preferences|Build,Execution,Deployment|Compiler:BuildprojectautomaticallyFile|Setting......
  • Nginx日志按天自动切割的shell脚本
    简介默认情况Nginx会把所有访问日志生成到一个指定的访问日志文件access.log里,但这样一来,时间长了就会导致日志内容很多,不利于分析日志和处理,因此,有必要对Nginx按天或按......
  • springboot 常用项目结构
    servicex//项目名|-admin-ui//管理服务前端代码(一般将UI和SERVICE放到一个工程中,便于管理)|-servicex-auth//模块1......
  • 学习笔记——SpringMVC简介;SpringMVC处理请求原理简图;SpringMVC搭建框架
    2023-01-19一、SpringMVC简介1、SpringMVC是Spring子框架2、SpringMVC是Spring为“控制层”提供的基于MVC设计理念的优秀的Web框架,是目前最主流的MVC框架。3、SpringMV......
  • SpringBoot(三)
    Swagger、分布式架构部分​​11、Swagger​​​​11.1、Swagger简介​​​​11.2、SpringBoot集成Swagger​​​​11.3、配置Swagger​​​​11.4、Swagger配置扫描接口​​......
  • SpringBoot(二)
    模板引擎、数据源、安全框架部分​​5、SpringBootWeb开发​​​​5.1、静态资源​​​​5.2、首页​​​​5.3、Thymeleaf模板引擎​​​​5.4、装配扩展SpringMVC​​​......
  • SpringBoot+MyBatils-puls+db2
    记录自己搭建db2数据库,链接数据库遇到的问题。 搭建db2我是在阿里云使用docker搭建的db2数据库,搭建数据库参考下面这个链接https://www.cnblogs.com/Yongzhouunknown/p......
  • SpringBoot源码学习3——SpringBoot启动流程
    系列文章目录和关于我一丶前言在《SpringBoot源码学习1——SpringBoot自动装配源码解析+Spring如何处理配置类的》中我们学习了SpringBoot自动装配如何实现的,在《Sprin......
  • 230118_50_SpringBoot入门
    yaml配置文件中,支持占位符配置person:name:bill${random.int}age:4happy:truebirth:2023/01/15maps:{k1:v1,k2:v2}hello:hellolists:-cat-dog-fish......