首页 星云 工具 资源 星选 资讯 热门工具
:

PDF转图片 完全免费 小红书视频下载 无水印 抖音视频下载 无水印 数字星空

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

编程知识
2024年08月23日 09:03

开心一刻

现实中,我有一个异性游戏好友,昨天我心情不好,找她聊天

我:我们两个都好久没有坐下来好好聊天了

她:你不是有女朋友吗

我:人家不需要我这种穷人啊

她:难道我需要吗

难道我需要吗

前情回顾

从源码分析 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

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包结构

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

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

slf4j 1.7 源码中其实是有 StaticLoggerBinder 的

slf4j_StaticLoggerBinder

只是打包的时候剔除了

slf4j_剔除StaticLogggerBinder

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

logback1.2.12_StaticLoggerBinder

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

又是个细节

StaticLoggerBinder

我们对它进行提炼下

/**
 * 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)

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 实例都被创建并缓存到 LoggerContext 的 loggerCache 中

logCache内容

配置文件加载

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

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

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

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

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

@Override
protected void reinitialize(LoggingInitializationContext initializationContext) {
    getLoggerContext().reset();
    getLoggerContext().getStatusManager().clear();
    loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
}

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

@Override
public void reset() {
    resetCount++;
    super.reset();
    initEvaluatorMap();
    initCollisionMaps();
    root.recursiveReset();
    resetTurboFilterList();
    cancelScheduledTasks();
    fireOnReset();
    resetListenersExceptResetResistant();
    resetStatusListeners();
}

super.reset()

public void reset() {

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

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

reset

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

loadConfiguration(initializationContext, getSelfInitializationConfig(), null);

不继续跟了,感兴趣的自行去跟;该方法执行完之后,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

    寻找日志配置文件

  3. 正因为 Spring Boot 启动过程中会重新加载日志配置文件内容到 LoggerContext,所以不管是 spring-jcl 还是 slf4j 打印,日志格式是一致的

    Spring Boot 拓展了日志配置文件的文件名

From:https://www.cnblogs.com/youzhibing/p/18365683
本文地址: http://www.shuzixingkong.net/article/1362
0评论
提交 加载更多评论
其他文章 什么?!90%的ThreadLocal都在滥用或错用!
最近发现系统里面在使用到了 ThreadLocal,乍一看,好像很高级的样子。再仔细一看,完全就是一个 ThreadLocal 滥用的典型案例啊!甚至,日常的业务系统中,90%以上都在滥用或者错用啊
什么?!90%的ThreadLocal都在滥用或错用! 什么?!90%的ThreadLocal都在滥用或错用! 什么?!90%的ThreadLocal都在滥用或错用!
除了按值和引用,方法参数的第三种传递方式
参数在方法种具有按“值(by value)”和“引用(by ref)”两种传递方式,这是每个.NET程序员深入骨髓得基本概念。但是我若告诉你,.NET规定的参数传递形式其实是三种,会不会颠覆你的认知。一、官方描述 二、TypedReference结构体 三、三个特殊的方法 四、三种参数传递方式 一、
LeetCode300.最长递增子序列
LeetCode300.最长递增子序列 力扣题目链接(opens new window) 给你一个整数数组 nums ,找到其中最长严格递增子序列的长度。 子序列是由数组派生而来的序列,删除(或不删除)数组中的元素而不改变其余元素的顺序。例如,[3,6,2,7] 是数组 [0,3,1,6,2,2,7
在VS Code中使用Snippet Craft扩展提高编码效率
Snippet Craft 一个VS Code代码片段管理插件 功能 创建和插入代码片段 在编辑器区域右键菜单中点击插入Snippet,或在代码片段视图中点击条目,则会将代码片段插入到当前激活文档的光标位置。 代码片段编辑 代码片段在左侧栏中,根据创建时的文件内容类型,分组显示代码片段,可编辑已有的
在VS Code中使用Snippet Craft扩展提高编码效率 在VS Code中使用Snippet Craft扩展提高编码效率 在VS Code中使用Snippet Craft扩展提高编码效率
wiz 为知笔记服务器 docker 跨服务器迁移爬坑指北
本文主要是介绍 wiz 为知笔记服务器 docker 从旧服务器迁移到新服务器的步骤以及问题排查。 旧服务器升级 wiz docker 目的:保持和新服务器拉取的镜像版本一致。 官方只留了 wiz docker 镜像最新版,拉取不了旧版本镜像,所以先升级旧服务器上的 wiz docker。 升级方法
wiz 为知笔记服务器 docker 跨服务器迁移爬坑指北 wiz 为知笔记服务器 docker 跨服务器迁移爬坑指北 wiz 为知笔记服务器 docker 跨服务器迁移爬坑指北
.NET 8 + Vue 3 极简 RABC 权限管理系统
前言 在日常工作中,几乎每家公司都需要一个后台管理系统来处理各种任务。为了帮助大家快速搭建这样一个系统,给大家介绍一个基于最新技术 .NET 8 和前端框架 Vue 3 实现的极简 RABC(基于角色的访问控制)权限管理系统。 该系统后端采用经过精心精简的 ABP框架,前端则使用了 vue-pure
.NET 8 + Vue 3 极简 RABC 权限管理系统 .NET 8 + Vue 3 极简 RABC 权限管理系统 .NET 8 + Vue 3 极简 RABC 权限管理系统
React项目接入代码编辑器aceEditor
不建议去查看aceEditor官方,最好去github查看 安装命令: npm install react-ace 引入包: import AceEditor from &#39;react-ace&#39;; import &#39;ace-builds/src-noconflict/mode-s
React项目接入代码编辑器aceEditor React项目接入代码编辑器aceEditor React项目接入代码编辑器aceEditor
Django集成腾讯COS对象存储
前言 最近遇到一个场景需要把大量的资源文件存储到 OSS 里,这里选的是腾讯的 COS 对象存储 (话说我接下来想搞的 SnapMix 项目也是需要大量存储的,我打算搭个 MinIO 把 24T 的服务器利用起来~) 为啥腾讯不搞个兼容 Amazon S3 协议的啊…… 官方的 SDK 和文档都奇奇