文章目录
一、故事引入
故事要从下面这段代码说起
java
public class App {
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main( String[] args ) throws Exception {
logger.info("abc");
}
}
然后再加上一段日志配置
logback.xml
xml
<configuration >
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %X{mdcKey} %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 定义日志级别和输出位置 -->
<root level="info">
<appender-ref ref="CONSOLE" />
</root>
</configuration>
pom.xml中的配置如下
xml
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.15</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.5.15</version>
</dependency>
以上是一个最简单的使用logback实现slf4j的demo, 运行之后我们可以在控制台看到打印如下
java
SLF4J(I): Connected with provider of type [ch.qos.logback.classic.spi.LogbackServiceProvider]
2025-01-16 10:03:52 [main] INFO per.qiao.App - abc
为什么一句logger.info("abc");
就把日志打印出来了, 其中原理有哪些, 那么本系列我们就一起来探究其中的门道。可以打开下载到的slf4j和logback源码项目, 跟着代码走...
二、原理探究
java
LoggerFactory.getLogger(App.class);
对应的是org.slf4j.LoggerFactory#getLogger(Class<?> clazz)
java
public static Logger getLogger(Class<?> clazz) {
// 获取class对应的logger对象
Logger logger = getLogger(clazz.getName());
// 系统属性: slf4j.detectLoggerNameMismatch
if (DETECT_LOGGER_NAME_MISMATCH) {
// 调用当前方法的类
Class<?> autoComputedCallingClass = Util.getCallingClass();
// 调用getLogger的类和传入的class是否相等
if (autoComputedCallingClass != null && nonMatchingClasses(clazz, autoComputedCallingClass)) {
//
Reporter.warn(String.format("Detected logger name mismatch. Given name: \"%s\"; computed name: \"%s\".", logger.getName(),
autoComputedCallingClass.getName()));
Reporter.warn("See " + LOGGER_NAME_MISMATCH_URL + " for an explanation");
}
}
return logger;
}
这里第一句getLogger是我们要探究的核心;
后面的判断用来校验当前执行getLogger方法的类和传入的Class对象是否是同一个类, 什么意思呢?? , 比如我们当前写代码的类是从别的类中复制而来, 那么可能这个private static final Logger logger = LoggerFactory.getLogger(App.class);
就没有修改其中的App.class, 可能还是App1.class, 如果此时你开启了这种校验, 那么就会打印下面的一串警告日志。
开启方法
1、属性配置
java
static {
System.setProperty("slf4j.detectLoggerNameMismatch", "true");
}
private static final Logger logger = LoggerFactory.getLogger(App.class);
public static void main( String[] args ) throws Exception {
logger.info("abc");
}
2、idea启动项中 Add VM options 中添加-Dslf4j.detectLoggerNameMismatch=true
, 也是可以的
-Dlogback.statusListenerClass=STDOUT
3、启动命令
java -Dslf4j.detectLoggerNameMismatch=true per.qiao.App
当然了, 2和3是一个东西
如果我们想要在main方法第一行加上属性设置行不行呢? , 就像下面这样
java
public static void main( String[] args ) throws Exception {
System.setProperty("slf4j.detectLoggerNameMismatch", "true");
logger.info("abc");
}
其实是不行的哈, 因为main方是static修饰的, 而我们定义的logger也是static修饰的, jvm调用main方法之前会先调用LoggerFactory.getLogger
进行日志初始化过程, 导致自己设置的失效; 关于一个类中默认的执行顺序, 大家也可以去了解下。
文归正传, 看下getLogger方法
java
public static Logger getLogger(String name) {
// 获取日志工厂
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
- 拿到工厂
- 使用工厂拿到Logger对象
门道就在这个getILoggerFactory
了
java
public static ILoggerFactory getILoggerFactory() {
return getProvider().getLoggerFactory();
}
static SLF4JServiceProvider getProvider() {
// 如果未初始化
if (INITIALIZATION_STATE == UNINITIALIZED) {
// double check
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
// 初始化中
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
// 执行初始化
performInitialization();
}
}
}
// ... 省略代码
}
关于synchronized
时的double check使用, 大家一定要了然于胸
直接跳到核心方法bind
java
private final static void bind() {
try {
// 1.获取SLF4JServiceProvider
List<SLF4JServiceProvider> providersList = findServiceProviders();
// 系统打印SLF4JServiceProvider获取的情况
reportMultipleBindingAmbiguity(providersList);
// 2.取第一个
if (providersList != null && !providersList.isEmpty()) {
PROVIDER = providersList.get(0);
// SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
// 3.初始化
PROVIDER.initialize();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 打印实际使用的provider对象
reportActualBinding(PROVIDER);
} else {
// ...
}
postBindCleanUp();
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
- spi获取SLF4JServiceProvider
- 取第一个SLF4JServiceProvider
- 初始化它
这里两个核心方法findServiceProviders
和PROVIDER.initialize()
findServiceProviders
java
static List<SLF4JServiceProvider> findServiceProviders() {
List<SLF4JServiceProvider> providerList = new ArrayList<>();
// 加载当前类的类加载器
final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();
// 1.获取系统指定的SLF4JServiceProvider
SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);
if (explicitProvider != null) {
providerList.add(explicitProvider);
return providerList;
}
// 2.spi获取SLF4JServiceProvider
ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
// 添加到集合中返回
Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
safelyInstantiate(providerList, iterator);
}
return providerList;
}
这里我们可以看到获取SLF4JServiceProvider有两个方式
- 系统指定可以使用哪个; 使用
-Dslf4j.provider=SLF4JServiceProvider的类全路径
指定, 当你的项目中由于种种原因配置了多个slf4j的实现模块的时候, 这时候你就可以用这个配置指定使用哪个具体实现, 例如slf4j-jdk14.jar
和logback-classic.jar
同时存在的话, 你可以排除某个依赖或者使用这个配置指定 - 使用spi获取获取所有的
SLF4JServiceProvider
, 只有其中一个生效
spi获取的顺序是按照包顺序获取的, 也就是按照自然排序, 所以第一个就是名字排序靠前的那个包中的
关于spi, 在一些源码中经常被用到, 例如springboot的自动装配, dubbo加载, 它俩用的这个思路, 但是属于spi的变种
java
// springboot
SpringFactoriesLoader.loadFactoryNames(type, classLoader)
// duoboo
ExtensionLoader.getExtensionLoader(ExtensionFactory.class)
.getAdaptiveExtension());
最核心的部分就是PROVIDER.initialize
SLF4JServiceProvider#initialize
这个方法有具体的实现模块提供, 本节不介绍
三、SLF4JServiceProvider
slf4j实现模块的入口类, 通过spi加载
java
/**
* 它取代了SLF4J 1.0版本中使用的旧的静态绑定机制。X到1.7.x。
*/
public interface SLF4JServiceProvider {
// 获取日志工厂实例
public ILoggerFactory getLoggerFactory();
// 日志标记工厂
public IMarkerFactory getMarkerFactory();
// 支持mdc的
public MDCAdapter getMDCAdapter();
// 版本校验的
public String getRequestedApiVersion();
// 用来初始化实现模块
public void initialize();
}
这里最核心的是initialize
方法
getMarkerFactory
方法, 在打印日志的时候, 有写方法有Marker参数, 例如
java
public void info(Marker marker, String msg);
public void info(Marker marker, String format, Object arg);
getMDCAdapter
方法, 在使用MDC的时候, 会用到它
四、总结
slf4j提供了操作日志的门面
- 日志初始化入口实
LoggerFactory.getLogger(App.class);
- 可以通过
-Dslf4j.provider=SLF4JServiceProvider的类全路径
来指定日志最终使用的slf4j实现 - 通过spi加载了
SLF4JServiceProvider
对象 - 初始化
SLF4JServiceProvider
后, 通过它得到具体的Logger对象