1、Java日志体系概述
老话都说,Java日志体系百花齐放,各式各类的日志很多并且繁杂,那么本片博客带你理清这些日志。
Java日志中体系,比较老牌的就是jcl、log4j、jul、logback、slf4j,相信这些日志你都听说过,而且并不陌生。那么我下面来挨个介绍这些日志框架。
这些日志框架各有各的特色,我们一般开发的业务系统中都是使用指定一个日志框架,但一个高扩展的项目,是不会仅限于使用指定单独一个日志框架的,下面会针对mybatis来解释这个东西。
1.1、JUL
叫JUL的原因是这个日志框架所处的包是:java.util.lang下的java.util.logging.Logger。使用方法如下:
Logger log = Logger.getLogger();
log.info(消息);
log.log();
这个没什么好说的,但是需要注意,我们的JUL没有debug,这个比较蛋疼,所以大部分时候我们是不使用JUL的。
1.2、Log4j
引入依赖:
<dependency>
<groupId>com.att.inno</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
加入log4j.properties:
### 设置###
log4j.rootLogger = debug,stdout,D,E
### 输出信息到控制抬 ###
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 = [%-5p] %d{yyyy-MM-dd HH:mm:ss,SSS} method:%l%n%m%n
### 输出DEBUG 级别以上的日志到=E://logs/error.log ###
log4j.appender.D = org.apache.log4j.DailyRollingFileAppender
log4j.appender.D.File = E://logs/log.log
log4j.appender.D.Append = true
log4j.appender.D.Threshold = DEBUG
log4j.appender.D.layout = org.apache.log4j.PatternLayout
log4j.appender.D.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
log4j.appender.E = org.apache.log4j.DailyRollingFileAppender
log4j.appender.E.File =E://logs/error.log
log4j.appender.E.Append = true
log4j.appender.E.Threshold = ERROR
log4j.appender.E.layout = org.apache.log4j.PatternLayout
log4j.appender.E.layout.ConversionPattern = %-d{yyyy-MM-dd HH:mm:ss} [ %t:%r ] - [ %p ] %m%n
使用:
public static void main(String[] args) {
Logger logger = Logger.getLogger("log4j");
logger.debug("哈哈哈哈哈");
}
这个没什么好说的,很常见的日志。
1.3、Logback
引入依赖:
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.0.6</version>
</dependency>
配置文件logback.xml:
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds" debug="true">
<property name="LOG_HOME" value="${catalina.home}/logs/loan"/>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT"/>
</root>
</configuration>
使用:
public static void main(String[] args) {
Logger logback1 = LoggerFactory.getLogger("Logback1");
logback1.debug("哈哈哈哈哈");
}
注意,LoggerFactory是sfl4j的,那么你可能会疑问,为什么不是logback的,此处我们看看刚才引入的依赖:
可以看到,其引入了slf4j,这个我们下面说到slf4j的时候详细说。
1.4、JCL
全名:Jakarta Commons-logging(JCL),老牌日志框架了,提供了很简单的日志功能以及其解耦。注意,这里的解耦,我们下面慢慢说:
先来操作JCL:
引入依赖:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
<version>1.1</version>
</dependency>
调用执行:
public static void main(String[] args) {
Log jcl1 = LogFactory.getLog("jcl1");
jcl1.info("哈哈哈哈哈");
}
这个代码没问题,然后你尝试调用debug方法,你会发现无任何日志出现。
这是因为此时JCL是使用的JUL去打打印日志,而JUL是没有debug级别的,所以当调用debug的时候,没有任何输出日志。
我们先不忙着去讨论其原因,我们再来看一个现象。
我们引入log4j的依赖:
当前的全部依赖如下:
<dependency>
<groupId>com.att.inno</groupId>
<artifactId>log4j</artifactId>
<version>1.2.13</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging-api</artifactId>
<version>1.1</version>
</dependency>
然后加入log4j配置文件,这里就不再赘述了。
然后去调用debug打印日志:
你会发现,日志出来了,而且你仔细看你的log4j配置文件的打印格式,你会发现这个日志就是log4j的日志。
那么我们有初步的判断,jcl它本身自己不打印日志,在我们没引入其他第三方日志的时候,其使用的是jul的日志实现去打印日志,而当我们引入第三方日志log4j的时候,就会使用log4j的日志实现去打印。
其重点的方法在于org.apache.commons.logging.LogFactory.getLog方法,我们进入其源码查看:
public static Log getLog(String name) throws LogConfigurationException {
return getFactory().getInstance(name);
}
此时的getFactory是获取到LogFactory的实现类,返回结果为:org.apache.commons.logging.impl.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;
}
instances是一个集合,里面存放的是已经创建了的指定name的日志对象,如果这个name的日志对象已经创建,则不用再去new了。
我们这里第一次调用,肯定为null,所以我们进入newInstance方法:
注意看这个discoverLogImplementation方法,这个方法就是实际的实现代码:
注意这里有findUserSpecifiedLogClassName这个是如果我们自己指定了非要用某个日志实现类的话,那么这个返回值就会有值,且为我们指定的实现类名,那么这个if里面就会把我们指定的实现类名给new出来,然会给我们。
但是如果我们没有指定(现在就是没有指定的情况),那么这个if就进不去,继续往下面走:
我们看这个for循环,其意思就是,当classesToDiscover的长度小于i 并且 result 等于空时,循环继续进行, 那么classesToDiscover这个数组里面放的是什么呢?
我们来看一下:
这个数组里面包含了四个元素,每个元素对应某一个日志框架的实现框架,例如第一个里面的Log4jLogger,这个类则可以使用log4j的日志类来进行日志打印 ,相对应下面的三个分别对应jul,jdk1.3以及以前的jul,简单的日志实现,一般来说,我们大多使用log4j。
那么回到上面的for循环里面来,这里面大概意思就是从上到下挨个实例化,如果实例化成功,那么result则不为null,那么则继续循环下来,直到数组结束或者实例化成功,如果循环结束还没实例化成功,那么则抛出异常。
此上就是对jcl的解释。
1.5、slf4j
slf4j也是目前比较常见的一款日志框架,但注意,slf4j并没有单独的日志实现,仅仅是提供了日志的解耦操作。
具体解释来看官网:
注意看这里,官网已经明确说明,slf4j充当各种日志框架的简单外观或抽象,说白了,就是可以让我们自由的选择所需要的日志框架,具体如何让我们能自由的选择,我们下面来解释。
引入依赖:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.6</version>
</dependency>
运行代码:
public static void main(String[] args) {
Logger slf4j1 = LoggerFactory.getLogger("slf4j1");
slf4j1.debug("哈哈哈哈");
}
你运行后,会发现,并没有日志,报错信息倒是有:
意思就是不能加载xxxx类,说直白一点,就是我们没有加入任何绑定器,这里就涉及到我们上面说的“自由选择”,slf4j就是使用“绑定器”来让我们实现自由选择的,此时我们加入一个依赖,这个依赖就是对应log4j的绑定器:
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.6</version>
</dependency>
然后加入log4j.properties文件后,运行上面的代码:
你会发现其按照log4j配置的格式进行输出日志了,那么这里说明,当我们加入了log4j的绑定器以后,日志就使用了log4j的日志实现,那么这里当我们切换为logback的绑定器以后,那么就会使用了logback的日志实现,那么这就体现出来了,slf4j使用绑定器让我们实现了自由选择。
2、Mybatis日志源码解析
注意我们的myabtis作为一个优秀的开源框架,日志方面肯定不会给写死,而是拥有解耦的操作,类似于JCL的操作。
我们看myabtis依赖中的LogFactory类:
这个类就是mybatis的日志工厂,注意这个类中,获取log的方法如下:
注意,当获取log的时候,则使用构造器类去创建一个对象返回回去,那么我们这里就可以来考虑看一下,这个logConstructor是如何被赋值的,掌握这个的话那就知道了myabtis是如何实现日志的。
我们看上面的是static代码块中:
传进去的是一个Runnable,我们看看这个tryImplementation方法:
这里判断,如果logConstructor为null的话,那么则说明当前logConstructor还没有值,那么则需要继续执行,我们现在回到useSlf4jLogging方法中去:
public static synchronized void useSlf4jLogging() {
setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
}
这里我们不需要详细的去看其实现规则,我们只需要知道,如果引入了slf4j的依赖,那么前面我们的logConstructor的值就是对接slf4j的日志实现类,如果没引入slf4j的依赖,那么logConstructor继续为null,那么就继续走static下面的useCommonsLogging方法、useLog4J2Logging方法等,直到logConstructor不为空,或者方法执行完。
这里大概的就知道了我们myabtis如何去使用日志方案的。
3、Spring5使用哪种日志
我们Spring5里面的日志使用的是spring-jcl模块,其自定义了jcl的日志操作,但其本质未变,我们在spring5任何一个类的日志打印操作下,我们能看到终究其会到达我们的spring-jcl模块的这个类的这个方法里面来:
这里对logApi的值进行switch,如果为LOG4J,那么则返回与log4j对应对接的日志实现类,相应的下面分别为slf4j,jul。
那么注意,我们这个logApi在哪里赋值的呢?这里是在LogFactory的static代码块中进行初始化的:
注意这里我们的logApi默认设置为JUL这个值。
然后在static代码块里面分别的去loadClass这些类(如果引入了相应的依赖,那么则会loadClass成功):log4j、slf4j,如果一直走的catch,那么说明这些依赖都不存在,那么则为默认的JUL。
到此我们简单的对java日志体系进行了描述,并简单的常见的两个开源框架的日志使用方案进行了描述。