本文转载自微信公众号「JAVA前线」,作者IT徐胖子 。转载本文请联系JAVA前线公众号。
1 日志规约
《阿里巴巴开发手册》日志规约章节有一条强制规定:应用中不可直接使用日志系统(Log4j、Logback)API,而应依赖使用日志框架SLF4J中的API。使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一:
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- private static final Logger logger = LoggerFactory.getLogger(Abc.class);
我们在使用日志框架过程中会发现,日志框架种类很多如slf4j、log4j、logback等等,在引入依赖时很容易混淆。那么这些框架是什么关系、应该如何使用就是本文需要回答的问题。
2 实例分析
在编写代码之前我们首先了解slf4j全称,我认为这会对理解这个框架有所帮助:
- Simple Logging Facade for Java
全称的含义就是Java简单日志门面,我们知道有一种设计模式被称为门面模式,其本质是化零为整,通过一个对象将散落在各处的功能整合在一起,这样外部只要通过与这个对象交互,由该对象选择具体实现细节。slf4j就是这样一个门面,应用程序只需要和slf4j进行交互,slf4j选择使用哪一个日志框架的具体实现。
2.1 slf4j-jdk14
(1) 引入依赖
- -- slf4j -->
-
-
org.slf4j -
slf4j-api -
1.7.30 -
-
- -- jdk14 -->
-
-
org.slf4j -
slf4j-jdk14 -
1.7.30 -
-
(2) 代码实例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 输出日志
- LogTest
- 三月 14, 2021 11:39:14 上午 com.my.log.test.jdk14.LogTest main
- 信息: info message
- 三月 14, 2021 11:39:14 上午 com.my.log.test.jdk14.LogTest main
- 严重: error message
2.2 slf4j-simple
(1) 引入依赖
- -- slf4j -->
-
-
org.slf4j -
slf4j-api -
1.7.30 -
-
- -- simple -->
-
-
org.slf4j -
slf4j-simple -
1.7.30 -
-
(2) 代码实例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 输出日志
- [main] INFO com.my.log.test.simple.LogTest - info message
- LogTest
- [main] ERROR com.my.log.test.simple.LogTest - error message
2.3 logback
(1) 引入依赖
- -- slf4j -->
-
-
org.slf4j -
slf4j-api -
1.7.30 -
-
- -- logback -->
-
-
ch.qos.logback -
logback-core -
1.2.3 -
-
-
ch.qos.logback -
logback-classic -
1.2.3 -
-
(2) 代码实例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 输出日志
- 11:40:53.406 [main] INFO com.my.log.test.logbck.LogTest - info message
- LogTest
- 11:40:53.410 [main] ERROR com.my.log.test.logbck.LogTest - error message
2.4 slf4j-log4j12
(1) 引入依赖
- -- slf4j -->
-
-
org.slf4j -
slf4j-api -
1.7.30 -
-
- -- log4j12 -->
-
-
org.slf4j -
slf4j-log4j12 -
1.7.30 -
-
(2) 代码实例
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
-
- public class LogTest {
- private final static Logger logger = LoggerFactory.getLogger(LogTest.class);
- public static void main(String[] args) {
- logger.info("info message");
- System.out.println("LogTest");
- logger.error("error message");
- }
- }
(3) 日志配置
'http://jakarta.apache.org/log4j/'> -
name ="myConsoleAppender" class="org.apache.log4j.ConsoleAppender"> -
"org.apache.log4j.PatternLayout" > - name="ConversionPattern" value="[%d{dd HH:mm:ss,SSS\} %-5p] [%t] %c{2\} - %m%n" />
-
- --过滤器设置输出级别 -->
-
"org.apache.log4j.varia.LevelRangeFilter" > - name="levelMin" value="debug" />
- name="levelMax" value="error" />
- name="AcceptOnMatch" value="true" />
-
-
-
-
"debug" /> -
"myConsoleAppender" /> -
-
(4) 输出日志
- [14 11:41:39,198 INFO ] [main] log4j.LogTest - info message
- LogTest
- [14 11:41:39,201 ERROR] [main] log4j.LogTest - error message
3 源码分析
我们发现上述实例中Java代码并没有变化,只是将引用具体日志框架实现进行了替换,例如依赖从simple替换为log4j,具体日志服务实现就替换成了log4j,这到底是怎么实现的?我们通过阅读源码回答这个问题。
3.1 阅读准备
(1) 源码地址
目前最新版本2.0.0-alpha2-SNAPSHOT
- https://github.com/qos-ch/slf4j
(2) 项目结构
我们从项目结构可以看出一些信息:门面是api模块,具体实现包括jdk14、log4j12、simple模块,需要注意logback是同一个作者的另一个项目不在本项目。
(3) 阅读入口
- package org.slf4j;
-
- public class NoBindingTest {
- public void testLogger() {
- Logger logger = LoggerFactory.getLogger(NoBindingTest.class);
- logger.debug("hello" + diff);
- assertTrue(logger instanceof NOPLogger);
- }
- }
3.2 源码分析
LoggerFactory.getLogger
- public final class LoggerFactory {
- 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(clazz.getName())
- public final class LoggerFactory {
- public static Logger getLogger(String name) {
- ILoggerFactory iLoggerFactory = getILoggerFactory();
- return iLoggerFactory.getLogger(name);
- }
- }
getILoggerFactory()
- public final class LoggerFactory {
- public static ILoggerFactory getILoggerFactory() {
- return getProvider().getLoggerFactory();
- }
- }
getProvider()
- public final class LoggerFactory {
- static SLF4JServiceProvider getProvider() {
- if (INITIALIZATION_STATE == UNINITIALIZED) {
- 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_FACTORY;
- case FAILED_INITIALIZATION:
- throw new IllegalStateException(UNSUCCESSFUL_INIT_MSG);
- case ONGOING_INITIALIZATION:
- return SUBST_PROVIDER;
- }
- throw new IllegalStateException("Unreachable code");
- }
- }
performInitialization()
- public final class LoggerFactory {
- private final static void performInitialization() {
- bind();
- if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
- versionSanityCheck();
- }
- }
- }
bind()
- public final class LoggerFactory {
- private final static void bind() {
- try {
- // 核心代码
- List
providersList = findServiceProviders(); - reportMultipleBindingAmbiguity(providersList);
- if (providersList != null && !providersList.isEmpty()) {
- PROVIDER = providersList.get(0);
- PROVIDER.initialize();
- INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
- reportActualBinding(providersList);
- }
- // 省略代码
- } catch (Exception e) {
- failedBinding(e);
- throw new IllegalStateException("Unexpected initialization failure", e);
- }
- }
- }
findServiceProviders()
这是加载具体日志实现的核心方法,使用SPI机制加载所有SLF4JServiceProvider实现类:
- public final class LoggerFactory {
- private static List
findServiceProviders() { - ServiceLoader
serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class); - List
providerList = new ArrayList(); - for (SLF4JServiceProvider provider : serviceLoader) {
- providerList.add(provider);
- }
- return providerList;
- }
- }
SPI(Service Provider Interface)是一种服务发现机制,本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件加载实现类,这样可以在运行时动态为接口替换实现类,通过SPI机制可以为程序提供拓展功能。本文以log4j为例说明使用SPI功能的三个步骤:
(a) 实现接口
- public class Log4j12ServiceProvider implements SLF4JServiceProvider
(b) 配置文件
- 文件位置:src/main/resources/META-INF/services/
- 文件名称:org.slf4j.spi.SLF4JServiceProvider
- 文件内容:org.slf4j.log4j12.Log4j12ServiceProvider
(c) 服务加载
- public final class LoggerFactory {
- private static List
findServiceProviders() { - ServiceLoader
serviceLoader = ServiceLoader.load(SLF4JServiceProvider.class); - List
providerList = new ArrayList(); - for (SLF4JServiceProvider provider : serviceLoader) {
- providerList.add(provider);
- }
- return providerList;
- }
- }
只要各种日志实现框架按照SPI约定进行代码编写和配置文件声明,即可以被LoggerFactory加载,slf4j会获取第一个作为实现。
- public final class LoggerFactory {
- private final static void bind() {
- try {
- // 使用SPI机制加载具体日志实现
- List
providersList = findServiceProviders(); - reportMultipleBindingAmbiguity(providersList);
- if (providersList != null && !providersList.isEmpty()) {
- // 获取第一个实现
- PROVIDER = providersList.get(0);
- PROVIDER.initialize();
- INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
- reportActualBinding(providersList);
- }
- // 省略代码
- } catch (Exception e) {
- failedBinding(e);
- throw new IllegalStateException("Unexpected initialization failure", e);
- }
- }
- }
分析到这里我们的问题应该可以得到解答:假设我们项目只引入了slf4j和log4j,相当于只有log4j这一个具体实现,那么本项目就会使用log4j框架。如果将log4j依赖换为logback,那么项目在不改动代码的情况下会使用logback框架。
4 文章总结
本文我们从阿里开发手册日志规约出发,首先分析了如何使用不同的日志框架,然后我们从问题出发(不修改代码即可替换具体日志框架)进行slf4j源码阅读,从源码中我们知道实现核心是SPI机制,这个机制可以动态加载具体日志实现。关于SPI源码分析请参看笔者文章JDK SPI机制,希望本文对大家有所帮助。