大數據

Spring Boot 2.x 實戰–日誌打印與輸出到文件

作者:小先,一個專注大數據、分佈式技術的非斜槓青年,愛Coding,愛閱讀、愛攝影,更愛生活!

源代碼倉庫:https://github.com/zhshuixian/learn-spring-boot-2

日誌對於程序的重要性不言而喻,當程序運行出現問題的時候,我們可以通過日誌快速的定位、分析問題。

在開發的時候,還可以通過 IDE 的調試功能或者 System.out 、System.err 來簡單打印一下信息,用來定位 bug,但這種方式是在任何時候都不推薦的,在生產環境中,撇開 System.out 會影響系統性能不說,出現問題的時候連錯誤日誌記錄在哪裡都難找。

作為企業應用開發來說,可能有不同的功能模塊,不同的功能模塊之間的相互調用可能存在許多問題,在開發和演示環境中,很多問題可能無法暴露出來。當某個功能發生異常了,然而你無法像開發環境一樣一步一步調試來找出問題,這時候能幫到你的只有日誌記錄。

Java 發展至今,有著非常成熟的生態體系,有著非常多成熟的日誌框架可以選擇, 因此也不推薦你從零搭建日誌框架。

這裡將實戰 Spring Boot 整合 Log4j2 與 Slf4j 實現日誌打印和輸出到文件。

1、Java 日誌框架和日誌門面

在 Java 生態中,日誌方面的技術主要分為日誌框架和日誌門面兩大方面。日誌框架是日誌功能的具體實現,日誌門面在日誌框架的上面再做一層封裝,對應用程序屏蔽底層日誌框架的實現及細節。這裡簡要介紹 Java 常用的日誌框架和日誌門面。

1.1、常用的日誌框架

java.util.logging : 是從 JDK 1.4 開始引入的 Java 原生日誌框架,定義了七個日誌級別,分別是:SEVERE、WARNING、INFO、CONFIG、FINE、FINER、FINEST。

Log4j : 是 Apache 的開源項目,出自 Ceki Gülcü 之手,我們可以靈活地控制日誌的輸出格式、控制日誌輸出到控制檯還文件,而這些無需對代碼進行更改,只需要簡單地更改一下配置文件即可。同樣定義了七個日誌級別:OFF、FATAL、ERROR、WARN、INFO、DEBUG、TRACE。

LogBack : 同樣是出自 Ceki Gülcü 之手的成熟日誌框架,可以看做是 Log4j 的改良加強版本。

Log4j2 : 不僅僅是 Log4j 升級版本,從頭到尾被重寫了,性能有著極大的提升,感興趣的讀者可以自行搜索 Log4j2 和其他日誌框架的性能評析。

1.2、日誌門面

在阿里巴巴《Java開發手冊》中,在日誌規範的第一條中,就是禁止直接使用上面所列的日誌框架的 API,而應該使用像 SLF4J 日誌門面的 API。

日誌規範

上面所列的幾種常用的日誌框架,不同的框架有著不同的 API,這大大增加了代碼與日誌框架的耦合性,當需要更換日誌框架的時候,我們幾乎要把日誌框架相關的代碼重新修改一遍。

為了解決這個問題,可以通過在程序和日誌框架在搭一箇中間層實現,這就是日誌門面,通過日誌門面屏蔽日誌框架的具體實現,即使我們更改了底層日誌框架,也無需對程序進行大修改,頂多就是改改一些配置即可。

日誌門面並不涉及具體的日誌實現,還需依賴 Log4j、Logback 等日誌框架,它僅僅是對程序屏蔽了不同日誌框架的 API 差異,降低程序和日誌框架之間的耦合度。

SLF4J : Java 簡易日誌門面(Simple Logging Facade for Java)的縮寫,也是出自 Log4j 、 LogBack 的作者Ceki Gülcü 之手。支持 Java Logging API、Log4j、logback等日誌框架。根據作者的說法,SLF4J 效率更高,比起 Apache Commons Logging (JCL) 更簡單、更穩定。

Commons Logging : Apache Commons Logging (JCL) 是基於Java的日誌記錄程序。

日誌框架和日誌門面的關係

2、實戰 Log4j2 與 SLF4J

對於 Java 程序來說,Log4j + SLF4J 這樣的組合來進行日誌輸出是個不錯的選擇,通過日誌門面屏蔽底層日誌框架的差異,即使在日後更換日誌框架也無需太多的成本。

