1. 程式人生 > >企業實戰之spring專案《介面響應體格式統一封裝》

企業實戰之spring專案《介面響應體格式統一封裝》

前言

在之前的文章中我們有介紹過,如何更好、更簡單的寫好一個介面(介面返回值篇),今天的這篇文章我們主要介紹,怎麼統一處理下介面的返回格式問題。

###問題分析
我們先來分析下我們所面臨的問題在哪裡,然後接著給出解決方案。在寫一個介面時,我們通常會先統一定義一下介面的返回格式是什麼,然後在跟前端去對接,通常的返回格式大體兩種(我們以儲存使用者為例):

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的格式進行返回,這個註解可以標記在類或方法上,好了,我們的目的明朗了許多,要做的就是標記這個註解讓它實現介面返回值格式控制這個功能,下面我們給出具體的實現方式。

實現思路

首先介紹下完成我們這次主要功能的幾個類:

  1. Result 是返回格式類的父介面(所有返回格式類都需要繼承它)
  2. PlatformResult 通用返回結果格式(我們上面說的第二種返回結果)
  3. DefaultErrorResult 全域性錯誤返回結果(我們上面說的第一種錯誤時的返回結果)
  4. GlobalExceptionHandler全域性異常處理
  5. ResponseResult 註解類(用於在Controller上指定返回值格式類)
  6. ResponseResultInterceptor 攔截器(主要用於將ResponseResult註解類的標記資訊傳入ResponseResultHandler中)
  7. 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