Java logback配置实践

Java log日志简介

基本在所有有关Java规范的书中,都会提到禁止使用System.out.print()来记录日志,而日志系统在基本的项目开发中,是绝对不可缺少的一部分。其实用了这么久的日志,基本配置文件都是直接复制的公司的标准格式,而前前后后使用过log4j,commons-logginglogback等,他们的区别又在哪里呢?

  • commons-logging

    commons-logging作为Apache最早提供的日志门面接口,实现日志接口和实现的解耦,其内部也有简单的日志实现,实际使用的地方并不多

  • Log4j

    logging for javalog4这类名字非常出现,同款的还有log4net等,它是一种经典的日志解决方案,内部把日志系统抽象封装成Loggerappenderpattern 等实现。刚开始出现的时候,受到了各个开源框架的青睐

  • logback

    算是Log4j的改进版,作者是Log4j的原作者,相比Log4j,性能更好,内存消耗更少,并且本身集成了slf4j,算是对slf4j最原生的支持

  • Log4j2

    log4j2Apache接手Log4j后推出的一个改进版,对比Log4jlog4j2性能快了很多,并且支持properties文件和json格式的配置文件的支持,在官方JMH测试中,Log4j2的性能稍微比logback快一点,虽然Log4j2拥有如此多的有点,但是它的流行程度却没有logback广泛

  • slf4j

    全称为Simple Logging Facade for Java,用于对不同日志框架提供一个简单的门面封装,可以在部署的时候不用修改任何配置即可接入任何一种日志方案,和commons-logging比较类似

​ 说了这么多,其实一般用的最多的还是slf4jlogback,关于slf4j这里多说两句:slf4j作为简单的日志门面接口,是不包含具体的日志的实现的,因此一般都是使用sl4j加上一种实现。然而在平时开发过程中,引入的包有时候会自带一种日志实现,此时可能会引起slf4j选择包冲突:

​ 比如引入的xx.jar使用的是slf4jLog4j的日志框架,而我们系统使用的是slf4j+logback,当引入xx.jar时候,slf4j在查找具体的实现包的时候,可能选择了Log4j,这个时候我们可能就会发现我们为Log4j所配置的配置文件不生效了。。


​ 解决方案其实很简单,排除其Log4j的依赖即可

配置Logback

其中logback一共包含三个模块:

  • logback-core:日志模块的基础构建,其他的模块基于它构建
  • logback-classiclogbackslf4j的实现,其中改进了很多slf4j的功能
  • logback-access:用于与Servlet容器(Tomcat,jetty)等交互的模块

因此我们想要使用logback,直接在Maven中引入logback-classic接口,它包含了logback-coreslf4j

<dependency>
    <groupId>ch.qos.logback</groupId>
    <artifactId>logback-classic</artifactId>
    <version>${logback.version}</version>
</dependency>
配置文件

logback在启动时,根据以下步骤寻找配置文件:

  • classpath中寻找logback-test.xml文件
  • 如果找不到logback-test.xml,则在 classpath中寻找logback.groovy文件
  • 如果找不到 logback.groovy,则在classpath中寻找logback.xml文件
  • 如果上述的文件都找不到,则logback会使用JDKSPI机制查找 META-INF/services/ch.qos.logback.classic.spi.Configurator中的 logback 配置实现类,这个实现类必须实现Configuration接口,使用它的实现来进行配置
  • 如果上述操作都不成功,logback 就会使用它自带的 BasicConfigurator 来配置,并将日志输出到console

以上查找方法,可以通过logbackdebug信息查看

配置logback.xml,大体从以下思路配置:

  • 首先,我们需要区分这是日志级别,对于不同的日志,我们所做的操作可能不同,比如debug信息直接打在控制台,info信息保存在info文件中,error信息保存在error文件中
  • 其次,我们需要有一个地方能够配置目前需要打开的日志级别,比如将日志级别调至warn,那么info信息便不会再存入文件中了

由以上思路,我们可以大概画出文件的总体框架:

