统一返回格式
# 统一返回格式与异常处理封装方案
在企业开发中,后端接口需要统一的返回格式,以保证前后端交互的一致性和易维护性。本文将详细总结如何在 Spring Boot 项目中实现企业级统一返回格式和异常处理的封装,包含以下部分:
- 状态码枚举类:
ResultCode
- 统一返回对象:
CommonResult<T>
- 自定义异常类:
CustomException
- 全局异常处理:
GlobalExceptionHandler
- 工具类:
ResultUtil
- Controller 层的使用示例
# 1. 状态码枚举类:ResultCode
说明:状态码枚举类用于统一管理接口返回的状态码及其对应的描述信息。这样可以避免在项目中直接使用硬编码状态码,提高代码的可维护性和可读性。
/**
* @description 状态码枚举类,用于统一管理所有接口的状态码和描述信息
*/
public enum ResultCode {
// 成功请求
SUCCESS(200, "操作成功"),
// 通用错误
FAILED(400, "请求失败"),
VALIDATE_FAILED(404, "参数校验失败"),
UNAUTHORIZED(401, "未登录或Token已过期"),
FORBIDDEN(403, "没有相关权限"),
// 客户端错误
BAD_REQUEST(400, "请求参数错误"),
NOT_FOUND(404, "资源未找到"),
METHOD_NOT_ALLOWED(405, "请求方法不支持"),
CONFLICT(409, "资源冲突"),
// 服务端错误
INTERNAL_SERVER_ERROR(500, "服务器内部错误"),
NOT_IMPLEMENTED(501, "未实现的功能"),
BAD_GATEWAY(502, "网关错误"),
SERVICE_UNAVAILABLE(503, "服务不可用"),
GATEWAY_TIMEOUT(504, "网关超时"),
// 数据库错误
DATABASE_ERROR(600, "数据库操作异常"),
DATA_NOT_FOUND(601, "数据未找到"),
DATA_CONFLICT(602, "数据冲突"),
// 业务错误
BUSINESS_ERROR(700, "业务逻辑错误"),
OPERATION_FAILED(701, "操作失败"),
DATA_PROCESSING_ERROR(702, "数据处理异常");
// 状态码
private final int code;
// 状态码描述信息
private final String message;
// 构造函数私有化,防止外部直接调用
ResultCode(int code, String message) {
this.code = code;
this.message = message;
}
/**
* 获取状态码
*
* @return 状态码
*/
public int getCode() {
return code;
}
/**
* 获取状态码描述信息
*
* @return 描述信息
*/
public String getMessage() {
return message;
}
/**
* 根据状态码获取枚举
*
* @param code 状态码
* @return 对应的枚举类型
*/
public static ResultCode fromCode(int code) {
for (ResultCode resultCode : values()) {
if (resultCode.getCode() == code) {
return resultCode;
}
}
throw new IllegalArgumentException("未知的状态码: " + code);
}
}
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
重要 API 和参数说明:
- getCode():返回枚举中的状态码,类型为
int
。 - getMessage():返回状态码对应的描述信息,类型为
String
。
# 2. 统一返回对象:CommonResult<T>
说明:CommonResult<T>
是一个通用的返回对象,用于封装接口的返回结果。它支持泛型,因此可以适用于不同类型的数据返回。通过统一返回结构,可以确保所有接口返回的数据格式一致,从而简化前后端交互的处理流程。
package com.scholar.springbootscaffolding.common;
import com.scholar.springbootscaffolding.enums.ResultCode;
import lombok.Data;
/**
* @param <T> 返回数据的类型
* @description 通用返回对象,用于统一接口的返回结构
*/
@Data
public class CommonResult<T> {
// 状态码(如 200 成功,400 失败等)
private Integer code;
// 返回消息(如 "操作成功","请求失败" 等)
private String message;
// 返回数据,支持任意类型
private T data;
// 私有构造函数,防止外部直接实例化
private CommonResult(Integer code, String message, T data) {
this.code = code;
this.message = message;
this.data = data;
}
/**
* 成功返回结果(携带数据)
*
* @param data 返回的数据
* @return 通用返回对象
*/
public static <T> CommonResult<T> success(T data) {
return new CommonResult<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
}
/**
* 成功返回结果(不携带数据)
*
* @return 通用返回对象
*/
public static <T> CommonResult<T> success() {
return new CommonResult<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), null);
}
/**
* 失败返回结果
*
* @param message 错误描述
* @return 通用返回对象
*/
public static <T> CommonResult<T> failed(String message) {
return new CommonResult<>(ResultCode.FAILED.getCode(), message, null);
}
/**
* 失败返回结果(使用默认的失败信息)
*
* @return 通用返回对象
*/
public static <T> CommonResult<T> failed() {
return new CommonResult<>(ResultCode.FAILED.getCode(), ResultCode.FAILED.getMessage(), null);
}
/**
* 自定义返回结果(支持自定义状态码和数据)
*
* @param resultCode 状态码枚举
* @param data 返回的数据
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode, T data) {
return new CommonResult<>(resultCode.getCode(), resultCode.getMessage(), data);
}
/**
* 自定义返回结果(不携带数据)
*
* @param resultCode 状态码枚举
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode) {
return new CommonResult<>(resultCode.getCode(), resultCode.getMessage(), null);
}
/**
* 自定义返回结果,支持自定义消息和数据
*
* @param resultCode 状态码枚举
* @param message 自定义的消息
* @param data 返回的数据
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode, String message, T data) {
return new CommonResult<>(resultCode.getCode(), message, data);
}
/**
* 自定义返回结果,支持自定义消息,不携带数据
*
* @param resultCode 状态码枚举
* @param message 自定义的消息
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode, String message) {
return new CommonResult<>(resultCode.getCode(), message, null);
}
}
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
重要 API 和参数说明:
- success(T data):返回成功的结果并携带数据。
- failed(String message):返回失败结果并附带错误消息。
- custom(ResultCode resultCode, T data):支持自定义状态码和数据返回。
# 3. 自定义异常类:CustomException
说明:自定义异常类用于处理业务逻辑中可预见的异常情况,如参数校验失败、权限不足等。通过自定义异常,可以在业务层更好地控制错误处理。
import lombok.Getter;
/**
* @description 自定义异常类,用于处理业务逻辑中的特定异常
*/
@Getter
public class CustomException extends RuntimeException {
private static final long serialVersionUID = 1L;
// 状态码
private final ResultCode resultCode;
/**
* 构造函数,直接传入状态码枚举
*
* @param resultCode 状态码枚举
*/
public CustomException(ResultCode resultCode) {
super(resultCode.getMessage());
this.resultCode = resultCode;
}
/**
* 构造函数,支持自定义错误信息
*
* @param resultCode 状态码枚举
* @param message 自定义错误信息
*/
public CustomException(ResultCode resultCode, String message) {
super(message);
this.resultCode = resultCode;
}
}
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
重要 API 和参数说明:
- getResultCode():返回异常对应的状态码,类型为
ResultCode
。
# 4. 全局异常处理:GlobalExceptionHandler
在全局异常处理器中,处理异常信息的关键在于如何有效地提取和记录异常信息,使开发者能够快速定位问题。通常,我们希望在记录日志时不带过多的堆栈信息,而是聚焦在有助于排查的关键信息上。以下是全局异常处理中可以定制的异常参数及方法的详细说明。
# 4.1.异常信息分类与重要参数
在处理异常时,我们可以将异常信息分为以下几个重要部分:
- 异常类型 (
e.getClass().getSimpleName()
):记录异常的类型,例如NullPointerException
、IllegalArgumentException
等。这有助于快速确定问题的性质。 - 异常消息 (
e.getMessage()
):提取异常的简短描述或原因,这是异常抛出时的核心提示信息。 - 异常堆栈跟踪 (
e.getStackTrace()
):记录异常的堆栈信息通常会产生大量输出,除非是深入调试,否则我们一般避免直接在日志中输出全部堆栈信息。可以只记录堆栈中的关键位置,例如抛出异常的类和方法。
# 4.2 详细异常参数说明
在全局异常处理中,以下是可以提取并定制打印的异常参数:
getClass().getSimpleName()
:获取异常的简单类名,这是异常类型的标识。适合在日志中简单明了地描述异常类型。String exceptionType = e.getClass().getSimpleName(); // 输出:NullPointerException
1getMessage()
:获取异常的消息内容,这是对错误的简要描述。通常是引发异常的主要原因或提示信息。String errorMessage = e.getMessage(); // 输出:空指针异常
1getStackTrace()[0]
:获取异常堆栈中的首个位置,一般表示异常抛出的具体类和方法。相较于打印完整堆栈信息,提取首个堆栈信息可以帮助定位代码中的问题位置。StackTraceElement firstStackTrace = e.getStackTrace()[0]; String location = firstStackTrace.toString(); // 输出:com.example.demo.MyClass.myMethod(MyClass.java:25)
1
2getCause()
:获取引发当前异常的原因(如果有)。这在处理多级异常时尤为有用,可以追溯到最初的异常源头。Throwable rootCause = e.getCause(); String causeMessage = (rootCause != null) ? rootCause.getMessage() : "无根本原因";
1
2getSuppressed()
:获取被抑制的异常信息(如果有)。在 Java 7 之后,异常处理机制支持抑制其他异常,例如在try-with-resources
结构中,资源关闭时抛出的异常会被抑制。Throwable[] suppressedExceptions = e.getSuppressed(); for (Throwable suppressed : suppressedExceptions) { logger.error("被抑制的异常:{}", suppressed.getMessage()); }
1
2
3
4
# 4.3 全局异常处理器中的定制打印
以下是经过定制后的全局异常处理器,简化了异常日志输出,重点记录关键信息,便于快速排查问题:
import com.scholar.springbootscaffolding.common.CommonResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Arrays;
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 处理所有未捕获的异常
*
* @param e 异常对象
* @return 通用返回对象,包含错误信息
*/
@ExceptionHandler(Exception.class)
public CommonResult<Object> handleException(Exception e) {
// 构建结构化的异常日志
String formattedLog = formatExceptionLog(e);
// 打印格式化的异常日志
logger.error(formattedLog);
// 返回通用的失败结果,避免直接暴露异常信息
return CommonResult.failed("系统异常,请联系管理员");
}
// 格式化异常信息
private String formatExceptionLog(Exception e) {
StringBuilder errorLog = new StringBuilder();
errorLog.append("\n\n==================== 异常捕获开始 ====================\n");
// 异常类型
errorLog.append("【异常类型】: ").append(e.getClass().getName()).append("\n");
// 解析异常信息,避免重复 Cause 信息
String filteredMessage = filterDuplicateCause(e.getMessage());
errorLog.append("【异常信息】: ").append(filteredMessage).append("\n");
// 异常发生的位置
StackTraceElement firstElement = e.getStackTrace()[0];
errorLog.append("【发生位置】: ")
.append(firstElement.getClassName())
.append(".")
.append(firstElement.getMethodName())
.append(" (")
.append(firstElement.getFileName())
.append(":")
.append(firstElement.getLineNumber())
.append(")\n");
// 嵌套异常的处理
Throwable cause = e.getCause();
if (cause != null && !cause.getMessage().equals(e.getMessage())) {
errorLog.append("【嵌套异常】: ").append(cause.getClass().getName())
.append(" - ").append(cause.getMessage()).append("\n");
}
// 堆栈信息
errorLog.append("【堆栈信息】: \n");
Arrays.stream(e.getStackTrace())
.limit(5) // 控制输出堆栈信息的深度
.forEach(stackTraceElement -> errorLog.append("\tat ").append(stackTraceElement).append("\n"));
errorLog.append("==================== 异常捕获结束 ====================\n");
return errorLog.toString();
}
// 过滤重复的 Cause 信息
private String filterDuplicateCause(String message) {
if (message == null) {
return "";
}
// 检测并移除重复的 "Cause" 信息
int firstCauseIndex = message.indexOf("Cause:");
if (firstCauseIndex != -1) {
int secondCauseIndex = message.indexOf("Cause:", firstCauseIndex + 6);
if (secondCauseIndex != -1) {
return message.substring(0, secondCauseIndex).trim();
}
}
return message;
}
/**
* 处理自定义业务异常
*
* @param e 自定义异常对象
* @return 统一格式的错误结果
*/
@ExceptionHandler(CustomException.class)
public CommonResult<Object> handleCustomException(CustomException e) {
// 记录自定义异常信息,包含业务相关提示
logger.warn("业务异常 - 信息: {}", e.getMessage());
// 根据自定义异常中的状态码返回特定的错误信息
return CommonResult.custom(e.getResultCode(), e.getMessage());
}
}
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
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# 4. 解读日志输出的结构
假设系统发生了一个 NullPointerException
,上面的异常处理器会生成如下日志输出:
系统异常 - 类型: NullPointerException | 信息: 空指针异常 | 位置: com.example.demo.MyClass.myMethod(MyClass.java:25)
日志包含以下关键信息:
- 异常类型:
NullPointerException
—— 明确问题的类型。 - 异常信息:
空指针异常
—— 异常的简要描述,通常用于提示开发人员问题的本质。 - 异常位置:
com.example.demo.MyClass.myMethod(MyClass.java:25)
—— 异常发生的具体代码位置,有助于定位问题源头。
# 5. 补充说明:如何优化堆栈信息的打印
对于复杂的异常场景,如果需要更多细节,但又不想打印完整堆栈,可以使用以下策略:
记录关键堆栈层级:除了首个堆栈信息,可能需要打印一些重要层级(如首层和最后一层)。
logger.error("异常位置 - 首层: {} | 最底层: {}", e.getStackTrace()[0].toString(), e.getStackTrace()[e.getStackTrace().length - 1].toString() );
1
2
3
4记录异常链条:如果异常是由其他异常引发的,可以递归遍历并记录所有引发的原因。
Throwable cause = e; while (cause != null) { logger.error("引发原因: {} - {}", cause.getClass().getSimpleName(), cause.getMessage()); cause = cause.getCause(); }
1
2
3
4
5
通过以上定制策略,你可以更清晰、更简洁地记录异常信息,避免被冗长的堆栈信息干扰,从而专注于解决关键问题。
# 5. 工具类:ResultUtil
说明:工具类封装了常用的返回结果生成方法,简化了开发过程。它提供了快速生成成功、失败结果的方法,还支持抛出自定义异常。
/**
* @description 结果工具类,简化返回结果的生成
*/
public class ResultUtil {
/**
* 返回成功结果(携带数据)
*
* @param data 返回的数据
* @return 通用返回对象
*/
public static <T> CommonResult<T> success(T data) {
return CommonResult.success(data);
}
/**
* 返回成功结果(不携带数据)
*
* @return 通用返回对象
*/
public static <T> CommonResult<T> success() {
return CommonResult.success();
}
/**
* 返回失败结果(自定义错误信息)
*
* @param message 错误描述
* @return 通用返回对象
*/
public static <T> CommonResult<T> failed(String message) {
return CommonResult.failed(message);
}
/**
* 返回失败结果(使用默认的错误信息)
*
* @return 通用返回对象
*/
public static <T> CommonResult<T> failed() {
return CommonResult.failed();
}
/**
* 返回自定义结果
*
* @param resultCode 状态码枚举
* @param data 返回的数据
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode, T data) {
return CommonResult.custom(resultCode, data);
}
/**
* 返回自定义结果(不携带数据)
*
* @param resultCode 状态码枚举
* @return 通用返回对象
*/
public static <T> CommonResult<T> custom(ResultCode resultCode) {
return CommonResult.custom(resultCode);
}
/**
* 抛出自定义异常
*
* @param resultCode 状态码枚举
* @param message 自定义错误信息
*/
public static void throwCustomException(ResultCode resultCode, String message) {
throw new CustomException(resultCode, message);
}
/**
* 抛出自定义异常(使用默认信息)
*
* @param resultCode 状态码枚举
*/
public static void throwCustomException(ResultCode resultCode) {
throw new CustomException(resultCode);
}
}
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
重要 API 和参数说明:
- success(T data):快速生成成功结果。
- failed(String message):快速生成失败结果。
- throwCustomException(ResultCode resultCode, String message):抛出带有自定义错误信息的异常。
# 6. Controller 层的使用
说明:在实际使用中,Controller 层通过封装好的工具类和异常处理机制,可以快速开发接口,并确保返回数据格式统一。
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @description 示例控制器,用于演示统一返回格式的使用
*/
@RestController
@RequestMapping("/api/example")
public class ExampleController {
/**
* 获取成功结果的示例
*
* @return 通用返回对象
*/
@GetMapping("/success")
public CommonResult<String> getSuccess() {
// 使用工具类生成成功结果
return ResultUtil.success("这是一个成功的响应");
}
/**
* 获取失败结果的示例
*
* @return 通用返回对象
*/
@GetMapping("/failed")
public CommonResult<String> getFailed() {
// 使用工具类生成失败结果
return ResultUtil.failed("这是一个失败的响应");
}
/**
* 触发自定义异常的示例
*
* @return 不返回数据,抛出异常
*/
@GetMapping("/exception")
public CommonResult<String> throwException() {
// 直接抛出自定义异常
ResultUtil.throwCustomException(ResultCode.VALIDATE_FAILED, "参数校验不通过");
return null; // 代码不会执行到这里
}
}
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
重要 API 和参数说明:CommonResult<T>
中的泛型 T
决定了 data
字段的数据类型。
@GetMapping("/success")
:演示成功结果的返回,返回的数据为"这是一个成功的响应"
。@GetMapping("/failed")
:演示失败结果的返回,返回的数据为"这是一个失败的响应"
。@GetMapping("/exception")
:演示自定义异常的抛出,触发参数校验失败的错误。
总结
通过以上封装和示例,您可以实现:
- 统一的返回格式:所有接口的返回数据都遵循相同的格式,包含状态码、消息和数据。
- 状态码的集中管理:通过枚举类确保状态码及其描述信息的一致性。
- 自定义异常处理:在业务逻辑中抛出自定义异常,并在全局异常处理器中进行统一捕获和处理。
- 简化开发流程:通过工具类
ResultUtil
,快速生成返回结果或抛出异常。
# 统一返回格式示例说明
所有接口返回的数据格式为一个 JSON 对象,包含以下字段:
- code:状态码,表示请求的处理结果。
- message:返回的消息,通常用于描述当前操作的结果信息。
- data:返回的数据,内容根据具体请求有所不同。
格式如下:
{
"code": "状态码",
"message": "描述信息",
"data": "实际返回的数据"
}
2
3
4
5
# 1. 成功返回示例
当请求成功且有数据返回时,接口会返回以下格式:
代码:
@GetMapping("/success")
public CommonResult<String> getSuccess() {
return ResultUtil.success("这是一个成功的响应");
}
2
3
4
返回给前端的格式:
{
"code": 200,
"message": "操作成功",
"data": "这是一个成功的响应"
}
2
3
4
5
- code:
200
表示操作成功。 - message:
操作成功
是默认的成功描述信息。 - data: 实际返回的业务数据,这里是
"这是一个成功的响应"
。
如果请求成功但没有数据需要返回:
{
"code": 200,
"message": "操作成功",
"data": null
}
2
3
4
5
# 2. 失败返回示例
当请求失败时,接口会返回以下格式:
代码:
@GetMapping("/failed")
public CommonResult<String> getFailed() {
return ResultUtil.failed("这是一个失败的响应");
}
2
3
4
返回给前端的格式:
{
"code": 400,
"message": "这是一个失败的响应",
"data": null
}
2
3
4
5
- code:
400
表示请求失败。 - message: 自定义的失败描述信息,这里是
"这是一个失败的响应"
。 - data: 在失败场景中通常为
null
。
# 3. 自定义状态码返回示例
当需要返回自定义的状态码和描述信息时:
代码:
@GetMapping("/custom")
public CommonResult<String> getCustom() {
return CommonResult.custom(ResultCode.FORBIDDEN, "您没有访问权限");
}
2
3
4
返回给前端的格式:
{
"code": 403,
"message": "您没有访问权限",
"data": null
}
2
3
4
5
- code:
403
表示没有权限。 - message: 自定义的错误信息,这里是
"您没有访问权限"
。 - data: 这里没有返回数据,因此为
null
。
# 4. 自定义异常返回示例
当在业务中抛出自定义异常时,返回格式如下:
代码:
@GetMapping("/exception")
public CommonResult<String> throwException() {
ResultUtil.throwCustomException(ResultCode.VALIDATE_FAILED, "参数校验不通过");
return null; // 代码不会执行到这里
}
2
3
4
5
返回给前端的格式:
{
"code": 404,
"message": "参数校验不通过",
"data": null
}
2
3
4
5
- code:
404
表示参数校验失败。 - message: 这里是自定义的错误信息
"参数校验不通过"
。 - data: 同样在异常情况下,通常为
null
。
# 5. 全局异常处理返回示例
当系统出现未捕获的异常时,返回的格式如下:
代码:(这里假设未捕获的异常发生在服务中)
返回给前端的格式:
{
"code": 400,
"message": "具体的异常信息",
"data": null
}
2
3
4
5
- code:
400
表示请求失败,这是默认的全局异常状态码。 - message: 异常的具体信息,由
Exception
的消息内容决定。 - data: 异常情况下,返回的数据通常为
null
。