企業實戰之spring專案《介面響應體格式統一封裝》
阿新 • • 發佈:2019-01-22
前言
在之前的文章中我們有介紹過,如何更好、更簡單的寫好一個介面(介面返回值篇),今天的這篇文章我們主要介紹,怎麼統一處理下介面的返回格式問題。
###問題分析
我們先來分析下我們所面臨的問題在哪裡,然後接著給出解決方案。在寫一個介面時,我們通常會先統一定義一下介面的返回格式是什麼,然後在跟前端去對接,通常的返回格式大體兩種(我們以儲存使用者為例):
1. 成功/失敗響應格式不一致(此種方式作為我們預設的介面響應方式)
- 儲存使用者成功,響應體
{
"id": 10000,
"pwd": "123456",
"nickname": "小竹馬",
"img": "http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg" ,
"status": "NORMAL",
"createTime": 1515075974540
}
- 失敗響應體(下面的格式是spring boot預設的錯誤響應格式,只不過我們在其基礎上增加了一個code欄位用於解釋更詳細的錯誤碼)
{
"status": 400,
"error": "Bad Request",
"message": "引數無效",
"code": 10001,
"path": "/zhuma-demo/users",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException" ,
"errors": [
{
"fieldName": "status",
"message": "值是無效的"
}
],
"timestamp": 1515076067369
}
2.成功/失敗響應體格式一致
- 儲存使用者成功,響應體
{
"code": 1,
"msg": "成功",
"data": {
"id": 10000,
"pwd": "123456",
"nickname": "小竹馬",
"img" : "http://avatar.csdn.net/0/E/9/1_aiyaya_.jpg",
"status": "NORMAL",
"createTime": 1515076287882
}
}
- 失敗響應體
{
"code": 10001,
"msg": "引數無效",
"data": [
{
"fieldName": "status",
"message": "值是無效的"
}
]
}
那麼如果我們想要的響應體格式是第二種,我們該如何寫我們的程式碼呢?你可能想是這樣麼?
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PlatformResult addUser(@Validated @RequestBody User user) {
user.setId(10000L);
user.setCreateTime(new Date());
return PlatformResult.success(user);
}
}
PlatformResult.success()這段邏輯顯然很多餘,每個方法都要這樣寫一遍,所以上述方式並不是我們想要的,我們要的是
@ResponseResult(PlatformResult.class)
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public User addUser(@Validated @RequestBody User user) {
user.setId(10000L);
user.setCreateTime(new Date());
return user;
}
}
我們加了一個自定義的註解@ResponseResult(PlatformResult.class),引數PlatformResult.class告訴這個Controller類下的所有方法都以這個類PlatformResult的格式進行返回,這個註解可以標記在類或方法上,好了,我們的目的明朗了許多,要做的就是標記這個註解讓它實現介面返回值格式控制這個功能,下面我們給出具體的實現方式。
實現思路
首先介紹下完成我們這次主要功能的幾個類:
- Result 是返回格式類的父介面(所有返回格式類都需要繼承它)
- PlatformResult 通用返回結果格式(我們上面說的第二種返回結果)
- DefaultErrorResult 全域性錯誤返回結果(我們上面說的第一種錯誤時的返回結果)
- GlobalExceptionHandler全域性異常處理
- ResponseResult 註解類(用於在Controller上指定返回值格式類)
- ResponseResultInterceptor 攔截器(主要用於將ResponseResult註解類的標記資訊傳入ResponseResultHandler中)
- ResponseResultHandler 響應體格式處理器(主要轉換邏輯都在這裡)
程式碼實現
下面將有一大片程式碼襲來,要頂住!O(∩_∩)O哈哈~
1. Result 介面類
package com.zhuma.demo.comm.result;
import java.io.Serializable;
/**
* @desc 響應格式父介面
*
* @author zhumaer
* @since 4/1/2018 3:00 PM
*/
public interface Result extends Serializable {
}
說明
理論上所有的返回格式類都需要實現該接口才能被使用
2. PlatformResult 通用返回結果
package com.zhuma.demo.comm.result;
import com.zhuma.demo.enums.ResultCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @desc 平臺通用返回結果
*
* @author zhumaer
* @since 10/9/2017 3:00 PM
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class PlatformResult implements Result {
private static final long serialVersionUID = 874200365941306385L;
private Integer code;
private String msg;
private Object data;
public static PlatformResult success() {
PlatformResult result = new PlatformResult();
result.setResultCode(ResultCode.SUCCESS);
return result;
}
public static PlatformResult success(Object data) {
PlatformResult result = new PlatformResult();
result.setResultCode(ResultCode.SUCCESS);
result.setData(data);
return result;
}
public static PlatformResult failure(ResultCode resultCode) {
PlatformResult result = new PlatformResult();
result.setResultCode(resultCode);
return result;
}
public static PlatformResult failure(ResultCode resultCode, Object data) {
PlatformResult result = new PlatformResult();
result.setResultCode(resultCode);
result.setData(data);
return result;
}
public static PlatformResult failure(String message) {
PlatformResult result = new PlatformResult();
result.setCode(ResultCode.PARAM_IS_INVALID.code());
result.setMsg(message);
return result;
}
private void setResultCode(ResultCode code) {
this.code = code.code();
this.msg = code.message();
}
}
3. DefaultErrorResult 預設全域性錯誤返回格式
package com.zhuma.demo.comm.result;
import java.util.Date;
import com.zhuma.demo.enums.ExceptionEnum;
import com.zhuma.demo.exception.BusinessException;
import com.zhuma.demo.util.RequestContextHolderUtil;
import com.zhuma.demo.util.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.boot.autoconfigure.web.DefaultErrorAttributes;
import com.zhuma.demo.enums.ResultCode;
import org.springframework.http.HttpStatus;
/**
* @desc 預設全域性錯誤返回結果
* 備註:該返回資訊是spring boot的預設異常時返回結果{@link DefaultErrorAttributes},目前也是我們服務的預設的錯誤返回結果
*
* @author zhumaer
* @since 9/29/2017 3:00 PM
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DefaultErrorResult implements Result {
private static final long serialVersionUID = 1899083570489722793L;
/**
* HTTP響應狀態碼 {@link org.springframework.http.HttpStatus}
*/
private Integer status;
/**
* HTTP響應狀態碼的英文提示
*/
private String error;
/**
* 異常堆疊的精簡資訊
*
*/
private String message;
/**
* 我們系統內部自定義的返回值編碼,{@link ResultCode} 它是對錯誤更加詳細的編碼
*
* 備註:spring boot預設返回異常時,該欄位為null
*/
private Integer code;
/**
* 呼叫介面路徑
*/
private String path;
/**
* 異常的名字
*/
private String exception;
/**
* 異常的錯誤傳遞的資料
*/
private Object errors;
/**
* 時間戳
*/
private Date timestamp;
public static DefaultErrorResult failure(ResultCode resultCode, Throwable e, HttpStatus httpStatus, Object errors) {
DefaultErrorResult result = DefaultErrorResult.failure(resultCode, e, httpStatus);
result.setErrors(errors);
return result;
}
public static DefaultErrorResult failure(ResultCode resultCode, Throwable e, HttpStatus httpStatus) {
DefaultErrorResult result = new DefaultErrorResult();
result.setCode(resultCode.code());
result.setMessage(resultCode.message());
result.setStatus(httpStatus.value());
result.setError(httpStatus.getReasonPhrase());
result.setException(e.getClass().getName());
result.setPath(RequestContextHolderUtil.getRequest().getRequestURI());
result.setTimestamp(new Date());
return result;
}
public static DefaultErrorResult failure(BusinessException e) {
ExceptionEnum ee = ExceptionEnum.getByEClass(e.getClass());
if (ee != null) {
return DefaultErrorResult.failure(ee.getResultCode(), e, ee.getHttpStatus(), e.getData());
}
DefaultErrorResult defaultErrorResult = DefaultErrorResult.failure(e.getResultCode() == null ? ResultCode.SUCCESS : e.getResultCode(), e, HttpStatus.OK, e.getData());
if (StringUtil.isNotEmpty(e.getMessage())) {
defaultErrorResult.setMessage(e.getMessage());
}
return defaultErrorResult;
}
}
4. GlobalExceptionHandler 全域性錯誤異常處理器
package com.zhuma.demo.handler;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import com.zhuma.demo.comm.handler.BaseGlobalExceptionHandler;
import com.zhuma.demo.comm.result.DefaultErrorResult;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
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.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import com.zhuma.demo.exception.BusinessException;
/**
* @desc 統一異常處理器
*
* @author zhumaer
* @since 8/31/2017 3:00 PM
*/
@RestController
@ControllerAdvice
public class GlobalExceptionHandler extends BaseGlobalExceptionHandler {
/* 處理400類異常 */
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
return super.handleConstraintViolationException(e, request);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public DefaultErrorResult handleConstraintViolationException(HttpMessageNotReadableException e, HttpServletRequest request) {
return super.handleConstraintViolationException(e, request);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public DefaultErrorResult handleBindException(BindException e, HttpServletRequest request) {
return super.handleBindException(e, request);
}
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public DefaultErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
return super.handleMethodArgumentNotValidException(e, request);
}
/* 處理自定義異常 */
@ExceptionHandler(BusinessException.class)
public ResponseEntity<DefaultErrorResult> handleBusinessException(BusinessException e, HttpServletRequest request) {
return super.handleBusinessException(e, request);
}
/* 處理執行時異常 */
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(RuntimeException.class)
public DefaultErrorResult handleRuntimeException(RuntimeException e, HttpServletRequest request) {
//TODO 可通過郵件、微信公眾號等方式傳送資訊至開發人員、記錄存檔等操作(這個後面我們文章我們單獨說明該怎麼處理)
return super.handleRuntimeException(e, request);
}
}
BaseGlobalExceptionHandler 全域性異常處理基礎類
package com.zhuma.demo.comm.handler;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import com.zhuma.demo.comm.result.DefaultErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import com.zhuma.demo.comm.result.ParameterInvalidItem;
import com.zhuma.demo.enums.ResultCode;
import com.zhuma.demo.exception.BusinessException;
import com.zhuma.demo.util.ConvertUtil;
/**
* @desc 全域性異常處理基礎類
*
* @author zhumaer
* @since 10/10/2017 9:54 AM
*/
@Slf4j
public class BaseGlobalExceptionHandler {
/**
* 違反約束異常
*/
protected DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.info("handleConstraintViolationException start, uri:{}, caused by: ", request.getRequestURI(), e);
List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertCVSetToParameterInvalidItemList(e.getConstraintViolations());
return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);
}
/**
* 處理驗證引數封裝錯誤時異常
*/
protected DefaultErrorResult handleConstraintViolationException(HttpMessageNotReadableException e, HttpServletRequest request) {
log.info("handleConstraintViolationException start, uri:{}, caused by: ", request.getRequestURI(), e);
return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST);
}
/**
* 處理引數繫結時異常(反400錯誤碼)
*/
protected DefaultErrorResult handleBindException(BindException e, HttpServletRequest request) {
log.info("handleBindException start, uri:{}, caused by: ", request.getRequestURI(), e);
List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertBindingResultToMapParameterInvalidItemList(e.getBindingResult());
return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);
}
/**
* 處理使用@Validated註解時,引數驗證錯誤異常(反400錯誤碼)
*/
protected DefaultErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
log.info("handleMethodArgumentNotValidException start, uri:{}, caused by: ", request.getRequestURI(), e);
List<ParameterInvalidItem> parameterInvalidItemList = ConvertUtil.convertBindingResultToMapParameterInvalidItemList(e.getBindingResult());
return DefaultErrorResult.failure(ResultCode.PARAM_IS_INVALID, e, HttpStatus.BAD_REQUEST, parameterInvalidItemList);
}
/**
* 處理通用自定義業務異常
*/
protected ResponseEntity<DefaultErrorResult> handleBusinessException(BusinessException e, HttpServletRequest request) {
log.info("handleBusinessException start, uri:{}, exception:{}, caused by: {}", request.getRequestURI(), e.getClass(), e.getMessage());
DefaultErrorResult defaultErrorResult = DefaultErrorResult