1. 程式人生 > 其它 >Java專案構建基礎:統一結果,統一異常,統一日誌

Java專案構建基礎:統一結果,統一異常,統一日誌

我剛剛看了一遍微信文章,看著不錯,在這裡記錄下,方便以後直接使用。

 

統一結果返回

目前的前後端開發大部分資料的傳輸格式都是json,因此定義一個統一規範的資料格式有利於前後端的互動與UI的展示。

統一結果的一般形式

  • 是否響應成功;

  • 響應狀態碼;

  • 狀態碼描述;

  • 響應資料

  • 其他識別符號

結果類列舉

前三者可定義結果列舉,如:success,code,message

@Getter  
public enum ResultCodeEnum {  
    SUCCESS(true,20000,"成功"),  
    UNKNOWN_ERROR(false,20001,"未知錯誤"),,  
    PARAM_ERROR(
false,20002,"引數錯誤"), ; // 響應是否成功 private Boolean success; // 響應狀態碼 private Integer code; // 響應資訊 private String message; ResultCodeEnum(boolean success, Integer code, String message) { this.success = success; this.code = code;
this.message = message; } }

統一結果類

第5個屬於自定義返回,利用前4者可定義統一返回物件

注意:

  • 外界只可以呼叫統一返回類的方法,不可以直接建立,因此構造器私有;

  • 內建靜態方法,返回物件;

  • 為便於自定義統一結果的資訊,建議使用鏈式程式設計,將返回物件設類本身,即return this;

  • 響應資料由於為json格式,可定義為JsonObject或Map形式;

@Data  
public class R {  
    private Boolean success;  
  
    private Integer code;  
  
    
private String message; private Map<String, Object> data = new HashMap<>(); // 構造器私有 private R(){} // 通用返回成功 public static R ok() { R r = new R(); r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess()); r.setCode(ResultCodeEnum.SUCCESS.getCode()); r.setMessage(ResultCodeEnum.SUCCESS.getMessage()); return r; } // 通用返回失敗,未知錯誤 public static R error() { R r = new R(); r.setSuccess(ResultCodeEnum.UNKNOWN_ERROR.getSuccess()); r.setCode(ResultCodeEnum.UNKNOWN_ERROR.getCode()); r.setMessage(ResultCodeEnum.UNKNOWN_ERROR.getMessage()); return r; } // 設定結果,形參為結果列舉 public static R setResult(ResultCodeEnum result) { R r = new R(); r.setSuccess(result.getSuccess()); r.setCode(result.getCode()); r.setMessage(result.getMessage()); return r; } /**------------使用鏈式程式設計,返回類本身-----------**/ // 自定義返回資料 public R data(Map<String,Object> map) { this.setData(map); return this; } // 通用設定data public R data(String key,Object value) { this.data.put(key, value); return this; } // 自定義狀態資訊 public R message(String message) { this.setMessage(message); return this; } // 自定義狀態碼 public R code(Integer code) { this.setCode(code); return this; } // 自定義返回結果 public R success(Boolean success) { this.setSuccess(success); return this; } }

控制層返回

檢視層使用統一結果

@RestController  
@RequestMapping("/api/v1/users")  
public class TeacherAdminController {  
  
    @Autowired  
    private UserService userService;  
  
    @GetMapping  
    public R list() {  
        List<Teacher> list = teacherService.list(null);  
        return R.ok().data("itms", list).message("使用者列表");  
    }  
}    

json結果

{  
  "success": true,  
  "code": 20000,  
  "message": "查詢使用者列表",  
  "data": {  
    "itms": [  
      {  
        "id": "1",  
        "username": "admin",  
        "role": "ADMIN",  
        "deleted": false,  
        "gmtCreate": "2019-12-26T15:32:29",  
        "gmtModified": "2019-12-26T15:41:40"  
      },{  
        "id": "2",  
        "username": "zhangsan",  
        "role": "USER",  
        "deleted": false,  
        "gmtCreate": "2019-12-26T15:32:29",  
        "gmtModified": "2019-12-26T15:41:40"  
      }  
    ]  
  }  
}  

統一結果類的使用參考了mybatis-plus中R物件的設計

統一異常處理

使用統一返回結果時,還有一種情況,就是程式的儲存是由於執行時異常導致的結果,有些異常我們可以無法提前預知,不能正常走到我們return的R物件返回。

因此,我們需要定義一個統一的全域性異常來捕獲這些資訊,並作為一種結果返回控制層

@ControllerAdvice

該註解為統一異常處理的核心

是一種作用於控制層的切面通知(Advice),該註解能夠將通用的@ExceptionHandler@InitBinder@ModelAttributes方法收集到一個型別,並應用到所有控制器上

該類中的設計思路:

  • 使用@ExceptionHandler註解捕獲指定或自定義的異常;

  • 使用@ControllerAdvice整合@ExceptionHandler的方法到一個類中;

  • 必須定義一個通用的異常捕獲方法,便於捕獲未定義的異常資訊;

  • 自定一個異常類,捕獲針對專案或業務的異常;

  • 異常的物件資訊補充到統一結果列舉中;

自定義全域性異常類

@Data  
public class CMSException extends RuntimeException {  
    private Integer code;  
  
    public CMSException(Integer code, String message) {  
        super(message);  
        this.code = code;  
    }  
  
    public CMSException(ResultCodeEnum resultCodeEnum) {  
        super(resultCodeEnum.getMessage());  
        this.code = resultCodeEnum.getCode();  
    }  
  
    @Override  
    public String toString() {  
        return "CMSException{" + "code=" + code + ", message=" + this.getMessage() + '}';  
    }  
} 

統一異常處理器

// ...  
import org.springframework.web.bind.annotation.ControllerAdvice;  
import org.springframework.web.bind.annotation.ExceptionHandler;  
import org.springframework.web.bind.annotation.ResponseBody;  
  
@ControllerAdvice  
public class GlobalExceptionHandler {  
  
    /**-------- 通用異常處理方法 --------**/  
    @ExceptionHandler(Exception.class)  
    @ResponseBody  
    public R error(Exception e) {  
        e.printStackTrace();  
        return R.error();    // 通用異常結果  
    }  
  
    /**-------- 指定異常處理方法 --------**/  
    @ExceptionHandler(NullPointerException.class)  
    @ResponseBody  
    public R error(NullPointerException e) {  
        e.printStackTrace();  
        return R.setResult(ResultCodeEnum.NULL_POINT);  
    }  
  
    @ExceptionHandler(HttpClientErrorException.class)  
    @ResponseBody  
    public R error(IndexOutOfBoundsException e) {  
        e.printStackTrace();  
        return R.setResult(ResultCodeEnum.HTTP_CLIENT_ERROR);  
    }  
  
    /**-------- 自定義定異常處理方法 --------**/  
    @ExceptionHandler(CMSException.class)  
    @ResponseBody  
    public R error(CMSException e) {  
        e.printStackTrace();  
        return R.error().message(e.getMessage()).code(e.getCode());  
    }  
}  

控制層展示

以下為展示當遇到null指定異常時,返回的結果資訊

{  
  "success": false,  
  "code": 20007,  
  "message": "空指標異常",  
  "data": {}  
}  

本節介紹統一異常較為簡略,詳細參考:

https://juejin.cn/post/6844903827791953928

統一日誌收集

日誌是追蹤錯誤定位問題的關鍵,尤其在生產環境中,需要及時修復熱部署,不會提供開發者debug的環境,此時日誌將會是最快解決問題的關鍵

日誌的框架比較豐富,由於spring boot對logback的整合,因此推薦使用logback在專案中使用。

Logback

關於logback的配置和介紹,可以參考官網或推薦部落格glmapper的logback部落格,logback-spring.xml配置檔案

https://blog.csdn.net/xu_san_duo/article/details/80364600

配置

以下直接貼出配置資訊,介紹資訊可以直接參考備註

<?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="10 seconds">  
    <contextName>logback</contextName>  
  
    <!-- name的值是變數的名稱,value的值時變數定義的值。通過定義的值會被插入到logger上下文中。定義後,可以使“${}”來使用變數。 -->  
    <property name="log.path" value="D:/Documents/logs/edu" />  
  
