SpringBoot2.1版本的個人應用開發框架 - 日誌自定義和全域性異常處理

人生長恨水發表於2019-04-11

本篇作為SpringBoot2.1版本的個人開發框架 子章節,請先閱讀SpringBoot2.1版本的個人開發框架再次閱讀本篇文章

專案地址:SpringBoot2.1版本的個人應用開發框架

日誌自定義

在之前的章節我們測試的時候,發現控臺臺輸出的日誌是預設的,並且有很多的日誌沒有列印,並且不能自定義設定我們的想要輸出的資訊,對於一個應用程式來說日誌記錄是必不可少的一部分。線上問題追蹤,基於日誌的業務邏輯統計分析等都離不日誌。

對於日誌的參考資料網上一搜一大堆,更詳細的介紹可以輕鬆的獲得,這裡貼出幾個參考資料:

Java有很多常用的日誌框架,如Log4j、Log4j 2、Commons Logging、Slf4j、Logback等。

Commons Logging和Slf4j是日誌門面,提供一個統一的高層介面,為各種loging API提供一個簡單統一的介面。log4j和Logback則是具體的日誌實現方案。可以簡單的理解為介面與介面的實現,呼叫者只需要關注介面而無需關注具體的實現,做到解耦。

比較常用的組合使用方式是Slf4j與Logback組合使用,Commons Logging與Log4j組合使用,基於下面的一些優點,選用Slf4j+Logback的日誌框架:

更快的執行速度,Logback重寫了內部的實現,在一些關鍵執行路徑上效能提升10倍以上。而且logback不僅效能提升了,初始化記憶體載入也更小了

自動清除舊的日誌歸檔檔案,通過設定TimeBasedRollingPolicy 或者 SizeAndTimeBasedFNATP的 maxHistory 屬性,你就可以控制日誌歸檔檔案的最大數量

Logback擁有遠比log4j更豐富的過濾能力,可以不用降低日誌級別而記錄低階別中的日誌。

Logback必須配合Slf4j使用。由於Logback和Slf4j是同一個作者,其相容性不言而喻。

預設情況下,Spring Boot會用Logback來記錄日誌,並用INFO級別輸出到控制檯。

配置日誌

由上可知我們springboot專案預設使用的就是Logback,我們可以通過設定yml檔案的方式來設定日誌的格式,也可以通過logback.xml的方式來設定日誌管理。

yml方式: 這種方式相對於xml的方式比較簡單,因為你不配置,springboot也會有預設的設定,在application-dev.yml開發環境新增以下配置即可生效,path的路徑在開發環境時可以是windows下的路徑,當你部署到liunx伺服器時需要使用application-prod生產環境的配置檔案,檔案中配置的路徑為liunx的路徑即可。

logging:
  file:
    #存放檔案的最大天數
    max-history: 15
    #存放日誌最大size
    max-size: 100MB
  #存放日誌檔案位置
  path: E:\logs
  pattern:
    #輸出到控制檯的格式
    console: "YWH - %d{yyyy-MM-dd HH:mm:ss} -%-4r  [%t]  %-5level %logger{36} - %msg%n"
  #日誌級別對映,可以指定包下的日誌級別 也可指定root為info級別
  level:
    root: info
    com.ywh.core: debug
*************************************************
<!--    %d{HH: mm:ss.SSS}——日誌輸出時間   -->
<!--    %thread  [%t] ——輸出日誌的程式名字,這在Web應用以及非同步任務處理中很有用  -->
<!--    %-5level——日誌級別,並且使用5個字元靠左對齊 -->
<!--    %logger{36}——日誌輸出者的名字   -->
<!--    %msg——日誌訊息  -->
<!--    %n——平臺的換行符  -->
複製程式碼

xml方式:這種方式需要配置多個標籤,相對與yml方式比較麻煩一點,在resources檔案下建立logback-spring.xml檔案,如果不想把xml檔案直接放在resources下的話,需要在yml檔案中配置logging.config= 指定位置

<?xml version="1.0" encoding="UTF-8"?>
<!-- 日誌級別從低到高分為TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果設定為WARN,則低於WARN的資訊都不會輸出 -->
<!-- scan:當此屬性設定為true時,配置文件如果發生改變,將會被重新載入,預設值為true -->
<!-- scanPeriod:設定監測配置文件是否有修改的時間間隔,如果沒有給出時間單位,預設單位是毫秒。
                 當scan為true時,此屬性生效。預設的時間間隔為1分鐘。 -->
