01 引言
博主在前面的文章里主要阐述了Java
日常开发中常用到的日志框架以及logback
日志框架实现,有兴趣的童鞋可以参阅:
日志在日常开发中我们是必定会接触到的,似乎我们只知道使用logger.info
、logger.error
等去打印日志,然后就不去深入了解了。
这也是本文的目的,一起来走走它的源码,揭开它的神秘面纱。
02 slf4j 使用案例
在阅读源码前,我们看看slf4j
的使用案例。
2.1 引入slf4j接口
使用slf4j的方法很简单,我们只需要在项目里面添加依赖(这里以maven项目为例子),然后直接使用即可。
使用的版本根据官网建议的稳定版本(即
2.0.9
版本):https://www.slf4j.org/download.html
新建maven项目之后,添加依赖:
然后随便写点测试代码:
/**
* slf4j测试
*
* @author : YangLinWei
* @createTime: 2023/10/8 09:44
* @version: 1.0.0
*/
public class Slf4jTest {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
logger.info("Current Timestamp: {}", System.currentTimeMillis());
}
}
运行之后,会发现无法打印日志:
根据错误提示,我们知道没有找到对应的SLF4J
实现,在前面的章节,我们知道SLF4J只是接口,这当然是无法打印日志的,所以我们还需要引入对应的日志框架实现:
上述是官网的实现SLF4J的日志框架实现,这里我们以logback来继续讲解。
2.2 引入实现slf4j的日志框架
目前项目只依赖了slf4j-api
,继续依赖logback
框架:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.3.6</version>
</dependency>
再次运行,可以看到正常打印日志了:
如果依赖多个日志实现呢?比如这里继续依赖log4j2:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>2.0.9</version>
</dependency>
再次运行,可以看到日志虽然有打印,但是会报红:
这种情况是不是在我们日常的开发中很常见?其实这里的意思指的是有多个实现的日志框架,找到的有“ch.qos.logback.classic.spi.LogbackServiceProvider
”、“org.slf4j.reload4j.Reload4jServiceProvider
”,然后使用的是“ch.qos.logback.classic.spi.LogbackServiceProvider
”。
这种情况的解决方式,可以参考:https://www.slf4j.org/codes.html#multiple_bindings。其实就是移除依赖,或依赖的包如果存在不需要的日志框架,直接排除即可,如下图:
也就是说,只能保留一种实现SLF4J的日志框架,如果存在多个,会报红。同时这也是SLF4J设计的巧妙之处,我们如果要替换框架,只需要修改日志框架的实现即可,java代码无需修改。
这种情况,博主在前面的文章里也有写过具体的解决方式:《SpringBoot 日志终极解决方案》
2.3 其它
SpringBoot项目也引入了日志的starter(即:spring-boot-starter-logging
),进入里面的pom文件,可以看到它的内容下:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.1.17.RELEASE</version>
</parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
<version>2.1.17.RELEASE</version>
<name>Spring Boot Logging Starter</name>
<description>Starter for logging using Logback. Default logging starter</description>
<url>https://projects.spring.io/spring-boot/#/spring-boot-parent/spring-boot-starters/spring-boot-starter-logging</url>
<organization>
<name>Pivotal Software, Inc.</name>
<url>https://spring.io</url>
</organization>
<licenses>
<license>
<name>Apache License, Version 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0</url>
</license>
</licenses>
<developers>
<developer>
<name>Pivotal</name>
<email>info@pivotal.io</email>
<organization>Pivotal Software, Inc.</organization>
<organizationUrl>https://www.spring.io</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/spring-projects/spring-boot.git</connection>
<developerConnection>scm:git:ssh://git@github.com/spring-projects/spring-boot.git</developerConnection>
<url>https://github.com/spring-projects/spring-boot</url>
</scm>
<issueManagement>
<system>Github</system>
<url>https://github.com/spring-projects/spring-boot/issues</url>
</issueManagement>
<dependencies>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.11.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.30</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
依赖配置表明,spring-boot-starter-logging
依赖同时包括了 Logback
、Log4j 2
和 SLF4J
的相应依赖项,Logback
作为默认的日志实现,这是SpringBoot
的默认配置,当然也可以配置Log4j 2
作为默认日志实现,这里就不再详述了。
备注:
jul-to-slf4j
这个依赖项是为了集成 Java Util Logging (JUL) 和 SLF4J。其中JUL 是 Java 标准库中的日志框架,通过这个依赖,它将被桥接到 SLF4J,从而允许你在应用程序中使用 SLF4J 接口来记录 JUL 的日志消息。
03 slf4j 源码剖析
3.1 ServiceLoader
通过阅读slf4j
的文档(https://www.slf4j.org/manual.html),可以知道从2.0.0
版本开始,slf4j
是通过ServiceLoader
机制来寻找其具体的日志实现的。
关于ServiceLoader
,这里不展开描述,有兴趣的童鞋可以查看文档:https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html
ok,直接进入slf4j的源码分析。
3.2 源码分析
clone
源码到本地,我们只需要看slf4j-api
这块的源码:
再来看看前面使用log4j的代码:
/**
* slf4j测试
*
* @author : YangLinWei
* @createTime: 2023/10/8 09:44
* @version: 1.0.0
*/
public class Slf4jTest {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(Slf4jTest.class);
logger.info("Current Timestamp: {}", System.currentTimeMillis());
}
}
可以看到Logger
对象是通过LoggerFactory
工厂去获取的,进入方法查看:
/***
* 返回一个与传递的类参数相对应的名称的日志记录器,使用静态绑定的 {@link ILoggerFactory} 实例。
*
* <p>
* 如果clazz参数与由SLF4J内部计算的调用者名称不同,那么只有在系统属性slf4j.detectLoggerNameMismatch设置为true时,才会打印日志名称不匹配警告。
* 默认情况下,此属性未设置,即使存在日志名称不匹配,也不会打印警告。
*
* @param clazz 返回的日志记录器将以clazz的名称命名
* @return 日志记录器
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:06
* @version: 1.0.0
*/
public static Logger getLogger(Class<?> clazz) {
// 获取日志记录器
Logger logger = getLogger(clazz.getName());
// 其它校验
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;
}
继续查看getLogger方法(获取日志记录器):
/**
* 返回一个根据名称参数命名的日志记录器,使用静态绑定的 {@link ILoggerFactory} 实例。
*
* @param name 日志记录器的名称。
* @return 日志记录器
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:06
* @version: 1.0.0
*/
public static Logger getLogger(String name) {
// 获取ILoggerFactory实例
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}
继续查看getILoggerFactory方法:
/**
* 返回当前正在使用的 {@link ILoggerFactory} 实例。
*
* ILoggerFactory 实例在编译时与此类绑定。
*
* @return 正在使用的 ILoggerFactory 实例
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:06
* @version: 1.0.0
*/
public static ILoggerFactory getILoggerFactory() {
// 可以看到,这里准备开始获取对应的日志实现了
return getProvider().getLoggerFactory();
}
点击查看getProvider方法:
/**
* 获取正在使用的provider
* @author : YangLinWei
* @createTime: 2023/10/8 11:06
* @version: 1.0.0
*
*/
static SLF4JServiceProvider getProvider() {
if(INITIALIZATION_STATE == UNINITIALIZED) {
// provider未初始化,进行初始化操作
synchronized(LoggerFactory.class) {
if(INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization(); // 这里进行初始化操作
}
}
}
switch(INITIALIZATION_STATE) {
// 根据状态,返回不同的内容
case SUCCESSFUL_INITIALIZATION:
return PROVIDER;
case NOP_FALLBACK_INITIALIZATION:
return NOP_FALLBACK_SERVICE_PROVIDER;
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_PROVIDER;
}
throw new IllegalStateException("Unreachable code");
}
到这里,可以查看到时如何初始化不同的日志实现了,进入performInitialization
方法:
/**
* 初始化Provider
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:16
* @version: 1.0.0
*/
private final static void performInitialization() {
bind();
if(INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
versionSanityCheck();
}
}
/**
* 绑定Provider
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:17
* @version: 1.0.0
*/
private final static void bind() {
try {
List<SLF4JServiceProvider> providersList = findServiceProviders(); // 查找可以用的provider日志实现
reportMultipleBindingAmbiguity(providersList);
if(providersList != null && !providersList.isEmpty()) {
// 如果provider日志实现为空,设置装填并报告
PROVIDER = providersList.get(0);
// SLF4JServiceProvider.initialize() is intended to be called here and nowhere else.
PROVIDER.initialize();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(providersList);
} else {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("No SLF4J providers were found.");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_PROVIDERS_URL + " for further details.");
Set<URL> staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportIgnoredStaticLoggerBinders(staticLoggerBinderPathSet);
}
postBindCleanUp();
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
}
}
可以看到,这里的日志,就是前面没有添加日志实现依赖时的打印内容,继续进入findServiceProviders
方法看看:
/**
* 查找并返回 SLF4JServiceProvider的列表。
*
* 这个方法用于查找并加载 SLF4JServiceProvider,以便在运行时提供日志记录功能。
* 它会优先尝试加载通过配置明确指定的服务提供者,然后再使用 Java 的 ServiceLoader 查找其他服务提供者。
*
* @return SLF4J 服务提供者的列表
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:17
* @version: 1.0.0
*/
static List<SLF4JServiceProvider> findServiceProviders() {
List<SLF4JServiceProvider> providerList = new ArrayList<>();
//使用加载当前类的类加载器来搜索Service。
final ClassLoader classLoaderOfLoggerFactory = LoggerFactory.class.getClassLoader();
// 尝试加载通过配置明确指定的ServiceProvider
SLF4JServiceProvider explicitProvider = loadExplicitlySpecified(classLoaderOfLoggerFactory);
if (explicitProvider != null) {
providerList.add(explicitProvider);
return providerList;
}
// 使用 Java 的 ServiceLoader 查找其他服务提供者
ServiceLoader<SLF4JServiceProvider> serviceLoader = getServiceLoader(classLoaderOfLoggerFactory);
Iterator<SLF4JServiceProvider> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
safelyInstantiate(providerList, iterator);
}
return providerList;
}
通过阅读以上的代码,可以知道slf4j通过ServiceLoader加载不同的日志框架,而不同的日志框架都需要实现SLF4JServiceProvider
接口,也就是所谓的“门面模式”。
3.3 SLF4JServiceProvider
继续查看SLF4JServiceProvider有哪些接口:
/**
* 这个接口基于java.util.ServiceLoader的范式。
*
* 它取代了在 SLF4J 版本 1.0.x 到 1.7.x 中使用的旧的静态绑定机制。
*
* @author : YangLinWei
* @createTime: 2023/10/8 11:17
* @version: 1.0.0
*/
public interface SLF4JServiceProvider {
/**
* 返回org.slf4j.LoggerFactory类应该绑定到的ILoggerFactory实例。
*
* @return ILoggerFactory实例
*/
public ILoggerFactory getLoggerFactory();
/**
* 返回org.slf4j.MarkerFactory 类应该绑定到的IMarkerFactory实例。
*
* @return IMarkerFactory实例
*/
public IMarkerFactory getMarkerFactory();
/**
* 返回MDC应该绑定到的MDCAdapter实例。
*
* @return MDCAdapter实例
*/
public MDCAdapter getMDCAdapter();
/**
* 返回日志实现支持的 SLF4J 的最大 API 版本。
*
* 例如:"2.0.1"
*
* @return 字符串形式的 API 版本
*/
public String getRequestedApiVersion();
/**
* 初始化日志后端。
*
* 注意:这个方法预期只会被LoggerFactory类调用,不应该从其他地方调用。
*
*/
public void initialize();
}
例如:logback对应的实现如下
后续的细节不再赘述了。
04 文末
本文主要是讲解SLF4J
的使用以及源码流程,博主主要是想表达的是slf4j
是用的是ServiceLoader
机制来动态加载不同的日志实现的,这也是实现门面设计模式的一个常用技巧。
相信大家还记得之前流行的log4j漏洞吧?如果使用了本文的门面模式,直接替换依赖就完美解决了,而无需再去修改代码。
log4j漏洞公告相关新闻:https://s.tencent.com/research/report/144
希望本文能帮助到大家,谢谢大家的阅读,本文完!