Spring Boot 统一RESTful接口响应和统一异常处理
一、简介
基于Spring Boot 框架开发的应用程序,大部分都是以提供RESTful接口为主要的目的。前端或者移动端开发人员通过调用后端提供的RESTful接口完成数据的交换。
统一的RESTful接口响应数据结构是基本的开发规范。能够减少团队内部不必要的沟通;减轻接口消费者校验数据的负担;降低其他同事接手代码的难度;提高接口的健壮性和可扩展性。
常见的统一响应数据结构如下所示:
public class GlobalResponseEntity<T>{
private Boolean success = true;
private String code = "000000";
private String message = "request successfully";
private T data;
}
统一的异常处理,是系统完备性的基本象征。通过对全局异常信息的捕获,能够避免将异常信息和系统敏感信息直接抛给客户端;针对特定类型异常捕获之后可以重新对输出数据做编排,提高交互友好度,同时可以记录异常信息以便监控和分析。
一般,在统一异常处理处会手动修改返回给客户端的http状态码,并编排响应给客户端的数据结构为GlobalResponseEntity,保证始终统一响应。
二、如何实现
使用RestControllerAdvice注解(或者ControllerAdvice注解)结合ResponseBodyAdvice接口
RestControllerAdvice注解导入了ControllerAdvice注解
@ControllerAdvice是在类上声明的注解,其用法主要有三点:
和@ExceptionHandler注解配合使用,@ExceptionHandler标注的方法可以捕获Controller中抛出的的异常,从而达到异常统一处理的目的
和@InitBinder注解配合使用,@InitBinder标注的方法可在请求中注册自定义参数的解析器,从而达到自定义请求参数格式化的目的
和@ModelAttribute注解配合使用,@ModelAttribute标注的方法会在执行目标Controller方法之前执行,可在入参上增加自定义信息
实现ResponseBodyAdvice接口来自定义响应给前端的内容
用法举例:
// 这里@RestControllerAdvice等同于@ControllerAdvice + @ResponseBody
@RestControllerAdvice
public class GlobalHandler {
private final Logger logger = LoggerFactory.getLogger(GlobalHandler.class);
// 这里@ModelAttribute("loginUserInfo")标注的modelAttribute()方法表示会在Controller方法之前
// 执行,返回当前登录用户的UserDetails对象
@ModelAttribute("loginUserInfo")
public UserDetails modelAttribute() {
return (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
// @InitBinder标注的initBinder()方法表示注册一个Date类型的类型转换器,用于将类似这样的2019-06-10
// 日期格式的字符串转换成Date对象
@InitBinder
protected void initBinder(WebDataBinder binder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
// 这里表示Controller抛出的MethodArgumentNotValidException异常由这个方法处理
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result exceptionHandler(MethodArgumentNotValidException e) {
Result result = new Result(BizExceptionEnum.INVALID_REQ_PARAM.getErrorCode(),
BizExceptionEnum.INVALID_REQ_PARAM.getErrorMsg());
logger.error("req params error", e);
return result;
}
// 这里表示Controller抛出的BizException异常由这个方法处理
@ExceptionHandler(BizException.class)
public Result exceptionHandler(BizException e) {
BizExceptionEnum exceptionEnum = e.getBizExceptionEnum();
Result result = new Result(exceptionEnum.getErrorCode(), exceptionEnum.getErrorMsg());
logger.error("business error", e);
return result;
}
// 这里就是通用的异常处理器了,所有预料之外的Exception异常都由这里处理
@ExceptionHandler(Exception.class)
public Result exceptionHandler(Exception e) {
Result result = new Result(1000, "网络繁忙,请稍后再试");
logger.error("application error", e);
return result;
}
}
在Controller里取出@ModelAttribute标注的方法返回的UserDetails对象。(这里只是用UserDetails举例,实际开发过程中,建议按照spring security的做法,将用户信息存放到spring上下文中,然后在controller层进行消费)
当入参为examOpDate=2019-06-10时,Spring会使用我们上面@InitBinder注册的时间类型转换器将2019-06-10转换examOpDate对象
RestController
@RequestMapping("/json/exam")
@Validated
public class ExamController {
@Autowired
private IExamService examService;
// ......
@PostMapping("/getExamListByOpInfo")
public Result<List<GetExamListResVo>> getExamListByOpInfo( @NotNull Date examOpDate,
@ModelAttribute("loginUserInfo") UserDetails userDetails) {
List<GetExamListResVo> resVos = examService.getExamListByOpInfo(examOpDate, userDetails);
Result<List<GetExamListResVo>> result = new Result(resVos);
return result;
}
}
@ExceptionHandler标注的多个方法分别表示只处理特定的异常。这里需要注意的是当Controller抛出的某个异常多个@ExceptionHandler标注的方法都适用时,Spring会选择最具体的异常处理方法来处理,也就是说@ExceptionHandler(Exception.class)这个标注的方法优先级最低,只有当其它方法都不适用时,才会来到这里处理。
这里仅列举了RestControllerAdvice简单用法,为了学习RestControllerAdvice注解使用
三、统一的响应处理
工程目录结构如下:

GlobalResponse是一个处理器类(handle),用来处理统一响应。
GlobalResponse类需要实现ResponseBodyAdvice接口
重写supports方法,可对响应进行过滤。实际开发中不一定所有的方法返回值都是相同的模板,这里可以根据MethodParameter进行过滤,此方法返回true则会走过滤,即会调用beforeBodyWrite方法,否则不会调用。
重写beforeBodyWrite方法,编写具体的响应数据逻辑
代码如下:
GlobalResponse
package com.naylor.globalresponsebody.handler.response;
import com.alibaba.fastjson.JSON;
import com.naylor.globalresponsebody.handler.GlobalResponseEntity;
import org.springframework.core.MethodParameter;
import org.springframework.core.io.Resource;
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.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
/**
* @BelongsProject: debris-app
* @BelongsPackage: com.naylor.globalresponsebody.response
* @Author: Chenml
* @CreateTime: 2020-09-02 15:26
* @Description: 全局响应
*/
@RestControllerAdvice("com.naylor")
public class GlobalResponse implements ResponseBodyAdvice<Object> {
/**
* 拦截之前业务处理,请求先到supports再到beforeBodyWrite
* <p>
* 用法1:自定义是否拦截。若方法名称(或者其他维度的信息)在指定的常量范围之内,则不拦截。
*
* @param methodParameter
* @param aClass
* @return 返回true会执行拦截;返回false不执行拦截
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
//TODO 过滤
return true;
}
/**
* 向客户端返回响应信息之前的业务逻辑处理
* <p>
* 用法1:无论controller返回什么类型的数据,在写入客户端响应之前统一包装,客户端永远接收到的是约定格式的内容
* <p>
* 用法2:在写入客户端响应之前统一加密
*
* @param responseObject 响应内容
* @param methodParameter
* @param mediaType
* @param aClass
* @param serverHttpRequest
* @param serverHttpResponse
* @return
*/
@Override
public Object beforeBodyWrite(Object responseObject, MethodParameter methodParameter,
MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
//responseObject是否为null
if (null == responseObject) {
return new GlobalResponseEntity<>("55555", "response is empty.");
}
//responseObject是否是文件
if (responseObject instanceof Resource) {
return responseObject;
}
//该方法返回值类型是否是void
//if ("void".equals(methodParameter.getParameterType().getName())) {
// return new GlobalResponseEntity<>("55555", "response is empty.");
//}
if (methodParameter.getMethod().getReturnType().isAssignableFrom(Void.TYPE)) {
return new GlobalResponseEntity<>("55555", "response is empty.");
}
//该方法返回值类型是否是GlobalResponseEntity。若是直接返回,无需再包装一层
if (responseObject instanceof GlobalResponseEntity) {
return responseObject;
}
//处理string类型的返回值
//当返回类型是String时,用的是StringHttpMessageConverter转换器,无法转换为Json格式
//必须在方法体上标注RequestMapping(produces = "application/json; charset=UTF-8")
if (responseObject instanceof String) {
String responseString = JSON.toJSONString(new GlobalResponseEntity<>(responseObject));
return responseString;
}
//该方法返回的媒体类型是否是application/json。若不是,直接返回响应内容
if (!mediaType.includes(MediaType.APPLICATION_JSON)) {
return responseObject;
}
return new GlobalResponseEntity<>(responseObject);
}
}
GlobalResponseEntity
GlobalResponseEntity是一个实体类,用来封装统一响应和统一异常处理的返回值模板
- GlobalResponseEntity类为一个泛型类,T为接口具体的返回数据
- success表示接口响应是否成功,一般的,这个是业务叫法,和http状态码无关
- code表示接口响应状态码,可以根据特定业务场景自己定义
- message是描述信息
- 实际开发中code和mesage的具体值可以用枚举来维护
具体代码如下:
@Data
@Accessors(chain = true)
public class GlobalResponseEntity<T> implements Serializable {
private Boolean success = true;
private String code = "000000";
private String message = "request successfully";
private T data;
public GlobalResponseEntity() {
super();
}
public GlobalResponseEntity(T data) {
this.data = data;
}
public GlobalResponseEntity(String code, String message) {
this.code = code;
this.message = message;
this.data = null;
}
public GlobalResponseEntity(String code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
public GlobalResponseEntity(Boolean success, String code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
public GlobalResponseEntity(Boolean success, String code, String message, T data) {
this.success = success;
this.code = code;
this.message = message;
this.data = data;
}
public static GlobalResponseEntity<?> badRequest(String code, String message) {
return new GlobalResponseEntity<>(false, code, message);
}
public static GlobalResponseEntity<?> badRequest() {
return new GlobalResponseEntity<>(false, "404", "无法找到您请求的资源");
}
}
四、统一的异常处理
新增GlobalException类,编写统一异常处理。类上面添加
@RestControllerAdvice("com.naylor")和
@ResponseBody注解,ResponseBody用来对响应内容进行编排,如http状态码。代码如下:
GlobalException
@RestControllerAdvice("com.naylor")
@ResponseBody
@Slf4j
public class GlobalException {
/**
* 捕获一般异常
* 捕获未知异常
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception e) {
return new ResponseEntity<>(
new GlobalResponseEntity<>(false, "555",
e.getMessage() == null ? "未知异常" : e.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 处理404异常
*
* @return
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseEntity<Object> handleNoHandlerFoundException(NoHandlerFoundException e) {
return new ResponseEntity<>(
new GlobalResponseEntity(false, "4040",
e.getMessage() == null ? "请求的资源不存在" : e.getMessage()),
HttpStatus.NOT_FOUND);
}
/**
* 捕获运行时异常
*
* @param e
* @return
*/
@ExceptionHandler(RuntimeException.class)
public ResponseEntity<Object> handleRuntimeException(RuntimeException e) {
log.error("handleRuntimeException:", e);
return new ResponseEntity<>(
new GlobalResponseEntity(false, "rt555",
e.getMessage() == null ? "运行时异常" : e.getMessage().replace("java.lang.RuntimeException: ", "")),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 捕获业务异常
* 捕获自定义异常
*
* @param e
* @return
*/
@ExceptionHandler(BizServiceException.class)
public ResponseEntity<Object> handleBizServiceException(BizServiceException e) {
return new ResponseEntity<>(
new GlobalResponseEntity(false, e.getErrorCode(), e.getMessage()),
HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 捕获参数校验异常
* javax.validation.constraints
*
* @param e
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
public ResponseEntity<Object> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
String msg = "参数校验失败";
List<FieldFailedValidate> fieldFailedValidates = this.extractFailedMessage(e.getBindingResult().getFieldErrors());
if (null != fieldFailedValidates && fieldFailedValidates.size() > 0) {
msg = fieldFailedValidates.get(0).getMessage();
}
return new ResponseEntity<>(
new GlobalResponseEntity<>(false, "arg555", msg, null),
HttpStatus.BAD_REQUEST);
}
/**
* 组装validate错误信息
*
* @param fieldErrors
* @return
*/
private List<FieldFailedValidate> extractFailedMessage(List<FieldError> fieldErrors) {
List<FieldFailedValidate> fieldFailedValidates = new ArrayList<>();
if (null != fieldErrors && fieldErrors.size() > 0) {
FieldFailedValidate fieldFailedValidate = null;
for (FieldError fieldError : fieldErrors) {
fieldFailedValidate = new FieldFailedValidate();
fieldFailedValidate.setMessage(fieldError.getDefaultMessage());
fieldFailedValidate.setName(fieldError.getField());
fieldFailedValidates.add(fieldFailedValidate);
}
}
return fieldFailedValidates;
}
}
FieldFailedValidate
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
@Data
@Accessors(chain = true)
public class FieldFailedValidate implements Serializable {
private String name;
private String message;
}
BizServiceException
@Data
public class BizServiceException extends Exception {
private String message;
private Integer code;
public BizServiceException(String message) {
super(message);
this.message = message;
}
public BizServiceException(String message, Integer code) {
super(message);
this.message = message;
this.code = code;
}
@Override
public String getMessage() {
return message;
}
}
五、解决无法捕获404异常的问题
五、解决因增加了ResponseBodyAdvice导致Swagger2-UI无法访问的问题
报错提示:
Unable to infer base url.
This is common when using dynamic servlet registration or when the API is behind an API Gateway.
The base url is the root of where all the swagger resources are served.
For e.g. if the api is available at http://example.org/api/v2/api-docs
then the base url is http://example.org/api/. Please enter the location manually:
原因:swagger相当于是寄宿在应用程序中的一个web服务,统一响应处理器拦截了应用所有的响应,对swagger-ui的响应产生了影响。
解决方案:修改统一响应处理器拦截的范围,配置包路径。
@RestControllerAdvice(value={"com.naylor","org.spring"})
public class GlobalResponseHandler implements ResponseBodyAdvice<Object> {
//......
}
六、解决RestControllerAdvice优先级问题
若在项目中写了好几个处理器类,都添加了@RestControllerAdvice的注解,由于加载存在先后顺序,可能会导致部分拦截器没有按照既定的方式工作,甚至出现一些奇奇怪怪的问题,此时可以在标注了RestControllerAdvice的类上增加@Order注解,来指定加载顺序。
引用
@RestControllerAdvice详解: https://zhuanlan.zhihu.com/p/73087879
@ResponseBodyAdvice详解:https://my.oschina.net/diamondfsd/blog/3069546/print
Spring Boot 统一RESTful接口响应和统一异常处理的更多相关文章
- Spring Boot中Restful Api的异常统一处理
我们在用Spring Boot去向前端提供Restful Api接口时,经常会遇到接口处理异常的情况,产生异常的可能原因是参数错误,空指针异常,SQL执行错误等等. 当发生这些异常时,Spring B ...
- Spring Boot提供RESTful接口时的错误处理实践
使用Spring Boot开发微服务的过程中,我们会使用别人提供的接口,也会设计接口给别人使用,这时候微服务应用之间的协作就需要有一定的规范. 基于rpc协议,我们一般有两种思路:(1)提供服务的应用 ...
- spring boot https --restful接口篇
我们写的接口默认都是http形式的,不过我们的接口很容易被人抓包,而且一抓全是明文的挺尴尬的 spring boot配置https生成证书大的方向有3种: 1.利用keytool自己生成证书 2.从免 ...
- 聊一聊 Spring Boot 中 RESTful 接口设计规范
在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性.接口的稳定性.接口的跨域性.接口的协议规则.接口的路径规则.接口单一原则.接口过滤和接口组合等诸多因素,本篇文章将简要分 ...
- 无规矩不成方圆,聊一聊 Spring Boot 中 RESTful 接口设计规范
在设计接口时,有很多因素要考虑,如接口的业务定位,接口的安全性,接口的可扩展性.接口的稳定性.接口的跨域性.接口的协议规则.接口的路径规则.接口单一原则.接口过滤和接口组合等诸多因素,本篇文章将简要分 ...
- spring boot / cloud (二) 规范响应格式以及统一异常处理
spring boot / cloud (二) 规范响应格式以及统一异常处理 前言 为什么规范响应格式? 我认为,采用预先约定好的数据格式,将返回数据(无论是正常的还是异常的)规范起来,有助于提高团队 ...
- Spring Boot 自定义注解,AOP 切面统一打印出入参请求日志
其实,小哈在之前就出过一篇关于如何使用 AOP 切面统一打印请求日志的文章,那为什么还要再出一篇呢?没东西写了? 哈哈,当然不是!原因是当时的实现方案还是存在缺陷的,原因如下: 不够灵活,由于是以所有 ...
- Spring Boot2 系列教程(三十一)Spring Boot 构建 RESTful 风格应用
RESTful ,到现在相信已经没人不知道这个东西了吧!关于 RESTful 的概念,我这里就不做过多介绍了,传统的 Struts 对 RESTful 支持不够友好 ,但是 SpringMVC 对于 ...
- Spring Boot构建 RESTful 风格应用
Spring Boot构建 RESTful 风格应用 1.Spring Boot构建 RESTful 风格应用 1.1 实战 1.1.1 创建工程 1.1.2 构建实体类 1.1.4 查询定制 1.1 ...
- 使用Spring boot开发RestFul 风格项目PUT/DELETE方法不起作用
在使用Spring boot 开发restful 风格的项目,put.delete方法不起作用,解决办法. 实体类Student @Data public class Student { privat ...
随机推荐
- 【万字干货】OpenMetric与时序数据库存储模型分析
摘要:解读OpenMetric规范和指标的模型定义基础上,结合当下主流的时序数据库核心存储及处理技术,尝试让用户(架构师.开发者或使用者)结合自身业务场景选择合适的产品,消除技术选型的困惑. 本文分享 ...
- 教你一个快速视频处理的神器:Python moviepy
摘要:python 中的视频处理模块,有一个叫做 moviepy,今天我们就来唠唠它. 本文分享自华为云社区<python moviepy 的用法,看这篇就能入门>,作者: 梦想橡皮擦. ...
- 云原生时代,领域驱动设计思想(DDD)如何落地?
摘要:随着数字化世界的持续演进,软件架构设计思想在碰撞中不断优化.云原生时代的到来,加速了行业对于领域驱动设计理念(Domain-Driven Design)的实践落地诉求. 本文分享自华为云社区&l ...
- Kubernetes(K8S) helm chart
感觉和放到一个 yaml 文件中,用 ---- 分隔,操作繁琐程度上,没有太大区别 创建自定义 Chart # 创建自定义的 chart 名为 mychart [root@k8smaster ~]# ...
- 使用formdata在vue和django之间传递文件
在前端页面中如果有文件或者图片需要上传的场景下,通用做法是使用formdata将文件从前端传输到后台,在后台上传文件并将url保存在数据库. 当前项目是使用vue + Element UI + dja ...
- RocketMQ事务消息在订单创建和库存扣减的使用
前言 下单的过程包括订单创建,还有库存的扣减,为提高系统的性能,将库存放在redis扣减,则会涉及到Mysql和redis之间的数据同步,其中,这个过程还涉及到,必须是订单创建成功才进行库存的扣减操作 ...
- Ubuntu20.04上安装MySQL8.0(绝对保证能够正常使用)
今天在学习 Spark 连接 MySQL时发现还没安装,便参考了厦门大学实验室的Blog进行操作.但安装完成之后发现没有显示设置密码的选择,但又改不掉root密码(头开始痛起来). 故记录一下安装My ...
- AISing Programming Contest 2021(AtCoder Beginner Contest 202) 简单题解记录
补题链接:Here A - Three Dice 水题,问给定三次摇色子的正面,请问3次结果以后相对面的点数和 cout << (21 - a - b - c) << &quo ...
- 绿色数治开采工艺: 3D 可视化智慧矿山
前言 2021 年 2 月底,国家矿山安监局综合司发布的<"十四五"矿山安全生产规划(征求意见稿)>中再次强调要"实时采集矿山安全监控.人员位置监测.视频监控 ...
- 《3D编程模式》写书-第4次记录
大家好,这段时间我完成了"再看设计原则"的初稿,包括了设计基础.单一职责原则.依赖倒置原则.接口隔离原则.合成复用原则.最少知识原则.开闭原则 目前我已经完成了所有的初稿,后面会进 ...