Jtoss Jtoss
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档

Jason Huang

后端程序猿
首页
  • 数据结构与算法

    • 数据结构与算法 - 概述
    • 数据结构与算法 - 复杂度分析
    • 数据结构 - 线性表
    • 算法 - 常见排序算法
  • 代码规范

    • 代码简洁之道
    • 阿里巴巴开发手册
    • 谷歌Java编程风格指南
  • 设计模式

    • 编写高质量代码概述
    • 面向对象
    • 设计原则
    • 设计模式-创建型
    • 设计模式-结构型
    • 设计模式-行为型(上)
    • 设计模式-行为型(下)
    • 浅析框架源码中的设计模式
    • 业务框架实战案例
  • MySQL 基础

    • MySQL - 数据库设计规范
    • MySQL - 必知必会
  • MySQL 进阶

    • MySQL - 基础架构
    • MySQL - InnoDB存储引擎
    • MySQL - InnoDB缓冲池
    • MySQL - 事务与锁
    • MySQL - 索引
    • MySQL - 查询执行计划
    • MySQL - 性能优化
  • Redis 系列

    • Redis入门 - 基础相关
    • Redis进阶 - 数据结构
    • Redis进阶 - 持久化RDB和AOF
    • Redis进阶 - 事件机制
    • Redis进阶 - 事务
    • Redis进阶 - 高可用高可扩展
    • Redis进阶 - 缓存问题
    • Redis进阶 - 性能调优
  • Java 基础

    • Java 基础 - 知识点
    • Java 基础 - 面向对象
    • Java 基础 - Q/A
  • Java 进阶 - 集合框架

    • Java 集合框架详解
  • Java 进阶 - 多线程与并发

    • Java 并发 - 理论基础
    • Java 并发 - 线程基础
    • Java 并发 - 各种锁
    • Java 并发 - 关键字 volatile
    • Java 并发 - 关键字 synchronized
    • JUC - CAS与原子操作
    • JUC - 锁核心类AQS
    • JUC - 锁接口和类简介
    • JUC - 并发容器简介
    • JUC - 通信工具类
    • JUC - Fork-Join框架
    • JUC - 线程池
  • Java 进阶 - JVM

    • JVM - 概述
    • JVM - 类加载机制
    • JVM - 内存结构
    • JVM - 垃圾回收机制
    • JVM - 性能调优
  • Maven系列

    • Maven基础知识
    • Maven项目构建
    • Maven多模块配置
  • Spring 框架

    • Spring 框架 - 框架介绍
    • Spring 框架 - IOC详解
    • Spring 框架 - AOP详解
    • Spring 框架 - SpringMVC详解
  • Spring Boot 系列

    • Spring Boot - 开发入门
    • Spring Boot - 接口相关
  • Spring Cloud 系列
  • Mybatis 系列

    • Mybatis - 总体框架设计
    • Mybatis - 初始化基本过程
    • Mybatis - sqlSession执行过程
    • Mybatis - 插件机制
    • Mybatis - 事务管理机制
    • Mybatis - 缓存机制
  • 业务常见问题

    • Java 业务开发常见错误(一)
    • Java 业务开发常见错误(二)
    • Java 业务开发常见错误(三)
    • Java 业务开发常见错误(四)
    • Java 业务开发常见错误(五)
    • Java 业务开发常见错误(六)
  • IDEA系列

    • IDEA 2021开发环境配置
    • IDEA 快捷键
  • Git系列

    • git status中文乱码
  • 其他

    • Typora+Picgo 自动上传图片
    • hsdis 和 jitwatch
  • 实用技巧
  • 收藏
  • 摄影
  • 学习
  • 标签
  • 归档
  • Maven系列

  • Spring 框架

  • Spring Boot

    • SpringBoot - Logback
    • SpringBoot - 热部署
    • SpringBoot - 常用注解
    • SpringBoot - 接口统一返回格式
      • 为什么要返回统一的标准格式
      • 定义返回标准格式
        • 定义返回对象
        • 定义状态码
        • 定义异常类
        • 统一返回格式
      • 错误码设计
        • 方案一
        • 方案二
      • 优雅实现方式
        • 使用ResponseBodyAdvice
        • 接口异常问题
        • 全局异常处理器
        • 处理非 Controller 异常
      • 另外
      • 易错场景
      • 源码
      • 参考
    • SpringBoot - 接口参数校验
    • SpringBoot - 接口文档之Swagger
    • SpringBoot - 接口文档之Smart-Doc
    • SpringBoot - 接口版本
  • Spring Cloud

  • Spring
  • Spring Boot