    <!--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}){faint} %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>  
  
    <!--2. 輸出到文件-->  
    <!-- 2.1 level為 DEBUG 日誌,時間滾動輸出  -->  
    <appender name="DEBUG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
        <!-- 正在記錄的日誌文件的路徑及文件名 -->  
        <file>${log.path}/edu_debug.log</file>  
        <!--日誌文件輸出格式-->  
        <encoder>  
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>  
            <charset>UTF-8</charset> <!-- 設定字符集 -->  
        </encoder>  
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!-- 日誌歸檔 -->  
            <fileNamePattern>${log.path}/web-debug-%d{yyyy-MM-dd}.%i.log</fileNamePattern>  
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">  
                <maxFileSize>100MB</maxFileSize>  
            </timeBasedFileNamingAndTriggeringPolicy>  
            <!--日誌文件保留天數-->  
            <maxHistory>15</maxHistory>  
        </rollingPolicy>  
        <!-- 此日誌文件只記錄debug級別的 -->  
        <filter class="ch.qos.logback.classic.filter.LevelFilter">  
            <level>debug</level>  
            <onMatch>ACCEPT</onMatch>  
            <onMismatch>DENY</onMismatch>  
        </filter>  
    </appender>  
  
    <!-- 2.2 level為 INFO 日誌,時間滾動輸出  -->  
    <appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
        <!-- 正在記錄的日誌文件的路徑及文件名 -->  
        <file>${log.path}/edu_info.log</file>  
        <!--日誌文件輸出格式-->  
        <encoder>  
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>  
            <charset>UTF-8</charset>  
        </encoder>  
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <!-- 每天日誌歸檔路徑以及格式 -->  
            <fileNamePattern>${log.path}/web-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>  
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">  
                <maxFileSize>100MB</maxFileSize>  
            </timeBasedFileNamingAndTriggeringPolicy>  
            <!--日誌文件保留天數-->  
            <maxHistory>15</maxHistory>  
        </rollingPolicy>  
        <!-- 此日誌文件只記錄info級別的 -->  
        <filter class="ch.qos.logback.classic.filter.LevelFilter">  
            <level>info</level>  
            <onMatch>ACCEPT</onMatch>  
            <onMismatch>DENY</onMismatch>  
        </filter>  
    </appender>  
  
    <!-- 2.3 level為 WARN 日誌,時間滾動輸出  -->  
    <appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
        <!-- 正在記錄的日誌文件的路徑及文件名 -->  
        <file>${log.path}/edu_warn.log</file>  
        <!--日誌文件輸出格式-->  
        <encoder>  
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>  
            <charset>UTF-8</charset> <!-- 此處設定字符集 -->  
        </encoder>  
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <fileNamePattern>${log.path}/web-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>  
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">  
                <maxFileSize>100MB</maxFileSize>  
            </timeBasedFileNamingAndTriggeringPolicy>  
            <!--日誌文件保留天數-->  
            <maxHistory>15</maxHistory>  
        </rollingPolicy>  
        <!-- 此日誌文件只記錄warn級別的 -->  
        <filter class="ch.qos.logback.classic.filter.LevelFilter">  
            <level>warn</level>  
            <onMatch>ACCEPT</onMatch>  
            <onMismatch>DENY</onMismatch>  
        </filter>  
    </appender>  
  
    <!-- 2.4 level為 ERROR 日誌,時間滾動輸出  -->  
    <appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">  
        <!-- 正在記錄的日誌文件的路徑及文件名 -->  
        <file>${log.path}/edu_error.log</file>  
        <!--日誌文件輸出格式-->  
        <encoder>  
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>  
            <charset>UTF-8</charset> <!-- 此處設定字符集 -->  
        </encoder>  
        <!-- 日誌記錄器的滾動策略,按日期,按大小記錄 -->  
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">  
            <fileNamePattern>${log.path}/web-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>  
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">  
                <maxFileSize>100MB</maxFileSize>  
            </timeBasedFileNamingAndTriggeringPolicy>  
            <!--日誌文件保留天數-->  
            <maxHistory>15</maxHistory>  
        </rollingPolicy>  
        <!-- 此日誌文件只記錄ERROR級別的 -->  
        <filter class="ch.qos.logback.classic.filter.LevelFilter">  
            <level>ERROR</level>  
            <onMatch>ACCEPT</onMatch>  
            <onMismatch>DENY</onMismatch>  
        </filter>  
    </appender>  
  
    <!--  
        <logger>用來設定某一個包或者具體的某一個類的日誌列印級別、  
        以及指定<appender>。<logger>僅有一個name屬性,  
        一個可選的level和一個可選的addtivity屬性。  
        name:用來指定受此logger約束的某一個包或者具體的某一個類。  
        level:用來設定列印級別,大小寫無關:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,  
              還有一個特俗值INHERITED或者同義詞NULL,代表強制執行上級的級別。  
              如果未設定此屬性,那麼當前logger將會繼承上級的級別。  
        addtivity:是否向上級logger傳遞列印資訊。預設是true。  
        <logger name="org.springframework.web" level="info"/>  
        <logger name="org.springframework.scheduling.annotation.ScheduledAnnotationBeanPostProcessor" level="INFO"/>  
    -->  
  
    <!--  
        使用mybatis的時候,sql語句是debug下才會列印,而這裡我們只配置了info,所以想要檢視sql語句的話,有以下兩種操作:  
        第一種把<root level="info">改成<root level="DEBUG">這樣就會列印sql,不過這樣日誌那邊會出現很多其他訊息  
        第二種就是單獨給dao下目錄配置debug模式,程式碼如下,這樣配置sql語句會列印,其他還是正常info級別:  
        【logging.level.org.mybatis=debug logging.level.dao=debug】  
     -->  
  
    <!--  
        root節點是必選節點,用來指定最基礎的日誌輸出級別,只有一個level屬性  
        level:用來設定列印級別,大小寫無關:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,  
        不能設定為INHERITED或者同義詞NULL。預設是DEBUG  
        可以包含零個或多個元素,標識這個appender將會新增到這個logger。  
    -->  
  
    <!-- 4. 最終的策略 -->  
    <!-- 4.1 開發環境:列印控制檯-->  
    <springProfile name="dev">  
        <logger name="com.cms" level="info"/>  
        <root level="info">  
            <appender-ref ref="CONSOLE" />  
            <appender-ref ref="DEBUG_FILE" />  
            <appender-ref ref="INFO_FILE" />  
            <appender-ref ref="WARN_FILE" />  
            <appender-ref ref="ERROR_FILE" />  
        </root>  
    </springProfile>  
  
  
    <!-- 4.2 生產環境:輸出到文件-->  
    <springProfile name="pro">  
        <logger name="com.cms" level="warn"/>  
        <root level="info">  
            <appender-ref ref="ERROR_FILE" />  
            <appender-ref ref="WARN_FILE" />  
        </root>  
    </springProfile>  
  
