Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >SpringBoot 用的 spring-jcl 打印日志,与 LoggingSystem 有鸡毛关系?

SpringBoot 用的 spring-jcl 打印日志,与 LoggingSystem 有鸡毛关系?

作者头像
青石路
发布于 2024-08-24 05:12:19
发布于 2024-08-24 05:12:19
12700
代码可运行
举报
文章被收录于专栏:开发技术开发技术
运行总次数:0
代码可运行

前情回顾

从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的 从源码的角度讲述了 Spring Boot 的 LoggingSystem 与日志组件的绑定,默认情况下绑定的是 Logback;但当我们具体去看 Spring Boot 的日志打印,却发现用的是 spring-jcl ,通过它适配了 slf4j,真正的日志打印还得依赖具体的日志组件,默认情况下使用的是 logback;那这么说来,Spring Boot 的日志打印与 Spring Boot 的 LoggingSystem 貌似没关系呀?

到底有没有关系,有何关系,我们慢慢往下看;先声明下

后面的分析都是基于 Spring Boot 默认的 Logback,其他日志组件可能有所不同,大家别带入错了

LoggerFactory

不管是我们用的 slf4j 方式

private static final Logger LOGGER = LoggerFactory.getLogger(TestWeb.class);

还是 Spring Boot 用的 spring-jcl 方式

private static final Log logger = LogFactory.getLog(SpringApplication.class);

都会通过 slf4j 的 org.slf4j.LoggerFactory#getLogger(java.lang.String) 方法来获取 Logger

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public static Logger getLogger(String name) {
    ILoggerFactory iLoggerFactory = getILoggerFactory();
    return iLoggerFactory.getLogger(name);
}

LoggerFactory 被 final 修饰,且其构造方法是 private,不能被继承,也不能在其他地方 new,纯纯就是一个工具类;它 importStaticLoggerBinder

import org.slf4j.impl.StaticLoggerBinder;

但大家去看下 slf4j-api 的包结构

slf4j包结构
slf4j包结构

根本就没有 StaticLoggerBinder 呀?这也可以?这里其实涉及到一个细节

编译后的 class,可以选择性的打包进 jar,运行的时候只要保证依赖的 class 被正常加载了就行,至于是否在同个 jar 包下并没有关系

slf4j 1.7 源码中其实是有 StaticLoggerBinder 的

slf4j_StaticLoggerBinder
slf4j_StaticLoggerBinder

只是打包的时候剔除了

slf4j_剔除StaticLogggerBinder
slf4j_剔除StaticLogggerBinder

所以,如果使用 1.7.x 及以下的 slf4j ,必须还得结合有 org.slf4j.impl.StaticLoggerBinder 的日志组件,比如 logback

logback1.2.12_StaticLoggerBinder
logback1.2.12_StaticLoggerBinder

这是不是又是个细节,你们是不是又学到了?

又是个细节
又是个细节

StaticLoggerBinder

我们对它进行提炼下

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/**
 * The unique instance of this class.
 */
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

static {
    SINGLETON.init();
}

private StaticLoggerBinder() {
    defaultLoggerContext.setName(CoreConstants.DEFAULT_CONTEXT_NAME);
}

private LoggerContext defaultLoggerContext = new LoggerContext();

public static StaticLoggerBinder getSingleton() {
    return SINGLETON;
}

这是不是 饿汉式单例 的实现?那么 StaticLoggerBinder 的 LoggerContext defaultLoggerContext 是不是也可以当做单例来看待?

LoggerContext

