日志是进行软件开发必不可少的一项功能,目前流行着很多开源日志库,比如log4j、log4j2、logback、JDK Logging、commons-logging、slf4j等。
几种日志产品的介绍
JDK Logging:Java标准库内置的日志包 java.util.logging,以下简称jul。
log4j:一种非常流行的日志框架,最新版本是2.x。
commons-logging:简称jcl,它是一个第三方的日志库,由Apache创建的日志模块。特点是可以挂接不同的日志系统,可以根据配置文件指定挂接的日志系统。默认情况下,jcl自动搜索并使用log4j,如果过没找到log4j,再使用JDK Logging。
前面介绍了Commons Logging和Log4j,它们一个负责充当日志API,一个负责实现日志底层,搭配使用非常便于开发。还有SLF4J和Logback,其实SLF4J类似于Commons Logging,也是一个日志接口,而Logback类似于Log4j,是一个日志的实现。
为什么有了Commons Logging和Log4j,又会蹦出来SLF4J和Logback?这是因为Java有着非常悠久的开源历史,不但OpenJDK本身是开源的,而且我们用到的第三方库,几乎全部都是开源的。开源生态丰富的一个特定就是,同一个功能,可以找到若干种互相竞争的开源库。因为对Commons Logging的接口不满意,有人就搞了SLF4J。因为对Log4j的性能不满意,有人就搞了Logback。
总结起来,几个日志产品的关系如图所示
spring日志测试
spring 4.x 及以前版本基本采用jcl,扩展机制根据用户手动依赖的日志产品进行挂接,改变spring默认日志框架,有兴趣的童鞋可以看下spring 4.x版本的源码。本文基于spring 5.x 版本。
书写了一个小demo进行spring的默认日志测试,pom文件中只添加了spring上下文环境依赖
12 org.springframework 3spring-context 45.0.4.RELEASE 5
运行以下spring上下文初始化代码,
1 public class SpringLogTest { 2 public static void main(String[] args) { 3 AnnotationConfigApplicationContext annotationConfigApplicationContext = new AnnotationConfigApplicationContext(BeanConfig.class); 4 } 5 }
console打印以下日志
十一月 27, 2019 4:38:09 下午 org.springframework.context.support.AbstractApplicationContext prepareRefresh
信息: Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@51081592: startup date [Wed Nov 27 16:37:30 CST 2019]; root of context hierarchy
spring日志源码分析
当然,以上测试中,日志的输出内容并不重要,重要的是spring是如何选择logging类型,并进行打印的。
打印上述日志的地方,是在AbstractApplicationContext类(AnnotationConfigApplicationContext继承自它)的prepareRefresh方法,该方法在AnnotationConfigApplicationContext初始化的时候执行。
1 protected void prepareRefresh() { 2 this.startupDate = System.currentTimeMillis(); 3 this.closed.set(false); 4 this.active.set(true); 5 6 if (logger.isInfoEnabled()) { 7 logger.info("Refreshing " + this); 8 }
这段代码里面有个logger是关键,该logger是在AbstractApplicationContext中的定义的一个属性:
1 protected final Log logger = LogFactory.getLog(getClass());
那我们继续看LogFactory类,会发现该类是在org.apache.commons.logging包下面,但是仔细查看spring包结构,发现该包位于spring-jcl目录下(这是与4.x不同的地方,5.x系列spring开发团队改写了commons-logging的内部源码)
在LogFactory类中,getLog方法是一个门面方法,调用了重载的getFactory方法。
1 public abstract class LogFactory { 2 private static LogFactory.LogApi logApi; //默认的 3 4 public LogFactory() { 5 } 6 7 public static Log getLog(Class> clazz) { 8 return getLog(clazz.getName()); 9 } 10 //根据logApi的值选择logger类型 11 public static Log getLog(String name) { 12 switch(logApi) { 13 case LOG4J: 14 return LogFactory.Log4jDelegate.createLog(name); 15 case SLF4J_LAL: 16 return LogFactory.Slf4jDelegate.createLocationAwareLog(name); 17 case SLF4J: 18 return LogFactory.Slf4jDelegate.createLog(name); 19 default: 20 return LogFactory.JavaUtilDelegate.createLog(name); 21 } 22 } 23 ......部分不太重要的代码,此处忽略42 43 static {
//logApi的默认值 44 logApi = LogFactory.LogApi.JUL; 45 ClassLoader cl = LogFactory.class.getClassLoader(); 46 //根据load的情况,动态更改logApi的值 47 try { 48 cl.loadClass("org.apache.logging.log4j.spi.ExtendedLogger"); 49 logApi = LogFactory.LogApi.LOG4J; 50 } catch (ClassNotFoundException var6) { 51 try { 52 cl.loadClass("org.slf4j.spi.LocationAwareLogger"); 53 logApi = LogFactory.LogApi.SLF4J_LAL; 54 } catch (ClassNotFoundException var5) { 55 try { 56 cl.loadClass("org.slf4j.Logger"); 57 logApi = LogFactory.LogApi.SLF4J; 58 } catch (ClassNotFoundException var4) { 59 ; 60 } 61 } 62 } 63 64 }
在重载的getLog方法中,会根据一个变量logApi的值返回不同的日志对象,logApi的值初始化为jul;
在目前的场景下,代码会返回一个JDK Logging logger进行打印信息,也就是以上的getLog方法中的default情况。
那么,现在问题来了,如果想改变spring的输出日志类型,如何修改?
从上述的代码看,返回logger对象是根据logApi的值的,但是spring在此处并未提供可供用户修改logApi值的方法或者接口。细看LogFactory中有一个static的静态代码块,在该代码块执行的时候会去load相关的日志包产品,根据加载到的情况去动态的修改logApi的值,从而可以达到用户自选择logger类型的目的。
那么我如果要改变spring的输出是log4j打印的消息,那么可以考虑采用slf4j的桥接方式,在pom中加入如下依赖
12 6org.slf4j 3slf4j-api 41.7.25 57 org.slf4j 8slf4j-log4j12 91.7.28 10
运行代码会发现此处打印的日志和之前默认的不同
INFO - Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@3e57cd70: startup date [Wed Nov 27 17:26:47 CST 2019]; root of context hierarchy
spring 5.x 日志的基本功能和核心源码基本说明完了,可能有不太准确的地方,希望各位大神给我留言讨论交流。