用SLF4j/Logback打印日志-1

浅谈后端日志系统 中已经写了很多日志方面的零散的非技术的东西。本篇更像一份入门说明,讲解一下SLF4j/Logback。SLF4J是一套抽象的日志API接口,logback它是的底层实现,所以在这里的大部分代码中,看到的API都是 SLF4J 提供的,但是日志系统的配置还是按照 logback的文档 配置的。

在后端logger系统中,有三个最基础的概念需要先熟悉:

  • Logger 日志记录器 - 日志记录器就是一个普通的Java类而已
    logger.debug("Hello world.Debug");

这里面的logger就是日志记录器,每个日志记录器都可以独立的配置属性。

  • Appender 输出源 - 输出源是日志最终输出的地方,比如控制台或者文件
  • Layout(Encoder) 布局 - 布局决定了日志的打印格式,比如使用 %r [%t] %-5p %c %x - %m%n可以打印出 467 [main] INFO org.apache.log4j.examples.Sort - Exiting main method.这样的日志。

这在后面会有更详细的介绍,接下来先看一个HelloWorld - 感受一下logback的魅力。

HelloWorld

入门很重要,这里还是采用IntelJ+Gradle的编程环境,按步骤来:

  1. 创建目录,这是一个非常简单的Java工程,仅有一个App.java 类
➜  logback-test tree .      
    .
    ├── build.gradle
    └── src
        └── main
            ├── java
            │   └── com
            │       └── hello
            │           └── App.java
  1. 配置build.gradle文件,这里仅添加了对 logback 的依赖,logback还会依赖 slf4j, Gradle会帮我们解决这些间接依赖的问题。
group 'com.hello'
  version '1.0-SNAPSHOT'

  apply plugin: 'java'

  repositories {
      mavenCentral()
  }

  dependencies {
      compile 'ch.qos.logback:logback-core:1.1.+'
      compile 'ch.qos.logback:logback-classic:1.1.+'
  }
  1. 实现App.java并右键运行
// App.java:
package com.hello;

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

public class App {
    private static final Logger logger = LoggerFactory.getLogger(App.class);

    public static void main(String[] args) {
        logger.debug("Hello world.");
    }
}

// 控制台打印的结果

Console:10:35:22.504 [main] DEBUG com.hello.App - Hello world.

这个例子没有太多值得关注的地方,我们仅仅调用了一个方法 logger.debug("Hello world."); 打印了一条日志。但是注意到打印出来的日志格式是这样的: 

Console:10:35:22.504 [main] DEBUG com.hello.App - Hello world.

除了我们要打印的信息“Hello world”之外,多了很多信息,比如 Console、时间戳、DEBUG符号,类名等。这些信息是在日志打印的时候logback默认加上去的,可以通过配置文件来修改这些信息。

默认情况下logback会在classpath下寻找名为logback.xml的配置文件(还有另外4种查找配置文件的方式,见文档)如果没有找到任何配置文件,会使用 BasicConfigurator 初始化一个默认配置,它会把结果打印到控制台,这就是上面“HelloWorld”示例的情况。为了让logback可以加载到配置文件,需要在 src/main/resources/ 目录下创建文件 logback.xml(Gradle会自动把 resources 目录下的文件放到classpath目录)。

logback.xml文件的内容为

<configuration>
  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <!-- encoders are assigned the type
         ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

这是和默认配置等效的一条配置。

日志记录器 - logger


在上面的 “HelloWorld” 示例中,我们通过 Logger logger = LoggerFactory.getLogger(App.class); 工厂方法获得了一个日志记录器。并通过 logger.debug("Hello world."); 方法打印了一条信息,但是如果我们通过 logger.info 方法是打印不出来的,原因是什么呢?

在logback中日志记录器是继承的,继承的规则是 com.hello.foo 会继承 com.hello 的日志配置,父子关系通过 . 来分割,所以 com 是 com.hello 的父节点。在logback中默认会有一个 root-logger(根 - 日志记录器)的存在,所有的其他日志记录器都会默认继承它的配置。在配置文件中看到的:

<root level="debug">
  <appender-ref ref="STDOUT" />
</root>

就是“根”。所以当我们调用 Logger logger = LoggerFactory.getLogger(App.class); 的时候,默认是从 root-logger 那里继承了日志输出配置,而 root-logger 默认的log打印级别是 debug,所以用 logger.info 打印不出日志。

那么如何配置一个普通的日志记录器呢?这是非常简单的:

<logger name="com.hello" level="INFO"/>

这样便配置了一个名为 com.hello 的日志记录器(一般会用包名作为日志记录器的名字),name 是必须配置的属性。然后可以通过 Logger logger = LoggerFactory.getLogger("com.hello") 来得到这个日志记录器。

下面的三种写法是等效的:

Logger logger = LoggerFactory.getLogger(App.class)
Logger logger = LoggerFactory.getLogger(App.class.getName())
Logger logger = LoggerFactory.getLogger("com.hello.App")

日志输出源 - Appenders


在上面的配置文件中其实已经见识到了日志输出源的配置:

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