同样,我们对它进行精炼,重点关注 rootsizeloggerCacheLoggerContext()getLogger(final String name)

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class LoggerContext extends ContextBase implements ILoggerFactory, LifeCycle {

    final Logger root;
    private int size;

    private Map<String, Logger> loggerCache;

    public LoggerContext() {
        super();
        this.loggerCache = new ConcurrentHashMap<String, Logger>();

        this.loggerContextRemoteView = new LoggerContextVO(this);
        this.root = new Logger(Logger.ROOT_LOGGER_NAME, null, this);
        this.root.setLevel(Level.DEBUG);
        loggerCache.put(Logger.ROOT_LOGGER_NAME, root);
        initEvaluatorMap();
        size = 1;
        this.frameworkPackages = new ArrayList<String>();
    }

    public final Logger getLogger(final Class<?> clazz) {
        return getLogger(clazz.getName());
    }

    @Override
    public final Logger getLogger(final String name) {

        if (name == null) {
            throw new IllegalArgumentException("name argument cannot be null");
        }

        // if we are asking for the root logger, then let us return it without
        // wasting time
        if (Logger.ROOT_LOGGER_NAME.equalsIgnoreCase(name)) {
            return root;
        }

        int i = 0;
        Logger logger = root;

        // check if the desired logger exists, if it does, return it
        // without further ado.
        Logger childLogger = (Logger) loggerCache.get(name);
        // if we have the child, then let us return it without wasting time
        if (childLogger != null) {
            return childLogger;
        }

        // if the desired logger does not exist, them create all the loggers
        // in between as well (if they don't already exist)
        String childName;
        while (true) {
            int h = LoggerNameUtil.getSeparatorIndexOf(name, i);
            if (h == -1) {
                childName = name;
            } else {
                childName = name.substring(0, h);
            }
            // move i left of the last point
            i = h + 1;
            synchronized (logger) {
                childLogger = logger.getChildByName(childName);
                if (childLogger == null) {
                    childLogger = logger.createChildByName(childName);
                    loggerCache.put(childName, childLogger);
                    incSize();
                }
            }
            logger = childLogger;
            if (h == -1) {
                return childLogger;
            }
        }
    }

    private void incSize() {
        size++;
    }

    int size() {
        return size;
    }
}
  1. root Logger root 定义了最顶层的日志记录规则,可以被视为所有其他Logger对象的父级,并且它的配置会应用于所有的日志记录,除非被特定的Logger配置所覆盖
  2. size Logger 数量,也就是 loggerCache 的 size
  3. loggerCache Map<String, Logger> loggerCache 缓存了应用中所有的 Logger 实例;Logger 实例之间存在父子关系,涉及到日志规则的继承与覆盖
  4. LoggerContext() 初始化 loggerCache,实例化 Logger root,并将 root 放到 loggerCache 中
  5. getLogger(final String name) 先判断是否是 root,是则直接返回,不是则从 loggerCache 获取,获取到则直接返回;若还是没获取到,则说明当前 Logger 还没被创建,则通过 while(true) 按产品包逐层创建 Logger,绑定好 Logger 之间的父子关系,都 put 进 loggerCache 中
Logger父子关系
Logger父子关系

当应用启动完成后,所有的 Logger 实例都被创建并缓存到 LoggerContext 的 loggerCache 中

logCache内容
logCache内容

配置文件加载

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static final Logger LOGGER = LoggerFactory.getLogger(TestWeb.class);

@GetMapping("hello")
public String hello(@RequestParam("name") String name) {
    LOGGER.info("hello接口入参:{}", name);
    return "hello, " + name;
}

直接 debug 跟进 LOGGER.info,几次跟进后会来到 ch.qos.logback.classic.Logger#buildLoggingEventAndAppend

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private void buildLoggingEventAndAppend(final String localFQCN, final Marker marker, final Level level, final String msg, 
                final Object[] params, final Throwable t) {
    LoggingEvent le = new LoggingEvent(localFQCN, this, level, msg, t, params);
    le.setMarker(marker);
    callAppenders(le);
}

这里涉及到事件机制,不细讲,大家可以去看:设计模式之观察者模式 → 事件机制的底层原理,我们把重点放到 callAppenders 上,直译就是调用 appender,appender 在哪?是不是在配置文件中

appender
appender

配置文件什么时候加载的,在 StaticLoggerBinder 加载的时候就完成了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static StaticLoggerBinder SINGLETON = new StaticLoggerBinder();

