Java 日志框架的使用

日志系统的几个概念

Logger

Logger负责生成日志。用户代码中需要生成日志的地方,调用Logger的API来产生日志。但是最终日志输出到哪里不归Logger负责,而是由Appender决定。

Logger具有层级结构。最高层的logger叫做root。Logger的name为其所在class的全路径名(<包名>.)。Logger层级结构的判定和class的全路径名紧密相关。例如com.example.partA.xxx的logger是com.example.partA这个logger的下层logger。它们的层级关系如下所示(从上到下为高级到低级):

root
com
com.example
com.example.partA
com.example.partA.xxx

Logger按照name来区分。使用getLogger方法多次获取name相同的logger,实际上获取到的是同一个对象。无论是log4j-api和slf4j都是如此。可用下面的代码验证:

// log4j-api
Logger log1 = LogManager.getLogger(Main.class);
Logger log2 = LogManager.getLogger(Main.class);
Logger logA = LogManager.getLogger(A.class);
System.out.println(log1 == log2);
System.out.println(log1 == logA);
// slf4j-api
org.slf4j.Logger logger1 = LoggerFactory.getLogger(Main.class);
org.slf4j.Logger logger2 = LoggerFactory.getLogger(Main.class);
org.slf4j.Logger loggerA = LoggerFactory.getLogger(A.class);
System.out.println(logger1 == logger2);
System.out.println(logger1 == loggerA);

运行的输出为:

true
false
true
false

那么问题来了,logger的层级有什么作用?在日志系统中,每个logger都对应有自己的配置(日志级别过滤和appender)。为每一个logger分别做配置是件很麻烦的事。因此常用的方式是对某一个层级的logger做统一的配置。下层的logger如果没有专门的配置,会自动继承它上层logger的配置。

Appender

Appender用来控制日志输出的目的地。比如输出到console(控制台),文件,滚动文件(按照大小或者时间自动切分文件),甚至是Kafka等。需要注意的是,不同的日志框架支持的appender种类和功能都不相同,配置方式也都不同,需要根据实际使用情况专门配置。

Level日志级别

通常日志有如下几个级别:

  • ALL
  • DEBUG
  • INFO
  • WARN
  • ERROR
  • OFF

在输出日志的时候可以按照级别过滤日志内容,减少不关心内容的输出。列表中ALL为输出所有级别日志,OFF为不输出任何日志。按照上面中列表的顺序,如果配置日志过滤级别为某个级别,则该级别及其后续级别的日志都会被打印。例如配置级别INFO,则INFO,WARN和ERROR级别信息都会被输出。

常见日志框架

log4j

这里的log4j特指log4j 1.x。log4j即Log for Java。1.x版本在新项目中不推荐使用。

log4j 1.x有一个包叫做log4j-1.2-api,作用是适配log4j 1.x版本接口到新的log4j2。

reload4j

Reload4j是log4j 1.x的作者从log4j 1.2.17版本拉出来的分支,旨在修复log4j 1.x中存在的安全问题。可以无缝替换log4j 1.x(即直接替换项目中的log4j.jar为reload4j.jar)。作者单独拉一个分支而不直接发布log4j 1.x新版本的原因是log4j 1.x在Apache社区已经EOL(End of Life),不会再发布升级版本。

新项目如果考虑使用log4j,建议使用较新的log4j2。

参考链接:https://reload4j.qos.ch/

logback

logback是log4j 1.x的大幅增强版本。

logback特性介绍:https://logback.qos.ch/reasonsToSwitch.html

另外,logback本地实现了slf4j API,这意味着使用logback作为slf4j的binding/provider的开销最小。新项目可考虑使用logback。

log4j2

log4j2是log4j 1.x的性能增强和配置简化版,是目前性能最强的日志框架。新项目中推荐使用log4j2。

log4j2增强特性一览:https://juejin.cn/post/6966060925803724836

需要注意的是log4j2的包名已变化,为org.apache.logging.log4j,而log4j 1.x则是org.apache.log4j

注意:log4j2 早期版本存在著名的远程代码执行漏洞(CVE-2021-44832)。为了保证安全,项目中一定要注意使用的版本。该漏洞在如下版本中修复:Log4j 2.17.1 (Java 8), 2.12.4 (Java 7) and 2.3.2 (Java 6)。需要根据项目使用的JDK版本,选择使用已修复的log4j2版本。

参见:

  • https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core。
  • https://logging.apache.org/log4j/2.x/security.html