这个输出源配置把日志打印到控制台,格式为 %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n,输出源的 name 和 class 属性是必须配置的选项。比如还可以配置把日志打印到文件:

<appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>myApp.log</file>
    <encoder>
      <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
    </encoder>
</appender>

有了输出源之后,就可以给logger配置输出源,一个logger可以配置多个输出源:

<root level="debug">
    <appender-ref ref="STDOUT" />
    <appender-ref ref="FILE" />
</root>

这里给root-logger配置了两个输出源,此时如果我们调用 logger.debug方法会发现控制台和根目录下的myApp.log文件都打印了相同的日志。在上面的“HelloWorld”工程中,我们这样配置logback.xml文件:

<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoders are assigned the type
             ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <appender name="file" class="ch.qos.logback.core.FileAppender">
        <file>log/demo.file.log</file>
        <encoder>
            <pattern>%date %level [%thread] %logger{10} [%file:%line] %msg%n</pattern>
        </encoder>
    </appender>

    <logger name="com.hello" level="INFO"></logger>

    <root level="debug">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="file" />
    </root>
</configuration>

再次操作会在工程根目录的 log/demo.file.log 文件中打印

2016-03-31 11:38:06,068 INFO [main] c.h.App [App.java:11] Hello world.Info

在控制台会打印:

11:38:06.068 [main] INFO  com.hello.App - Hello world.Info

在这里创建了两个输出源,一个基于文件的输出源 <appender name="file" class="ch.qos.logback.core.FileAppender"> 并配置了文件路径和日志输出格式。另一个是基于控制台的输出源 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
然后在 root-logger 下面添加了这个输出源,所以这条日志会在文件和控制台各打印一份(注意因为配置的格式不同,打印出来的结果也略有不同)。

在logback中,level的配置会被继承,但是appender的配置会被子logger保留。这么说有点抽象,看下面的例子:

<configuration>

  <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
  </appender>

  <logger name="com.hello">
    <appender-ref ref="STDOUT" />
  </logger>

  <root level="debug">
    <appender-ref ref="STDOUT" />
  </root>
</configuration>

这个配置会导致控制台打印两次:

11:38:06.068 [main] INFO  com.hello.App - Hello world.Info
11:38:06.068 [main] INFO  com.hello.App - Hello world.Info
当调用 LoggerFactory.getLogger("com.hello.App") 的时候,它继承了来自 <logger name="com.hello"> 和 root-logger 的输出源,而他们的输出源都是控制台,所以导致在控制台输出了两次。解决办法是,要么在有继承关系的logger中配置不同的输出源(从业务上避免),要么在子logger中覆盖父节点中的配置。可以通过 additivity="false" 配置实现:
<logger name="com.hello" additivity="false">
    <appender-ref ref="STDOUT" />
</logger>

additivity 属性告诉logback不要继承来自父类的设置。

布局 - Layout 和 编码 - Encoder

Layouts are logback components responsible for transforming an incoming event into a String.

Layout主要用来把log事件转换成String。一般布局都是配置在 Appender 里面的:

<appender name="CONSOLE" class="org.apache.log4j.ConsoleAppender">
    <layout class="org.apache.log4j.PatternLayout">
        <param name="ConversionPattern" value="%d{yyyy-MM-dd HH:mm:ss} [%c]-[%p] %m%n" />
    </layout>
</appender>

注意,上面的示例使用的Appender是org.apache.log4j.ConsoleAppender ,这是log4j的配置而不是这里讲的logback,这是因为在过去日志系统确实都是使用Layout来做日志转换的,但是由于一些 固有的问题 ,logback在Layout上面又封装了一层 - Encoder,表现在配置上就是这样的(这才是logback的配置):

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
    </encoder>
</appender>

但是Encoder的出现并不影响我们的配置,只是在形式上多了一个<encoder/>标签。一般使用最多的是 PatternLayout 或 PatternLayouEncoder ,他们的特点是提供了很多约定的标记,这些标记都以 % 开头,比如logger名称、日志等级日期、线程名等。比如:%level [%thread]: %message%n可以打印出日志等级,线程名字和日志本身。

Layouts, as discussed in detail in the next chapter, are only able to transform an event into a String. Moreover, given that a layout has no 
control over when events get written out, layouts cannot aggregate events into batches. Contrast this with encoders which not only have total
control over the format of the bytes written out, but also control when (and if) those bytes get written out.

事实上我们在使用logback的时候很少见到直接使用layout,这是因为layout只能把一条log事件转化成String,layout不能控制log什么时候被写入文件,也不能做到批量处理。Encoder是被发明出来解决这些问题的。目前为止Encoders只有一个实现 - PatternLayoutEncoder,它内部包含了一个PatternLayout,所以可以像使用PatternLayout一样使用
Encoder。

至此已经粗略介绍完Logback的三个最重要的部分,下面会记录一些常用且比较重要的配置属性

常用配置

有一个最重要的配置 - TimeBasedRollingPolicy 策略配置限于限于篇幅只能转移到下一篇了。


1. 变量定义和替换

这种功能在Maven里面也经常用到,用法很简答:

