详解SpringBoot统一响应体解决方案

作者:Null 时间:2023-03-08 08:54:13 

前言

最近在优化自己之前基于Spring AOP的统一响应体的实现方案。

什么是统一响应体呢?在目前的前后端分离架构下,后端主要是一个RESTful API的数据接口。

但是HTTP的状态码数量有限,而随着业务的增长,HTTP状态码无法很好地表示业务中遇到的异常情况。

那么可以通过修改响应返回的JSON数据,让其带上一些固有的字段,例如以下这样的


{
"code": 10000,
"msg": "success",
"data": {
 "id": 2,
 "name": "test"
}
}

其中关键属性的用途如下:

  • code为返回结果的状态码

  • msg为返回结果的消息

  • data为返回的业务数据

这3个属性为固有属性,每次响应结果都会有带有它们。

需求

希望实现一个能够代替基于AOP的实现方案,需要满足以下几点:

  1. 原有的基于AOP的实现方案需要Controller的返回类型为Object,需要新方案不限制返回类型

  2. 原有的基于AOP的实现方案需要通过切面表达式+注解控制切点的Controller(注解的包名修改会导致切面表达式的修改,即需要修改两处地方),需要新方案能够基于注解,而不需要修改切面表达式

方案思路

基于上述的需求,选择使用Spring的Controller增强机制,其中关键的类为以下3个:

  • @ControllerAdvice:类注解,用于指定Controller增强处理器类。

  • ResponseBodyAdvice:接口,实现后beforeBodyWrite()方法后可以对响应的body进行修改,需要结合@ControllerAdvice使用。

  • @ExceptionHandler:方法注解,用于指定异常处理方法,需要结合@ControllerAdvice和@ResponseBody使用。

示例关键代码

本示例使用的Spring Boot版本为2.1.6.RELEASE,同时需要开发工具安装lombok插件

引入依赖


<dependencies>
 <!--web-starter-->
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
 </dependency>

<!--lombok-->
 <dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
  <optional>true</optional>
 </dependency>

<!--test-starter-->
 <dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
 </dependency>
</dependencies>

统一响应体

Controller增强后统一响应体对应的对象


import lombok.AllArgsConstructor;
import lombok.Data;

import java.io.Serializable;

/**
* 统一的公共响应体
* @author NULL
* @date 2019-07-16
*/
@Data
@AllArgsConstructor
public class ResponseResult implements Serializable {
/**
 * 返回状态码
 */
private Integer code;
/**
 * 返回信息
 */
private String msg;
/**
 * 数据
 */
private Object data;

}

统一响应注解

统一响应注解是一个标记是否开启统一响应增强的注解


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* 统一响应注解<br/>
* 添加注解后,统一响应体才能生效
* @author NULL
* @date 2019-07-16
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface BaseResponse {

}

状态码枚举

统一响应体中返回的状态码code和状态信息msg对应的枚举类


/**
* 返回状态码
*
* @author NULL
* @date 2019-07-16
*/
public enum ResponseCode {
/**
 * 成功返回的状态码
 */
SUCCESS(10000, "success"),
/**
 * 资源不存在的状态码
 */
RESOURCES_NOT_EXIST(10001, "资源不存在"),
/**
 * 所有无法识别的异常默认的返回状态码
 */
SERVICE_ERROR(50000, "服务器异常");
/**
 * 状态码
 */
private int code;
/**
 * 返回信息
 */
private String msg;

ResponseCode(int code, String msg) {
 this.code = code;
 this.msg = msg;
}

public int getCode() {
 return code;
}

public String getMsg() {
 return msg;
}
}

业务异常类

业务异常类是用于识别业务相关的异常,需要注意这个异常类强制需要以ResponseCode作为构造方法入参,这样可以通过捕获异常获得返回的状态码信息


import com.rjh.web.response.ResponseCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
* 业务异常类,继承运行时异常,确保事务正常回滚
*
* @author NULL
* @since 2019-07-16
*/
@Data
@EqualsAndHashCode(callSuper = false)
public class BaseException extends RuntimeException{

private ResponseCode code;

public BaseException(ResponseCode code) {
 this.code = code;
}

public BaseException(Throwable cause, ResponseCode code) {
 super(cause);
 this.code = code;
}
}

异常处理类

用于处理Controller运行时未捕获的异常的处理类。


import com.rjh.web.exception.BaseException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

/**
* 异常处理器
*
* @author NULL
* @since 2019-07-16
*/
@ControllerAdvice(annotations = BaseResponse.class)
@ResponseBody
@Slf4j
public class ExceptionHandlerAdvice {
/**
 * 处理未捕获的Exception
 * @param e 异常
 * @return 统一响应体
 */
@ExceptionHandler(Exception.class)
public ResponseResult handleException(Exception e){
 log.error(e.getMessage(),e);
 return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
}

/**
 * 处理未捕获的RuntimeException
 * @param e 运行时异常
 * @return 统一响应体
 */
@ExceptionHandler(RuntimeException.class)
public ResponseResult handleRuntimeException(RuntimeException e){
 log.error(e.getMessage(),e);
 return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null);
}

/**
 * 处理业务异常BaseException
 * @param e 业务异常
 * @return 统一响应体
 */
