美文网首页Java
Java 日志框架的使用

Java 日志框架的使用

作者: AlienPaul | 来源:发表于2023-06-14 16:20 被阅读0次

    日志系统的几个概念

    Logger

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

    Logger具有层级结构。最高层的logger叫做root。Logger的name为其所在class的全路径名(<包名>.<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版本。

    参见:

    和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 使用和配置

    项目依赖

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.apache.logging.log4j</groupId>
        <artifactId>log4j-slf4j-impl</artifactId>
        <version>2.18.0</version>
    </dependency>
    

    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。

    <?xml version="1.0" encoding="UTF-8"?>
    <Configuration status="WARN">
        <Appenders>
            <Console name="Console" target="SYSTEM_OUT">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/>
            </Console>
        </Appenders>
        <Loggers>
            <Root level="debug">
                <AppenderRef ref="Console"/>
            </Root>
        </Loggers>
    </Configuration>
    

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

    <?xml version="1.0" encoding="UTF-8"?>
        <!-- status:log4j2框架自身的日志输出级别 -->
        <!-- monitorInterval:log4j支持配置文件热更新。该参数为多长时间检测一次配置文件是否发生变更 -->
        <configuration status="WARN" monitorInterval="30">
        <!-- 定义appender -->
        <appenders>
            <!-- console appender是输出日志到控制台 -->
            <Console name="Console" target="SYSTEM_OUT">
            <!-- pattern为输出日志的格式 -->
                <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
            </Console>
            <!-- file appender输出日志到文件。append属性决定每次运行时日志文件是清空还是追加 -->
            <File name="log" fileName="log/test.log" append="false">
                <PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n"/>
            </File>
            <!-- rolling file appender是基于文件的滚动日志。支持按照配置的条件触发日志滚动,生成新的日志文件 -->
            <RollingFile name="RollingFileInfo" fileName="${sys:user.home}/logs/info.log"
                          filePattern="${sys:user.home}/logs/$${date:yyyy-MM}/info-%d{yyyy-MM-dd HH}-%i.log">
                <!-- 日志级别过滤 -->        
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
                <PatternLayout pattern="[%d{HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
                <Policies>
                    <!-- 如果同时使用多个触发策略,这些策略间是或的关系 -->
                    <!-- 基于时间触发的滚动策略,interval的单位为filePattern中的最小时间单位 -->
                    <TimeBasedTriggeringPolicy interval="1"/>
                    <!-- 基于文件大小触发的滚动策略 -->
                    <SizeBasedTriggeringPolicy size="100 MB"/>
                    <!-- 结合filePattern中的%i(整数计数器)使用,最多保留max个归档的日志文件 -->
                    <DefaultRolloverStrategy max="20"/>
                </Policies>
            </RollingFile>
        </appenders>
        <!-- 定义logger,将logger和appender绑定 -->
        <loggers>
            <!-- 使用name描述logger,指定logger的日志输出级别 -->
            <logger name="org.springframework" level="INFO"></logger>
            <logger name="com.xxx" level="DEBUG">
                <!-- 绑定appender到logger。ref指向appender的name -->
                <AppenderRef ref="Console"/>
            </logger>
            <!-- 定义root logger -->
            <root level="all">
                <AppenderRef ref="Console"/>
                <AppenderRef ref="RollingFileInfo"/>
            </root>
        </loggers>
    </configuration>
    

    参考材料:

    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的时候什么也不做。详细情况读者可以查看它们的源代码。

    相关文章

      网友评论

        本文标题:Java 日志框架的使用

        本文链接:https://www.haomeiwen.com/subject/awqkydtx.html