<!-- debug:當此屬性設定為true時,將列印出logback內部日誌資訊,實時檢視logback執行狀態。預設值為false。 -->
<configuration  scan="true" scanPeriod="60 seconds">
    <contextName>Y-W-H</contextName>
    <!-- name的值是變數的名稱,value的值時變數定義的值。通過定義的值會被插入到logger上下文中。定義後,可以使“${}”來使用變數。 -->
    <property name="log.path" value="E:/logs/" />
 
    <!--0. 日誌格式和顏色渲染 -->
    <!-- 彩色日誌依賴的渲染類 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter" />
    <conversionRule conversionWord="wex" converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter" />
    <conversionRule conversionWord="wEx" converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter" />
    <!-- 彩色日誌格式 -->
    <property name="CONSOLE_LOG_PATTERN" value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS} %contextName ) [%thread] %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
 
 
    <!--1. 輸出到控制檯-->
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <!--此日誌appender是為開發使用,只配置最底級別,控制檯輸出的日誌級別是大於或等於此級別的日誌資訊-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>debug</level>
        </filter>
        <encoder>
            <Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
            <!-- 設定字符集 -->
            <charset>UTF-8</charset>
        </encoder>
    </appender>
 
    。。。。。。省略程式碼,具體程式碼可前往github檢視
 
</configuration>
複製程式碼

通過以上兩種方式的任意一種配置好以後啟動專案以後,就會發現我們已經使用了我們自定義的輸出格式來輸出日誌了,在我們指定下的路徑下出現了日誌檔案。

自定義異常以及全域性異常類

當日志級別設定到INFO級別後,只會輸出INFO以上的日誌,如INFO、WARN、ERROR,這沒毛病,問題是,程式中丟擲的異常堆疊(執行時異常)都沒有列印了,不利於排查問題。

而且,在某些情況下,我們在Service中想直接把異常往Controller丟擲不做處理,但我們不能直接把異常資訊輸出到客戶端,這是非常不友好的,而且我們想要精準的定位錯誤的所在,這就要我們自己來定義異常的輸出了,並且把錯誤的異常以我們之前封裝的Result的統一格式返回給前端,所以我們需要自定義異常以及定義全域性異常類,我們先定義自定義異常類然後再定義全域性異常類。

根據菜鳥教程中的異常資訊分類,異常分為三種情況

檢查性異常:最具代表的檢查性異常是使用者錯誤或問題引起的異常,這是程式設計師無法預見的。例如要開啟一個不存在檔案時,一個異常就發生了,這些異常在編譯時不能被簡單地忽略。

執行時異常: 執行時異常是可能被程式設計師避免的異常。與檢查性異常相反,執行時異常可以在編譯時被忽略。

錯誤: 錯誤不是異常,而是脫離程式設計師控制的問題。錯誤在程式碼中通常被忽略。例如,當棧溢位時,一個錯誤就發生了,它們在編譯也檢查不到的。

而我們所要做的就是繼承執行時異常,對此類異常進行自定義處理,在common下exception包中建立MyException類繼承RuntimeException。

package com.ywh.common.exception;
 
/**
 * CreateTime: 2018-11-21 19:07
 * ClassName: MyXiyiException
 * Package: com.ywh.common.exception
 * Describe:
 * 自定義異常,可以throws的時候用自己的異常類
 *
 * @author YWH
 */
public class MyException extends RuntimeException {
 
    public MyException(String msg) {
        super(msg);
    }
 
    public MyException(String message, Throwable throwable) {
        super(message, throwable);
    }
 
    public MyException(Throwable throwable) {
        super(throwable);
    }
}
複製程式碼

在common下的utils包中建立MyExceptionUtil工具類快速建立異常類

package com.ywh.common.utils;

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ywh.common.exception.MyException;

/**
 * CreateTime: 2018-12-18 22:32
 * ClassName: MyExceptionUtil
 * Package: com.ywh.common.utils
 * Describe:
 * 異常工具類
 *
 * @author YWH
 */
public class MyExceptionUtil {

    public MyExceptionUtil() {
    }

