SpringBoot 用的 spring-jcl 打印日志,与 LoggingSystem 有鸡毛关系?
开心一刻
现实中,我有一个异性游戏好友,昨天我心情不好,找她聊天
我:我们两个都好久没有坐下来好好聊天了
她:你不是有女朋友吗
我:人家不需要我这种穷人啊
她:难道我需要吗
前情回顾
从源码分析 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
,纯纯就是一个工具类;它 import
了 StaticLoggerBinder
import org.slf4j.impl.StaticLoggerBinder;
但大家去看下 slf4j-api
的包结构
根本就没有 StaticLoggerBinder
呀?这也可以?这里其实涉及到一个细节
编译后的 class,可以选择性的打包进 jar,运行的时候只要保证依赖的 class 被正常加载了就行,至于是否在同个 jar 包下并没有关系
slf4j 1.7 源码中其实是有 StaticLoggerBinder 的
只是打包的时候剔除了
所以,如果使用 1.7.x 及以下的 slf4j
,必须还得结合有 org.slf4j.impl.StaticLoggerBinder
的日志组件,比如 logback
这是不是又是个细节,你们是不是又学到了?
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
同样,我们对它进行精炼,重点关注 root
、size
、loggerCache
、LoggerContext()
、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;
}
}
root
Logger root 定义了最顶层的日志记录规则,可以被视为所有其他
Logger
对象的父级,并且它的配置会应用于所有的日志记录,除非被特定的Logger
配置所覆盖size
Logger 数量,也就是 loggerCache 的 size
loggerCache
Map<String, Logger> loggerCache 缓存了应用中所有的 Logger 实例;Logger 实例之间存在父子关系,涉及到日志规则的继承与覆盖
LoggerContext()
初始化 loggerCache,实例化 Logger root,并将 root 放到 loggerCache 中
getLogger(final String name)
先判断是否是 root,是则直接返回,不是则从 loggerCache 获取,获取到则直接返回;若还是没获取到,则说明当前 Logger 还没被创建,则通过
while(true)
按产品包逐层创建 Logger,绑定好 Logger 之间的父子关系,都 put 进 loggerCache 中
当应用启动完成后,所有的 Logger 实例都被创建并缓存到 LoggerContext 的 loggerCache 中
配置文件加载
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 在哪?是不是在配置文件中
配置文件什么时候加载的,在 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
简单来说,就是将日志配置文件 (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 被置空了
说白了就是 Spring Boot 把 Logback 加载的日志配置给清空了,接下来就是 Spring Boot 加载日志配置信息到 LoggerContext 中,也就是如下代码完成的事
loadConfiguration(initializationContext, getSelfInitializationConfig(), null);
不继续跟了,感兴趣的自行去跟;该方法执行完之后,LoggerContext 的 objectMap 又有内容了
总结下
- StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext 的 objectMap 中
- 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 对日志配置文件的加载
总结
StaticLoggerBinder 类加载的时候,会加载日志配置文件内容到 LoggerContext
Logback 1.2.12 默认日志配置文件的优先级
logback.configurationFile > logback-test.xml > logback.xml
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
寻找日志配置文件
正因为 Spring Boot 启动过程中会重新加载日志配置文件内容到 LoggerContext,所以不管是
spring-jcl
还是slf4j
打印,日志格式是一致的Spring Boot 拓展了日志配置文件的文件名
SpringBoot 用的 spring-jcl 打印日志,与 LoggingSystem 有鸡毛关系?的更多相关文章
- springboot aop + logback + 统一异常处理 打印日志
1.src/resources路径下新建logback.xml 控制台彩色日志打印 info日志和异常日志分不同文件存储 每天自动生成日志 结合myibatis方便日志打印(debug模式) < ...
- SpringBoot自定义注解、AOP打印日志
前言 在SpringBoot中使用自定义注解.aop切面打印web请求日志.主要是想把controller的每个request请求日志收集起来,调用接口.执行时间.返回值这几个重要的信息存储到数据库里 ...
- SpringBoot系列之集成logback实现日志打印(篇二)
SpringBoot系列之集成logback实现日志打印(篇二) 基于上篇博客SpringBoot系列之集成logback实现日志打印(篇一)之后,再写一篇博客进行补充 logback是一款开源的日志 ...
- springboot整合mybatis将sql打印到日志(转)
在前台请求数据的时候,sql语句一直都是打印到控制台的,有一个想法就是想让它打印到日志里,该如何做呢? 见下面的mybatis配置文件: <?xml version="1.0" ...
- gradle结合spring-boot生成可运行jar包,并打印日志
1.用gradle把springboot项目打包成jar 1.1 build.gradle 中添加 buildscript { repositories { mavenLocal() maven { ...
- springboot整合mybatis将sql打印到日志
在前台请求数据的时候,sql语句一直都是打印到控制台的,有一个想法就是想让它打印到日志里,该如何做呢? 见下面的mybatis配置文件: <?xml version="1.0" ...
- Spring Boot(三):logback打印日志
springboot对logback的支持是非常好的,不需要任何配置,只需要在resource下加logback.xml就可以实现功能直接贴代码: <?xml version="1.0 ...
- 关于spring 事务 和 AOP 管理事务和打印日志问题
关于spring 事务 和 AOP 管理事务和打印日志问题 1. 就是支持事务注解的(@Transactional) . 可以在server层总使用@Transactional,进行方法内的事务管 ...
- springboot中logback打印日志(转)
springboot对logback的支持是非常好的,不需要任何配置,只需要在resource下加logback.xml就可以实现功能 直接贴代码: <?xml version="1. ...
- springboot+mybatis 配置sql打印日志
第一种: 配置类型 # 配置slq打印日志 logging.level.com.lawt.repository.mapper=debug重点: #其中 com.lawt.repository.ma ...
随机推荐
- zRAM内存压缩技术原理与应用
zRAM内存压缩技术原理与应用 作者: 发布于:2020-3-8 8:38 分类:内存管理 http://www.wowotech.net/memory_management/458.html/com ...
- 嵌入式Linux如何设置获取uboot参数
--- title: 嵌入式Linux如何设置获取uboot参数 EntryName: embeded-linux-debug-get-and-set-u-boot-envarg date: 2020 ...
- 量子算法抛转(以及Oracle函数初步)
接下来要接触量子算法了,我们会看到怎么利用量子并行机制和干涉原理.干涉在算法对结果进行测量求值时举足轻重. Deutsch-Jozsa 算法 DJ算法是量子算法的入门算法,就像编程界的"He ...
- Window版 MySQL可视化工具 Navicat 面安装免激活绿色版
网盘地址 链接:https://pan.baidu.com/s/1T0WyhGAFEt28GaU4wXhfrg 提取码:z4ww navicat15破解版 链接:https://pan.baidu.c ...
- 深度学习pytorch常用操作以及流程
在微信公众号上看到这篇文章,担心以后想找的时候迷路,所以记录到了自己的博客上,侵扰致歉,随时联系可删除. 1.基本张量操作 1. 1 创建张量 介绍: torch.tensor() 是 PyTorch ...
- 1.1 第一个hello程序
还记得在每一个编程平台上的第一个程序都是hello world,现在就以这个程序为载体,先浅聊一下计算机系统吧. 1.预处理阶段,预处理器cpp根据字符#开头的命令修改原始的程序,并把头文件里的内容直 ...
- 免费CDN使用整理
免费CDN使用整理 最近在使用web优化的时候,需要用到cdn,遇到了一些问题,比如某些cdn在特定的条件下访问不同,整理一波免费的CDN,任君采撷 名称 国家 链接 测速 特色 UNPKG 国外 h ...
- 题解:P10781 【MX-J1-T1】『FLA - III』Spectral
本题的主要思路就是数学. 首先,让我们先来打一个表. \(i\) \(1\) \(2\) \(3\) \(4\) \(\dots\) \(T_{i}\) \(k\) \(1.5k\) \(1.5k\) ...
- 云原生 .NET Aspire 8.1 新增对 构建容器、编排 Python的支持
.NET Aspire 用于云原生应用开发,提供用于构建.测试和部署分布式应用的框架,这些应用通常利用微服务.容器.无服务器体系结构等云构造.2024年7月23日宣布的新 8.1 版本是该平台自 5 ...
- 【Mybatis】04 官方文档指北阅读 vol2 配置 其一
https://mybatis.org/mybatis-3/zh/configuration.html 配置 MyBatis 的配置文件包含了会深深影响 MyBatis 行为的设置和属性信息. 配置文 ...