SpringBoot中如何統一接口返回與全局異常處理詳解
背景
在分佈式、微服務盛行的今天,絕大部分項目都采用的微服務框架,前後端分離方式。前端和後端進行交互,前端按照約定請求URL路徑,並傳入相關參數,後端服務器接收請求,進行業務處理,返回數據給前端。維護一套完善且規范的接口是非常有必要的, 這樣不僅能夠提高對接效率,也可以讓我的代碼看起來更加簡潔優雅。
使用統一返回結果時,還有一種情況,就是程序的報錯是由於運行時異常導致的結果,有些異常是我們在業務中拋出的,有些是無法提前預知。
因此,我們需要定義一個統一的全局異常,在Controller捕獲所有異常,並且做適當處理,並作為一種結果返回。
統一接口返回
定義API返回碼枚舉類
public enum ResultCode { /* 成功狀態碼 */ SUCCESS(200, "成功"), /* 錯誤狀態碼 */ NOT_FOUND(404, "請求的資源不存在"), INTERNAL_ERROR(500, "服務器內部錯誤"), PARAMETER_EXCEPTION(501, "請求參數校驗異常"), /* 業務狀態碼 */ USER_NOT_EXIST_ERROR(10001, "用戶不存在"), ; private Integer code; private String message; public Integer code() { return this.code; } public String message() { return this.message; } ResultCode(Integer code, String message) { this.code = code; this.message = message; } }
定義正常響應的API統一返回體
@Data public class Result<T> implements Serializable { private Integer code; private String message; private boolean success = true; private T data; @JsonIgnore private ResultCode resultCode; private Result() { } public void setResultCode(ResultCode resultCode) { this.resultCode = resultCode; this.code = resultCode.code(); this.message = resultCode.message(); } public Result(ResultCode resultCode, T data) { this.code = resultCode.code(); this.message = resultCode.message(); this.data = data; } public static <T> Result<T> success() { Result<T> result = new Result<>(); result.setResultCode(ResultCode.SUCCESS); return result; } public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setResultCode(ResultCode.SUCCESS); result.setData(data); return result; } }
定義異常響應的API統一返回體
@Data public class ErrorResult implements Serializable { private Integer code; private String message; private boolean success = false; @JsonIgnore private ResultCode resultCode; public static ErrorResult error() { ErrorResult result = new ErrorResult(); result.setResultCode(ResultCode.INTERNAL_ERROR); return result; } public static ErrorResult error(String message) { ErrorResult result = new ErrorResult(); result.setCode(ResultCode.INTERNAL_ERROR.code()); result.setMessage(message); return result; } public static ErrorResult error(Integer code, String message) { ErrorResult result = new ErrorResult(); result.setCode(code); result.setMessage(message); return result; } public static ErrorResult error(ResultCode resultCode, String message) { ErrorResult result = new ErrorResult(); result.setResultCode(resultCode); result.setMessage(message) return result; } }
編寫包裝返回結果的自定義註解
@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD}) //作用於方法和類(接口)上 @Documented public @interface ResponseResult { }
定義返回結果攔截器
@Component public class ResponseResultInterceptor implements HandlerInterceptor { /* 使用統一返回體的標識 */ private static final String RESPONSE_RESULT_ANNOTATION = "RESPONSE-RESULT-ANNOTATION"; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 正在處理請求的方法bean if (handler instanceof HandlerMethod) { final HandlerMethod handlerMethod = (HandlerMethod) handler; // 獲取當前類 final Class<?> clazz = handlerMethod.getBeanType(); // 獲取當前方法 final Method method = handlerMethod.getMethod(); // 判斷是否在類對象上加瞭註解 if (clazz.isAnnotationPresent(ResponseResult.class)) { // 設置該請求返回體,需要包裝,往下傳遞,在ResponseBodyAdvice接口進行判斷 request.setAttribute(RESPONSE_RESULT_ANNOTATION, clazz.getAnnotation(ResponseResult.class)); } // 判斷是否在方法上加瞭註解 else if (method.isAnnotationPresent(ResponseResult.class)) { // 設置該請求返回體,需要包裝,往下傳遞,在ResponseBodyAdvice接口進行判斷 request.setAttribute(RESPONSE_RESULT_ANNOTATION, method.getAnnotation(ResponseResult.class)); } } return true; } }
WebMvc配置類攔截器註冊者添加返回結果攔截器
@Configuration public class WebMvcConfig implements WebMvcConfigurer { /** * 添加自定義攔截器 */ @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new ResponseResultInterceptor()).addPathPatterns("/**"); } }
編寫響應體處理器
/** * 統一處理響應體,用Result.success靜態方法包裝, * 在API接口使用時就可以直接返回原始類型 */ @RestControllerAdvice public class ResponseResultHandler implements ResponseBodyAdvice<Object> { /* 使用統一返回體的標識 */ private static final String RESPONSE_RESULT_ANNOTATION = "RESPONSE-RESULT-ANNOTATION"; @Override public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) { ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = Objects.requireNonNull(sra).getRequest(); ResponseResult responseResult = (ResponseResult) request.getAttribute(RESPONSE_RESULT_ANNOTATION); // 判斷返回體是否需要處理 return responseResult != null; } @Override public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { // 異常響應體則直接返回code+message的消息體 if (body instanceof ErrorResult) { return body; } // 正常響應體則返回Result包裝的code+message+data的消息體 return Result.success(body); } }
接口調用
@Api("用戶管理") @RestController @RequestMapping("user") @ResponseResult // 作用於類上,對所有接口有效 public class UserController { @Autowired private UserService userService; @ResponseResult // 作用於方法上 @ApiOperation("根據ID查詢用戶") @GetMapping("one") public User selectOne(Long id) { // 由於在ResponseResultHandler中已經統一將返回數據用Result.success包裝瞭, // 直接返回原始類型即可,代碼更簡潔 return this.userService.queryById(id); } @ResponseResult @ApiOperation("查詢所有用戶") @GetMapping("all") public List<User> selectAll(Page page) { // 由於在ResponseResultHandler中已經統一將返回數據用Result.success包裝瞭, // 直接返回原始類型即可,代碼更簡潔 return this.userService.queryAllByLimit(page); } }
測試結果
全局異常處理
編寫自定義異常基類
@Data public class BaseException extends RuntimeException { private static final int BASE_EXCEPTION_CODE = ResultCode.INTERNAL_ERROR.code(); private static final String BASE_EXCEPTION_MESSAGE = ResultCode.INTERNAL_ERROR.message(); private Integer code; private String message; public BaseException() { super(BASE_EXCEPTION_MESSAGE); this.code = BASE_EXCEPTION_CODE; this.message = BASE_EXCEPTION_MESSAGE; } public BaseException(String message) { super(message); this.code = BASE_EXCEPTION_CODE; this.message = message; } public BaseException(ResultCode resultCode) { super(resultCode.message()); this.code = resultCode.code(); this.message = resultCode.message(); } public BaseException(Throwable cause) { super(cause); this.code = BASE_EXCEPTION_CODE; this.message = BASE_EXCEPTION_MESSAGE; } public BaseException(String message, Throwable cause) { super(message, cause); this.code = BASE_EXCEPTION_CODE; this.message = message; } public BaseException(Integer code, String message) { super(message); this.code = code; this.message = message; } public BaseException(Integer code, String message, Throwable cause) { super(message, cause); this.code = code; this.message = message; } }
編寫自定義業務異常類
public class BizException extends BaseException { public BizException(ResultCode resultCode) { super(resultCode); } }
定義全局異常處理類
通過@ExceptionHandler註解來統一處理某一類異常
@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { /** * 統一處理自定義基礎異常 */ @ExceptionHandler(BaseException.class) public ErrorResult baseException(BaseException e) { if (StringUtils.isEmpty(e.getCode())) { return ErrorResult.error(e.getMessage()); } return ErrorResult.error(e.getCode(), e.getMessage()); } /** * 統一處理自定義業務異常 */ @ExceptionHandler(BizException.class) public ErrorResult bizException(BizException e) { if (StringUtils.isEmpty(e.getCode())) { return ErrorResult.error(e.getMessage()); } return ErrorResult.error(e.getCode(), e.getMessage()); } /** * 統一處理非自定義異常外的所有異常 */ @ExceptionHandler(Exception.class) public ErrorResult handleException(Exception e) { log.error(e.getMessage(), e); return ErrorResult.error(e.getMessage()); } /** * 兼容Validation校驗框架:忽略參數異常處理器 */ @ExceptionHandler(MissingServletRequestParameterException.class) public ApiResult<String> parameterMissingExceptionHandler(MissingServletRequestParameterException e) { log.error(e.getMessage(), e); return ErrorResult.error(PARAMETER_EXCEPTION, "請求參數 " + e.getParameterName() + " 不能為空"); } /** * 兼容Validation校驗框架:缺少請求體異常處理器 */ @ExceptionHandler(HttpMessageNotReadableException.class) public ErrorResult parameterBodyMissingExceptionHandler(HttpMessageNotReadableException e) { log.error(e.getMessage(), e); return ErrorResult.error(PARAMETER_EXCEPTION, "參數體不能為空"); } /** * 兼容Validation校驗框架:參數效驗異常處理器 */ @ExceptionHandler(MethodArgumentNotValidException.class) public ErrorResult parameterExceptionHandler(MethodArgumentNotValidException e) { log.error(e.getMessage(), e); // 獲取異常信息 BindingResult exceptions = e.getBindingResult(); // 判斷異常中是否有錯誤信息,如果存在就使用異常中的消息,否則使用默認消息 if (exceptions.hasErrors()) { List<ObjectError> errors = exceptions.getAllErrors(); if (!errors.isEmpty()) { // 這裡列出瞭全部錯誤參數,按正常邏輯,隻需要第一條錯誤即可 FieldError fieldError = (FieldError) errors.get(0); return ErrorResult.error(PARAMETER_EXCEPTION, fieldError.getDefaultMessage()); } } return ErrorResult.error(PARAMETER_EXCEPTION, "請求參數校驗異常"); } }
接口調用
@ResponseResult @GetMapping public User update() { // 非自定義的運行時異常 long id = 10 / 0; return userService.queryById(id); } @ResponseResult @PostMapping public User insert() { // 拋出自定義的基礎異常 throw new BaseException(); } @ResponseResult @DeleteMapping public boolean delete() { // 拋出自定義的業務異常 throw new BizException(USER_NOT_EXIST_ERROR); }
測試結果
總結
到此這篇關於SpringBoot中如何統一接口返回與全局異常處理的文章就介紹到這瞭,更多相關SpringBoot統一接口返回內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- SpringBoot 統一公共返回類的實現
- Springboot處理異常的常見方式
- SpringBoot實現統一封裝返回前端結果集的示例代碼
- SpringBoot自定義註解開發指南
- Feign調用全局異常處理解決方案