<configuration>
    <appender>    //输出到控制台的信息配置
       //....
    </appender>

    <appender>   //输出到info文件的配置
       //...
    </appender>

    <appender>   //输出到error文件的配置
    </appender>

    <logger>     //特殊处理日志定义
        //..
    </logger>

    <root level="debug">  //总日志开关
        //...
    </root>     
</configuration>

上面便是logback的总体日志配置,下面详细说说每个节点的作用

其中每个appender配置一种行为,比如这个appender定义仅将信息输出到控制台中,另外一个appender定义将信息输出到某个文件中,最后一个appender可以配置将信息输出到某个邮件/数据库/网络请求中,总之appender便是用来定义不同的行为.

有时候在配置文件中,可能包含多个appender,而有时候这些appender不可能什么时候都要使用,比如发送邮件这个appender,可能只有测试和正式上线的时候才会使用,这个时候,便是loggerroot的功能,root作为所有logger的爸爸,用来定义默认appender,在root中,需要定义需要使用的appender,比如在平时开发过程中,不论什么级别的日志,仅仅将日志信息打在控制台就行了,这个时候root中就只用包含行为为控制台的appender即可,在正式上线后,再讲其他的appender添加到root中即可。

说完root,那为什么还有logger呢?logger是用来添加特权包和类的,某些时候,可能某个包/类涉及到非常重要的业务,这个包报错了的话,需要立即处理,这个时候便可以定义一个邮件appender,但是其他包的日志可以稍后来排查,如果直接在root中添加发邮件的appender,那么只要报错就会发邮件,比较麻烦,这个时候就可以使用logger定义一个特权包,这个特权包可以单独定义appender

不过需要知道的是,在logback中,消息都是冒泡的,而root作为所有的logger的父亲,消息经过logger处理后,会再次冒泡到logger的父类中,最后一直到root

到这里,logback.xml大体框架我们已经了解,下面来具体了解各个节点。

先来一个完整的logback.xml,这个配置文件,在日常项目中,已经完全够用:

<?xml version="1.0" encoding="UTF-8"?>

<!--

scan:    当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。

scanPeriod:  设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。

debug:    当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。

configuration 子节点为 appender、logger、root

-->
<configuration scan="false" scanPeriod="60 seconds" debug="false">
    <!--用于区分不同应用程序的记录-->
    <contextName>logback-test</contextName>
    <!--日志文件所在目录,实际路径为 %{classpath}/logs-->
    <property name="LOG_HOME" value="logs"/>
    <!--统一输出格式-->
    <property name="STANDARD_PATTERN" value= "%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ---- [%15thread] %logger{36} : %msg%n"/>

    <!--关闭logback自身的日志-->
    <statusListener class="ch.qos.logback.core.status.NopStatusListener"/>

    <!--控制台输出-->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder charset="UTF-8">
            <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %logger输出日志的logger名 %msg:日志消息,%n是换行符 -->
            <!--这里是按照SpringBoot输出格式配置-->
            <pattern>{STANDARD_PATTERN}</pattern>
        </encoder>
    </appender>

    <!--INFO日志文件-->
    <appender name="FILE_INFO" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:临界值过滤器,只存放比INFO日志级别更高的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>INFO</level>
        </filter>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <!--

            官方解释.i:当某天:比如2019-01-01,产生了总共30M的日志的时候,则会分别生成
            2019-01-01.0,2019-01-01.1,2019-01-01.2分别三个日志文件,
            .i的作用便是用来分割超过日志大小的日志

            -->
            <fileNamePattern>{LOG_HOME}/app_stdout.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
            <maxFileSize>10MB</maxFileSize><!--单个文件最大10M-->
            <totalSizeCap>20G</totalSizeCap><!--总文件最多存放20G-->
        </rollingPolicy>

        <encoder charset="UTF-8">
            <pattern>{STANDARD_PATTERN}</pattern>
        </encoder>
    </appender>

    <!--ERROR日志文件-->
    <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!-- ThresholdFilter:临界值过滤器,过滤掉 TRACE 和 DEBUG 级别的日志 -->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>error</level>
        </filter>

        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">

            <fileNamePattern>{LOG_HOME}/app_stderr.%d{yyyy-MM-dd}.%i.log</fileNamePattern>
            <maxHistory>30</maxHistory><!--保存最近30天的日志-->
            <maxFileSize>10MB</maxFileSize><!--单个文件最大10M-->
            <totalSizeCap>20G</totalSizeCap><!--总文件最多存放20G-->
        </rollingPolicy>

        <encoder charset="UTF-8">
            <pattern>${STANDARD_PATTERN}</pattern>
        </encoder>
    </appender>


    <!--这里如果是info,spring、mybatis等框架则不会输出:TRACE < DEBUG < INFO <  WARN < ERROR-->
    <!--root是所有logger的祖先,均继承root,如果某一个自定义的logger没有指定level,就会寻找
    父logger看有没有指定级别,直到找到root。-->
    <root level="debug">
        <appender-ref ref="STDOUT"/>
        <appender-ref ref="FILE_INFO"/>
        <appender-ref ref="FILE_ERROR"/>
    </root>

    <!--为某个包单独配置logger

    <logger name="com.dengchengchao.logbacktest" level="DEBUG" additivity="false">
      <appender-ref ref="task" />
    </logger>
    注意:additivity必须设置为false,这样只会交给task这个appender,否则其他appender也会打印com.dengchengchao.logbacktest里的log信息。

    -->