<configuration>
  <property name="USER_HOME" value="/home/sebastien" />
  <appender name="FILE" class="ch.qos.logback.core.FileAppender">
    <file>${USER_HOME}/myApp.log</file>
    <encoder>
      <pattern>%msg%n</pattern>
    </encoder>
  </appender>
  <root level="debug">
    <appender-ref ref="FILE" />
  </root>
</configuration>

这里定义了一个变量 <property name="USER_HOME" value="/home/sebastien" /> 并通过 ${USER_HOME} 引用。变量定义的意义在于可以对一些全局性的属性/变量做统一的配置。如果有很多变量的时候,可以把变量定义在单独的文件中,通过 <property file="src/main/java/chapters/configuration/variables1.properties" /> 来引用。

// variables1.properties
USER_HOME=/home/sebastien
fileName=myApp.log
destination=${USER_HOME}/${fileName}

2. Scope配置
通过配置scope可以设置变量的作用空间,用法

<property scope="context" name="nodeId" value="firstNode" />

有三种Scope是可以配置的:

1. LOCAL SCOPE 
2. CONTEXT SCOPE
3. SYSTEM SCOPE

** 3. if表达式**
在xml中写代码的既视感:

<!-- if-then-else form -->
<if condition="some conditional expression">
  <then>
    ...
  </then>
  <else>
    ...
  </else>    
</if>

** 4. 常用条件表达式函数**
1. property('key')
2. isDefined('key')
3. isNull("key")
** 5. 文件引用 **

// 引用方式
<include file="src/main/java/chapters/configuration/includedConfig.xml"/>
<include resource="includedConfig.xml"/>
<include url="http://some.host.com/includedConfig.xml"/>
// 被引用文件
<included>
  <appender name="includedConsole" class="ch.qos.logback.core.ConsoleAppender">
    <encoder>
      <pattern>"%d - %m%n"</pattern>
    </encoder>
  </appender>
</included>

6. 使用时间戳属性

这种属性在哪些很短命的需要经常重启的App中用的比较多,每次重启会产生新的log文件(下面还有更牛逼的)

<timestamp key="bySecond" datePattern="yyyyMMdd'T'HHmmss"/>
<file>log-${bySecond}.txt</file>

7. 常用的转移标记:

  1. %c{length} /%lo{length} /logger{length} 打印logger名称,并控制长度,比如;%logger{5} mainPackage.sub.sample.Bar m.s.s.Bar
  2. %C{length} /class{length} 打印出产生这条log的调用方的完整名字,并控制长度。这个方法效率不高,慎用。(目测要调用堆栈才能查到调用方的类名或者方法)
  3. %contextName / %cn 打印出logger的上下文信息。在Configuration一节中我们曾经看到过这个的使用
  4. %d{pattern} / %date{pattern} /%d{pattern, timezone} /%date{pattern, timezone} 打印出当前时间,通过pattern来控制日期格式,比如:%date{HH:mm:ss.SSS} 14:06:49.812
  5. %F / %file 打印出产生这条log的java源文件,这个方法效率不高,慎用。
  6. 打印出产生这条log的堆栈信息,并控制长度,效率不高%L / %line 打印出产生这条log的源码行号
    1. %caller{depth}
    2. %caller{depthStart..depthEnd}
    3. %caller{depth, evaluator-1, ... evaluator-n}
    4. %caller{depthStart..depthEnd, evaluator-1, ... evaluator-n}
  7. %m / %msg / %message 打印loggger方法传入的消息,比如.loggger.info("hello"),指代这里的“hello”
  8. %M / %method 打印出产生这条log的方法名,效率不高
  9. %n 换行
  10. %p / %le /%level 打印出日志等级
  11. %r / %relative 打印出自动App启动至今流逝的时间(单位:毫秒)
  12. %t / %thread 打印出产生这条log的线程名字
  13. %X{key:-defaultVal} / mdc{key:-defaultVal} 打印出MDC中定义的上下文
  14. 打印出异常的堆栈信息,并控制深度,默认情况下会打印所有的堆栈信息
    1. %ex{depth}
    2. %exception{depth}
    3. %throwable{depth}
    4. %ex{depth, evaluator-1, ..., evaluator-n}
    5. %exception{depth, evaluator-1, ..., evaluator-n}
    6. %throwable{depth, evaluator-1, ..., evaluator-n}
  15. 和 %ex一样,但是会给出jar包信息%nopex /%nopexception 忽略异常信息
    1. %xEx{depth}
    2. %xException{depth}
    3. %xThrowable{depth}
    4. %xEx{depth, evaluator-1, ..., evaluator-n}
    5. %xException{depth, evaluator-1, ..., evaluator-n}
    6. %xThrowable{depth, evaluator-1, ..., evaluator-n}
  16. %marker 打印marker信息,格式:parentName [ child1, child2 ]
  17. %property{key} 引用之前定义的变量(或属性)
  18. %replace(p){r, t} 举例说明:%replace(%msg){'s', ''},会把消息中的所有空格移除

Refer:

https://www.jianshu.com/p/d1a565f192b9

原文地址:https://www.cnblogs.com/winner-0715/p/9056933.html