    public static MyException mxe(String msg, Throwable t, Object... params){
        return new MyException(StringUtils.format(msg, params),t);
    }

    public static MyException mxe(String msg, Object... params){
        return new MyException(StringUtils.format(msg, params));
    }

    public static MyException mxe(Throwable t){
        return new MyException(t);
    }

}
複製程式碼

建立完自定義異常以後我們要對自定義異常進行捕獲然後處理,這就需要我們定義全域性異常類來進行捕獲後進行處理了,在common下exception中新增GlobalExceptionHandler類。

package com.ywh.common.exception;
 
/**
 * @Author: YWH
 * @Description: 全域性異常處理類,攔截controller  RestControllerAdvice此註解為ResponseBody和ControllerAdvice混合註解
 * @Date: Create in 17:16 2018/11/17
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
 
    /**
     *
     * 全域性異常類中定義的異常都可以被攔截,只是觸發條件不一樣,如IO異常這種必須丟擲異常到
     * controller中才可以被攔截,或者在類中用try..catch自己處理
     * 絕大部分不需要向上丟擲異常即可被攔截,返回前端json資料,如陣列下標越界,404 500 400等錯誤
     * 如果自己想要寫,按著以下格式增加異常即可
     *HttpMessageNotReadableException
     */
 
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
 
    /**
     *   啟動應用後,被 @ExceptionHandler@InitBinder@ModelAttribute 註解的方法,
     *   都會作用在 被 @RequestMapping 註解的方法上。
     * @param binder
     */
    @InitBinder
    public void initWebBinder(WebDataBinder binder){
 
    }
 
    /**
     * 系統錯誤,未知的錯誤   已測試
     * @param ex 異常資訊
     * @return 返回前端異常資訊
     */
    @ExceptionHandler({Exception.class})
    public Result exception(Exception ex){
        log.error("錯誤詳情:" + ex.getMessage(),ex);
        return Result.errorJson(BaseEnum.SYSTEM_ERROR.getMsg(),BaseEnum.SYSTEM_ERROR.getIndex());
    }
 
  。。。。。。省略程式碼,具體程式碼請前往github檢視
 
    /**
     * 自定義異常資訊攔截
     * @param ex 異常資訊
     * @return 返回前端異常資訊
     */
    @ExceptionHandler(MyException.class)
    public Result myCustomizeException(MyException ex){
        log.warn("錯誤詳情:" + ex);
        return Result.errorJson(BaseEnum.CUSTOMIZE_EXCEPTION.getMsg(),BaseEnum.CUSTOMIZE_EXCEPTION.getIndex());
    }
 
}
複製程式碼

在GlobalExceptionHandler中我們對很多異常進行了攔截後自定義處理,並把我們上邊自定義的執行時異常進行攔截,我在類中的方法上都寫了註釋,並根據網上的資料應該很好理解,我對大部分的異常都做了測試,都是可以進行攔截成功的。

測試示例

我們用postman通過post方式請求一個get的方法,可以看到返回了我們自定義的json格式,並且告訴我們這是因為介面型別所導致的錯誤,這樣我們很快就能定位到錯誤進行解決。

異常資訊攔截

以上錯誤都是系統替我們捕獲並且通過全域性異常類進行了攔截之後返回自定義的json格式,而我們的自定義異常如何使用呢,自定義異常需要我們手動捕獲異常,並且丟擲異常,這樣我們的全域性異常類才能攔截到。

我們在ExampleServiceImpl中定義一個方法,並在Controller層中呼叫此方法,用postman呼叫此介面

    /**
     * 測試自定義異常
     * @return 返回字串
     */
    @Override
    public String myException() {
        int i = 0;
        int a = 10;
        if( i > a){
            System.out.println("測試!!!");
        }else{
            throw MyExceptionUtil.mxe("出錯了,比他小啊!!");
        }
        return "沒有進行攔截,失敗了";
    }
複製程式碼
    @Autowired
    private ExampleService exampleService;
    
    @GetMapping("myExceptionTest")
    public Result myExceptionTest(){
        return Result.successJson(exampleService.myException());
    }
複製程式碼

自定義一場攔截

控制檯資訊

可以看到我們的自定義異常被攔截到並且在控制檯中列印了我們想要的資訊。

相關文章