2、開發環境在日常項目開發過程當中,程序不免會出現運行時異常,或者業務異常。難道要針對每一處可能出現的異常進行編寫代碼進行處理?或者直接不處理異常,將一大屏堆滿英文的異常信息顯示給用戶?那用戶體驗性是何等極差。
因此,當程序拋異常時,爲了日誌的可讀性
,排查 Bug簡單
,以及更好的用戶體驗性
,因此咱們要對全局異常進行處理。前端
3、添加依賴
- JDK 1.8 或者1.8以上
- Springboot (此演示版本爲 Springboot 2.1.18.RELEASE)
- Gradle (固然也可用Maven,其實目的都是爲構建項目,管理依賴等)
plugins { id "org.springframework.boot" version "2.1.18.RELEASE" id "io.spring.dependency-management" version "1.0.10.RELEASE" id "java" } group = 'com.nobody' version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' configurations { developmentOnly runtimeClasspath { extendsFrom developmentOnly } compileOnly { extendsFrom annotationProcessor } } repositories { mavenLocal() maven { url "http://maven.aliyun.com/nexus/content/groups/public/" } mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' // 添加lombok,主要爲程序中經過註解,不用編寫getter和setter等代碼 compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor' annotationProcessor 'org.projectlombok:lombok' }4、自定義異常錯誤類
在咱們項目開發中,確定會有跟業務相關的異常,例如添加用戶的業務,系統要求用戶名不能爲空,可是添加用戶的請求接口,用戶名值爲空,這時咱們程序要報
用戶名不能爲空
的異常錯誤;或者查詢用戶信息的接口,可能會報用戶不存在
的錯誤異常等等。java
由於要作成通用性,因此咱們定義一個異常基礎接口類,自定義的異常枚舉類需實現該接口。web
package com.nobody.exception; /** * @Description 自定義異常基礎接口類,自定義的異常信息枚舉類需實現該接口。 * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ public interface BaseErrorInfo { /** * 獲取錯誤碼 * * @return 錯誤碼 */ String getErrorCode(); /** * 獲取錯誤信息 * * @return 錯誤信息 */ String getErrorMsg(); }
通用異常信息枚舉類,這裏定義的全部異常信息是整個程序通用的。spring
package com.nobody.exception; import lombok.Getter; /** * @Description 自定義通用異常信息枚舉類 * @Author Mr.nobody * @Date 2020/10/23 * @Version 1.0 */ @Getter public enum CommonErrorEnum implements BaseErrorInfo { /** * 成功 */ SUCCESS("200", "成功!"), /** * 請求的數據格式不符! */ BODY_NOT_MATCH("400", "請求的數據格式不符!"), /** * 未找到該資源! */ NOT_FOUND("404", "未找到該資源!"), /** * 服務器內部錯誤! */ INTERNAL_SERVER_ERROR("500", "服務器內部錯誤!"), /** * 服務器正忙,請稍後再試! */ SERVER_BUSY("503", "服務器正忙,請稍後再試!"); private String errorCode; private String errorMsg; CommonErrorEnum(String errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } }
若是程序中異常信息太多,能夠針對每一個模塊功能定義業務異常枚舉類,方便維護,例如和用戶相關的異常信息枚舉類以下。數組
package com.nobody.exception; import lombok.Getter; /** * @Description 自定義用戶相關異常信息枚舉類 * @Author Mr.nobody * @Date 2020/10/23 * @Version 1.0 */ @Getter public enum UserErrorEnum implements BaseErrorInfo { /** * 用戶不存在 */ USER_NOT_FOUND("1001", "用戶不存在!"); private String errorCode; private String errorMsg; UserErrorEnum(String errorCode, String errorMsg) { this.errorCode = errorCode; this.errorMsg = errorMsg; } }
業務異常類,主要用於業務錯誤,或者異常時手動拋出的異常。服務器
package com.nobody.exception; import lombok.Getter; import lombok.Setter; import org.slf4j.MDC; /** * @Description 自定義業務異常類 * @Author Mr.nobody * @Date 2020/10/23 * @Version 1.0 */ @Getter @Setter public class BizException extends RuntimeException { private static final long serialVersionUID = 5564446583860234738L; // 錯誤碼 private String errorCode; // 錯誤信息 private String errorMsg; // 日誌追蹤ID private String traceId = MDC.get("traceId"); public BizException(BaseErrorInfo errorInfo) { super(errorInfo.getErrorMsg()); this.errorCode = errorInfo.getErrorCode(); this.errorMsg = errorInfo.getErrorMsg(); } public BizException(BaseErrorInfo errorInfo, String errorMsg) { super(errorMsg); this.errorCode = errorInfo.getErrorCode(); this.errorMsg = errorMsg; } public BizException(BaseErrorInfo errorInfo, Throwable cause) { super(errorInfo.getErrorMsg(), cause); this.errorCode = errorInfo.getErrorCode(); this.errorMsg = errorInfo.getErrorMsg(); } public BizException(String errorCode, String errorMsg) { super(errorMsg); this.errorCode = errorCode; this.errorMsg = errorMsg; } public BizException(String errorCode, String errorMsg, Throwable cause) { super(errorMsg, cause); this.errorCode = errorCode; this.errorMsg = errorMsg; } }5、接口返回統一格式
爲方便前端對接口返回的數據進行處理,也是規範問題,因此咱們要定義接口返回統一格式。app
package com.nobody.pojo.vo; import lombok.Getter; import lombok.Setter; /** * @Description 接口返回統一格式 * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ @Getter @Setter public class GeneralResult<T> { private boolean success; private String errorCode; private String message; private T data; private String traceId; private GeneralResult(boolean success, T data, String message, String errorCode) { this.success = success; this.data = data; this.message = message; this.errorCode = errorCode; } public static <T> GeneralResult<T> genResult(boolean success, T data, String message) { return genResult(success, data, message, null); } public static <T> GeneralResult<T> genSucce***esult(T data) { return genResult(true, data, null, null); } public static <T> GeneralResult<T> genErrorResult(String message) { return genResult(false, null, message, null); } public static <T> GeneralResult<T> genSucce***esult() { return genResult(true, null, null, null); } public static <T> GeneralResult<T> genErrorResult(String message, String errorCode) { return genResult(false, null, message, errorCode); } public static <T> GeneralResult<T> genResult(boolean success, T data, String message, String errorCode) { return new GeneralResult<>(success, data, message, errorCode); } public static <T> GeneralResult<T> genErrorResult(String message, String errorCode, String traceId) { GeneralResult<T> result = genResult(false, null, message, errorCode); result.setTraceId(traceId); return result; } }6、全局異常處理
此類是對全局異常的處理,根據本身狀況,是否對不一樣種類的異常進行處理。例如如下是單獨對業務異常,接口參數異常,以及剩餘的全部異常進行處理,並生成接口統一格式信息,返回給調用接口的客戶端,進行展現。
首先咱們須要在處理全局異常的類上面,加上@ControllerAdvice
或者@RestControllerAdvice
註解。@ControllerAdvice 註解能處理@Controller
和@RestController
類型的接口調用時產生的異常,而 @RestControllerAdvice 註解只能處理@RestController
類型接口調用時產生的異常。咱們通常用 @ControllerAdvice 註解。
@ExceptionHandler
只能註解在方法上,表示這是一個處理異常的方法,value
屬性能夠填寫須要處理的異常類,能夠是數組。@ResponseBody
註解表示咱們返回的信息是響應體數據。dom
package com.nobody.exception; import javax.servlet.http.HttpServletRequest; import com.nobody.pojo.vo.GeneralResult; import org.slf4j.MDC; import org.springframework.util.StringUtils; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.RestControllerAdvice; /** * @Description 統一異常處理 * @Author Mr.nobody * @Date 2020/10/23 * @Version 1.0 */ @ControllerAdvice @Slf4j public class GlobalExceptionHandler { // 處理自定義的業務異常 @ExceptionHandler(value = BizException.class) @ResponseBody public GeneralResult<Object> restErrorHandler(HttpServletRequest request, BizException e) { String err = "requestURI:" + request.getRequestURI() + ",errorCode:" + e.getErrorCode() + ",errorMsg:" + e.getErrorMsg(); log.error(err, e); return GeneralResult.genErrorResult(e.getMessage(), e.getErrorCode(), e.getTraceId()); } // 處理接口參數數據格式錯誤異常 @ExceptionHandler(value = MethodArgumentNotValidException.class) @ResponseBody public GeneralResult<Object> errorHandler(HttpServletRequest request, MethodArgumentNotValidException e) { StringBuilder message = new StringBuilder(); String err = null; e.getBindingResult().getAllErrors() .forEach(error -> message.append(error.getDefaultMessage()).append(";")); String des = message.toString(); if (!StringUtils.isEmpty(des)) { err = des.substring(0, des.length() - 1); } log.error(err + ",requestURI:" + request.getRequestURI(), e); return GeneralResult.genErrorResult(CommonErrorEnum.BODY_NOT_MATCH.getErrorMsg(), CommonErrorEnum.BODY_NOT_MATCH.getErrorCode(), MDC.get("traceId")); } // 處理其餘異常 @ExceptionHandler(value = Exception.class) @ResponseBody public GeneralResult<Object> errorHandler(HttpServletRequest request, Exception e) { log.error("internal server error,requestURI:" + request.getRequestURI(), e); return GeneralResult.genErrorResult(CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorMsg(), CommonErrorEnum.INTERNAL_SERVER_ERROR.getErrorCode(), MDC.get("traceId")); } }7、測試
測試會針對不一樣狀況進行驗證,如下是一些測試須要用到的類。maven
package com.nobody.pojo.entity; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.Setter; import java.io.Serializable; /** * @Description 用戶實體類 * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ @AllArgsConstructor @Getter @Setter public class UserEntity implements Serializable { private static final long serialVersionUID = 5564446583860234738L; private String id; private String name; private int age; }
package com.nobody.pojo.dto; import lombok.Getter; import lombok.Setter; import javax.validation.constraints.Min; import javax.validation.constraints.NotEmpty; /** * @Description 添加用戶時參數類 * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ @Getter @Setter public class UserDTO { @NotEmpty(message = "用戶名不能爲空") private String name; @Min(value = 0, message = "年齡最小不能低於0") private int age; }
如下簡單模擬 User 相關業務,而後產生不一樣的異常。ide
package com.nobody.service; import com.nobody.pojo.dto.UserDTO; import com.nobody.pojo.entity.UserEntity; /** * @Description * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ public interface UserService { UserEntity add(UserDTO userDTO); UserEntity getById(String id); void marry(String age); }
package com.nobody.service.impl; import com.nobody.exception.BizException; import com.nobody.exception.UserErrorEnum; import com.nobody.pojo.dto.UserDTO; import com.nobody.pojo.entity.UserEntity; import com.nobody.service.UserService; import org.springframework.stereotype.Service; import java.util.Objects; import java.util.UUID; /** * @Description * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ @Service public class UserServiceImpl implements UserService { @Override public UserEntity add(UserDTO userDTO) { String userId = UUID.randomUUID().toString(); return new UserEntity(userId, userDTO.getName(), userDTO.getAge()); } @Override public UserEntity getById(String id) { // 模擬業務異常 if (Objects.equals(id, "000")) { throw new BizException(UserErrorEnum.USER_NOT_FOUND); } return new UserEntity(id, "Mr.nobody", 18); } @Override public void marry(String age) { // 當age不是數字字符串時,拋出異常 Integer integerAge = Integer.valueOf(age); System.out.println(integerAge); } }
接口類定義,根據不一樣參數調用接口,可產生不一樣的異常錯誤。
package com.nobody.controller; import com.nobody.pojo.dto.UserDTO; import com.nobody.pojo.entity.UserEntity; import com.nobody.pojo.vo.GeneralResult; import com.nobody.service.UserService; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import javax.validation.Valid; /** * @Description * @Author Mr.nobody * @Date 2021/2/6 * @Version 1.0 */ @RestController @RequestMapping("user") public class UserController { private UserService userService; public UserController(final UserService userService) { this.userService = userService; } @PostMapping("add") public GeneralResult<UserEntity> add(@RequestBody @Valid UserDTO userDTO) { UserEntity user = userService.add(userDTO); return GeneralResult.genSucce***esult(user); } @GetMapping("find/{userId}") public GeneralResult<UserEntity> find(@PathVariable String userId) { UserEntity user = userService.getById(userId); return GeneralResult.genSucce***esult(user); } @GetMapping("marry/{age}") public GeneralResult<UserEntity> marry(@PathVariable String age) { userService.marry(age); return GeneralResult.genSucce***esult(); } }
啓動服務,進行接口調用,本此演示用的 IDEA 自帶的
HTTP Client
工具進行調用,固然你也可使用Postman
進行調用。
首先演示正常的接口調用,服務沒有報錯,接口也返回正常數據。
仍是調用查詢用戶接口,演示用戶不存在狀況,服務報錯打印日誌,接口也返回錯誤信息。
再演示添加用戶操做,用戶名不填值,程序報錯打印日誌,接口也返回錯誤信息。
再演示其餘異常狀況,例如解析數字出錯。