Spring Boot 全局异常处理与统一响应封装

一个健壮的后端系统,异常处理至关重要。没有统一的异常处理,接口返回格式混乱,前端难以处理,排查问题困难。本文介绍如何构建一套完整的全局异常处理与统一响应框架。

@ControllerAdvice 核心原理

@ControllerAdvice 是 Spring 3.2 引入的注解,用于实现全局异常处理。

原理

@ControllerAdvice 实际上是一个@Component,被所有 @Controller 共享。它的增强方法会在 @Controller 方法被调用前后执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 本质上是一个切面(Aspect)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
// 可以指定增强哪些 Controller
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@interface Selectors {
ControllerAdvice selector...
}
}

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestControllerAdvice  // = @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {

@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}

@ExceptionHandler(Exception.class)
public Result<?> handleException(Exception e) {
log.error("系统异常", e);
return Result.error("系统繁忙,请稍后重试");
}
}

@ExceptionHandler 处理异常

@ExceptionHandler 指定处理哪种异常。

基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(value = BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
return Result.error(e.getCode(), e.getMessage());
}

@ExceptionHandler(value = NullPointerException.class)
public Result<Void> handleNullPointer(NullPointerException e) {
return Result.error("空指针异常");
}

@ExceptionHandler(value = Exception.class)
public Result<Void> handleGeneral(Exception e) {
return Result.error("未知错误");
}
}

异常匹配规则

1
2
3
4
5
@ExceptionHandler(value = {AException.class, BException.class})
// 匹配 AException 或 BException

@ExceptionHandler // 不指定 value,默认处理所有未匹配的异常
// 建议放在最后,作为兜底

异常处理优先级

Spring 会匹配最具体的异常类型:

1
2
3
4
5
6
7
8
// 1. 先匹配这个(最具体)
@ExceptionHandler(NullPointerException.class)

// 2. 再匹配这个
@ExceptionHandler(RuntimeException.class)

// 3. 最后匹配这个(最通用)
@ExceptionHandler(Exception.class)

@ResponseBodyAdvice 统一响应

有时候不仅需要处理异常,还需要统一修改响应格式,比如给所有响应加上时间戳、签名等。

ResponseBodyAdvice 接口

1
2
3
4
5
6
7
8
9
10
public interface ResponseBodyAdvice<T> {
// 是否支持处理
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

// 修改响应体
T beforeBodyWrite(T body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}

统一响应封装

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
@RestControllerAdvice
public class ResponseWrapper implements ResponseBodyAdvice<Object> {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 只处理返回 Result 类型的方法
return returnType.getParameterType().isAssignableFrom(Result.class);
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType contentType,
Class converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 确保已经是 Result 类型
if (body instanceof Result) {
Result<?> result = (Result<?>) body;
// 补充时间戳
result.setTimestamp(System.currentTimeMillis());
return result;
}
return body;
}
}

业务异常设计

业务异常应该包含错误码错误信息

错误码定义

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
public interface ErrorCode {
int getCode();
String getMessage();
}

public enum CommonError implements ErrorCode {
SUCCESS(0, "成功"),
PARAM_ERROR(400, "参数错误"),
NOT_FOUND(404, "资源不存在"),
UNAUTHORIZED(401, "未授权"),
FORBIDDEN(403, "禁止访问"),
SERVER_ERROR(500, "服务器错误");

private final int code;
private final String message;

CommonError(int code, String message) {
this.code = code;
this.message = message;
}

@Override
public int getCode() {
return code;
}

@Override
public String getMessage() {
return 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
public class BusinessException extends RuntimeException implements ErrorCode {

private final int code;

public BusinessException(int code, String message) {
super(message);
this.code = code;
}

public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
}

public BusinessException(ErrorCode errorCode, String message) {
super(message);
this.code = errorCode.getCode();
}

@Override
public int getCode() {
return code;
}
}

统一响应类

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
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {

private int code;
private String message;
private T data;
private long timestamp;

public static <T> Result<T> ok() {
return new Result<>(0, "成功", null, System.currentTimeMillis());
}

public static <T> Result<T> ok(T data) {
return new Result<>(0, "成功", data, System.currentTimeMillis());
}

public static <T> Result<T> error(int code, String message) {
return new Result<>(code, message, null, System.currentTimeMillis());
}

public static <T> Result<T> error(ErrorCode errorCode) {
return new Result<>(errorCode.getCode(), errorCode.getMessage(),
null, System.currentTimeMillis());
}

public static <T> Result<T> error(ErrorCode errorCode, String message) {
return new Result<>(errorCode.getCode(), message, null,
System.currentTimeMillis());
}
}

实际使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserService {

@Autowired
private UserMapper userMapper;

public User getUser(Long userId) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BusinessException(CommonError.NOT_FOUND, "用户不存在");
}
return user;
}

public void updateUser(Long userId, UserUpdateRequest request) {
if (request.getAge() != null && request.getAge() < 0) {
throw new BusinessException(CommonError.PARAM_ERROR, "年龄不能为负数");
}
// ...
}
}

