Spring 日志
AbstractApplicationContext
prepareRefresh() --------> logger.trace("Refreshing " + this)
public class LogMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext();
applicationContext.refresh();
applicationContext.close();
}
}
上面这段代码很简单,就是手动调用AbstractApplication的refresh方法。我们将会基于此段代码来分析Spring5日志与Spring4日志的区别。
- 先引入Spring 5的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.0.9.RELEASE</version>
</dependency>
我们看下日志输出
可以看到这里会打印一段话,我们去源码里面找下这段日志是在哪里打印出来的。
protected void prepareRefresh() {
this.startupDate = System.currentTimeMillis();
this.closed.set(false);
this.active.set(true);
if (this.logger.isInfoEnabled()) {
this.logger.info("Refreshing " + this);
}
this.initPropertySources();
this.getEnvironment().validateRequiredProperties();
this.earlyApplicationEvents = new LinkedHashSet();
}
看到上文代码中有个成员变量 logger ,本篇文章的内容主要分析下这个 logger 具体指的是什么。
- 更换成Spring 4的依赖,另外再添加 log4j的依赖
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.24.RELEASE</version>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
添加 log4j.properties的配置文件
### set log levels ###
log4j.rootLogger = error , stdout
### 输出到控制台 ###
log4j.appender.stdout = org.apache.log4j.ConsoleAppender
log4j.appender.stdout.Target = System.out
log4j.appender.stdout.layout = org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern = %d{yyyy-MM-dd HH:mm:ss,SSS} %p [%t] %C.%M(%L) | %m%n
可以发现,将 log4j 的日志级别配置成 error 级别,日志不见了(日志级别为info的时候,日志可以正常输出,可以自己试下,这里就不贴出来了)。在前面的代码中可以看到,日志的打印是通过 logger.info 输出的,说明在Spring 4 中,Spring 中的 logger 被替换成了logger。这里有三个疑问:1、为什么会被替换?2、Spring 4 和 Spring 5 中的 logger 有什么区别?3、Spring 4 和 Spring 5中的原有的 logger 是什么?我们就带着这几个疑问来阅读本篇文章。
先来看下常见的日志框架 log4j、logback、log4j2、jul(java.util.logging)、slf4j、jcl(commons-logging) .... 可以发现在Java开发中,有特别多的日志框架可以选择,初学Java时完全是懵的,不知道要用哪个,公司同事用什么,就跟着用什么。这里我们先分析下jcl。
JCL
Log log = LogFactory.getLog("JCL");
log.info("我依赖的是 commons-logging");
上面这段代码就是jcl的打印日志的api。这里先说下结果,jcl 严格来说不是一个日志框架,因为它本身不提供日志的打印,主要是借助于其他框架,像 log4j、jul 都可以为jcl提供日志输出。这里应该会想到什么时候使用log4j啊,什么时候使用jul啊。好,带着这个问题,我们一层一层拨开jcl的源码。上面可以看到是通过LogFactory.getLog方法来获取到log对象。那我们就来看看这个方法里面干了什么。
public static Log getLog(String name) throws LogConfigurationException {
return getFactory().getInstance(name);
}
可以看到,这个方法里面没有做什么事情,都交给了getFactory().getInstance() 。首先看getFactory方法做了什么事情。
public static final String FACTORY_PROPERTIES = "commons-logging.properties";
Properties props = getConfigurationFile(contextClassLoader, FACTORY_PROPERTIES);
// 由于没有配置 properties文件,所以这里为空。下面对于props变量的判断都不走了。
// Determine whether we will be using the thread context class loader to
// load logging classes or not by checking the loaded properties file (if any).
ClassLoader baseClassLoader = contextClassLoader;
if (props != null) { .... }
....
try {
String factoryClass = getSystemProperty(FACTORY_PROPERTY, null);
if (factoryClass != null) { .... } else { .... }
} catch (SecurityException e) {
.....
} catch (RuntimeException e) {
.....
}
// Second, try to find a service by using the JDK1.3 class
// discovery mechanism, which involves putting a file with the name
// of an interface class in the META-INF/services directory, where the
// contents of the file is a single line specifying a concrete class
// that implements the desired interface.
if (factory == null) {
.....
try {
// 这里没有配置 META-INF/services/org.apache.commons.logging.LogFactory
final InputStream is = getResourceAsStream(contextClassLoader, SERVICE_ID);
.....
} catch (Exception ex) {
.....
}
}
// Third try looking into the properties file read earlier (if found)
if (factory == null) {
if (props != null) { .... } else { .... }
}
// Fourth, try the fallback implementation class
if (factory == null) {
.....
//public static final String FACTORY_DEFAULT = "org.apache.commons.logging.impl.LogFactoryImpl";
factory = newFactory(FACTORY_DEFAULT, thisClassLoader, contextClassLoader);
}
删掉了一些无关紧要的代码,在代码的最后一行,可以知道最后返回的是 LogFactoryImpl,这个类是继承自Factory(抽象类),所以最后的代码就到了LogFactoryImpl.getInstance里面了,继续看这个方法里面做了什么。
public Log getInstance(String name) throws LogConfigurationException {
Log instance = (Log) instances.get(name);
if (instance == null) {
instance = newInstance(name);
instances.put(name, instance);
}
return instance;
}
protected Log newInstance(String name) throws LogConfigurationException {
Log instance;
try {
if (logConstructor == null) { // instance 就是通过这个方法得到的
instance = discoverLogImplementation(name);
}
....
return instance;
} ....
}
}
private Log discoverLogImplementation(String logCategory)
throws LogConfigurationException {
....
// Checks system properties and the attribute map for
// a Log implementation specified by the user under the
//property names {@link #LOG_PROPERTY} or {@link #LOG_PROPERTY_OLD}.
//看到这个方法的注释,可以知道这个方法的作用是检查系统的properties(这里主要是启动时候配置的参数)和配置文件。但是这两个地方我们都没有配置,所以不会走下面这段代码
String specifiedLogClassName = findUserSpecifiedLogClassName();
if (specifiedLogClassName != null) { .... }
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
return result;
}
这里我们主要是看这个for循环里面的代码。createLogFromClass 这个方法,我们就不分析了,里面主要是通过反射,拿到对象。这里涉及到一个变量classesToDiscover,来看看这个是个什么东西。
private static final String[] classesToDiscover = {
LOGGING_IMPL_LOG4J_LOGGER,
"org.apache.commons.logging.impl.Jdk14Logger",
"org.apache.commons.logging.impl.Jdk13LumberjackLogger",
"org.apache.commons.logging.impl.SimpleLog"
};
private static final String LOGGING_IMPL_LOG4J_LOGGER = "org.apache.commons.logging.impl.Log4JLogger";
看到这里是不是恍然大悟,原来是这样,这个数组的顺序,就是jcl获取各个日志框架的策略。可以看到这里总共有四个,这也就解释了,我们前面提到的,jcl可以使用log4j、jul作为它的日志输出。jcl的思想还是挺好的,可以随时替换日志框架,但是,jcl已经基本上不更新了,而且也有更好的框架出现。这里作分析,只是为了解释曾今的疑惑。
SLF4J
The Simple Logging Facade for Java (SLF4J) serves as a simple facade or abstraction for various logging frameworks (e.g. java.util.logging, logback, log4j) allowing the end user to plug in the desired logging framework at deployment time.
Before you start using SLF4J, we highly recommend that you read the two-page SLF4J user manual.
Note that SLF4J-enabling your library implies the addition of only a single mandatory dependency, namely slf4j-api.jar. If no binding is found on the class path, then SLF4J will default to a no-operation implementation.
In case you wish to migrate your Java source files to SLF4J, consider our migrator tool which can migrate your project to use the SLF4J API in just a few minutes.
In case an externally-maintained component you depend on uses a logging API other than SLF4J, such as commons logging, log4j or java.util.logging, have a look at SLF4J's binary-support for legacy APIs.
这里主要的意思是,slf4j 是一个门面模式,可以适配多种日志框架。slf4j需要添加 slf4j-api的依赖,如果不添加 binding, slf4j将什么都不做。
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.29</version>
</dependency>
<!--binding-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.29</version>
</dependency>
需要依赖slf4j-api以及binding log4j12或者其他的日志框架,这个效果跟jcl是相似的。具体的源码就不带大家看了,不难,主要是StaticLoggerBinder 这个类,会得到一个logFacgory,这个logFactory就是你添加的binding(绑定器)的依赖。但是slf4j还有个厉害的地方就是Bridge。
这张图就是slf4j官网提供的Bridge的图片,还是蛮清晰的。log4j或者jcl或者jul都可以通过对应的桥接器桥接到slf4j上面去,然后再通过slf4j做日志输出。Binding和Bridge这两个功能还是蛮厉害的,当你的项目当中依赖了比较多的日志框架,就可以通过这两个功能将所有的日志框架统一起来,做统一的日志输出。当然了,有多个依赖的时候也需要注意避免日志依赖出现死循环。比如说,依赖了slf4j-log4j12作为binding(绑定器),又引入了log4j-over-slf4j作为birdge(桥接器),就会有stackoverflow的异常等着你。
就像图中这样,当然,一般来说在项目当中使用桥接器,是因为难免会依赖一些第三方的库,当第三方的日志库与你的日志库不统一时,桥接器就派上用场啦。
可以看到甚至 Log4j 和 JUL 都可以桥接到SLF4J,再通过 SLF4J 适配到到 Logback!在这里需要注意不能搞出循环的桥接,比如下面这些依赖就不能同时存在:
- jcl-over-slf4j 和 slf4j-jcl
- log4j-over-slf4j 和 slf4j-log4j12
- jul-to-slf4j 和 slf4j-jdk14
上面主要是分析了 Spring 4中依赖的 jcl(commons-logging)和slf4j。到这里我们只解决了一个问题就是,在Spring 4中jcl最终会选择具体的日志框架(jul 或者log4j或者其他)。下面我们来看看Spring 5中有什么不一样的地方,探寻Spring 4和Spring 5之前的区别。
private static LogApi logApi = LogApi.JUL;
static {
ClassLoader cl = LogFactory.class.getClassLoader();
try {
// Try Log4j 2.x API
cl.loadClass("org.apache.logging.log4j.spi.ExtendedLogger");
logApi = LogApi.LOG4J;
}
catch (ClassNotFoundException ex1) {
try {
// Try SLF4J 1.7 SPI
cl.loadClass("org.slf4j.spi.LocationAwareLogger");
logApi = LogApi.SLF4J_LAL;
}
catch (ClassNotFoundException ex2) {
try {
// Try SLF4J 1.7 API
cl.loadClass("org.slf4j.Logger");
logApi = LogApi.SLF4J;
}
catch (ClassNotFoundException ex3) {
// Keep java.util.logging as default
}
}
}
}
public static Log getLog(String name) {
switch (logApi) {
case LOG4J:
return Log4jDelegate.createLog(name);
case SLF4J_LAL:
return Slf4jDelegate.createLocationAwareLog(name);
case SLF4J:
return Slf4jDelegate.createLog(name);
default:
return JavaUtilDelegate.createLog(name);
}
}
这段代码是 Spring 5中的获取Log实现类,可以看到跟Spring 4中完全的不一样。默认的是选择jul,还有三个选项LOG4J(注意这里是log4j2,代码中有说明)、SLF4J_LAL、SLF4J. static代码块说明了,日志的选择策略。
看下spring 5的依赖,发现有个spring-jcl。其实,spring 5自己实现了jcl的功能。