static {
    SINGLETON.init();
}

/**
 * Package access for testing purposes.
 */
void init() {
    try {
        try {
            new ContextInitializer(defaultLoggerContext).autoConfig();
        } catch (JoranException je) {
            Util.report("Failed to auto configure default logger context", je);
        }
        // logback-292
        if (!StatusUtil.contextHasStatusListener(defaultLoggerContext)) {
            StatusPrinter.printInCaseOfErrorsOrWarnings(defaultLoggerContext);
        }
        contextSelectorBinder.init(defaultLoggerContext, KEY);
        initialized = true;
    } catch (Exception t) { // see LOGBACK-1159
        Util.report("Failed to instantiate [" + LoggerContext.class.getName() + "]", t);
    }
}

autoConfig() 就不细跟了(感兴趣的可以去看:从源码来理解slf4j的绑定,以及logback对配置文件的加载),执行完之后,我们看下 LoggerContext 的 objectMap

LoggerContext
LoggerContext

简单来说,就是将日志配置文件 (logback.xml)加载到了 LoggerContext 的 objectMap 中;我们再回到 Spring Boot 的 LoggingSystem,以 LoggingApplicationListener#onApplicationEnvironmentPreparedEvent 方法作为起点(细节就不跟了,大家直接去看:从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的),我们直接来看 LogbackLoggingSystem#reinitialize

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
protected void reinitialize(LoggingInitializationContext initializationContext) {
    getLoggerContext().reset();
    getLoggerContext().getStatusManager().clear();
    loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}

getLoggerContext() 就不用多说了吧,就是获取全局唯一的 LoggerContext 实例,重点看它的 reset()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Override
public void reset() {
    resetCount++;
    super.reset();
    initEvaluatorMap();
    initCollisionMaps();
    root.recursiveReset();
    resetTurboFilterList();
    cancelScheduledTasks();
    fireOnReset();
    resetListenersExceptResetResistant();
    resetStatusListeners();
}

super.reset()

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public void reset() {

    removeShutdownHook();
    getLifeCycleManager().reset();
    propertyMap.clear();
    objectMap.clear();
}

reset 执行完之后,LoggerContext 的 objectMap 被置空了

reset
reset

说白了就是 Spring Boot 把 Logback 加载的日志配置给清空了,接下来就是 Spring Boot 加载日志配置信息到 LoggerContext 中,也就是如下代码完成的事

loadConfiguration(initializationContext, getSelfInitializationConfig(), null);

不继续跟了,感兴趣的自行去跟;该方法执行完之后,LoggerContext 的 objectMap 又有内容了

reset之后LoggerContext_objectMap
reset之后LoggerContext_objectMap

总结下

  1. StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext 的 objectMap 中
  2. Spring Boot 启动过程中会重置 LoggerContext,其中包括 LoggerContext 的 objectMap,然后重新加载日志配置文件内容到 LoggerContext 的 objectMap中

所以甭管是使用 spring-jcl ,还是使用 slf4j 进行的日志打印,用到的 Appenders 都是 Spring Boot 启动过程中从日志配置文件中加载的,那么 spring-jcl 与 LoggingSystem 有什么关系,大家清楚了吗?

补充个问题

将 logback.xml 重命名成 logback-spring.xml,为什么 Spring Boot 的日志以及我们的业务日志都能正常打印,并且与使用 logback.xml 时一样?

这个问题要是答不上来,那你们肯定是没仔细看 从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的,里面详细介绍了 Spring Boot 对日志配置文件的加载

总结

  1. StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext Logback 1.2.12 默认日志配置文件的优先级 logback.configurationFile > logback-test.xml > logback.xml
  2. Spring Boot 启动过程中会重置 LoggerContext,然后重新加载日志配置文件内容到 LoggerContext Spring Boot 2.7.18 先按优先级 logback-test.groovy > logback-test.xml > logback.groovy > logback.xml

如果如上四个都不存在,则继续按优先级

