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