Jason
目录

SpringBoot - 接口统一返回格式

# SpringBoot - 接口统一返回格式

# 为什么要返回统一的标准格式

在默认情况下,SpringBoot的返回格式常见的有三种:

第一种:返回 String

@GetMapping("/hello")
public String getStr(){
  return "hello,javadaily";
}
1
2
3
4

此时调用接口获取到的返回值是这样:

hello,javadaily
1

第二种:返回自定义对象

@GetMapping("/aniaml")
public Aniaml getAniaml(){
  Aniaml aniaml = new Aniaml(1,"pig");
  return aniaml;
}
1
2
3
4
5

此时调用接口获取到的返回值是这样:

{
  "id": 1,
  "name": "pig"
}
1
2
3
4

第三种:接口异常

@GetMapping("/error")
public int error(){
    int i = 9/0;
    return i;
}
1
2
3
4
5

此时调用接口获取到的返回值是这样:

{
  "timestamp": "2021-07-08T08:05:15.423+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/wrong"
}
1
2
3
4
5
6

基于以上种种情况,如果你和前端开发人员联调接口她们就会很懵逼,由于我们没有给他一个统一的格式,前端人员不知道如何处理返回值。

还有甚者,有的同学比如小张喜欢对结果进行封装,他使用了Result对象,小王也喜欢对结果进行包装,但是他却使用的是Response对象,当出现这种情况时我相信前端人员一定会抓狂的。

所以我们项目中是需要定义一个统一的标准返回格式的。

# 定义返回标准格式

一个标准的返回格式至少包含3部分:

  1. status 状态值:由后端统一定义各种返回结果的状态码
  2. message 描述:本次接口调用的结果描述
  3. data 数据:本次返回的数据。
  4. timestamp: 接口调用时间(可选)
{
  "status":"200",
  "message":"操作成功",
  "data":"hello,javadaily"
}
1
2
3
4
5

# 定义返回对象

/**
 * 统一返回格式
 *
 * @author jason
 */
public class ResultResponse<T> implements Serializable {

    @Getter
    @Setter
    private Integer code;

    @Getter
    @Setter
    private String message;

    @Getter
    @Setter
    private T data;

    private long timestamp;

    public ResultResponse() {
        this.timestamp = System.currentTimeMillis();
    }

    private static final int SUCCESS_CODE = 200;
    public static final String DEFAULT_SUCCESS_MESSAGE = "SUCCESS";
    public static final String DEFAULT_ERROR_MESSAGE = "UNKNOWN ERROR";

    public static <T> ResultResponse<T> createFailureResponse(int code, String msg, T data) {
        return new ResultResponse<T>().genError(code, msg, data);
    }

    public static <T> ResultResponse<T> createSuccessResponse(T data) {
        return new ResultResponse<T>().genSuccess(data);
    }

    public static <T> ResultResponse<T> createFailureResponse(BizException bizException) {
        return new ResultResponse<T>().genError(bizException);
    }

    public static <T> ResultResponse<T> createFailureResponse(BizExceptionCode bizExceptionCode) {
        return new ResultResponse<T>().genError(bizExceptionCode);
    }

    public static <T> ResultResponse<T> createFailureResponse(BizExceptionCode bizExceptionCode, T data) {
        return new ResultResponse<T>().genError(bizExceptionCode, data);
    }

    public ResultResponse<T> genSuccess(T data) {
        code = SUCCESS_CODE;
        message = DEFAULT_SUCCESS_MESSAGE;
        this.data = data;
        return this;
    }

    public ResultResponse<T> genError(int code, String msg, T data) {
        this.code = code;
        this.message = msg;
        this.data = data;
        return this;
    }

    public ResultResponse<T> genError(BizException ex) {
        code = ex.getErrorCode();
        message = ex.getMessage();
        data = null;
        return this;
    }

    public ResultResponse<T> genError(BizExceptionCode bizExceptionCode) {
        code = bizExceptionCode.code;
        message = bizExceptionCode.message;
        data = null;
        return this;
    }