异常信息国际化

支持多语言错误信息。

配置

1
2
3
4
spring:
messages:
basename: i18n/messages
encoding: UTF-8

资源文件

1
2
3
4
5
6
7
# i18n/messages_zh_CN.properties
user.not.found=用户不存在
param.invalid=参数错误:{0}

# i18n/messages_en_US.properties
user.not.found=User not found
param.invalid=Invalid parameter: {0}

国际化异常处理

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
public class GlobalExceptionHandler {

@Autowired
private MessageSource messageSource;

@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusinessException(BusinessException e,
HttpServletRequest request) {
String message = e.getMessage();

// 如果是国际化 key,解析
if (e.getMessage().startsWith("i18n:")) {
String key = e.getMessage().substring(5);
Locale locale = request.getLocale();
message = messageSource.getMessage(key, null, locale);
}

return Result.error(e.getCode(), message);
}
}

// 使用
throw new BusinessException(CommonError.NOT_FOUND, "i18n:user.not.found");

实战:构建统一响应框架

完整代码结构

1
2
3
4
5
6
7
8
9
10
├── result
│ ├── Result.java # 统一响应类
│ ├── ResultFactory.java # 响应工厂
│ └── ResultCode.java # 响应码接口
├── exception
│ ├── BusinessException.java
│ ├── ValidationException.java
│ └── GlobalExceptionHandler.java
└── config
└── WebConfig.java # 配置类

Result.java

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
@Data
public class Result<T> {

private int code;
private String message;
private T data;
private long timestamp;
private String traceId;

public static <T> Result<T> ok() {
return ok(null);
}

public static <T> Result<T> ok(T data) {
return success(0, "成功", data);
}

public static <T> Result<T> success(int code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
result.setTraceId(getTraceId());
return result;
}

public static <T> Result<T> error(int code, String message) {
return error(code, message, null);
}

public static <T> Result<T> error(int code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
result.setTimestamp(System.currentTimeMillis());
result.setTraceId(getTraceId());
return result;
}

private static String getTraceId() {
// 从 ThreadLocal 或 MDC 获取
return TraceContext.getTraceId();
}
}

GlobalExceptionHandler.java

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
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

// 业务异常
@ExceptionHandler(BusinessException.class)
public Result<Void> handleBusiness(BusinessException e) {
log.warn("业务异常: {} - {}", e.getCode(), e.getMessage());
return Result.error(e.getCode(), e.getMessage());
}

// 参数校验异常
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<Void> handleValidation(MethodArgumentNotValidException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(", "));
return Result.error(CommonError.PARAM_ERROR.getCode(), message);
}

// 绑定异常
@ExceptionHandler(BindException.class)
public Result<Void> handleBind(BindException e) {
String message = e.getBindingResult().getFieldErrors().stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.error(CommonError.PARAM_ERROR.getCode(), message);
}

// 404 异常
@ExceptionHandler(NoHandlerFoundException.class)
public Result<Void> handleNotFound(NoHandlerFoundException e) {
return Result.error(404, "接口不存在");
}

// 限流异常
@ExceptionHandler(RateLimitException.class)
public Result<Void> handleRateLimit(RateLimitException e) {
return Result.error(429, "请求过于频繁,请稍后重试");
}

// 其他异常
@ExceptionHandler(Exception.class)
public Result<Void> handleException(Exception e) {
log.error("系统异常", e);
return Result.error(CommonError.SERVER_ERROR.getCode(), "系统繁忙,请稍后重试");
}
}

TraceId 中间件

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
@Component
public class TraceIdFilter extends OncePerRequestFilter {

private static final String TRACE_ID_HEADER = "X-Trace-Id";

@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
String traceId = request.getHeader(TRACE_ID_HEADER);
if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}

TraceContext.setTraceId(traceId);
response.setHeader(TRACE_ID_HEADER, traceId);

try {
chain.doFilter(request, response);
} finally {
TraceContext.clear();
}
}
}

响应示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 成功
{
"code": 0,
"message": "成功",
"data": {
"id": 1,
"name": "张三"
},
"timestamp": 1712758400000,
"traceId": "550e8400-e29b-41d4-a716-446655440000"
}

// 失败
{
"code": 404,
"message": "用户不存在",
"data": null,
"timestamp": 1712758400000,
"traceId": "550e8400-e29b-41d4-a716-446655440000"
}

总结

全局异常处理框架的核心要点:

组件 职责
@ControllerAdvice 全局异常增强
@ExceptionHandler 匹配并处理特定异常
@ResponseBodyAdvice 统一修改响应格式
Result 统一响应结构
ErrorCode 规范化错误码
BusinessException 业务异常携带错误码

设计原则:

  1. 统一格式:所有接口返回相同结构
  2. 错误码规范:定义清晰的错误码体系
  3. 日志记录:区分警告(业务异常)和错误(系统异常)
  4. 信息安全:对外不暴露内部异常细节
  5. 可追踪:添加 traceId 便于排查