源码仓库:
- 基于Spring源码:https://gitee.com/Ethanzyc/spring-learn.git
- spring-maven项目:
主流日志技术
JUL
java.util.logging.Logger
这是 jdk 中自带的
代码
package com.example.springmaven.log;
import org.junit.jupiter.api.Test;
import java.util.logging.Logger;
public class TestJul {
@Test
public void jul() {
Logger logger = Logger.getLogger("a");
logger.info("aaa");
}
}
Log4j
代码
log4j 一代的依赖如下
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
提一嘴:二代的依赖如下
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.13.2</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.13.2</version>
</dependency>
创建 log4j.properties 文件
log4j.rootLogger=DEBUG, stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=log4j %5p [%t] - %m%n
测试代码如下
package com.example.springmaven.log;
import org.apache.log4j.Logger;
import org.junit.jupiter.api.Test;
public class TestLog4j {
@Test
public void log4j() {
Logger logger = Logger.getLogger("a");
logger.info("11");
}
}
缺点
这时候 JUL 和 Log4j 互不影响,但这也会产生一些问题,比如我整个项目的日志就不是统一的,有各种格式,为了解决这个问题,诞生了 commons-logging。
commons-longging
全程叫 jarkartCommins-logging
,简称 JCL。
代码
依赖:
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
测试代码如下
package com.example.springmaven.log;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.junit.jupiter.api.Test;
public class TestJCL {
@Test
public void jcl() {
Log log = LogFactory.getLog("a");
log.info("jjjjjjj");
}
}
为什么是log4j去打印的呢?
jcl 并不是一个具体的日志打印技术,他是一个抽象类,可以看下他的 getLog()
方法。
public static Log getLog(String name) throws LogConfigurationException {
return getFactory().getInstance(name);
}
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;
}
主要看下 newInstance(name)
方法
protected Log newInstance(String name) throws LogConfigurationException {
Log instance;
try {
if (logConstructor == null) {
instance = discoverLogImplementation(name);
}
else {
Object params[] = { name };
instance = (Log) logConstructor.newInstance(params);
}
if (logMethod != null) {
Object params[] = { this };
logMethod.invoke(instance, params);
}
return instance;
} catch (LogConfigurationException lce) {
// this type of exception means there was a problem in discovery
// and we've already output diagnostics about the issue, etc.;
// just pass it on
throw lce;
} catch (InvocationTargetException e) {
// A problem occurred invoking the Constructor or Method
// previously discovered
Throwable c = e.getTargetException();
throw new LogConfigurationException(c == null ? e : c);
} catch (Throwable t) {
handleThrowable(t); // may re-throw t
// A problem occurred invoking the Constructor or Method
// previously discovered
throw new LogConfigurationException(t);
}
}
主要看下 discoverLogImplementation(name)
方法里,里面有个 _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"
};
_classesToDiscover_
会依次遍历这四个类,调用 createLogFromClass
方法
for(int i=0; i<classesToDiscover.length && result == null; ++i) {
result = createLogFromClass(classesToDiscover[i], logCategory, true);
}
createLogFromClass
方法中会用 Class._forName_(logAdapterClassName)
去加载,如果加载到了,会调用如下方法去实例化。
constructor = c.getConstructor(logConstructorSignature);
Object o = constructor.newInstance(params);
但这里实例化的时候其实还包了一层,log4j的类名 private static final String _LOGGING_IMPL_LOG4J_LOGGER _= "org.apache.commons.logging.impl.Log4JLogger";
其实还是 commons.logging 包下面的,它实例化的时候是通过 getLogger()
方法去实例化 log4j 本体。
public Logger getLogger() {
Logger result = logger;
if (result == null) {
synchronized(this) {
result = logger;
if (result == null) {
logger = result = Logger.getLogger(name);
}
}
}
return result;
}
流程图
去掉 log4j
所以,如果把 log4j 的依赖去掉,就是 jul 去打印日志了
番外
Class._forName_()
的时候能看它也打印了日志,这时候其实日志打印都还没准备好,点进去看下它是用什么打印的
点到最后发现用的是 PrintStream,所以我们经常看到的日志相关的错误都是直接红字显示的。
private static PrintStream diagnosticsStream = null;
缺点
- 还是存在硬编码。
- 之前假如有log4j打印的,还是不会变,必须
LogFactory.getLog("a")
这样写才能做到统一。Slf4j
和 commons logging 不一样,slf4j 采用的是绑定器的思路,可以看下图流程图
代码
依赖如下
测试代码如下 ```java import org.junit.Test; import org.slf4j.Logger; import org.slf4j.LoggerFactory;<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
public class TestSlf4j { @Test public void testJCL() { Logger logger = LoggerFactory.getLogger(“a”); logger.info(“slf4j”); } }
这时候执行会报错,可以看到错误就是没有绑定器。<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/764382/1640422336344-4b998703-a274-4735-989c-52d9262e93ae.png#clientId=u0a258d75-3ff5-4&from=paste&height=191&id=uf1c50d04&margin=%5Bobject%20Object%5D&name=image.png&originHeight=382&originWidth=2006&originalType=binary&ratio=1&size=102746&status=done&style=none&taskId=u7c70f2de-a207-42a4-87af-d36cf1304c5&width=1003)<br />这时候我们项目虽然有 slf4j 和 log4j的依赖,但我们并没有把他们绑定在一起,所以 slf4j 并不知道要用什么日志技术去打印。<br />从 slf4j 的官网可以看到有哪些官方的绑定器 [https://www.slf4j.org/manual.html](https://www.slf4j.org/manual.html)<br />![image.png](https://cdn.nlark.com/yuque/0/2021/png/764382/1640422578430-9f5527ae-5153-4e8f-b5f9-03d4c07cf3ea.png#clientId=u0a258d75-3ff5-4&from=paste&height=542&id=u8e0cc576&margin=%5Bobject%20Object%5D&name=image.png&originHeight=1084&originWidth=2146&originalType=binary&ratio=1&size=315269&status=done&style=none&taskId=u9f97900f-937e-480d-b880-be5d425339e&width=1073)<br />所以我们加入 slf4j 和 log4j 的绑定器
```java
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.32</version>
</dependency>
再执行测试代码就正常了
这时候假如把 log4j 的绑定器换成 jul 的,log4j 本身的依赖还是不动
<!-- <dependency>-->
<!-- <groupId>org.slf4j</groupId>-->
<!-- <artifactId>slf4j-log4j12</artifactId>-->
<!-- <version>1.7.32</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.32</version>
</dependency>
缺点
绑定器还是没有解决之前代码用别的日志框架打印的日志。
但slf4j其实也有解决方式,就是 slf4j 的桥接器。
桥接器
现在依赖如下
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.32</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.4</version>
</dependency>
logback-classic 包含了 logback 本身和绑定器。也就是说现在我们用 Slf4j 打印的应该是 logback 打印的,但如果是 log4j 打印还是 log4j。如下:
这时候,我们想让 log4j 打印出来的还是用 slf4j 输出,那就要用到桥接器。
依赖如下:
<!-- https://mvnrepository.com/artifact/org.slf4j/log4j-over-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.32</version>
</dependency>
去掉一些冲突依赖,最终依赖如下
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.slf4j/log4j-over-slf4j -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>log4j-over-slf4j</artifactId>
<version>1.7.32</version>
</dependency>
可以看到如下打印,虽然是 log4j 实例化的对象打印的,但还是用 logback 打印的。