前言
在之前的文章中我们有介绍过,如何更好、更简单的写好一个接口《接口返回值》,今天的这篇文章我们主要介绍,怎么统一处理下接口的返回格式问题。
问题分析
我们先来分析下我们所面临的问题在哪里,然后接着给出解决方案。在写一个接口时,我们通常会先统一定义一下接口的返回格式是什么,然后在跟前端去对接,通常的返回格式大体两种(我们以保存用户为例):
1.成功/失败响应格式不一致(此种方式作为我们默认的接口响应方式)
保存用户成功,响应体:
{
"id": 10000,
"pwd": "123123",
"nickname": "nickname1",
"img": "http://xxx.com/1.png",
"status": "NORMAL",
"createTime": 1517762718278
}
失败响应体(下面的格式是 spring boot 默认的错误响应格式,只不过我们在其基础上增加了一个 code 字段用于解释更详细的错误码)
{
"status": 400,
"error": "Bad Request",
"message": "参数无效",
"code": 10001,
"path": "/user",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"fieldName": "status",
"message": "值是无效的"
}
],
"timestamp": 1515076067369
}
2.成功/失败响应体格式一致
保存用户成功,响应体:
{
"code": 1,
"msg": "成功",
"data": {
"id": 10000,
"pwd": "123123",
"nickname": "nickname1",
"img": "http://xxx.com/1.png",
"status": "NORMAL",
"createTime": 1515076287882
}
}
失败响应体:
{
"code": 10001,
"msg": "参数无效",
"data": [
{
"fieldName": "status",
"message": "值是无效的"
}
]
}
那么如果我们想要的响应体格式是第二种,我们该如何写我们的代码呢?你可能想是这样么?
@RestController
@RequestMapping("/user")
public class UserController {
@PostMapping
@ResponseStatus(HttpStatus.CREATED)
public PlatformResult addUser(@Validated @RequestBody User user) {
user.setId("10000");
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("10000");
user.setCreateTime(new Date());
return user;
}
}
我们加了一个自定义的注解 @ResponseResult(PlatformResult.class)
,参数 PlatformResult.class 告诉这个 Controller 类下的所有方法都以这个类 PlatformResult 的格式进行返回,这个注解可以标记在类或方法上,好了,我们的目的明朗了许多,要做的就是标记这个注解让它实现接口返回值格式控制这个功能,下面我们给出具体的实现方式。
实现思路
首先介绍下完成我们这次主要功能的几个类:
Result
是返回格式类的父接口(所有返回格式类都需要继承它)
PlatformResult
通用返回结果格式(我们上面说的第二种返回结果)
DefaultErrorResult 全局错误返回结果(我们上面说的第一种错误时的返回结果)
GlobalExceptionHandler
全局异常处理
ResponseResult
注解类(用于在Controller上指定返回值格式类)
ResponseResultInterceptor
拦截器(主要用于将 ResponseResult 注解类的标记信息传入 ResponseResultHandler 中)
ResponseResultHandler
响应体格式处理器(主要转换逻辑都在这里)
代码实现
下面将有一大片代码袭来,要顶住!哈哈~
Result 接口类
package cn.notemi.demo.result;
import java.io.Serializable;
/**
* @desc 响应格式父接口
*/
public interface Result extends Serializable {
}
说明
理论上所有的返回格式类都需要实现该接口才能被使用
PlatformResult 通用返回结果
package cn.notemi.demo.result;
import cn.notemi.demo.enums.ResultCode;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @desc 平台通用返回结果
*/
@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();
}
}
DefaultErrorResult 默认全局错误返回格式
package cn.notemi.demo.result;
import cn.notemi.demo.enums.BusinessExceptionEnum;
import cn.notemi.demo.enums.ResultCode;
import cn.notemi.demo.exceptions.BusinessException;
import cn.notemi.demo.util.RequestContextUtil;
import cn.notemi.demo.util.StringUtil;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.HttpStatus;
import java.util.Date;
/**
* @desc 默认全局错误返回结果
* 备注:该返回信息是spring boot的默认异常时返回结果,目前也是我们服务的默认返回结果
*/
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
public class DefaultErrorResult implements Result {
private static final long serialVersionUID = 1899083570489722793L;
/**
* HTTP响应状态码 {@link 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());
String path = RequestContextUtil.getRequest().getRequestURI();
result.setPath(path);
result.setTimestamp(new Date());
return result;
}
public static DefaultErrorResult failure(BusinessException e) {
BusinessExceptionEnum ee = BusinessExceptionEnum.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;
}
}
GlobalExceptionHandler 全局错误异常处理器
package cn.notemi.demo.handler;
import cn.notemi.demo.exceptions.BusinessException;
import cn.notemi.demo.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 javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
/**
* @desc 统一异常处理器
*/
@RestController
@ControllerAdvice
public class GlobalExceptionHandler extends BaseGlobalExceptionHandler {
/** 处理400类异常 */
@Override
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
return super.handleConstraintViolationException(e, request);
}
@Override
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(HttpMessageNotReadableException.class)
public DefaultErrorResult handleConstraintViolationException(HttpMessageNotReadableException e, HttpServletRequest request) {
return super.handleConstraintViolationException(e, request);
}
@Override
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(BindException.class)
public DefaultErrorResult handleBindException(BindException e, HttpServletRequest request) {
return super.handleBindException(e, request);
}
@Override
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public DefaultErrorResult handleMethodArgumentNotValidException(MethodArgumentNotValidException e, HttpServletRequest request) {
return super.handleMethodArgumentNotValidException(e, request);
}
/** 处理自定义异常 */
@Override
@ExceptionHandler(BusinessException.class)
public ResponseEntity<DefaultErrorResult> handleBusinessException(BusinessException e, HttpServletRequest request) {
return super.handleBusinessException(e, request);
}
/** 处理运行时异常 */
@Override
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Throwable.class)
public DefaultErrorResult handleThrowable(Throwable e, HttpServletRequest request) {
//TODO 可通过邮件、微信公众号等方式发送信息至开发人员、记录存档等操作
return super.handleThrowable(e, request);
}
}
BaseGlobalExceptionHandler 全局异常处理基础类
package cn.notemi.demo.handler;
import cn.notemi.demo.enums.ResultCode;
import cn.notemi.demo.exceptions.BusinessException;
import cn.notemi.demo.helper.ParameterInvalidItemHelper;
import cn.notemi.demo.model.bo.ParameterInvalidItem;
import cn.notemi.demo.result.DefaultErrorResult;
import lombok.extern.slf4j.Slf4j;
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 javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolationException;
import java.util.List;
/**
* @desc 全局异常处理基础类
*/
@Slf4j
public abstract class BaseGlobalExceptionHandler {
/**
* 违反约束异常
*/
protected DefaultErrorResult handleConstraintViolationException(ConstraintViolationException e, HttpServletRequest request) {
log.info("handleConstraintViolationException start, uri:{}, caused by: ", request.getRequestURI(), e);
List<ParameterInvalidItem> parameterInvalidItemList = ParameterInvalidItemHelper.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 = ParameterInvalidItemHelper.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 = ParameterInvalidItemHelper.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.failure(e);
return ResponseEntity
.status(HttpStatus.valueOf(defaultErrorResult.getStatus()))
.body(defaultErrorResult);
}
/**
* 处理未预测到的其他错误(反500错误码)
*/
protected DefaultErrorResult handleThrowable(Throwable e, HttpServletRequest request) {
log.error("handleThrowable start, uri:{}, caused by: ", request.getRequestURI(), e);
return DefaultErrorResult.failure(ResultCode.SYSTEM_INNER_ERROR, e, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
说明
上面用到了一些自定义异常基类(BusinessException),对于这个类它会有很多子类去继承它,例如:
参数异常、数据已存在异常、无权限异常等等,这些类大家可以根据自己的业务扩展
ResponseResult 注解类
package cn.notemi.demo.annotations;
import cn.notemi.demo.result.PlatformResult;
import cn.notemi.demo.result.Result;
import java.lang.annotation.*;
/**
* @desc 接口返回结果增强 会通过拦截器拦截后放入标记,在WebResponseBodyHandler进行结果处理
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResponseResult {
Class<? extends Result> value() default PlatformResult.class;
}
说明
这里我们默认 PlatformResult 使用这个类作为返回格式,所以@ResponseResult 等价于@ResponseResult(PlatformResult.class)
ResponseResultInterceptor 拦截器
package cn.notemi.demo.intercepter;
import cn.notemi.demo.annotations.ResponseResult;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* @desc 接口响应体控制拦截器
*/
public class ResponseResultInterceptor implements HandlerInterceptor {
public static final String RESPONSE_RESULT = "RESPONSE-RESULT";
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod) {
final HandlerMethod handlerMethod = (HandlerMethod) handler;
final Class<?> clazz = handlerMethod.getBeanType();
final Method method = handlerMethod.getMethod();
if (clazz.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT, clazz.getAnnotation(ResponseResult.class));
} else if (method.isAnnotationPresent(ResponseResult.class)) {
request.setAttribute(RESPONSE_RESULT, method.getAnnotation(ResponseResult.class));
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
// nothing to do
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// nothing to do
}
}
开启拦截器配置
package cn.notemi.demo.config;
import cn.notemi.demo.intercepter.HeaderParamsCheckInterceptor;
import cn.notemi.demo.intercepter.LoginAuthInterceptor;
import cn.notemi.demo.intercepter.ResponseResultInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class InterceptorConfig extends WebMvcConfigurerAdapter {
@Bean
public ResponseResultInterceptor responseResultInterceptor() {
return new ResponseResultInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
//响应结果控制拦截
registry.addInterceptor(responseResultInterceptor()).addPathPatterns("/**");
}
}
ResponseResultHandler 响应体格式处理器
package cn.notemi.demo.handler;
import cn.notemi.demo.annotations.ResponseResult;
import cn.notemi.demo.constant.HeaderConstants;
import cn.notemi.demo.enums.ApiStyleEnum;
import cn.notemi.demo.intercepter.ResponseResultInterceptor;
import cn.notemi.demo.result.DefaultErrorResult;
import cn.notemi.demo.result.PlatformResult;
import cn.notemi.demo.result.Result;
import cn.notemi.demo.util.JsonUtil;
import cn.notemi.demo.util.RequestContextUtil;
import org.springframework.core.MethodParameter;
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.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import javax.servlet.http.HttpServletRequest;
/**
* @desc 接口响应体处理器
*/
@ControllerAdvice
public class ResponseResultHandler implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
HttpServletRequest request = RequestContextUtil.getRequest();
ResponseResult responseResultAnn = (ResponseResult) request.getAttribute(ResponseResultInterceptor.RESPONSE_RESULT);
return responseResultAnn != null && !ApiStyleEnum.NONE.name().equalsIgnoreCase(request.getHeader(HeaderConstants.API_STYLE));
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
ResponseResult responseResultAnn = (ResponseResult) RequestContextUtil.getRequest().getAttribute(ResponseResultInterceptor.RESPONSE_RESULT);
Class<? extends Result> resultClazz = responseResultAnn.value();
if (resultClazz.isAssignableFrom(PlatformResult.class)) {
if (body instanceof DefaultErrorResult) {
DefaultErrorResult defaultErrorResult = (DefaultErrorResult) body;
return PlatformResult.builder()
.code(defaultErrorResult.getCode())
.msg(defaultErrorResult.getMessage())
.data(defaultErrorResult.getErrors())
.build();
} else if (body instanceof String) {
return JsonUtil.object2Json(PlatformResult.success(body));
}
return PlatformResult.success(body);
}
return body;
}
}
说明
上述代码用到了一个工具类(RequestContextHolderUtil)用于获取request对象,如果感兴趣可以看下这篇文章 工具类分享之《RequestContextHolderUtil》
@ControllerAdvice、ResponseBodyAdvice 这两个类是本功能的关键使用类,用于接口的响应体增强,其中 supports 方法用于判断是否需要做增强转化,beforeBodyWrite 方法用于增加逻辑实现
supports 方法中,加了个小功能,当调用人员不想要封装结果时,可以在 header 上设置参数 Api-Style=none
测试
package cn.notemi.demo.controller;
import cn.notemi.demo.annotations.LoginAuth;
import cn.notemi.demo.annotations.ResponseResult;
import cn.notemi.demo.model.bo.LoginUser;
import cn.notemi.demo.model.po.User;
import cn.notemi.demo.model.qo.LoginQO;
import cn.notemi.demo.model.vo.LoginVO;
import cn.notemi.demo.repository.UserRepository;
import cn.notemi.demo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* Title:UserController
**/
@RestController
@RequestMapping("/api/user")
@ResponseResult
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping("/all")
public List<User> users() {
return userRepository.findAll();
}
}
结束语
接口响应体统一格式转化,这个功能就完成了,这样的封装可以少些很多重复且无用的代码,何乐而不为呢?
GitHub地址:https://github.com/FlickerMi/notemi-demo
Scott he 2024-04-25 10:40
有个问题,结合 swagger 的话,好像不能生成实际的接口信息,因为最终信封体的封装是在 responsebodyadvice 里面,springdoc 无法知道
青山 2023-09-01 22:36
很认真看完了博主的文章,GitHub代码也看了,很不错,文章写的也很认真,但是提个小小的建议哈,封装太过严重了,统一状态码,统一响应格式,统一异常不用这么复杂的,而且有很多东西在spring boot中就已经实现了,直接拿来用就行的
Flicker博主 2023-09-12 17:09
谢谢建议,现在确实有更优雅的做法了。
Scott he 2024-04-25 10:36
请问现在有什么更优雅的方式?
grow 2020-12-23 16:56
学习了,很详细,谢谢. 受益匪浅
新手村程序员 2020-11-02 16:52
有没有数据库,发一份呀
柒月君 2020-06-02 21:55
看完受益匪浅,我现在自己写东西就想遵守restful和写出封装比较多的 简短代码。这次可以继续改造我得代码了。
我就是所有都返回一样的东西。写的有点累
Flicker博主 2020-06-05 18:32
有重复的代码,能抽出来就抽吧。
柒月君 2020-06-02 21:00
刚刚看到@Builder 我从来没用过,刚刚百度了下,
发现别人介绍 @Builder 是会生成全参数的构造方法的。
所以@AllArgsConstructor是不是就不用写了?
Lombok 我只用get,set,data,顶多有个链式结构,类似builder吧
Flicker博主 2020-06-05 18:31
@AllArgsConstructor和@Builder还是有点不一样的,前者是生成全参数的构造方法,后者其实就是实现的建造者模式,也可以很方便的构建对象,二者根据情况使用吧。
柒月君 2020-06-06 10:33
不加AllArgsConstructor 编译不通过,
我搜的文章是说@Builder实现建造者模式并且声称全参数的构造方法。诶
Flicker博主 2020-06-06 14:34
我刚刚看了编译后的class,@Builder确实会生成全参数的构造函数,不过该构造函数访问修饰符为default(friendly),只能当前包能访问。
柒月君 2020-06-02 20:51
第六个代码块
user.setId("10000);缺个分号
Flicker博主 2020-06-05 18:31
谢谢指正。