    public ResultResponse<T> genError(BizExceptionCode bizCode, T data) {
        code = bizCode.code;
        message = bizCode.message;
        this.data = data;
        return this;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84

# 定义状态码

/**
 * @author jason
 */
public enum BizCode {
    /**服务异常**/
    BAD_REQUEST(400, "参数错误"),
    SYSTEM_ERROR(500,"系统异常,请稍后重试"),
    INVALID_TOKEN(2001,"访问令牌不合法"),
    ACCESS_DENIED(2003,"没有权限访问该资源");

    public final Integer code;
    public final String message;

    BizCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# 定义异常类

业务逻辑代码通常封装在service类中,且需要抛出异常(自定义状态码和异常信息)

/**
 * @author jason
 */
public abstract class BaseException extends RuntimeException {
    private static final long serialVersionUID = -5099085051827483520L;

    @Getter
    @Setter
    private int errorCode;

    public BaseException(String errorMessage) {
        super(errorMessage);
    }

    public BaseException(int errorCode, String errorMessage) {
        super(errorMessage);
        this.errorCode = errorCode;
    }

    public BaseException(String errorMessage, Throwable e) {
        super(errorMessage, e);
    }

    public BaseException(int errorCode, String errorMessage, Throwable e) {
        super(errorMessage, e);
        this.errorCode = errorCode;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

以及子类 BizException:

/**
 * 自定义业务异常,有明确的业务语义,不需要记录Error日志,不需要Retry
 *
 * @author jason
 */
public class BizException extends BaseException {
    private static final int DEFAULT_ERROR_CODE = 510;
    private static final long serialVersionUID = -7646408688819800761L;

    public BizException(String errorMessage) {
        super(DEFAULT_ERROR_CODE, errorMessage);
    }

    public BizException(int errorCode, String errorMessage) {
        super(errorCode, errorMessage);
    }

    public BizException(String errorMessage, Throwable e) {
        super(errorMessage, e);
    }

    public BizException(int errorCode, String errorMessage, Throwable e) {
        super(errorCode, errorMessage, e);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 统一返回格式

/**
 * @author jason
 */
@RestController
public class DemoController {
    @GetMapping("/success")
    public ResultResponse<String> success() {
        return ResultResponse.createSuccessResponse("hello!");
    }
    
    @GetMapping("/exception")
    public ResultResponse<String> exception() {
        return ResultResponse.createFailureResponse(BizCode.ACCESS_DENIED);
    }
    
    @GetMapping("/exception2")
    public ResultResponse<String> exception2() {
        try {
            return ResultResponse.createSuccessResponse(mockServiceMethod());
        } catch (BizException e) {
            return ResultResponse.createFailureResponse(e);
        }
    }
    
    private String mockServiceMethod() {
        throw new BizException(BizCode.ACCESS_DENIED.code, BizCode.ACCESS_DENIED.message);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

返回结果:

success结果:
{
  "code": 200,
  "message": "SUCCESS",
  "data": "hello!",
  "timestamp": 1682001192443
}
exception和exception2结果:
{
    "code": 2003,
    "message": "没有权限访问该资源",
    "data": null,
    "timestamp": 1682001467136
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 错误码设计

# 方案一

根据《Java 开发手册》给出的错误码定义建议设计出的面向外部传递的错误码共五位,并且有如下分类:

  • 错误类型,表示错误来源,一位字母。
  • 错误编码,表示具体错误,四位数字。

错误码的后三位编号与 HTTP 状态码没有任何关系。

错误码即人性,感性认知+口口相传,使用纯数字来进行错误码编排不利于感性记忆和分类

说明:数字是一个整体,每位数字的地位和含义是相同的。 反例:一个五位数字 12345,第1位是错误等级,第 2 位是错误来源,345 是编号,人的大脑不会主动地分辨每位数字的不同含义。

下图是《手册》给出的错误码示例:

来源:阿里云开发 https://zhuanlan.zhihu.com/p/152115647

# 方案二

需求说明

设计错误码规约前,首先要说清楚我们的需求:

  1. 让客户端能够感知到 HTTP 请求是否成功,以便客户端决定下一步该如何处理。
  2. 错误返回时需要携带错误码和出错信息,可以让开发者通过错误码快速定位问题原因。

错误码设计建议

错误码主要是为了体现业务系统的报错位置及原因,因此在设计时需要给它赋予一定的业务属性。

  1. 谁的错?错在哪?

    程序开发人员拿到错误码后,必须能够快速知晓错误来源,判断是谁的问题。这也就要求我们设计的错误码首先要具有一定的识别度,方便记忆和对比,并且要有利于团队快速对错误原因达到一致认知。

  2. 内容格式统一

    错误码应该是统一长度,且内容格式也应该固定的,而不应该是随机编码产生的。其内容可以是一个整数,也可以是一个字符串,但它必须是错误的唯一标识。

    通过研究对比各个平台的错误码,我总结出了一个比较实用的错误码设计方案:使用字符数字组合表示,一共有4段组成,不同段表示不同含义。

    合理的内容格式,更便于错误码的管理和使用,一方面我们可以根据业务需求有规则的自行扩展;另一方面通过错误码能够精准地定位到具体是哪里出现的什么错误。

  3. 错误语义信息

    错误码之外的业务独特信息应由 message 来承载,而不是让错误码本身涵盖过多的具体业务属性。通常在请求出现错误时,我们需要返回的错误描述一般包括:错误码,错误说明以及参考文档(可选)

    图片

    需要注意的是,错误说明是展示给用户的,最好直接告诉用户“该怎么做”而不是“哪里错了”,并且错误说明中不应该包含敏感信息(例如:堆栈信息)。但是为了方便问题排查,可以在系统内部日志中打印详细的错误堆栈信息。

合理使用 HTTP 状态码

当请求出错时,应该合理的返回 HTTP 状态码,以便让客户端感知到 HTTP 是否请求成功。为了更清晰的表述和区分状态码的含义,将 HTTP 状态码分成如下5类:

图片

HTTP 状态码有很多,但是在错误码设计时,你只需要重点关注下面这些状态码:

  • 200 - 表示请求成功执行
  • 400 - 表示客户端出问题
  • 401 - 表示认证失败
  • 403 - 表示授权失败
  • 404 - 表示资源找不到
  • 405 - 客户端请求中的方法被禁止
  • 500 - 表示服务端出问题

在项目开发中,你需要根据实际情况,灵活控制状态码的使用,让客户端的处理更容易。

综上所述,一个合理的错误返回应该包含 HTTP 状态码和错误码,并且错误码需要对外暴露错误说明,最好还要支持返回参考文档。例如:

HTTP/1.1 405
Content-Type: application/json
Transfer-Encoding: chunked
Date: Thu, 28 Apr 2022 09:55:54 GMT
Keep-Alive: timeout=60
Connection: keep-alive

{"code":"U00N03", "msg":"HTTP请求方式不受支持"}
1
2
3
4
5
6
7
8

总结

总的来说,制定一套错误码规约还是相对容易的,难的是在长期的实践中如何管理并正确使用错误码,保证不被滥用,其复杂程度跟项目规模相匹配。

所以我建议在规约设计时,应该以服务业务为导向,避免过度设计,保持简洁;在管理使用时,应该以先到先得的原则统一审批生效,生效后永久固定。

以上就是我对于错误码规约制定一些想法,规则的制定是灵活的,可变通的,同样也没有绝对的好与坏,只要可以满足你的业务需求就是好的,优秀的。

本人能力有限,各位勉强一观,若能抛砖引玉,不失为一件幸事。

来源:作者 小瓦匠学编程 https://mp.weixin.qq.com/s/2Puw3y3mIHjIkfPMA8vQBQ

# 优雅实现方式

# 使用ResponseBodyAdvice

要优化这段代码很简单,我们只需要借助SpringBoot提供的ResponseBodyAdvice即可。

ResponseBodyAdvice的作用:拦截Controller方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。

先来看下ResponseBodyAdvice的源码:

public interface ResponseBodyAdvice<T> {
    boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

    @Nullable
    T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType, Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response);
}
1
2
3
4
5
6

我们只需要编写一个具体实现类即可:

/**
 * @author jason
 */
@RestControllerAdvice(basePackages = "cn.jtoss")
@Slf4j
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        if (object instanceof String) {
            return objectMapper.writeValueAsString(ResultResponse.createSuccessResponse(object));
        }

        return ResultResponse.createSuccessResponse(object);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

需要注意两个地方:

  • @RestControllerAdvice注解

    @RestControllerAdvice是@RestController注解的增强,可以实现三个方面的功能:

    1. 全局异常处理
    2. 全局数据绑定
    3. 全局数据预处理
  • String类型判断

if (object instanceof String) {
    return objectMapper.writeValueAsString(ResultResponse.createSuccessResponse(object));
}
1
2
3

如果Controller直接返回String的话,SpringBoot是直接返回,故我们需要手动转换成json。

经过上面的处理我们就再也不需要通过ResultResponse.createSuccessResponse()来进行转换了,直接返回原始数据格式,SpringBoot自动帮我们实现包装类的封装。

@GetMapping("/successElegant")
public String successElegant() {
    return "Elegant";
}
1
2
3
4

返回结果:

{
    "code": 200,
    "message": "SUCCESS",
    "data": "Elegant",
    "timestamp": 1682003906603
}
1
2
3
4
5
6

# 接口异常问题

此时有个问题,由于我们没对Controller的异常进行处理,当我们调用的方法一旦出现异常,就会出现问题,比如下面这个接口

@GetMapping("/error")
public int error() {
    int i = 1/0;
    return i;
}
1
2
3
4
5

返回结果为Spring Boot框架定义的状态码为500的HTML页面或json格式,这显然不是我们想要的结果。

# 全局异常处理器

使用全局异常处理器可以解决如下问题:

  1. 不用手写try...catch,由全局异常处理器统一捕获
  2. 对于自定义异常,只能通过全局异常处理器来处理
  3. 当我们引入Validator参数校验器时,参数校验不通过会抛出异常,此时是无法用try...catch捕获的,只能使用全局异常处理器。

实现方式如下:

/**
 * @author jason
 */
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
    /**
     * 抓取全局异常(保底)
     */
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultResponse<String> exception(Exception e) {
        log.error("全局异常信息: {}", e.getMessage(), e);
        return ResultResponse.createFailureResponse(BizCode.SYSTEM_ERROR.code, e.getMessage(), null);
    }
    
    /**
     * 抓取自定义异常
     */
    @ExceptionHandler(BaseException.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ResultResponse<String> exception(BaseException e) {
        return ResultResponse.createFailureResponse(e.getErrorCode(), e.getMessage(), null);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

有三个细节需要说明一下:

  1. @RestControllerAdvice,RestController的增强类,可用于实现全局异常处理器
  2. @ExceptionHandler,统一处理某一类异常,从而减少代码重复率和复杂度,比如要获取自定义异常可以@ExceptionHandler(BaseException.class)
  3. @ResponseStatus指定客户端收到的http 状态码,可以统一设置为 200,具体状态码以 response body 中的 code 为准;或者根据情况设置为 500,401,403 等状态

这时候之前的exception2接口就可以修改为:

@GetMapping("/exception3")
public String exception3() {
    return mockServiceMethod();
}
1
2
3
4

但是当我们同时启用统一标准格式封装功能ResponseAdvice和RestExceptionHandler后,为同时兼容上面两种方式返回结果,需要在 ResponseAdvice 类中修改 beforeBodyWrite方法:

@SneakyThrows
@Override
public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    if (object instanceof String) {
        return objectMapper.writeValueAsString(ResultResponse.createSuccessResponse(object));
    }
    // 如果返回的结果是ResultResponse对象,直接返回即可
    if (object instanceof ResultResponse) {
        return object;
    }

    return ResultResponse.createSuccessResponse(object);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 处理非 Controller 异常

@RestControllerAdvice 与 @ExceptionHandler 只能全局捕获由 Controller 抛出的异常,如 Spring Security Filter 中抛出的异常默认情况下则无法该方式捕获,比如 AccessDeniedException、AuthenticationException 或 业务自定义异常。这是因为这些异常是在 Controller 之前抛出的。

而统一处理 Spring Security 中抛出的异常可以通过下面两种方式:

  • 通过自定义类实现 AuthenticationEntryPoint 接口并重写 commence 方法,自定义返回 body
  • 通过自定义类实现 AuthenticationEntryPoint 接口并重写 commence 方法,并将异常交由框架的 HandlerExceptionResolver 处理,再通过 @RestControllerAdvice 与 @ExceptionHandler 处理

具体实现方式见这篇文章:https://www.baeldung.com/spring-security-exceptionhandler

更加方便的处理方式:重写 BasicErrorController 的 /error 接口

@RestController
@Slf4j
public class GlobalErrorController extends BasicErrorController {
    public GlobalErrorController() {
        this(new DefaultErrorAttributes(), new ErrorProperties());
    }

    public GlobalErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        super(errorAttributes, errorProperties);
    }

    public GlobalErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties, List<ErrorViewResolver> errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
    }

    @SneakyThrows
    @Override
    @AnonymousAccess
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
        if (throwable != null) {
            if (throwable instanceof BizException) {
                throw (BizException) throwable;
            }
            if (throwable instanceof Exception) {
                throw (Exception) throwable;
            }
        }

        HttpStatus status = getStatus(request);
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        String errorMessage = (String) body.getOrDefault("error", "未知错误");
        ResultResponse<String> resultResponse = ResultResponse.createFailureResponse(status.value(), errorMessage, null);
        ObjectMapper objectMapper = new ObjectMapper();
        Map<String, Object> data = objectMapper
                .convertValue(resultResponse, new TypeReference<Map<String, Object>>() {});

        return new ResponseEntity<>(data, status);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

直接重新抛出异常,由于是 Controller 中抛出的,所以会被 @RestControllerAdvice 与 @ExceptionHandler 捕获。而最初的异常可以通过 request 中的属性获取:

Throwable throwable = (Throwable) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
1

如果 throwable 为 null,就需要手动返回 ResponseEntity,由于此时 ResponseEntity 的 body 是 Map 类型,所以同时需要在 ResponseAdvice 类中修改 beforeBodyWrite方法:

@SneakyThrows
@Override
public Object beforeBodyWrite(Object object, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
    if (object instanceof String) {
        return objectMapper.writeValueAsString(ResultResponse.createSuccessResponse(object));
    }
    if (object instanceof ResultResponse) {
        return object;
    }
    // 如果是 Map 则直接返回,因为已经处理过了
    if (object instanceof Map) {
        return object;
    }

    return ResultResponse.createSuccessResponse(object);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

需要注意的是由于重写了 /error ,需要在 Spring Security 将该 url 重新设置为任何人可访问。

# 另外

在实际开发中,非生成环境为了方便定位调用接口返回的异常,可以将异常 stackTrace 部分信息也返回。

比如下面代码:

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultResponse<String> exception(Exception e) {
    log.error("全局异常信息: {}", e.getMessage(), e);
    return ResultResponse.createFailureResponse(BizCode.SYSTEM_ERROR.code, e.getMessage(), stackTraceMessage(e));
}

private String stackTraceMessage(Exception e) {
    if (EnvironmentEnum.PRODUCT.getProfile().equalsIgnoreCase(profile)) {
        return null;
    }

    int defaultDeep = 10;
    StringBuilder sb = new StringBuilder();
    StackTraceElement[] stackTrace = e.getStackTrace();
    int deep = Math.min(stackTrace.length, defaultDeep);
    for (int i = 0; i < deep; i++) {
        sb.append(stackTrace[i]);
        sb.append("\r\n  ");
    }
    return sb.toString();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

# 易错场景

例如当前接口调用了其他服务接口,其他服务接口也会返回结果,那当前接口应该如何返回。详细可见:接口的响应要明确表示处理结果

# 源码

源码可见:https://github.com/hengwen/spring-demo/tree/main/springbootresponse

# 参考

  • https://www.cnblogs.com/jianzh5/p/15018838.html
  • https://www.baeldung.com/spring-security-exceptionhandler
  • https://www.baeldung.com/exception-handling-for-rest-with-spring
  • 《错误码如何设计才合理?》https://zhuanlan.zhihu.com/p/152115647
  • 《错误码应该如何设计?》https://mp.weixin.qq.com/s/2Puw3y3mIHjIkfPMA8vQBQ
#Spring Boot
上次更新: 2024-08-19
SpringBoot - 常用注解
SpringBoot - 接口参数校验

← SpringBoot - 常用注解 SpringBoot - 接口参数校验→

最近更新
01
开始
01-09
02
AI工具分享
01-09
03
AI 导读
01-07
更多文章>
Theme by Vdoing | Copyright © 2022-2025 Jason Huang | 闽ICP备2025088096号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式