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

前言

最近在优化自己之前基于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

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • SpringBoot初始教程之统一异常处理详解

    1.介绍 在日常开发中发生了异常,往往是需要通过一个统一的异常处理处理所有异常,来保证客户端能够收到友好的提示.SpringBoot在页面发生异常的时候会自动把请求转到/error,SpringBoot内置了一个BasicErrorController对异常进行统一的处理,当然也可以自定义这个路径 application.yaml server: port: 8080 error: path: /custom/error BasicErrorController提供两种返回错误一种是页面返回.当

  • SpringBoot使用WebJars统一管理静态资源的方法

    传统管理静态资源主要依赖于复制粘贴,不利于后期维护,为了让大家往后更舒心,让WebJars给静态资源来一次搬家革命吧!! 学习目标 简单两步!快速学会使用WebJars统一管理前端依赖. 快速查阅 源码下载:SpringBoot Webjars Learning 使用教程 一.引入相关依赖 在 WebJars官网找到项目中需要的依赖,例如在项目中引入jQuery.BootStrap前端组件等.例如: 版本定位工具:webjars-locator-core 前端组件:jquery .bootstr

  • SpringBoot 统一异常处理详解

    代码结构 配置pom文件 <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/

  • SpringBoot使用统一异常处理详解

    场景:针对异常处理,我们原来的做法是一般在最外层捕获异常即可,例如在Controller中 @Controller public class HelloController { private static final Logger logger = LoggerFactory.getLogger(HelloController.class); @GetMapping(value = "/hello") @ResponseBody public Result hello() { try

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

    前言 最近在优化自己之前基于Spring AOP的统一响应体的实现方案. 什么是统一响应体呢?在目前的前后端分离架构下,后端主要是一个RESTful API的数据接口. 但是HTTP的状态码数量有限,而随着业务的增长,HTTP状态码无法很好地表示业务中遇到的异常情况. 那么可以通过修改响应返回的JSON数据,让其带上一些固有的字段,例如以下这样的 { "code": 10000, "msg": "success", "data"

  • 详解SpringBoot 统一后端返回格式的方法

    目录 为什么要对SpringBoot返回统一的标准格式 定义返回标准格式 定义返回对象 定义状态码 统一返回格式 高级实现方式 接口异常问题 SpringBoot为什么需要全局异常处理器 如何实现全局异常处理器 体验效果 全局异常接入返回的标准格式 首先我们来看看为什么要返回统一的标准格式? 为什么要对SpringBoot返回统一的标准格式 在默认情况下,SpringBoot的返回格式常见的有三种: 第一种:返回 String @GetMapping("/hello") public

  • 详解SpringBoot如何统一后端返回格式

    目录 为什么要对SpringBoot返回统一的标准格式 第一种:返回 String 第二种:返回自定义对象 第三种:接口异常 定义返回标准格式 高级实现方式 接口异常问题 SpringBoot为什么需要全局异常处理器 体验效果 全局异常接入返回的标准格式 今天我们来聊一聊在基于SpringBoot前后端分离开发模式下,如何友好的返回统一的标准格式以及如何优雅的处理全局异常. 首先我们来看看为什么要返回统一的标准格式? 为什么要对SpringBoot返回统一的标准格式 在默认情况下,SpringB

  • 详解SpringBoot基于Dubbo和Seata的分布式事务解决方案

    1. 分布式事务初探 一般来说,目前市面上的数据库都支持本地事务,也就是在你的应用程序中,在一个数据库连接下的操作,可以很容易的实现事务的操作. 但是目前,基于SOA的思想,大部分项目都采用微服务架构后,就会出现了跨服务间的事务需求,这就称为分布式事务. 本文假设你已经了解了事务的运行机制,如果你不了解事务,那么我建议先去看下事务相关的文章,再来阅读本文. 1.1 什么是分布式事务 对于传统的单体应用而言,实现本地事务可以依赖Spring的@Transactional注解标识方法,实现事务非常简

  • 浅谈SpringBoot如何封装统一响应体

    一.前言 在上一篇 SpringBoot 参数校验中我们对参数校验添加了异常处理,但是还是有不规范的地方,没有用统一响应体进行返回,在这篇文章中介绍如何封装统一响应体. 关于统一响应体的封装,没有一个标准答案,我在各种技术社区看了一遍,汇总了一个复用性比较好的方案. 二.添加结果类枚举 在项目目录下面建一个 responseEntity 的 package,然后在里面建一个 ResultEnum 枚举类,添加如下代码: 这边介绍一下枚举类的用法.枚举类的作用实际上就是定义常量,如果不使用枚举类,

  • 详解SpringBoot如何实现统一后端返回格式

    目录 1.为什么要对SpringBoot返回统一的标准格式 1.1 返回String 1.2 返回自定义对象 1.3 接口异常 2.定义返回对象 3.定义状态码 4.统一返回格式 5.高级实现方式 5.1 ResponseBodyAdvice的源码 5.2 String类型判断 在前后端分离的项目中后端返回的格式一定要友好,不然会对前端的开发人员带来很多的工作量.那么SpringBoot如何做到统一的后端返回格式呢?今天我们一起来看看. 1.为什么要对SpringBoot返回统一的标准格式 在默

  • 详解Java分布式Session共享解决方案

    分布式Session一致性? 说白了就是服务器集群Session共享的问题 Session的作用? Session 是客户端与服务器通讯会话跟踪技术,服务器与客户端保持整个通讯的会话基本信息. 客户端在第一次访问服务端的时候,服务端会响应一个sessionId并且将它存入到本地cookie中,在之后的访问会将cookie中的sessionId放入到请求头中去访问服务器,如果通过这个sessionid没有找到对应的数据那么服务器会创建一个新的sessionid并且响应给客户端. 分布式Sessio

  • 详解SpringBoot定制@ResponseBody注解返回的Json格式

     1.引言 在SpringMVC的使用中,后端与前端的交互一般是使用Json格式进行数据传输,SpringMVC的@ResponseBody注解可以很好的帮助我们进行转换,但是后端返回数据给前端往往都有约定固定的格式,这时候我们在后端返回的时候都要组拼成固定的格式,每次重复的操作非常麻烦. 2.SpringMVC对@ResponseBody的处理 SpringMVC处理@ResponseBody注解声明的Controller是使用默认的.RequestResponseBodyMethodProc

  • 详解SpringBoot中添加@ResponseBody注解会发生什么

    SpringBoot版本2.2.4.RELEASE. [1]SpringBoot接收到请求 ① springboot接收到一个请求返回json格式的列表,方法参数为JSONObject 格式,使用了注解@RequestBody 为什么这里要说明返回格式.方法参数.参数注解?因为方法参数与参数注解会影响你使用不同的参数解析器与后置处理器!通常使用WebDataBinder进行参数数据绑定结果也不同. 将要调用的目标方法如下: @ApiOperation(value="分页查询") @Re

  • 详解SpringBoot集成消息队列的案例应用

    目录 背景 方案规划 统一设计 集成Redis消息队列 集成ActiveMQ消息队列 使用示例 背景 最近在对公司开发框架进行优化,框架内涉及到多处入库的日志记录,例如登录日志/操作日志/访问日志/业务执行日志,集成在业务代码中耦合度较高且占用业务操作执行时间,所以准备集成相关消息队列进行代码解耦 方案规划 现有的成熟消息队列组件非常多,例如RabbitMQ,ActiveMQ,Kafka等,考虑到业务并发量不高且框架已经应用于多个项目平稳运行,准备提供基于Redis的消息队列和集成ActiveM

随机推荐