spring5日志框架-常用日志框架使用介绍

Java的Log技术

概念理解

1
2
日志门面:一般采取facade设计模式(外观设计模式:外观模式定义了一个高层的功能,为子系统中的多个模块协同的完成某种功能需求提供简单的对外功能调用方式,使得这一子系统更加容易被外部使用)设计的一组接口应用。
日志实现:接口的实现

日志实现底层基本组成如下:

1
2
3
Loggers:Logger负责捕捉事件并将其发送给合适的Appender。
Appenders:也被称为Handlers,负责从Logger中取出日志消息并将消息发送出去,比如发送到控制台、文件、网络上的其他日志服务或操作系统日志等
Layouts:也被称为Formatters,它负责对日志事件中的数据进行转换和格式化。Layouts决定了数据在一条日志记录中的最终形式。

实现:当Logger记录一个事件时,它将事件转发给适当的Appender。然后Appender使用Layout来对日志记录进行格式化,并将其发送给控制台、文件或者其它目标位置。另外,Filters可以让你进一步指定一个Appender是否可以应用在一条特定的日志记录上。在日志配置中,Filters并不是必需的,但可以让你更灵活地控制日志消息的流动。

20200420223637

主流的Log技术

日志门面 commons-logging,slf4j

日志实现 log4j,jdk-logging,logback,log4j2

这也符合Java的面向对象设计理念,将接口与实现相分离。

日志门面系统的出现其实已经很大程度上缓解了日志系统的混乱,很多库的作者也已经意识到了日志门面系统的重要性,不在库中直接使用具体的日志实现框架。slf4j作为现代的日志门面系统,已经成为事实的标准,并且为其他日志系统做了十足的兼容工作。

我们能做的就是选一个日志实现框架。logback,log4j2是现代的高性能日志实现框架

20200420223741

log4j 和 log4j2

log4j是Apache的一个开源项目,log4j2和log4j是一个作者,只不过log4j2是重新架构的一款日志组件,他抛弃了之前log4j的不足,以及吸取了优秀的logback的设计重新推出的一款新组件。

log4j是通过一个.properties的文件作为主配置文件的,而现在的log4j 2则已经弃用了这种方式,采用的是.xml,.json或者.jsn这种方式来做,可能这也是技术发展的一个必然性,毕竟properties文件的可阅读性真的是有点差。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.16</version>
</dependency>
<!-- log4j 2则是需要2个核心 -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.5</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.5</version>
</dependency>

不需要再依赖第三方的技术

JCL(Jakarta Commons Logging)

是apache公司开发的一个抽象日志通用框架,本身不实现日志记录,但是提供了记录日志的抽象方法即接口(info,debug,error…….)。

1
2
3
4
5
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.1.2</version>
</dependency>

JCL不直接记录日志,通过第三方记录日志:如果没有log4j的依赖情况下用JUL(JUL是jdk自带的日志实现)。如果有了log4j则使用log4j。

通过查看源码得知,底层通过一个数组存放具体的日志框架的类名,然后循环数组依次去匹配这些类名是否在程序中被依赖了,如果找到被依赖的则直接使用。默认有四种:

1
2
3
4
5
classesToDiscover = {String[4]@513} 
3 = "org.apache.commons.logging.impl.SimpleLog"
2 = "org.apache.commons.logging.impl.Jdk13LumberjackLogger"
1 = "org.apache.commons.logging.impl.Jdk14Logger"
0 = "org.apache.commons.logging.impl.Log4JLogger"

循环加载类 获取Class:

20200420223807

成功过的到直接返回Class

20200420223820

JUL

java自带的一个日志记录的技术,直接使用。

1
2
3
4
5
6
public static void main(String[] args) throws IOException {
log.info("info"); //信息日志
log.warning("warning"); //警告日志
log.log(Level.SEVERE,"server"); //严重日志
log.fine("fine");
}

JUL日志等级划分(优先级递减)及内置代表的整数如下:

1
2
3
4
5
6
7
8
9
OFF(Integer.MAX_VALUE)
SEVERE(1000)
WARNING(900)
INFO(800)
CONFIG(700)
FINE(500)
FINER(400)
FINEST(300)
ALL(Integer.MIN_VALUE)

当为 Logger 指定了一个 Level, 该 Logger 会包含当前指定级别以及更高级别的日志,logger默认的级别是INFO,比INFO更低的日志将不显示。JUL的默认配置文件loging.properties,该配置文件位于jdk安装目录的lib包下。

如果要更改logger的输出等级,可以通过修改对应配置项的level等级,也可以通过代码的方式去动态设置logger的等级。

1
log.setLevel(Level.ALL);//设置logger的日志级别为全部,默认输出所有级别日志信息

Handler