</configuration>

下面一一解释

  • configuration: 配置节点
    • scan:是否启动热部署,默认为true
    • scanPeriod:热部署扫苗时间间隔,默认单位毫秒
    • debug:是否开始logback本身日志信息,默认false
  • contextName:相当于为每个APP设置的单独的配置域,用于区分不同的引用程序的记录
  • property:和所有配置文件一样,key-value键值对,用于定义变量
  • appender:定义配置行为
    • name:唯一ID,方便在后面logger中指定
    • class:所对应的行为类,比如要将信息输出到控制台,就得引入ch.qos.logback.core.ConsoleAppender
    • encoder:针对appender的具体配置
    • charset:日志编码,用于解决乱码问题,一般为UTF-8
    • pattern:日志格式定义
    • filter:日志级别过滤,低于此级别的日志不会触发此appender
  • logger:特权logger定义
  • additivity:是否继续将消息冒泡
  • level:开启的日志级别,低于此级别的消息不会被记录
  • appender-ref:需要使用的appender

其他的在上面的日志中,都有详细的解释。

logback使用

日志的使用,基本上大家都会,不过在这里需要说几点注意事项:

  • 由于logback是完全实现的slf4j的接口,因此使用logback依然是使用日志门面slf4j

  • 在获取日志对象过程中,我们有两种方式:

    private static final Logger log = LoggerFactory.getLogger(Test.class);
    private Logger log = LoggerFactory.getLogger(Test.class);
    

    很多人认为使用第一种方式效率更高,然后再slf4j中,实际上是做了对象缓存的,每次初始化的对象都会被缓存在缓存池中,第二次使用的时候就会被直接赋值,因此两者差别其实并不大

  • 很多人的疑问:为什么使用logger需要传入一个xxx.class?其实这个是logback的设计初衷:设计者为了使得每个类都单独使用一个logger,因此使用这个类的名字来作为key,同时这样在日志输出的时候,能够很方便的定位到时哪个类打印的日志。如果你为了方便,依然随便传一个字符串进去。

  • Tomcat源码中,出现了很多类似这样的代码:

    if(log.isDebugEnabled()){
      log.debug("xxx"+"ddd"+"xxx");
    }
    

    为的是在正式环境中,节约一些字符串拼接的效率,然而在slf4j中,提供了更友好的字符串拼接日志:

    log.info("{},ddd,{}","test","hello");
    

    效果完全一样,并且看起来更加美观

参考资料:

[简书]—maxwellyue:slf4j+logback的配置及使用

[掘金]—glmapper:看完这个不会配置 logback ,请你吃瓜!

[logback官网]

[CSDN]—排骨瘦肉丁:logback同时按照日期和大小分割日志

[代码日志]:如何防止logback在每个日志开始时输出自己的状态?