</configuration>  

日誌收集異常資訊

日誌資訊往往伴隨著異常資訊的輸出,因此,我們需要修改統一異常的處理器,將異常資訊以流的方式寫到日誌檔案中

異常資訊檔案工具類

@Slf4j  
public class ExceptionUtil {  
  
    /**  
     * 列印異常資訊  
     */  
    public static String getMessage(Exception e) {  
        String swStr = null;  
        try (StringWriter sw = new StringWriter(); PrintWriter pw = new PrintWriter(sw)) {  
            e.printStackTrace(pw);  
            pw.flush();  
            sw.flush();  
            swStr = sw.toString();  
        } catch (IOException ex) {  
            ex.printStackTrace();  
            log.error(ex.getMessage());  
        }  
        return swStr;  
    }  
}  

修改統一異常處理器,將異常方法中的直接列印改為日誌輸入並列印

// ...  
import lombok.extern.slf4j.Slf4j;  
  
@ControllerAdvice  
@Slf4j  
public class GlobalExceptionHandler {  
  
    /**-------- 通用異常處理方法 --------**/  
    @ExceptionHandler(Exception.class)  
    @ResponseBody  
    public R error(Exception e) {  
        // e.printStackTrace();  
        log.error(ExceptionUtil.getMessage(e));  
        return R.error();  
    }  
  
   // ...  
}
    

注意

  • 日誌的環境即spring.profiles.acticve,跟隨專案啟動;

  • 啟動後,即可到自定目錄查詢到生成的日誌檔案;

  • 本地idea除錯時,推薦Grep Console外掛可實現控制檯的自定義顏色輸出

詳細過程,可參考原始碼:

https://github.com/chetwhy/cloud-flow