JUL中用的比较多的是两个Handler类:ConsoleHandler和FileHandler,其中,ConsoleHandler是对控制台输出的默认处理类,FileHandler是对文件输出的默认处理类

1
2
3
4
5
6
7
8
9
10
11
log.setUseParentHandlers(false); //禁用日志原本处理类
ConsoleHandler consoleHandler = new ConsoleHandler(); //创建控制台输出控制Handler
consoleHandler.setLevel(Level.INFO); //设置控制台输出级别
log.addHandler(consoleHandler); //将Handler加入logger中

//文件输出日志方式
log.setUseParentHandlers(false); //禁用日志原本处理类

FileHandler fileHandler = new FileHandler("日志路径/testlog.log");
fileHandler.setLevel(Level.ALL); //记录级别
log.addHandler(fileHandler); //添加Handler

如果没有 log.setUseParentHandlers(false); 父Handler与子Handler都会生效,此时会输出两遍日志内容

自定义Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class MyHandler extends Handler {

private LogRecord record;

@Override
public void publish(LogRecord record) {
this.record = record;
//自定义内容
}

@Override
public void flush() {
System.out.println("logger:"+this.record.getLoggerName()+"flush");
}

@Override
public void close() throws SecurityException {
System.out.println("logger:"+this.record.getLoggerName()+"close");
}
}


//测试
public static void main(String[] args) throws IOException {

log.setUseParentHandlers(false); //禁用日志原本处理类

MyHandler myHandler = new MyHandler(); //创建自定义日志处理类实体
log.addHandler(myHandler); //添加日志处理实体类

//...
}

Formatter
默认输出是xml格式,修改显示方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyFormate extends Formatter {
@Override
public String format(LogRecord record) {
return new Date()+"-["+record.getSourceClassName()+"."+record.getSourceMethodName()+"]"+record.getLevel()+":"+record.getMessage()+"\n";
}
}


man方法中修改为:
FileHandler fileHandler = new FileHandler("日志路径/testlog.log");
fileHandler.setLevel(Level.ALL); //记录级别
fileHandler.setFormatter(new MyFormate()); //设置自定义样式
log.addHandler(fileHandler); //添加Handler
slf4j

slf4j不记录日志,通过绑定器绑定一个具体的日志记录来完成日志记录。slf4j是门面模式的典型应用

引入包:

1
2
3
4
5
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>

测试运行

1
2
3
4
5
6
public class SLF4j {
public static void main(String[] args){
Logger log = LoggerFactory.getLogger("SLF4j");
log.info("SLF4j");
}
}

报错:

1
2
3
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.

这是因为SLF4J的绑定器没有一个具体的日志处理实现日志功能,如果再引入一个绑定包,如:log4j的绑定 即可。

1
2
3
4
5
6
7
8
9
10
11
        <dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.25</version>
</dependency>
或绑定jul
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-jdk14</artifactId>
<version>1.7.25</version>
</dependency>
slf4j实现原理

Logger log = LoggerFactory.getLogger(SLF4j.class);

主要是通过这句代码去拿具体的实现,获取log对象的源码:

1
2
3
4
5
6
7
8
9
10
11
12
public static Logger getLogger(Class<?> clazz) {
Logger logger = getLogger(clazz.getName());
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;
}

绑定日志框架:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
    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()) {
//去classpath下找STATIC_LOGGER_BINDER_PATH,STATIC_LOGGER_BINDER_PATH值为"org/slf4j/impl/StaticLoggerBinder.class",
//即所有slf4j的实现,\提供的jar包路径下,一定是有"org/slf4j/impl/StaticLoggerBinder.class"存在
staticLoggerBinderPathSet = findPossibleStaticLoggerBinderPathSet();
reportMultipleBindingAmbiguity(staticLoggerBinderPathSet);
}
// the next line does the binding 选取一个StaticLoggerBinder.class来创建一个单例
StaticLoggerBinder.getSingleton();
INITIALIZATION_STATE = SUCCESSFUL_INITIALIZATION;
reportActualBinding(staticLoggerBinderPathSet);
fixSubstituteLoggers();
replayEvents();
// release all resources in SUBST_FACTORY
SUBST_FACTORY.clear();
} catch (NoClassDefFoundError ncde) {
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) {
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);
}
}

//获取所有加载的实现放到set中 避免在系统中同时引入多个slf4j的实现,所以接收的地方是一个Set。
static Set<URL> findPossibleStaticLoggerBinderPathSet() {
// 通过ClassLoader加载"org/slf4j/impl/StaticLoggerBinder.class"
Set<URL> staticLoggerBinderPathSet = new LinkedHashSet<URL>();
try {
ClassLoader loggerFactoryClassLoader = LoggerFactory.class.getClassLoader();
Enumeration<URL> paths;
if (loggerFactoryClassLoader == null) {
paths = ClassLoader.getSystemResources(STATIC_LOGGER_BINDER_PATH);
} else {
paths = loggerFactoryClassLoader.getResources(STATIC_LOGGER_BINDER_PATH);
}
while (paths.hasMoreElements()) {
URL path = paths.nextElement();
staticLoggerBinderPathSet.add(path);
}
} catch (IOException ioe) {
Util.report("Error getting resources from path", ioe);
}
return staticLoggerBinderPathSet;
}