前面提到,Log4j2 對比 Log4j 有著巨大的提升,因此這裡選擇 Log4j2 + SLF4J 的組合。實戰如何控制日誌的控制檯輸出和日誌輸出到文件中。

2.1、引用 Log4j2 依賴

Spring Boot 默認使用 Logback 作為日誌框架,默認引入了 spring-boot-starter-logging 這個依賴,修改 build.gradle(pom.xml),添加 Log4j2 依賴和排除 spring-boot-starter-logging 。

項目添加 Log4j2 依賴、排除 spring-boot-starter-logging

build.gradle

// 排除 spring-boot-starter-logging
configurations {
    compile.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    implementation.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
    testImplementation.exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
dependencies {
    // 其他依賴省略
    // 引入 Log4j2
    // https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-log4j2
    compile group: 'org.springframework.boot', name: 'spring-boot-starter-log4j2'
}

pom.xml

  <dependencies>
    <dependency>
    <!-- 排除 spring-boot-starter-logging -->
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-logging</artifactId>
      <exclusions>
        <exclusion>
          <groupId>*</groupId>
          <artifactId>*</artifactId>
        </exclusion>
      </exclusions>
    </dependency>
    <!-- 其他依賴省略 -->
    <!-- 引入 Log4j2 https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-log4j2 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-log4j2</artifactId>
    </dependency>
  </dependencies>

刷新項目,可以發現 spring-boot-starter-logging 已經被移除和新引入了 spring-boot-starter-log4j2 。

2.2、 Log4j2 的配置

Log4j2 默認配置文件是 resources/log4j2-spring.xml ,新建Log4j2 的配置文件,如果你使用其他文件名稱,可以通過如下方式指定。

application.properties

# 指定 Log4j2 配置文件
logging.config=classpath:other-filename.xml

Log4j2-spring.xml 配置模板

<?xml version="1.0" encoding="UTF-8"?>
<!-- Log4j2 配置文件 參考 https://www.cnblogs.com/keeya/p/10101547.html  -->
<!--日誌級別以及優先級排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->
<!-- monitorInterval=“N” 自動間隔 N 秒檢測配置文件是否修改,有修改則自動重新加載配置 可以不設置  -->
<!-- status="warn" Log4j2 本身日誌輸出級別 可以不設置 -->
<configuration monitorInterval="30" status="warn">
  <!-- 變量配置 -->
  <Properties>
    <!-- 日誌輸出格式 -->
    <property name="LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight{%-5level} [%t] %highlight{%c{1.}.%M(%L)}: %msg%n"/>
    <!-- 日誌輸出到文件的路徑和文件名 根據項目情況更改 value 值 -->
    <property name="LOG_FILE_PATH" value="logger"/>
    <property name="LOG_FILE_NAME" value="log4j2"/>
  </Properties>
  <!-- 定義 appenders -->
  <appenders>
    <!-- console 設定 控制檯輸出 -->
    <console name="Console" target="SYSTEM_OUT">
      <!-- 指定 輸出格式 默認 %msg%n -->
      <PatternLayout pattern="${LOG_PATTERN}"/>
      <!-- onMatch="ACCEPT" 只輸出 level 級別及級別優先級更高的 Log , onMismatch="DENY" 其他拒絕輸出  -->
      <ThresholdFilter level="debug" onMatch="ACCEPT" onMismatch="DENY"/>
    </console>
    <!-- 將日誌全部輸出到 test.log,append="true" 表示重新運行時不刪除日誌 -->
    <File name="FileLog" fileName="${LOG_FILE_PATH}/test.log" append="true">
      <PatternLayout pattern="${LOG_PATTERN}"/>
    </File>
    <!-- RollingFile 滾動輸出日誌到文件 -->
    <!-- 輸出 warn 及更高優先級的 log 到 LOG_FILE_PATH 目錄下的 warn.log 文件  -->
    <!-- filePattern 指定 warn.log 文件大於 size 大小時候文件處理規則, %d 日期;%i 編號(最大為下方設置的 max 值) -->
    <RollingFile name="RollingFileWarn" fileName="${LOG_FILE_PATH}/warn.log"
                 filePattern="${LOG_FILE_PATH}/%d{yyyy-MM-dd}/WARN_${LOG_FILE_NAME}_%i.log.gz">
      <PatternLayout pattern="${LOG_PATTERN}" />
      <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <!-- interval="N" ,N小時滾動一次,默認是1 hour-->
        <TimeBasedTriggeringPolicy interval="1"/>
        <!-- size="5MB" 指定日誌輸出文件大小,若大小超過size,則日誌會自動存入按 filePattern 規則建立的文件夾下面並進行壓縮 -->
        <SizeBasedTriggeringPolicy size="5MB"/>
      </Policies>
      <!-- DefaultRolloverStrategy 不設置的情況下,默認為最多同一文件夾下7個 filePattern 規矩建立的壓縮文件,多於 max 的值將用新的文件覆蓋就的壓縮文件 -->
      <DefaultRolloverStrategy max="10"/>
    </RollingFile>
    <!-- 輸出 error 及更高優先級的 log 到 LOG_FILE_PATH 目錄下的 error.log 文件  -->
    <RollingFile name="RollingFileError" fileName="${LOG_FILE_PATH}/error.log"
                 filePattern="${LOG_FILE_PATH}/%d{yyyy-MM-dd}/ERROR_${LOG_FILE_NAME}_%i.log.gz">
      <PatternLayout pattern="${LOG_PATTERN}" />
      <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <TimeBasedTriggeringPolicy interval="1"/>
        <SizeBasedTriggeringPolicy size="5MB"/>
      </Policies>
      <DefaultRolloverStrategy max="10"/>
    </RollingFile>
    <!-- 輸出 info 及更高優先級的 log 到 LOG_FILE_PATH 目錄下的 info.log 文件  -->
    <RollingFile name="RollingFileInfo" fileName="${LOG_FILE_PATH}/info.log"
                 filePattern="${LOG_FILE_PATH}/%d{yyyy-MM-dd}/Info_${LOG_FILE_NAME}_%i.log.gz">
      <PatternLayout pattern="${LOG_PATTERN}"/>
      <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
      <Policies>
        <TimeBasedTriggeringPolicy interval="1"/>
        <SizeBasedTriggeringPolicy size="5MB"/>
      </Policies>
      <DefaultRolloverStrategy max="10"/>
    </RollingFile>
  </appenders>

  <!-- 在 Loggers 引入 Appender 使其生效 -->
  <loggers>
    <!-- Logger 節點用來單獨指定 package 包下的 class 的日誌輸出格式等信息 -->
    <logger  name="org.springframework" level="info" additivity="false">
      <!-- 指定 org.springframework 的 level 及更高優先級的日誌只在控制檯輸出 -->
      <!-- additivity="false" 只在自定義的Appender中進行輸出 -->
      <AppenderRef ref="Console"/>
    </logger >

    <Root level="info">
      <!-- 用來指定項目的 Root 日誌規則,如果沒有單獨指定Logger,那麼就會默認使用 Root 日誌輸出 -->
      <!-- AppenderRef 用來指定日誌輸出到哪個 Appender -->
      <AppenderRef ref="Console"/>
      <AppenderRef ref="FileLog"/>
      <AppenderRef ref="RollingFileInfo"/>
      <AppenderRef ref="RollingFileWarn"/>
      <AppenderRef ref="RollingFileError"/>
    </Root>
  </loggers>
</configuration>

2.3、示例代碼

public class HelloSpringBoot {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(BootApplication.class);
    
    @RequestMapping("string")
    @ResponseStatus(HttpStatus.OK)
    public String helloString(){
        log.trace("trace");
        log.debug("debug");
        log.warn("warn");
        log.info("info");
        log.error("error");
        return "Hello Spring Boot";
    }
    // 其他代碼省略
}

或者 @Slf4j

@Slf4j
public class HelloSpringBoot {

    @RequestMapping("string")
    @ResponseStatus(HttpStatus.OK)
    public String helloString(){
        log.trace("trace");
        log.debug("debug");
        log.warn("warn");
        log.info("info");
        log.error("error");
        return "Hello Spring Boot";
    }
    // 其他代碼省略
}

運行項目,通過訪問 https://localhost:8000/hello/string 可以看到如下輸出

2020-02-18 01:15:20.620 WARN  [https-jsse-nio-8000-exec-10] o.x.b.HelloSpringBoot.helloString(20): warn
2020-02-18 01:15:20.620 INFO  [https-jsse-nio-8000-exec-10] o.x.b.HelloSpringBoot.helloString(21): info
2020-02-18 01:15:20.621 ERROR [https-jsse-nio-8000-exec-10] o.x.b.HelloSpringBoot.helloString(22): error

2.4、日誌配置詳解

2.4.1、日誌級別

如果日誌優先級等於或者大於配置的 level 就將其輸出。如果設置為 Info,則 WARN 、ERROR 、FATAL 信息也會輸出。而 DEBUG 、TRACE 則不會。

日誌優先級排序 OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL

  • TRACE : 追蹤,一般不使用,就是程序執行一步,就打個日誌
  • DEBUG : 調試信息,一般作為最低級別
  • INFO : 重要的提示信息
  • WARN : 警告信息
  • ERROR : 錯誤信息
  • FATAL : 致命錯誤信息

2.4.2、 配置文件

根節點 Configuration 有兩個屬性和兩個子節點 AppenderLoggers

  • status 指定 Log4j2 本身日誌輸出級別
  • monitorInterval=“N” 自動間隔 N 秒檢測配置文件是否修改,有修改則自動重新加載配置

Appender 定義日誌輸出格式

  • Console : 輸出到控制檯

    • 屬性 name : Appender 的名稱
    • 屬性 target : SYSTEM_OUT 或 SYSTEM_ERR,一般設置為 SYSTEM_OUT
    • 子節點 PatternLayout : 日誌輸出格式
    • 子節點 ThresholdFilter : 日誌輸出級別 level
  • File : 輸出到文件

    • 屬性 name : Appender 的名稱
    • 屬性 fileName : 文件路徑和名稱
    • 屬性 append : 重新運行項目是否保留先前的文件
    • 子節點 PatternLayout : 日誌輸出格式
  • RollingFile : 滾動輸出到文件,超過某個大小時候,自動覆蓋舊日誌,建議在線上項目使用

    • 屬性 name : Appender 的名稱
    • 屬性 fileName : 文件路徑和名稱
    • 屬性 filePattern : fileName 文件超過指定 size 時候,歸檔文件存放目錄和命名規則
    • 子節點 PatternLayout : 日誌輸出格式
    • 子節點 ThresholdFilter : 日誌輸出級別 level
    • 子節點 Policies : 滾動輸出策略

      • 子節點 : TimeBasedTriggeringPolicy 的 interval 屬性用來指定多久滾動一次,單位小時
      • 子節點 : SizeBasedTriggeringPolicy 的 size 屬性指定日誌超過此大小按照 filePattern 壓縮歸檔
    • 子節點 DefaultRolloverStrategy : max 屬性值指定同一文件下 filePattern 壓縮歸檔數量最大值。

Loggers

  • Logger : 單獨指定某個 package 包下的 class 輸出格式

    • 屬性 name : package 的名稱
    • 屬性 level:日誌級別
    • 屬性 additivity : 是否只在子節點 AppenderRef 裡輸出
    • 子節點 AppenderRef : 指定日誌輸出到那個 Appender
  • Root:項目 Root 日誌,如果沒有使用 Logger 單獨指定,則使用 Root 規則輸出日誌

    • 屬性 level :日誌級別,如果 Appender 為指定,則使用這個
    • 子節點 AppenderRef : 指定日誌輸出到那個 Appender

PatternLayout 自定義日誌輸出格式

%d{yyyy-MM-dd HH:mm:ss.SSS}: 毫秒級別的日誌產生時間
%highlight : 高亮顯示
%-5level : 日誌級別,-5表示左對齊並且固定輸出5個字符,如果不足在右邊補0
%c : 日誌名稱
%t : 線程名
%msg : 日誌內容
%n : 換行符
%C : 類名
%L : 行數
%M : 方法名
%l : 包括類名、方法名、文件名、行數

參考文檔

為什麼阿里巴巴禁止工程師直接使用日誌系統(Log4j、Logback)中的 API https://mp.weixin.qq.com/s/vCixKVXys5nTTcQQnzrs3w

https://www.cnblogs.com/keeya/p/10101547.html

SpringBoot整合log4j2日誌全解 https://www.cnblogs.com/keeya/p/10101547.html

下一節,將實戰 Spring Boot 2.X 連接 MySQL,分為 Spring Data JPA 和 MyBatis 兩大塊內容。更多內容,敬請關注《編程技術進階》。

Leave a Reply

Your email address will not be published. Required fields are marked *