和log4j 1.x不同的是,log4j2把接口和实现分离开了,分别为log4j-apilog4j-core。其中log4j-api作用和下面即将讨论的slf4j一样,是一种日志门面(应用代码和日志框架的兼容层)。和slf4j不同的地方是log4j-api只能对接自己的实现log4j-core,不能使用其他的日志框架。

联系上面提到的logback。社区目前形成了2套体系(官方推荐的日志门面和日志实现的组合):

  • slf4j-api logback
  • log4j-api log4j-core

log4j-api只能对接log4j-core,而slf4j-api则兼容log4j1和2,reload4j,logback和java.util.logging等,明显适用范围更广。因此建议项目中使用slf4j-api而不是log4j-api。当然如果为了更好的性能(理论上),也可以选择log4j-api + log4j-core体系。

java util logging

它是JDK自带的日志框架。和log4j非常相似,但Java util logging的迭代速度较慢,不容易升级(只随着JDK发布)。因此,log4j等其他日志框架的功能和灵活性远强于java util logging,且版本的迭代速度和漏洞bug的响应速度也快于它。因此不建议在项目中使用java util logging。

java util logging和log4j的详细对比参见:http://www.blogjava.net/lhulcn618/articles/16996.html

日志门面

上面提到了多种日志框架,它们的API互不相同。这带来了问题:我们很难将一个项目从某日志框架迁移到另一个日志框架。另外如果我们的项目引用了其他多个模块,这些模块如果使用的日志框架各不相同的话,就需要维护多套日志框架。复杂度和维护量完全不可控。为了解决这个问题,引入了日志门面。将日志的实现和接口分离开。这样项目可以在不改写代码的前提下更换日志实现框架,应用代码和日志框架之间彻底解耦,提高了项目的可维护性。

因此,强烈建议项目不要直接使用某个具体的日志框架API,统一使用日志门面。

Apache commons logging

Apache较早的一个日志接口。内部有一个很简单的日志实现。当然commons logging在更多情况下是配合第三方的日志实现来使用。

log4j-api

如前面所说log4j2将API和实现部分分离开。这样log4j-api就相当于是日志门面了。但是log4j-api只能和log4j2的实现配合使用,无法和其他日志框架结合。如果使用log4j2可考虑在新项目中使用log4j-api。如果项目以后可能更换日志框架,或者和其他项目结合使用,建议使用下文中提到的slf4j而不是log4j-api。

slf4j

slf4j是目前最流行的日志门面。编写代码的时候仅使用slf4j提供的接口。在运行的时候,classpath放入slf4j的binding/provider就可以工作。Binding/provider为slf4j的日志实现,可以为上面提到的Log4j,logback等。slf4j具有最好的日志框架兼容性,推荐在新项目中使用。

slf4j官网手册:https://www.slf4j.org/manual.html

二进制兼容性

不要混用不同版本的slf4j-api和slf4j的log binding。可能会造成未知的问题。运行的时候slf4j会给出警告。

不同版本的slf4j-api是相互兼容的。slf4j-api的版本可以放心更换。

参考链接:https://www.slf4j.org/manual.html#compatibility

log binding/provider

Binding或者是provider为slf4j的具体日志实现。官网介绍可参考:

https://www.slf4j.org/manual.html#swapping

主要包含如下binding/provider:

  • slf4j-log4j12:log4j 1.2的binding。
  • log4j-slf4j-impl:log4j2的binding。
  • slf4j-reload4j:reload4j的binding。
  • logback-classic:logback的binding。
  • slf4j-jdk14:java.util.logging的binding。
  • slf4j-jcl:Apache Common Logging的binding。
  • slf4j-nop:一个不打印任何日志信息的binding。
  • slf4j-simple:打印INFO以上级别信息,输出所有时间到System.err的binding。适合小型程序使用。

桥接器

桥接器用于将项目中正在使用的非slf4j的接口转换为slf4j形式。

官网桥接器介绍:https://www.slf4j.org/legacy.html
官网的图很清晰的指出了桥接器的使用情形:
[图片上传失败...(image-effed1-1686817217906)]

较为常用的集中桥接场景:

  • log4j -> log4j2:引入log4j-1.2-api,log4j-api和log4j-core。不需要中间转换为slf4j。
  • log4j -> slf4j:引入log4j-over-slf4j替换原log4j包。引入slf4j-api。
  • log4j2 -> slf4j:引入log4j-to-slf4j和slf4j-api。
  • Apache commons logging -> slf4j:引入jcl-over-slf4j和slf4j-api。
  • Java util logging -> slf4j:引入jul-to-slf4j。

