一、簡介
基於Spring Boot 框架開發的應用程序,大部分都是以提供RESTful接口為主要的目的。前端或者移動端開發人員通過調用后端提供的接口完成數據的交換。
在一個項目中RESTful接口響應數據結構是統一的是基本的開發規范。能夠減少團隊內部不必要的溝通;減輕接口消費者校驗數據的負擔;降低其他同事接手代碼的難度;提高接口的健壯性和可擴展性。
public class GlobalResponseEntity<T>{
private Boolean success = true;
private String code = "000000";
private String message = "request successfully";
private T data;
}
統一的異常處理,是系統完備性的基本象征。通過對全局異常信息的捕獲,能夠避免將異常信息和系統敏感信息直接拋給客戶端;針對特定類型異常捕獲之后可以重新對輸出數據做編排,提高交互友好度,同時可以記錄異常信息以便監控和分析。
二、如何實現
運用RestControllerAdvice或者ControllerAdvice注解實現。(ControllerAdvice是RestControllerAdvice的爸爸)
@ControllerAdvice是在類上聲明的注解,其用法主要有三點:
-
和@ExceptionHandler注解配合使用,@ExceptionHandler標注的方法可以捕獲Controller中拋出的的異常,從而達到異常統一處理的目的
-
和@InitBinder注解配合使用,@InitBinder標注的方法可在請求中注冊自定義參數的解析器,從而達到自定義請求參數格式化的目的
-
和@ModelAttribute注解配合使用,@ModelAttribute標注的方法會在執行目標Controller方法之前執行,可在入參上增加自定義信息
用法舉例:
// 這里@RestControllerAdvice等同於@ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
// 這里@ModelAttribute("loginUserInfo")標注的modelAttribute()方法表示會在Controller方法之前
// 執行,返回當前登錄用戶的UserDetails對象
@ModelAttribute("loginUserInfo")
public UserDetails modelAttribute() {
return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
// @InitBinder標注的initBinder()方法表示注冊一個Date類型的類型轉換器,用於將類似這樣的2019-06-10
// 日期格式的字符串轉換成Date對象
@InitBinder
protected void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// 這里表示Controller拋出的MethodArgumentNotValidException異常由這個方法處理
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result exceptionHandler(MethodArgumentNotValidException e) {
Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
logger.error("req params error", e);
return result;
}
// 這里表示Controller拋出的BizException異常由這個方法處理
@ExceptionHandler(BizException.class)
public Result exceptionHandler(BizException e) {
BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
logger.error("business error", e);
return result;
}
// 這里就是通用的異常處理器了,所有預料之外的Exception異常都由這里處理
@ExceptionHandler(Exception.class)
public Result exceptionHandler(Exception e) {
Result result = new Result(1000, "網絡繁忙,請稍后再試");
logger.error("application error", e);
return result;
}
}
在Controller里取出@ModelAttribute標注的方法返回的UserDetails對象:
RestController
@RequestMapping("/json/exam")
@Validated
public class ExamController {
@Autowired
private IExamService examService;
// ......
@PostMapping("/getExamListByOpInfo")
public Result<List<GetExamListResVo>> getExamListByOpInfo( @NotNull Date examOpDate,
@ModelAttribute("loginUserInfo") UserDetails userDetails) {
List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
Result<List<GetExamListResVo>> result = new Result(resVos);
return result;
}
}
當入參為examOpDate=2019-06-10時,Spring會使用我們上面@InitBinder注冊的類型轉換器將2019-06-10轉換examOpDate對象:
@PostMapping("/getExamListByOpInfo")
public Result<List<GetExamListResVo>> getExamListByOpInfo(@NotNull Date examOpDate,
@ModelAttribute("loginUserInfo") UserDetails userDetails) {
List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
Result<List<GetExamListResVo>> result = new Result(resVos);
return result;
}
@ExceptionHandler標注的多個方法分別表示只處理特定的異常。這里需要注意的是當Controller拋出的某個異常多個@ExceptionHandler標注的方法都適用時,Spring會選擇最具體的異常處理方法來處理,也就是說@ExceptionHandler(Exception.class)這里標注的方法優先級最低,只有當其它方法都不適用時,才會來到這里處理。
三、統一的響應處理
工程目錄結構如下:

GlobalResponse是一個處理器類(handle),用來處理統一響應,代碼如下:
package com.naylor.globalresponsebody.handler.response;
import com.alibaba.fastjson.JSON;
import com.naylor.globalresponsebody.handler.GlobalResponseEntity;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.Resource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @BelongsProject: debris-app
* @BelongsPackage: com.naylor.globalresponsebody.response
* @Author: Chenml
* @CreateTime: 2020-09-02 15:26
* @Description: 全局響應
*/
@RestControllerAdvice("com.naylor")
public class GlobalResponse implements ResponseBodyAdvice<Object> {
/**
* 攔截之前業務處理,請求先到supports再到beforeBodyWrite
* <p>
* 用法1:自定義是否攔截。若方法名稱(或者其他維度的信息)在指定的常量范圍之內,則不攔截。
*
* @param methodParameter
* @param aClass
* @return 返回true會執行攔截;返回false不執行攔截
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
//TODO 過濾
return true;
}
/**
* 向客戶端返回響應信息之前的業務邏輯處理
* <p>
* 用法1:無論controller返回什么類型的數據,在寫入客戶端響應之前統一包裝,客戶端永遠接收到的是約定的格式
* <p>
* 用法2:在寫入客戶端響應之前統一加密
*
* @param responseObject 響應內容
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return
*/
@Override
public Object beforeBodyWrite(Object responseObject, MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
//responseObject是否為null
if (null == responseObject) {
return new GlobalResponseEntity<>("55555", "response is empty.");
}
//responseObject是否是文件
if (responseObject instanceof Resource) {
return responseObject;
}
//該方法返回值類型是否是void
//if ("void".equals(methodParameter.getParameterType().getName())) {
// return new GlobalResponseEntity<>("55555", "response is empty.");
//}
if (methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)) {
return new GlobalResponseEntity<>("55555", "response is empty.");
}
//該方法返回值類型是否是GlobalResponseEntity。若是直接返回,無需再包裝一層
if (responseObject instanceof GlobalResponseEntity) {
return responseObject;
}
//處理string類型的返回值
//當返回類型是String時,用的是StringHttpMessageConverter轉換器,無法轉換為Json格式
//必須在方法體上標注RequestMapping(produces = "application/json; charset=UTF-8")
if (responseObject instanceof String) {
String responseString = JSON.toJSONString(new GlobalResponseEntity<>(responseObject));
return responseString;
}
//該方法返回的媒體類型是否是application/json。若不是,直接返回響應內容
if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
return responseObject;
}
return new GlobalResponseEntity<>(responseObject);
}
}
-
GlobalResponse類需要實現ResponseBodyAdvice接口
-
重寫supports方法,可對響應進行過濾。實際開發中不一定所有的方法返回值都是相同的模板,這里可以根據MethodParameter進行過濾,此方法返回true則會走過濾,即會調用beforeBodyWrite方法,否則不會調用。
-
重寫beforeBodyWrite方法,編寫具體的響應數據邏輯
GlobalResponseEntity是一個實體類,用來封裝統一響應和統一異常處理的返回值模板,具體代碼如下:
@Data
@Accessors(chain = true)
public class GlobalResponseEntity<T> implements Serializable {
private Boolean success = true;
private String code = "000000";
private String message = "request successfully";
private T data;
public GlobalResponseEntity() {
super();
}
public GlobalResponseEntity(T data) {
this.data = data;
}
public GlobalResponseEntity(String code, String message) {
this.code = code;
this.message = message;
this.data = null;
}
public GlobalResponseEntity(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public GlobalResponseEntity(Boolean success, String code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
public GlobalResponseEntity(Boolean success, String code, String message, T data) {
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
public static GlobalResponseEntity<?> badRequest(String code, String message) {
return new GlobalResponseEntity<>(false, code, message);
}
public static GlobalResponseEntity<?> badRequest() {
return new GlobalResponseEntity<>(false, "404", "無法找到您請求的資源");
}
}
- GlobalResponseEntity類為一個泛型類,T為接口具體的返回數據
- success表示接口響應是否成功,更多情況下這個是業務叫法,和http狀態碼關系不大
- code表示接口響應狀態碼,可以根據特定業務場景自己定義
- message是描述信息
- 實際開發中code和mesage的具體值可以用枚舉來維護
四、統一的異常處理
新增GlobalException類,編寫統一異常處理。類上面添加
@RestControllerAdvice("com.naylor")和
@ResponseBody注解,ResponseBody用來對響應內容進行編排,如http狀態碼。代碼如下:
@RestControllerAdvice("com.naylor")
@ResponseBody
@Slf4j
public class GlobalException {
/**
* 捕獲一般異常
* 捕獲未知異常
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception e) {
return new ResponseEntity<>(
new GlobalResponseEntity<>(false, "555",
e.getMessage() == null ? "未知異常" : e.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 處理404異常
*
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException e) {
return new ResponseEntity<>(
new GlobalResponseEntity(false, "4040",
e.getMessage() == null ? "請求的資源不存在" : e.getMessage()),
HttpStatus.NOT_FOUND);
}
/**
* 捕獲運行時異常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleRuntimeException(RuntimeException e) {
log.error("handleRuntimeException:", e);
return new ResponseEntity<>(
new GlobalResponseEntity(false, "rt555",
e.getMessage() == null ? "運行時異常" : e.getMessage().replace("java.lang.RuntimeException: ", "")),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 捕獲業務異常
* 捕獲自定義異常
*
* @param e
* @return
*/
@ExceptionHandler(BizServiceException.class)
public ResponseEntity<Object> handleBizServiceException(BizServiceException e) {
return new ResponseEntity<>(
new GlobalResponseEntity(false, e.getErrorCode(), e.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 捕獲參數校驗異常
* javax.validation.constraints
*
* @param e
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String msg = "參數校驗失敗";
List<FieldFailedValidate> fieldFailedValidates = this.extractFailedMessage(e.getBindingResult().getFieldErrors());
if (null != fieldFailedValidates && fieldFailedValidates.size() > 0) {
msg = fieldFailedValidates.get(0).getMessage();
}
return new ResponseEntity<>(
new GlobalResponseEntity<>(false, "arg555", msg, null),
HttpStatus.BAD_REQUEST);
}
/**
* 組裝validate錯誤信息
*
* @param fieldErrors
* @return
*/
private List<FieldFailedValidate> extractFailedMessage(List<FieldError> fieldErrors) {
List<FieldFailedValidate> fieldFailedValidates = new ArrayList<>();
if (null != fieldErrors && fieldErrors.size() > 0) {
FieldFailedValidate fieldFailedValidate = null;
for (FieldError fieldError : fieldErrors) {
fieldFailedValidate = new FieldFailedValidate();
fieldFailedValidate.setMessage(fieldError.getDefaultMessage());
fieldFailedValidate.setName(fieldError.getField());
fieldFailedValidates.add(fieldFailedValidate);
}
}
return fieldFailedValidates;
}
}
引用
@RestControllerAdvice詳解: https://zhuanlan.zhihu.com/p/73087879
@ResponseBodyAdvice詳解:https://my.oschina.net/diamondfsd/blog/3069546/print