/**
* 若在class path中找到多个绑定类,则打印警告信息
*/
private static void reportMultipleBindingAmbiguity(Set<URL> binderPathSet) {
if (isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Class path contains multiple SLF4J bindings.");
for (URL path : binderPathSet) {
Util.report("Found binding in [" + path + "]");
}
Util.report("See " + MULTIPLE_BINDINGS_URL + " for an explanation.");
}
}

编译期间,编译器会选择其中一个StaticLoggerBinder.class进行绑定,这个地方sfl4j也在reportActualBinding方法中报告了绑定的是哪个日志框架:

1
2
3
4
5
6
private static void reportActualBinding(Set<URL> binderPathSet) {
// binderPathSet can be null under Android
if (binderPathSet != null && isAmbiguousStaticLoggerBinderPathSet(binderPathSet)) {
Util.report("Actual binding is of type [" + StaticLoggerBinder.getSingleton().getLoggerFactoryClassStr() + "]");
}
}

最后得到日志对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public static Logger getLogger(String name) {
//不同的StaticLoggerBinder其getLoggerFactory实现不同 返回一个ILoggerFactory实例 StaticLoggerBinder.getSingleton().getLoggerFactory();
ILoggerFactory iLoggerFactory = getILoggerFactory();
return iLoggerFactory.getLogger(name);
}

public Logger getLogger(String name) {
Logger slf4jLogger = (Logger)this.loggerMap.get(name);
if (slf4jLogger != null) {
return slf4jLogger;
} else {
org.apache.log4j.Logger log4jLogger;
////调用日志框架实现生成"org.apache.log4j.Logger "
if (name.equalsIgnoreCase("ROOT")) {
log4jLogger = LogManager.getRootLogger();
} else {
log4jLogger = LogManager.getLogger(name);
}
//使用适配器包装"org.apache.log4j.Logger "
Logger newInstance = new Log4jLoggerAdapter(log4jLogger);
Logger oldInstance = (Logger)this.loggerMap.putIfAbsent(name, newInstance);
return (Logger)(oldInstance == null ? newInstance : oldInstance);
}
}

桥接器会调用的日志框架实现的相关代码生成其内部的Logger(此Logger与org.slf4j.Logger)不兼容,再通过适配器包装日志框架实现内部的Logger。

日志系统桥接器

我们在项目中一般不直接使用日志实现框架,而是使用外观模式:日志门面组件+桥接器+日志实现框架,这样即使项目更换日志种类,只需更换桥接器和日志实现框架,也就是只更换Jar包就可以了,代码无需做任何改动。

1
2
3
4
5
6
7
8
9
日志系统桥接器说白了就是一种偷天换日的解决方案。

比如log4j-over-slf4j,即log4j -> slf4j的桥接器,这个库定义了与log4j一致的接口(包名、类名、方法签名均一致),但是接口的实现却是对slf4j日志接口的包装,即间接调用了slf4j日志接口,实现了对日志的转发。

但是,jul-to-slf4j是个意外例外,毕竟JDK自带的logging包排除不掉啊,其实是利用jdk-logging的Handler机制,在root logger上install一个handler,将所有日志劫持到slf4j上。要使得jul-to-slf4j生效,需要执行:
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();

spring boot中的日志初始化模块已经包括了该逻辑,故无需手动调用。在使用其他框架时,建议在入口类处的static{ }区执行,确保尽早初始化。

想想一下:

如果log4j -> slf4j,slf4j -> log4j两个桥接器同时存在会出现什么情况?互相委托,无限循环,堆栈溢出。

slf4j -> logback,slf4j -> log4j两个桥接器同时存在会如何?

两个桥接器都会被slf4j发现,在slf4j中定义了优先顺序,优先使用logback,仅会报警,发现多个日志框架绑定实现;

但有一些框架中封装了自己的日志facade,如果其对绑定日志实现定义的优先级顺序与slf4j不一致,优先使用log4j,那整个程序中就有两套日志系统在工作。

log4j2桥接器由log4j2提供,其他桥接器由slf4j提供。

官方桥接案例说明:

20200420223904

详细说明这三个案例

左上图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
现状:目前的应用程序中已经使用了如下混杂方式的API来进行日志的编程:

commons-logging
log4j1
jdk-logging
现在想统一将日志的输出交给logback

解决办法:

第一步:将上述日志系统全部无缝先切换到slf4j

去掉commons-logging(其实去不去都可以),使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j
去掉log4j1(必须去掉),使用log4j-over-slf4j,将log4j1的日志输出切换到slf4j
使用jul-to-slf4j,将jul的日志输出切换到slf4j

第二步:使slf4j选择logback来作为底层日志输出加入以下jar包:
slf4j-api
logback-core
logback-classic
下面的2张图和上面就很类似

右上图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
现状:目前的应用程序中已经使用了如下混杂方式的API来进行日志的编程:

commons-logging
jdk-logging
现在想统一将日志的输出交给log4j1

解决办法:
第一步:将上述日志系统全部无缝先切换到slf4j

去掉commons-logging(其实去不去都可以),使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j
使用jul-to-slf4j,将jul的日志输出切换到slf4j

第二步:使slf4j选择log4j1来作为底层日志输出 加入以下jar包:
slf4j-api
log4j
slf4j-log4j12(集成包)

左下图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
现状:目前的应用程序中已经使用了如下混杂方式的API来进行日志的编程:
commons-logging
log4j

现在想统一将日志的输出交给jdk-logging

解决办法:

第一步:将上述日志系统全部无缝先切换到slf4j
去掉commons-logging(其实去不去都可以),使用jcl-over-slf4j将commons-logging的底层日志输出切换到slf4j
去掉log4j1(必须去掉),使用log4j-over-slf4j,将log4j1的日志输出切换到slf4j

第二步:使slf4j选择jdk-logging来作为底层日志输出

加入以下jar包:

slf4j-api
slf4j-jdk14(集成包)
logback
simple-log

各种jar包总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
log4j1:

log4j:log4j1的全部内容

log4j2:

log4j-api:log4j2定义的API
log4j-core:log4j2上述API的实现

logback:

logback-core:logback的核心包
logback-classic:logback实现了slf4j的API

commons-logging:

commons-logging:commons-logging的原生全部内容
log4j-jcl:commons-logging到log4j2的桥梁
jcl-over-slf4j:commons-logging到slf4j的桥梁
slf4j绑定实际的日志框架
1
2
3
4
5
6
7
使用slf4j的API进行编程,底层想使用log4j1来进行实际的日志输出

slf4j-jdk14:slf4j到jdk-logging的桥梁
slf4j-log4j12:slf4j到log4j1的桥梁
log4j-slf4j-impl:slf4j到log4j2的桥梁
logback-classic:slf4j到logback的桥梁
slf4j-jcl:slf4j到commons-logging的桥梁
日志框架转向slf4j
1
2
3
4
5
使用log4j1的API进行编程,但是想最终通过logback来进行输出,所以就需要先将log4j1的日志输出转交给slf4j来输出,slf4j再交给logback来输出。日志框架之间的切换

jul-to-slf4j:jdk-logging到slf4j的桥梁
log4j-over-slf4j:log4j1到slf4j的桥梁
jcl-over-slf4j:commons-logging到slf4j的桥梁
冲突说明
jcl-over-slf4j 与 slf4j-jcl 冲突
1
2
jcl-over-slf4j: commons-logging切换到slf4j
slf4j-jcl : slf4j切换到commons-logging

如果这两者共存的话,必然造成相互委托,造成内存溢出

log4j-over-slf4j 与 slf4j-log4j12 冲突
1
2
log4j-over-slf4j : log4j1切换到slf4j
slf4j-log4j12 : slf4j切换到log4j1

如果这两者共存的话,必然造成相互委托,造成内存溢出。但是log4j-over-slf4内部做了一个判断,可以防止造成内存溢出:

即判断slf4j-log4j12 jar包中的org.slf4j.impl.Log4jLoggerFactory是否存在,如果存在则表示冲突了,抛出异常提示用户要去掉对应的jar包,代码如下,在slf4j-log4j12 jar包的org.apache.log4j.Log4jLoggerFactory中:

1
2
3
4
5
6
7
8
9
10
11
12
static {
try {
Class.forName("org.apache.log4j.Log4jLoggerFactory");
String part1 = "Detected both log4j-over-slf4j.jar AND bound slf4j-log4j12.jar on the class path, preempting StackOverflowError. ";
String part2 = "See also http://www.slf4j.org/codes.html#log4jDelegationLoop for more details.";
Util.report(part1);
Util.report(part2);
throw new IllegalStateException(part1 + part2);
} catch (ClassNotFoundException var2) {
//TODO:增加日志
}
}
jul-to-slf4j 与 slf4j-jdk14 冲突
1
2
jul-to-slf4j : jdk-logging切换到slf4j
slf4j-jdk14 : slf4j切换到jdk-logging

如果这两者共存的话,必然造成相互委托,造成内存溢出