slf4j + log4j2 使用和配置

项目依赖


    org.slf4j
    slf4j-api
    1.7.25


    org.apache.logging.log4j
    log4j-slf4j-impl
    2.18.0

slf4j使用

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

public class Main {
    public static final Logger logger = LoggerFactory.getLogger(Main.class);
    public static void main(String[] args) {
        logger.info("haha");
    }
}

日志中经常涉及到字符串中带有变量的情况。不建议在打印日志时使用字符串拼接。因为这样会生成大量的String对象,占据过多的字符串常量池空间。

建议的做法是使用参数化消息(占位符)。示例代码如下:

String hostname = "manager";
int port = 22;
logger.info("Hostname: {}. Port: {}", hostname, port);

除了上面的情形。还会遇到如下的情况:打印的日志中包含一些需要准备的信息。这些信息的准备过程比较耗时。我们需要做出优化:如果该级别的日志被过滤掉不需要输出,那么这些信息也就没有必要再准备。这种情形可以使用loggerisXxxEnabled判断完成。isXxxEnabled针对每一个日志级别都对应一个判断方法。

举个例子,debug级别的日志需要打印CPU占用率。获取CPU占用率这个过程较为影响系统性能。因此只需要在开启debug级别日志的时候,才有必要获取CPU占用率。代码如下所示:

if(logger.isDebugEnabled() {
    // 获取CPU占用率
    String cpuUsage = ...
    logger.debug("CPU usage: {}", cpuUsage);
}

参见:https://slf4j.org/faq.html#logging_performance

由于日志实现使用了log4j2,因此项目中日志的配置使用log4j2的配置方式。参见下一节。

log4j2使用

需要引入的依赖为对应版本的log4j-apilog4j-core。使用方法和slf4j基本是相同的,代码如下:

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class Main {
    public static final Logger logger = LogManager.getLogger(Main.class);
    public static void main(String[] args) {
        logger.info("haha");
    }
}

除此之外需要在classpath放入log4j2的配置文件(log4j2.xml)。Maven项目对应着resources目录。

一个简单版的log4j2.xml配置文件内容示例如下。仅仅配置了console appender和root logger。



    
        
            
        
    
    
        
            
        
    

一个复杂版的例子。配置了console,file和rolling file appender以及多个logger:


    
    
    
    
    
        
        
        
            
        
        
        
            
        
        
        
                    
            
            
            
                
                
                
                
                
                
                
            
        
    
    
    
        
        
        
            
            
        
        
        
            
            
        
    

参考材料:

log4j2使用:https://logging.apache.org/log4j/2.x/manual/usage.html

log4j2支持的appender:https://logging.apache.org/log4j/2.x/manual/appenders.html

日志输出格式PatternLayout:https://logging.apache.org/log4j/2.x/manual/layouts.html#PatternLayout

Log4j2中RollingFile的文件滚动更新机制:https://www.cnblogs.com/yeyang/p/7944899.html

log4j2配置文件:https://www.cnblogs.com/bestlmc/p/12012875.html

问题和解答

log4j2使用出现java.lang.NoClassDefFoundError: org/apache/logging/log4j/util/StacklocatorUtil

项目classpath中存在版本较老的log4j-api导致。org/apache/logging/log4j/util/StacklocatorUtil类在项目代码中第一次出现是2017年3月27日(commit-id: 34552d7d725c3b7547e1c19f6ce803b83c60bd94)。需要删除项目中所有版本号在2.8.x之前的log4j-api,重新引入2.9.x或者更新的log4j-api。

打印stacktrace到日志

try {
    Class.forName("com.doesnotexist.A");
} catch (ClassNotFoundException e) {
    StringWriter sw = new StringWriter();
    PrintWriter pw = new PrintWriter(sw);
    e.printStackTrace(pw);
    logger.info(sw.toString());
}

注意:这里的PrintWriterStringWriter不需要close。PrintWriter close的时候实际上关闭的是内部的StringWriter,而StringWriter close的时候什么也不做。详细情况读者可以查看它们的源代码。

或者使用:

logger.info("Exception: ", e);

将exception对象作为方法第二个参数传入。

log4j/log4j2不打印日志或者是日志配置不生效

问题原因很可能是日志配置文件错误(名字,存放路径等)或者是项目中有多个日志配置文件。可以在应用启动的时候增加-Dlog4j.debug虚拟机参数,打印出log4j/log4j2日志配置文件的加载过程,从而帮助定位问题。

你可能感兴趣的:(Java 日志框架的使用)