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
希望本文能帮助到大家,谢谢大家的阅读,本文完!