一个健壮的后端系统,异常处理至关重要。没有统一的异常处理,接口返回格式混乱,前端难以处理,排查问题困难。本文介绍如何构建一套完整的全局异常处理与统一响应框架。
@ControllerAdvice 核心原理 @ControllerAdvice 是 Spring 3.2 引入的注解,用于实现全局异常处理。
原理 @ControllerAdvice 实际上是一个@Component,被所有 @Controller 共享。它的增强方法会在 @Controller 方法被调用前后执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Component public @interface ControllerAdvice { @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 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}) @ExceptionHandler
异常处理优先级 Spring 会匹配最具体 的异常类型:
1 2 3 4 5 6 7 8 @ExceptionHandler(NullPointerException.class) @ExceptionHandler(RuntimeException.class) @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) { return returnType.getParameterType().isAssignableFrom(Result.class); } @Override public Object beforeBodyWrite (Object body, MethodParameter returnType, MediaType contentType, Class converterType, ServerHttpRequest request, ServerHttpResponse response) { 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 user.not.found =用户不存在 param.invalid =参数错误:{0} 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(); 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 () { 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); } @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
业务异常携带错误码
设计原则:
统一格式 :所有接口返回相同结构
错误码规范 :定义清晰的错误码体系
日志记录 :区分警告(业务异常)和错误(系统异常)
信息安全 :对外不暴露内部异常细节
可追踪 :添加 traceId 便于排查