logback-test-spring.groovy > logback-test-spring.xml > logback-spring.groovy > logback-spring.xml

寻找日志配置文件

  1. 正因为 Spring Boot 启动过程中会重新加载日志配置文件内容到 LoggerContext,所以不管是 spring-jcl 还是 slf4j 打印,日志格式是一致的 Spring Boot 拓展了日志配置文件的文件名
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-08-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Slf4j适配日志原理
看了之前的文章Java日志体系总结后,相信大家对slf4j以及其他日志组件的关系有了一定理解。slf4j只是为日志的输出提供了统一接口,并没有具体的实现,就好像JDBC一样。那么,大家会不会好奇slf4j是怎么绑定/适配/桥接到log4j或者logback其他日志实现组件的呢?这篇文章为大家详细讲述。
Erwin
2020/07/30
1K0
Slf4j适配日志原理
动态修改日志级别,太有用了!
我们在系统中一般都会打印一些日志,并且在开发、测试、生产各个环境中的日志级别可能不一样。在开发过程中为了方便调试打印了很多debug日志,但是生产环境为了性能,为了节约存储资源,我们会将日志级别设置为info或error较高的级别,只保留一些关键的必要的日志。
编程大道
2023/09/05
1.1K0
动态修改日志级别,太有用了!
从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的
SpringBoot2.7 霸王硬上弓 Logback1.3 → 不甜但解渴 实现了 spring-boot 2.x.x 与 logback 1.3.x 的集成,分两步
青石路
2024/08/23
2220
从源码分析 SpringBoot 的 LoggingSystem → 它是如何绑定日志组件的
SpringBoot3.x日志生产最佳实践原来是这样!
SpringBoot对日志的配置和加载进行了封装,让我们可以很方便地使用一些日志框架,只需要定义对应日志框架的配置文件,如LogBack、Log4j、Log4j2等,代码内部便可以直接使用。
JavaEdge
2023/08/15
2.4K0
SpringBoot3.x日志生产最佳实践原来是这样!
Slf4j的优势与原理
业务中经常用到slf4j来写日志,但是没有深入研究过为啥通过这个就可以调用log4j或者logback的函数来写日志呢?
明明如月学长
2021/08/31
7940
Slf4j的优势与原理
深入浅出日志体系(logback最佳实践)
今天跟大家分享一篇我比较敬佩的业界大佬—张建飞同学(阿里高级技术专家,cola框架作者)刚新鲜出炉的一篇关于日志实践的文章。大致分享的是如下几点内容:
陶朱公Boy
2023/01/07
1.1K0
深入浅出日志体系(logback最佳实践)
【SpringBoot专题】Java平台下日志的那些事前言日志框架漫谈看SpringBoot如何对日志进行统一处理SpringBoot日志使用结束语
本篇是【SpringBoot专题】系列的第三篇,将介绍SpringBoot对日志的支持,讲解Java平台下日志的那些事,彻底揭开日志框架在使用过程中的那些坑~
用户2890438
2018/08/21
5910
【SpringBoot专题】Java平台下日志的那些事前言日志框架漫谈看SpringBoot如何对日志进行统一处理SpringBoot日志使用结束语
Java日志体系框架总结:JUL、JCL、SLF4J、Log4j、Logback、Log4j2
日志记录是应用程序运行中必不可少的一部分。具有良好格式和完备信息的日志,可以在程序出现问题时帮助开发人员迅速地定位错误的根源。日志所能提供的功能是多种多样的,包括记录程序运行时产生的错误信息、状态信息、调试信息和执行时间信息等。
johnny666
2024/09/24
3300
Spring Boot 自定义日志详解
Spring Boot 内部代码使用的是 commons-logging 来记录日志的,但是底层日志实现框架是可以随意替换的。Spring Boot为 Java Util Logging, Log4J2, 和 Logback 日志框架提供了默认配置。
Java技术栈
2018/12/14
7040
SpringBoot日志源码解析:日志监听器的执行
LoggingApplicationListener 的主要作用是配置LoggingSystem, 如果 环境 包含 loggingconfig 属性,LoggingApplicationListener 将用于引导 日志记录系统,否则使用默认配置。
愿天堂没有BUG
2022/10/28
1K0
SpringBoot日志源码解析:日志监听器的执行
日志那些事儿——slf4j集成logback/log4j
在日志Logger漫谈中提到了slf4j仅仅是作为日志门面,给用户提供统一的API使用,而真正的日志系统的实现是由logback或者log4j这样的日志系统实现,那究竟slf4j是怎样集成logback或者log4j的呢?
LNAmp
2018/09/05
1.9K0
SpringBoot整合日志框架
​ 1、System.out.println("");将关键数据打印在控制台;去掉?写在一个文件?
别团等shy哥发育
2023/02/25
7490
SpringBoot整合日志框架
springboot通过javaconfig实现logback配置
继承springboot默认的LogbackLoggingSystem,并修改默认日志配置实现方法
路过君
2020/09/07
9740
【编程开发】- 01 日志框架
Log4j是目前最为流行的Java日志框架之一,1999年发布首个版本,2012年发布最后一个版本,2015年正式宣布终止,官方也已不建议使用,并逐步被Logback和Log4j2等日志框架所替代,可是无法掩饰光辉历程,以及优良的设计理念。尽管Log4j有着出色的历史战绩,但早已不是Java日志框架的最优选择,还在使用该日志框架的项目往往是历史遗留问题。
Reactor2020
2023/03/22
1.4K0
【编程开发】- 01 日志框架
SpringBoot 笔记 ( 三 ):日志系统
SpringBoot 笔记 ( 三 ):日志系统 1、日志框架 日志框架就是防止我们再去像以前那样,一直进行System.out.println(“”)将关键数据打印在控制台。框架来记录系统的一些运行时信息,但是随着日志框架的增长,和接口的不一致,导致了使用上的差别很大,​这里采用了一个类似于数据库驱动的模式,数据库驱动是 Java 提供的一个 API,然后真正的实现是需要各个数据库厂商去完成的,而 log 也开始采用这种面向接口编程的方法采用日志抽象层。 市面上的日志框架 JUL、JCL、Jboss-l
lwen
2018/04/16
2.1K0
Spring 全家桶之 Spring Boot 2.6.4(三)- Logging
Spring Boot对所有的内部日志使用Commons Logging,但是对底层的日志实现是开放的。提供了Java Util Logging、Log4J2和Logback的默认配置。并且会预先配置使用控制台输出,也可以选择文件保存日志记录
RiemannHypothesis
2022/08/24
5570
Spring 全家桶之 Spring Boot 2.6.4(三)- Logging
聊聊springboot的LogbackLoggingSystem
org/springframework/boot/logging/LoggingSystem.java
code4it
2023/11/03
2030
聊聊springboot的LogbackLoggingSystem
集成动态日志,“消灭”logback-spring.xml
动态调整线上日志级别是一个非常常见的场景,借助apollo这种配置中心组件非常容易实现。作为apollo的官方技术支持,博主经常在技术群看到有使用者询问apollo是否可以托管logback的配置文件,毕竟有了配置中心后,消灭所有的本地配置全部交给apollo管理是我们的最终目标。可是,apollo不具备直接托管logback-spring.xml配置文件能力,但是,我们可以基于spring和logback的装载机制,完全取缔logback-spring.xml配置,以apollo中的配置驱动。而且,改造后,大大提高了日志系统的灵活性和可扩展性。
用户5546570
2020/12/09
1.4K0
Spring学习笔记(十八)——spring日志框架的配置和使用
JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j....
不愿意做鱼的小鲸鱼
2022/09/26
1.9K0
SpringBoot之基本配置
市面上的日志框架:JUL、JCL、Jbooss-loggin、logback、log4j、log4j2、slf4j….
OY
2022/03/12
8030
SpringBoot之基本配置
推荐阅读
相关推荐
Slf4j适配日志原理
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档