一、簡介
基於Spring Boot 框架開發的應用程序,大部分都是以提供RESTful接口為主要的目的。前端或者移動端開發人員通過調用后端提供的RESTful接口完成數據的交換。
統一的RESTful接口響應數據結構是基本的開發規范。能夠減少團隊內部不必要的溝通;減輕接口消費者校驗數據的負擔;降低其他同事接手代碼的難度;提高接口的健壯性和可擴展性。
常見的統一響應數據結構如下所示:
public class GlobalResponseEntity<T>{
private Boolean success = true;
private String code = "000000";
private String message = "request successfully";
private T data;
}
統一的異常處理,是系統完備性的基本象征。通過對全局異常信息的捕獲,能夠避免將異常信息和系統敏感信息直接拋給客戶端;針對特定類型異常捕獲之后可以重新對輸出數據做編排,提高交互友好度,同時可以記錄異常信息以便監控和分析。
一般,在統一異常處理處會手動修改返回給客戶端的http狀態碼,並編排響應給客戶端的數據結構為GlobalResponseEntity,保證始終統一響應。
二、如何實現
使用RestControllerAdvice注解(或者ControllerAdvice注解)結合ResponseBodyAdvice接口
RestControllerAdvice注解導入了ControllerAdvice注解
@ControllerAdvice是在類上聲明的注解,其用法主要有三點:
-
和@ExceptionHandler注解配合使用,@ExceptionHandler標注的方法可以捕獲Controller中拋出的的異常,從而達到異常統一處理的目的
-
和@InitBinder注解配合使用,@InitBinder標注的方法可在請求中注冊自定義參數的解析器,從而達到自定義請求參數格式化的目的
-
和@ModelAttribute注解配合使用,@ModelAttribute標注的方法會在執行目標Controller方法之前執行,可在入參上增加自定義信息
實現ResponseBodyAdvice接口來自定義響應給前端的內容
用法舉例:
// 這里@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對象。(這里只是用UserDetails舉例,實際開發過程中,建議按照spring security的做法,將用戶信息存放到spring上下文中,然后在controller層進行消費)
當入參為examOpDate=2019-06-10時,Spring會使用我們上面@InitBinder注冊的時間類型轉換器將2019-06-10轉換examOpDate對象
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;
}
}
@ExceptionHandler標注的多個方法分別表示只處理特定的異常。這里需要注意的是當Controller拋出的某個異常多個@ExceptionHandler標注的方法都適用時,Spring會選擇最具體的異常處理方法來處理,也就是說@ExceptionHandler(Exception.class)這個標注的方法優先級最低,只有當其它方法都不適用時,才會來到這里處理。
這里僅列舉了RestControllerAdvice簡單用法,為了學習RestControllerAdvice注解使用
三、統一的響應處理
工程目錄結構如下:
GlobalResponse是一個處理器類(handle),用來處理統一響應。
-
GlobalResponse類需要實現ResponseBodyAdvice接口
-
重寫supports方法,可對響應進行過濾。實際開發中不一定所有的方法返回值都是相同的模板,這里可以根據MethodParameter進行過濾,此方法返回true則會走過濾,即會調用beforeBodyWrite方法,否則不會調用。
-
重寫beforeBodyWrite方法,編寫具體的響應數據邏輯
代碼如下:
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);
}
}
GlobalResponseEntity是一個實體類,用來封裝統一響應和統一異常處理的返回值模板
- GlobalResponseEntity類為一個泛型類,T為接口具體的返回數據
- success表示接口響應是否成功,一般的,這個是業務叫法,和http狀態碼無關
- code表示接口響應狀態碼,可以根據特定業務場景自己定義
- message是描述信息
- 實際開發中code和mesage的具體值可以用枚舉來維護
具體代碼如下:
@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", "無法找到您請求的資源");
}
}
四、統一的異常處理
新增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;
}
}
五、解決無法捕獲404異常的問題
五、解決因增加了ResponseBodyAdvice導致Swagger2-UI無法訪問的問題
報錯提示:
Unable to infer base url.
This is common when using dynamic servlet registration or when the API is behind an API Gateway.
The base url is the root of where all the swagger resources are served.
For e.g. if the api is available at http://example.org/api/v2/api-docs
then the base url is http://example.org/api/. Please enter the location manually:
原因:swagger相當於是寄宿在應用程序中的一個web服務,統一響應處理器攔截了應用所有的響應,對swagger-ui的響應產生了影響。
解決方案:修改統一響應處理器攔截的范圍,配置包路徑。
@RestControllerAdvice(value={"com.naylor","org.spring"})
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
//......
}
六、解決RestControllerAdvice優先級問題
若在項目中寫了好幾個處理器類,都添加了@RestControllerAdvice的注解,由於加載存在先后順序,可能會導致部分攔截器沒有按照既定的方式工作,甚至出現一些奇奇怪怪的問題,此時可以在標注了RestControllerAdvice的類上增加@Order注解,來指定加載順序。
引用
@RestControllerAdvice詳解: https://zhuanlan.zhihu.com/p/73087879
@ResponseBodyAdvice詳解:https://my.oschina.net/diamondfsd/blog/3069546/print