本文简介
- 第一部分,介绍spring-jcl适配各种日志框架的方式
- 第二部分,介绍slf4j适配各种日志框架的方式
- 第三部分,介绍下logback框架的使用及原理
一、spring-jcl分析
说明
Spring5.x开始自己实现了日志框架的适配器,就叫spring-jcl模块 ,该模块是对输出的日志框架进行适配,是从Apache的commons-logging改造而来。spring-jcl默认绑定的日志框架是JUL(全称Java util Logging,是java原生的日志框架)。准备
本文依赖的Spring版本号:5.2.9.RELEASE(Spring5.X版本源码基本一致) 搜索打开LogAdapter类,maven依赖org.springframework:spring-jcl(如下图所示)源码分析
LogAdapter类 LogAdapter类定义的常量如下,看一眼就意识到,这是根据类的全限定名来查找各个日志框架的。private static final String LOG4J_SPI= "org.apache.logging.log4j.spi.ExtendedLogger";
private static final String LOG4J_SLF4J_PROVIDER= "org.apache.logging.slf4j.SLF4JProvider";
private static final String SLF4J_SPI= "org.slf4j.spi.LocationAwareLogger";
private static final String SLF4J_API= "org.slf4j.Logger";
直接看下LogAdapter类的静态代码块,保留原有的英文注释,我加上了中文的逻辑说明,这段代码完整展示了spring-jcl日志框架的加载顺序
static {
// 优先log4j
if (isPresent(LOG4J_SPI)) {
// 如果存在log4j-to-slf4j和slf4j,优先选择slf4j
//(这里slf4j最终会绑定log4j-to-slf4j,本质上使用的还是log4j)
if (isPresent(LOG4J_SLF4J_PROVIDER) && isPresent(SLF4J_SPI)) {
// log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI;
// however, we still prefer Log4j over the plain SLF4J API since
// the latter does not have location awareness support.
logApi = LogApi.SLF4J_LAL;
}
// 否则直接使用log4j
else {
// Use Log4j 2.x directly, including location awareness support
logApi = LogApi.LOG4J;
}
}
// 存在Full SLF4J SPI
else if (isPresent(SLF4J_SPI)) {
// Full SLF4J SPI including location awareness support
logApi = LogApi.SLF4J_LAL;
}
// 存在Minimal SLF4J API
else if (isPresent(SLF4J_API)) {
// Minimal SLF4J API without location awareness support
logApi = LogApi.SLF4J;
}
// 没有导入日志框架,默认使用JUL
else {
// java.util.logging as default
logApi = LogApi.JUL;
}
}
其中isPresent()方法源码如下,就是通过类加载器加载对应的日志框架类,返回是否导入了对应的日志框架。
private static boolean isPresent(String className) {
try {
Class.forName(className, false, LogAdapter.class.getClassLoader());
return true;
}
catch (ClassNotFoundException ex) {
return false;
}
}
LogAdapter是怎么被调用的呢?
顺着日志框架的使用方式即可看到,我依次贴下代码
// spring-jcl使用标准代码
import org.apache.commons.logging.LogFactory;
LogFactory.getLog(AdvanceEverydayApplication.class).info("hello world!");
// 截取LogFactory的getlog方法,
// 可以看到,最终就是调用了LogAdapter的createLog方法
public abstract class LogFactory {
public static Log getLog(Class<?> clazz) {
return getLog(clazz.getName());
}
public static Log getLog(String name) {
return LogAdapter.createLog(name);
}
}
// 截取LogAdapter的createLog方法
// 可以看到,这里就是根据logApi这个属性的值,调用不同框架的适配器,分别创建不同框架的实例
public static Log createLog(String name) {
switch (logApi) {
case LOG4J:
return Log4jAdapter.createLog(name);
case SLF4J_LAL:
return Slf4jAdapter.createLocationAwareLog(name);
case SLF4J:
return Slf4jAdapter.createLog(name);
default:
// Defensively use lazy-initializing adapter class here as well since the
// java.logging module is not present by default on JDK 9. We are requiring
// its presence if neither Log4j nor SLF4J is available; however, in the
// case of Log4j or SLF4J, we are trying to prevent early initialization
// of the JavaUtilLog adapter - e.g. by a JVM in debug mode - when eagerly
// trying to parse the bytecode for all the cases of this switch clause.
return JavaUtilAdapter.createLog(name);
}
}
这个logApi就是最开始的那个静态代码块,确定导入了哪个日志框架
总结
spring-jcl是Spring自己的日志框架适配器,在什么依赖都未导入的情况下,默认使用JUL。引入了对应的日志框架依赖后,会自动使用对应的框架。 同时引入多个日志框架,spring-jcl的优先加载顺序是:log4j-to-slf4j > log4j > full slf4j > slf4j > jul 注意:- 这里的slf4j只是日志门面(facade模式),它必须绑定具体的日志框架才能工作(下一节详细分析)
- full slf4j指的是SLF4J including location awareness support,对应org.slf4j.spi.LocationAwareLogger类,它可以提取发生的位置信息(例如方法名、行号等),必须配合桥接器(brigdes)使用
- slf4j指的是标准的SLF4J接口,对应org.slf4j.Logger类
二、slf4j分析
概述
上面介绍了spring自带的日志适配器框架spring-jcl,但实际项目中大家更喜欢使用SLF4J,它不依赖于spring,架构设计更合理,可以兼容未来的日志框架。 SLF4J(Simple Logging Façade for Java)日志框架,是各种日志框架的简单门面(simple facade)或抽象接口,允许用户部署时选择具体的日志实现。准备
slf4j与spring-jcl的使用方法是不同的(logger与log)//slf4j api
LoggerFactory.getLogger(AdvanceEverydayApplication.class)
.info("hello world!");
//spring-jcl api
LogFactory.getLog(AdvanceEverydayApplication.class)
.info("hello world!");
slf4j的优点
- 采用静态绑定,简单,健壮,并且避免了jcl的类加载器问题
- 增强了参数化日志,提升性能
- 可以支持未来的日志系统
- 添加slf4j-api的依赖
-
绑定具体的日志实现框架
- 绑定已经实现了slf4j的日志框架,直接添加对应依赖
- (或者)绑定没有实现slf4j的日志框架,先添加日志的适配器,再添加实现类的依赖
- 使用slf4j的API在项目中进行统一的日志记录
源码分析
下面源码是基于slf4j-api-1.7.30版本,依赖如下图所示 我们直接从LoggerFactory.getLogger()方法开始,一直到slf4j的绑定实现为止// 只关注下面第3行代码即可
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
// 如果系统属性-Dslf4j.detectLoggerNameMismatch=true,进行检查
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;
}
// 关注第3行,怎样创建的ILoggerFactory?
public static Logger getLogger(String name) {
ILoggerFactory iLoggerFactory = getILoggerFactory();
// 利用工厂对象获取Logger对象
return iLoggerFactory.getLogger(name);
}
// 重点关注第8行即可,performInitialization()是只执行一次的初始化
// 常量代表了LoggerFactory的初始化状态,给出了中文注释
public static ILoggerFactory getILoggerFactory() {
if (INITIALIZATION_STATE == UNINITIALIZED) {
synchronized (LoggerFactory.class) {
if (INITIALIZATION_STATE == UNINITIALIZED) {
INITIALIZATION_STATE = ONGOING_INITIALIZATION;
performInitialization();
}
}
}
switch (INITIALIZATION_STATE) {
case SUCCESSFUL_INITIALIZATION:
// 初始化成功,存在StaticLoggerBinder,返回具体的工厂
return StaticLoggerBinder.getSingleton().getLoggerFactory();
case NOP_FALLBACK_INITIALIZATION:
// 没有找到日志框架,返回一个无操作的工厂
return NOP_FALLBACK_FACTORY;
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_FACTORY;
}
throw new IllegalStateException("Unreachable code");
}
// 第3行bind,就是大家常说的slf4j静态绑定
private final static void performInitialization() {
bind();
if (INITIALIZATION_STATE == SUCCESSFUL_INITIALIZATION) {
// 判断是否匹配
versionSanityCheck();
}
}
// 代码很长,重点关注第13行,catch里面是slf4j-api的版本兼容提示
private final static void bind() {
try {
Set<URL> staticLoggerBinderPathSet = null;
// skip check under android, see also
// http://jira.qos.ch/browse/SLF4J-328
if (!isAndroid()) {
// 如果有多个日志框架,这里进行检测并提示
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding
StaticLoggerBinder.getSingleton();
// 设置为成功初始化状态
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
// 报告实际绑定的日志框架
reportActualBinding(staticLoggerBinderPathSet);
} catch (NoClassDefFoundError ncde) {
// StaticLoggerBinder这个类不存在,设置状态并打印提示
String msg = ncde.getMessage();
if (messageContainsOrgSlf4jImplStaticLoggerBinder(msg)) {
INITIALIZATION_STATE = NOP_FALLBACK_INITIALIZATION;
Util.report("Failed to load class \"org.slf4j.impl.StaticLoggerBinder\".");
Util.report("Defaulting to no-operation (NOP) logger implementation");
Util.report("See " + NO_STATICLOGGERBINDER_URL + " for further details.");
} else {
failedBinding(ncde);
throw ncde;
}
} catch (java.lang.NoSuchMethodError nsme) {
// StaticLoggerBinder.getSingleton()这个方法不存在,设置状态并打印提示
String msg = nsme.getMessage();
if (msg != null && msg.contains("org.slf4j.impl.StaticLoggerBinder.getSingleton()")) {
INITIALIZATION_STATE = FAILED_INITIALIZATION;
Util.report("slf4j-api 1.6.x (or later) is incompatible with this binding.");
Util.report("Your binding is version 1.5.5 or earlier.");
Util.report("Upgrade your binding to version 1.6.x.");
}
throw nsme;
} catch (Exception e) {
failedBinding(e);
throw new IllegalStateException("Unexpected initialization failure", e);
} finally {
postBindCleanUp();
}
}
这一句就是关键代码:StaticLoggerBinder.getSingleton()
StaticLoggerBinder这个类指定了包路径: import org.slf4j.impl.StaticLoggerBinder;
但是这个类并不在slf4j-api的包内,它是由各个日志框架提供的。
例如,logback-classic包里自带这个类,单例模式,直接适配slf4j
如果一个日志框架不带这个类怎么办?