@ExceptionHandler(BaseException.class)
public ResponseResult handleBaseException(BaseException e){
 log.error(e.getMessage(),e);
 ResponseCode code=e.getCode();
 return new ResponseResult(code.getCode(),code.getMsg(),null);
}
}

响应增强类

Conrtoller增强的统一响应体处理类,需要注意异常处理类已经进行了增强,所以需要判断一下返回的对象是否为统一响应体对象。


import lombok.extern.slf4j.Slf4j;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
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;

/**
* 统一响应体处理器
* @author NULL
* @date 2019-07-16
*/
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ResponseResultHandlerAdvice implements ResponseBodyAdvice {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
 log.info("returnType:"+returnType);
 log.info("converterType:"+converterType);
 return true;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
 if(MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)){ // 判断响应的Content-Type为JSON格式的body
  if(body instanceof ResponseResult){ // 如果响应返回的对象为统一响应体,则直接返回body
   return body;
  }else{
   // 只有正常返回的结果才会进入这个判断流程,所以返回正常成功的状态码
   ResponseResult responseResult =new ResponseResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),body);
   return responseResult;
  }
 }
 // 非JSON格式body直接返回即可
 return body;
}
}

使用示例

首先准备一个User对象


import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;

/**
* 用户类
* @author NULL
* @date 2019-07-16
*/
@Data
@EqualsAndHashCode
public class User implements Serializable {

private Integer id;

private String name;

}

然后是准备一个简单的UserController即可


import com.rjh.web.entity.User;
import com.rjh.web.exception.BaseException;
import com.rjh.web.response.BaseResponse;
import com.rjh.web.response.ResponseCode;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
* 测试用的Controller
*
* @author NULL
* @date 2019-07-16
*/
@BaseResponse
@RestController
@RequestMapping("users")
public class UserController {

@GetMapping("/{userId}")
public User getUserById(@PathVariable Integer userId){
 if(userId.equals(0)){
  throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST);
 }
 if(userId.equals(1)){
  throw new RuntimeException();
 }
 User user=new User();
 user.setId(userId);
 user.setName("test");
 return user;
}

}

运行结果

在浏览器直接访问http://127.0.0.1:8080/users/0,则返回结果如下(结果经过格式化处理):


{
"code": 10001,
"msg": "资源不存在",
"data": null
}

在浏览器直接访问http://127.0.0.1:8080/users/1,则返回结果如下(结果经过格式化处理):


{
"code": 50000,
"msg": "服务器异常",
"data": null
}

在浏览器直接访问http://127.0.0.1:8080/users/2,则返回结果如下(结果经过格式化处理):


{
"code": 10000,
"msg": "success",
"data": {
 "id": 2,
 "name": "test"
}
}

由运行结果可以得知统一响应增强其实已经生效了,而且能够很好的处理异常。

示例代码地址

下面是这个示例的代码地址,如果觉得不错或者帮助到你,希望大家给个Star:

https://github.com/spring-based-solutions/spring-web-unified-response-demo

参考资料

https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-controller-advice
https://docs.spring.io/spring/docs/5.1.8.RELEASE/spring-framework-reference/web.html#mvc-ann-exceptionhandler

来源:https://segmentfault.com/a/1190000019795918

标签:SpringBoot,统一响应体
0
投稿

猜你喜欢

  • C#读取文本文件到listbox组件的方法

    2022-11-18 02:54:05
  • Java之Algorithm_analysis案例详解

    2022-03-07 01:34:50
  • java使用spring实现读写分离的示例代码

    2023-03-03 06:44:34
  • Java将字符串转化为数组的两种方法

    2021-07-07 20:33:49
  • Android自定义View实现课程表表格

    2023-03-11 19:33:08
  • Java doGet, doPost方法和文件上传实例代码

    2023-01-19 03:24:19
  • C# 操作符之二 算数操作符

    2023-06-19 20:21:13
  • 详解Java8如何使用Lambda表达式进行比较

    2023-12-09 19:27:16
  • SpringBoot项目找不到javax.servlet.Filter的问题及解决

    2022-03-13 10:49:18
  • C#语法相比其它语言比较独特的地方(二)

    2021-12-20 20:56:03
  • 解决@CachePut设置的key值无法与@CacheValue的值匹配问题

    2021-09-10 13:28:49
  • IDEA的默认快捷键设置与Eclipse的常用快捷键的设置方法

    2023-04-09 18:32:40
  • Kotlin LinearLayout与RelativeLayout布局使用详解

    2021-12-06 02:07:30
  • 关于C# TabPage如何隐藏的问题

    2023-01-10 00:01:39
  • 基于Java中两种jersey文件上传方式

    2022-03-02 10:47:29
  • C# string格式的日期时间字符串转为DateTime类型的方法

    2022-04-07 09:34:30
  • SpringBoot万字爆肝高级配置

    2022-09-17 06:34:08
  • 浅谈Android Studio导出javadoc文档操作及问题的解决

    2023-07-07 14:32:53
  • springboot配置加密的正确姿势分享

    2023-10-03 03:44:29
  • Android开发人脸识别登录功能

    2022-03-02 14:16:12
  • asp之家 软件编程 m.aspxhome.com