《别再到处try-catch了!SpringBoot全局异常处理这样设计》
每天5分钟,掌握一个SpringBoot核心知识点。大家好,我是SpringBoot指南的创始人小明。昨天我们讲解了自动配置,今天来聊聊异常处理。 零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取 :关注公众号:小坏睡说Java
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
引言
想象一下这样的场景:你的项目有50个Controller,每个Controller方法里都有类似的try-catch代码,而且每个开发者处理异常的方式都不一样。线上出了问题,日志里要么没有记录,要么记录得乱七八糟...
这就是为什么我们需要统一的全局异常处理。今天,我将带你设计一个优雅的全局异常处理方案,让你的代码干净、统一、易于维护。
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
先看两个对比:
❌ 混乱的异常处理方式:
java
@RestController
public class UserController {
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserDTO user) {
try {
User registered = userService.register(user);
return ResponseEntity.ok(registered);
} catch (ValidationException e) {
return ResponseEntity.badRequest().body("参数错误:" + e.getMessage());
} catch (DuplicateException e) {
return ResponseEntity.status(409).body("用户已存在");
} catch (Exception e) {
log.error("注册失败", e);
return ResponseEntity.status(500).body("系统繁忙");
}
}
@GetMapping("/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
try {
User user = userService.findById(id);
return ResponseEntity.ok(user);
} catch (NotFoundException e) {
return ResponseEntity.status(404).body("用户不存在");
} catch (Exception e) {
log.error("查询用户失败", e);
return ResponseEntity.status(500).body("系统错误");
}
}
// ... 其他方法重复类似的模式
}
✅ 优雅的全局异常处理:
java
@RestController
public class UserController {
@PostMapping("/register")
public ResponseResult<User> register(@RequestBody UserDTO user) {
User registered = userService.register(user);
return ResponseResult.success(registered);
}
@GetMapping("/{id}")
public ResponseResult<User> getUser(@PathVariable Long id) {
User user = userService.findById(id);
return ResponseResult.success(user);
}
// ... 干净的业务代码,不包含任何异常处理逻辑
}
所有的异常都在一个地方统一处理!下面,让我们一步步实现这个目标。
一、SpringBoot异常处理核心注解
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
1. @ControllerAdvice:全局异常处理的基石
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface ControllerAdvice {
// 可以指定要处理的包
String[] value() default {};
// 指定要处理的Controller类型
Class<?>[] basePackageClasses() default {};
// 指定要处理的注解
Class<? extends Annotation>[] annotations() default {};
}
三种常见用法:
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
java
// 方式1:处理所有Controller的异常(最常用)
@ControllerAdvice
public class GlobalExceptionHandler {
// 处理逻辑...
}
// 方式2:只处理指定包下的Controller
@ControllerAdvice("com.example.user")
public class UserExceptionHandler {
// 只处理user模块的异常
}
// 方式3:只处理带有特定注解的Controller
@ControllerAdvice(annotations = RestController.class)
public class RestExceptionHandler {
// 只处理@RestController注解的类
}
2. @RestControllerAdvice:更便捷的选择
@RestControllerAdvice 是 @ControllerAdvice 和 @ResponseBody 的组合,专门用于REST API开发。
java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
// 同@ControllerAdvice
}
3. @ExceptionHandler:指定处理什么异常
java
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
// 指定要处理的异常类型
Class<? extends Throwable>[] value() default {};
}
二、设计优雅的异常体系
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
1. 异常分类策略
php
Throwable
├── Error(系统错误,如OutOfMemoryError,通常不捕获)
└── Exception(异常)
├── RuntimeException(运行时异常)
│ ├── BusinessException(自定义业务异常)
│ │ ├── ValidationException(参数校验异常)
│ │ ├── DuplicateException(数据重复异常)
│ │ ├── NotFoundException(数据不存在异常)
│ │ └── PermissionException(权限异常)
│ └── SystemException(自定义系统异常)
└── 其他Checked Exception(已检查异常)
2. 基础异常类设计
java
// 基础业务异常
public class BusinessException extends RuntimeException {
private final int code;
private final String message;
public BusinessException(int code, String message) {
super(message);
this.code = code;
this.message = message;
}
public BusinessException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.code = errorCode.getCode();
this.message = errorCode.getMessage();
}
// getters...
}
// 错误码枚举
public enum ErrorCode {
// 系统错误
SUCCESS(200, "成功"),
SYSTEM_ERROR(500, "系统错误"),
// 业务错误
PARAM_VALID_ERROR(10001, "参数校验失败"),
DATA_NOT_FOUND(10002, "数据不存在"),
DATA_DUPLICATE(10003, "数据已存在"),
PERMISSION_DENIED(10004, "权限不足"),
USER_NOT_LOGIN(10005, "用户未登录"),
// 第三方服务错误
THIRD_PARTY_ERROR(20001, "第三方服务异常");
private final int code;
private final String message;
ErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
// getters...
}
// 具体的业务异常
public class ValidationException extends BusinessException {
public ValidationException(String message) {
super(ErrorCode.PARAM_VALID_ERROR.getCode(), message);
}
}
public class NotFoundException extends BusinessException {
public NotFoundException(String message) {
super(ErrorCode.DATA_NOT_FOUND.getCode(), message);
}
}
三、统一响应体设计
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
1. 响应体基础结构
java
// 统一响应体
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ResponseResult<T> {
private int code; // 状态码
private String message; // 消息
private T data; // 数据
private long timestamp; // 时间戳
public static <T> ResponseResult<T> success() {
return success(null);
}
public static <T> ResponseResult<T> success(T data) {
return ResponseResult.<T>builder()
.code(ErrorCode.SUCCESS.getCode())
.message(ErrorCode.SUCCESS.getMessage())
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> ResponseResult<T> fail(int code, String message) {
return ResponseResult.<T>builder()
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
public static <T> ResponseResult<T> fail(ErrorCode errorCode) {
return fail(errorCode.getCode(), errorCode.getMessage());
}
}
四、实现全局异常处理器
1. 完整异常处理器实现
java
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 处理业务异常
*/
@ExceptionHandler(BusinessException.class)
public ResponseResult<Void> handleBusinessException(BusinessException e) {
log.warn("业务异常:code={}, message={}", e.getCode(), e.getMessage());
return ResponseResult.fail(e.getCode(), e.getMessage());
}
/**
* 处理参数校验异常(@Validated触发的异常)
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseResult<Void> handleMethodArgumentNotValidException(
MethodArgumentNotValidException e) {
String message = e.getBindingResult().getAllErrors().stream()
.map(DefaultMessageSourceResolvable::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("参数校验失败:{}", message);
return ResponseResult.fail(ErrorCode.PARAM_VALID_ERROR.getCode(), message);
}
/**
* 处理请求参数绑定异常(类型转换错误等)
*/
@ExceptionHandler(BindException.class)
public ResponseResult<Void> handleBindException(BindException e) {
String message = e.getBindingResult().getAllErrors().stream()
.map(ObjectError::getDefaultMessage)
.collect(Collectors.joining(", "));
log.warn("参数绑定失败:{}", message);
return ResponseResult.fail(ErrorCode.PARAM_VALID_ERROR.getCode(), message);
}
/**
* 处理参数解析异常(JSON解析错误等)
*/
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseResult<Void> handleHttpMessageNotReadableException(
HttpMessageNotReadableException e) {
log.warn("请求参数解析失败", e);
return ResponseResult.fail(ErrorCode.PARAM_VALID_ERROR.getCode(), "请求参数格式错误");
}
/**
* 处理404异常
*/
@ExceptionHandler(NoHandlerFoundException.class)
public ResponseResult<Void> handleNoHandlerFoundException(
NoHandlerFoundException e) {
log.warn("接口不存在:{} {}", e.getHttpMethod(), e.getRequestURL());
return ResponseResult.fail(404, "接口不存在");
}
/**
* 处理请求方法不支持异常
*/
@ExceptionHandler(HttpRequestMethodNotSupportedException.class)
public ResponseResult<Void> handleHttpRequestMethodNotSupportedException(
HttpRequestMethodNotSupportedException e) {
log.warn("请求方法不支持:{},支持的方法:{}",
e.getMethod(), e.getSupportedHttpMethods());
return ResponseResult.fail(405, "请求方法不支持");
}
/**
* 处理所有未捕获的异常
*/
@ExceptionHandler(Exception.class)
public ResponseResult<Void> handleException(Exception e) {
log.error("系统异常", e);
// 生产环境返回模糊的错误信息,开发环境返回详细错误
String message = isProduction() ? "系统繁忙,请稍后重试" : e.getMessage();
return ResponseResult.fail(ErrorCode.SYSTEM_ERROR.getCode(), message);
}
private boolean isProduction() {
String env = System.getProperty("spring.profiles.active");
return "prod".equals(env);
}
}
2. 增强版:带异常日志记录的处理器
java
@Slf4j
@RestControllerAdvice
public class EnhancedGlobalExceptionHandler {
/**
* 异常信息上下文,用于记录额外信息
*/
private static final ThreadLocal<Map<String, Object>> exceptionContext =
ThreadLocal.withInitial(HashMap::new);
/**
* 添加上下文信息(可在业务代码中调用)
*/
public static void addContext(String key, Object value) {
exceptionContext.get().put(key, value);
}
/**
* 清理上下文(在过滤器或拦截器中调用)
*/
public static void clearContext() {
exceptionContext.remove();
}
@ExceptionHandler(BusinessException.class)
public ResponseResult<Void> handleBusinessException(
BusinessException e, HttpServletRequest request) {
Map<String, Object> context = exceptionContext.get();
log.warn("业务异常 | 路径:{} | 参数:{} | 错误码:{} | 消息:{} | 上下文:{}",
request.getRequestURI(),
getRequestParams(request),
e.getCode(),
e.getMessage(),
context);
clearContext();
return ResponseResult.fail(e.getCode(), e.getMessage());
}
@ExceptionHandler(Exception.class)
public ResponseResult<Void> handleException(
Exception e, HttpServletRequest request) {
Map<String, Object> context = exceptionContext.get();
log.error("系统异常 | 路径:{} | 参数:{} | 上下文:{}",
request.getRequestURI(),
getRequestParams(request),
context,
e);
clearContext();
return ResponseResult.fail(ErrorCode.SYSTEM_ERROR.getCode(),
isProduction() ? "系统繁忙" : e.getMessage());
}
private String getRequestParams(HttpServletRequest request) {
Map<String, String[]> params = request.getParameterMap();
return params.entrySet().stream()
.map(entry -> entry.getKey() + "=" +
Arrays.toString(entry.getValue()))
.collect(Collectors.joining(", "));
}
// ... 其他异常处理方法
}
五、最佳实践与高级用法
1. 参数校验的优雅处理
传统方式:
java
@RestController
public class UserController {
@PostMapping("/user")
public ResponseResult<User> createUser(@Valid @RequestBody UserDTO user) {
// 需要手动处理BindingResult
// 或者依赖全局异常处理器
}
}
增强方式:自定义校验注解
java
// 自定义手机号校验注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = PhoneValidator.class)
public @interface Phone {
String message() default "手机号格式错误";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
// 实现校验逻辑
public class PhoneValidator implements ConstraintValidator<Phone, String> {
private static final Pattern PHONE_PATTERN =
Pattern.compile("^1[3-9]\\d{9}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) {
return true; // 由@NotNull处理空值
}
return PHONE_PATTERN.matcher(value).matches();
}
}
// 使用方式
@Data
public class UserDTO {
@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度2-20")
private String username;
@Phone // 使用自定义注解
private String phone;
}
2. 多环境差异化处理
yaml
# application.yml
spring:
profiles:
active: dev
app:
exception:
# 开发环境返回详细错误
detail-in-dev: true
# 生产环境屏蔽堆栈
hide-stack-in-prod: true
java
@ConfigurationProperties(prefix = "app.exception")
@Data
public class ExceptionProperties {
private boolean detailInDev = true;
private boolean hideStackInProd = true;
}
@RestControllerAdvice
public class EnvironmentAwareExceptionHandler {
@Autowired
private ExceptionProperties properties;
@ExceptionHandler(Exception.class)
public ResponseResult<Void> handleException(Exception e,
HttpServletRequest request) {
// 记录异常日志(生产环境记录简要,开发环境记录详细)
if (isProduction()) {
log.error("系统异常 | 路径:{} | 错误:{}",
request.getRequestURI(), e.getMessage());
} else {
log.error("系统异常 | 路径:{}", request.getRequestURI(), e);
}
// 构建响应
ErrorResponse error = new ErrorResponse();
error.setCode(ErrorCode.SYSTEM_ERROR.getCode());
error.setMessage(buildErrorMessage(e));
if (!isProduction() && properties.isDetailInDev()) {
error.setDetail(e.getMessage());
error.setPath(request.getRequestURI());
error.setException(e.getClass().getName());
}
if (!isProduction() || !properties.isHideStackInProd()) {
error.setTrace(getStackTrace(e));
}
return ResponseResult.fail(error);
}
private String buildErrorMessage(Exception e) {
if (isProduction()) {
return "系统繁忙,请稍后重试";
}
return e.getMessage();
}
}
3. 集成Sentinel进行流控异常处理
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
java
@Component
public class SentinelExceptionHandler {
@ExceptionHandler(BlockException.class)
public ResponseResult<Void> handleBlockException(BlockException e) {
String message = "系统繁忙,请稍后重试";
if (e instanceof FlowException) {
message = "接口限流,请稍后重试";
} else if (e instanceof DegradeException) {
message = "接口降级,请稍后重试";
} else if (e instanceof ParamFlowException) {
message = "热点参数限流";
} else if (e instanceof SystemBlockException) {
message = "系统保护(CPU/负载)";
} else if (e instanceof AuthorityException) {
message = "授权规则不通过";
}
log.warn("Sentinel流控:{}", message);
return ResponseResult.fail(429, message);
}
}
六、完整示例:电商系统异常处理实战
1. 业务异常定义
java
// 库存不足异常
public class InsufficientStockException extends BusinessException {
private final Long productId;
private final Integer requested;
private final Integer available;
public InsufficientStockException(Long productId,
Integer requested, Integer available) {
super(ErrorCode.INSUFFICIENT_STOCK);
this.productId = productId;
this.requested = requested;
this.available = available;
}
@Override
public String getMessage() {
return String.format("商品[%d]库存不足,需要%d,可用%d",
productId, requested, available);
}
}
// 订单状态异常
public class OrderStateException extends BusinessException {
private final Long orderId;
private final String currentState;
private final String requiredState;
public OrderStateException(Long orderId,
String currentState, String requiredState) {
super(ErrorCode.ORDER_STATE_ERROR);
this.orderId = orderId;
this.currentState = currentState;
this.requiredState = requiredState;
}
}
2. 业务使用示例
java
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {
@Override
@Transactional
public Order createOrder(OrderCreateDTO dto) {
// 参数校验(会自动抛出MethodArgumentNotValidException)
validateOrder(dto);
try {
// 检查库存
checkStock(dto);
// 创建订单
Order order = buildOrder(dto);
orderRepository.save(order);
// 扣减库存
reduceStock(dto);
// 发送创建订单事件
eventPublisher.publishEvent(new OrderCreatedEvent(order));
return order;
} catch (InsufficientStockException e) {
// 添加上下文信息,便于日志记录
EnhancedGlobalExceptionHandler.addContext("orderDTO", dto);
throw e;
}
}
private void checkStock(OrderCreateDTO dto) {
dto.getItems().forEach(item -> {
Integer available = stockService.getAvailableStock(item.getProductId());
if (available < item.getQuantity()) {
throw new InsufficientStockException(
item.getProductId(),
item.getQuantity(),
available);
}
});
}
private void validateOrder(OrderCreateDTO dto) {
if (dto.getItems() == null || dto.getItems().isEmpty()) {
throw new ValidationException("订单商品不能为空");
}
if (dto.getTotalAmount() == null || dto.getTotalAmount().compareTo(BigDecimal.ZERO) <= 0) {
throw new ValidationException("订单金额必须大于0");
}
}
}
3. Controller层示例
java
@RestController
@RequestMapping("/orders")
@Validated
@Slf4j
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping
public ResponseResult<Order> createOrder(
@Valid @RequestBody OrderCreateDTO dto) {
log.info("创建订单,用户:{},商品数量:{}",
dto.getUserId(), dto.getItems().size());
Order order = orderService.createOrder(dto);
return ResponseResult.success(order);
}
@PostMapping("/{orderId}/cancel")
public ResponseResult<Void> cancelOrder(@PathVariable Long orderId) {
orderService.cancelOrder(orderId);
return ResponseResult.success();
}
@GetMapping("/{orderId}")
public ResponseResult<Order> getOrder(@PathVariable Long orderId) {
Order order = orderService.getOrder(orderId);
return ResponseResult.success(order);
}
}
七、测试你的异常处理器
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
1. 单元测试
java
@SpringBootTest
@AutoConfigureMockMvc
class GlobalExceptionHandlerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testBusinessException() throws Exception {
mockMvc.perform(get("/api/test/business-error"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(10001))
.andExpect(jsonPath("$.message").value("业务异常测试"));
}
@Test
void testValidationException() throws Exception {
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"\"}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(10001))
.andExpect(jsonPath("$.message").contains("不能为空"));
}
@Test
void testSystemException() throws Exception {
mockMvc.perform(get("/api/test/system-error"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(500));
}
}
2. 集成测试异常处理器
java
@WebMvcTest(OrderController.class)
@Import(GlobalExceptionHandler.class)
class OrderControllerExceptionTest {
@MockBean
private OrderService orderService;
@Autowired
private MockMvc mockMvc;
@Test
void createOrder_InsufficientStock() throws Exception {
// 模拟库存不足异常
when(orderService.createOrder(any()))
.thenThrow(new InsufficientStockException(1L, 10, 5));
mockMvc.perform(post("/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{...}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(ErrorCode.INSUFFICIENT_STOCK.getCode()))
.andExpect(jsonPath("$.message").value("商品[1]库存不足,需要10,可用5"));
}
}
八、个人建议性能与注意事项
资源获取 :关注公众号:小坏睡说Java
1. 性能考虑
- 异常创建开销:异常对象的创建比普通对象开销大
- 栈追踪开销:
fillInStackTrace()方法比较耗时 - 建议:对于频繁发生的业务异常,考虑使用错误码而非异常
总结
今天我们一起构建了一个完整的全局异常处理体系:
- 核心机制 :
@RestControllerAdvice+@ExceptionHandler - 异常体系:业务异常与系统异常分离
- 统一响应:标准化的响应格式
- 日志记录:完整的异常上下文信息
- 多环境适配:开发/生产环境差异化处理
- 最佳实践:参数校验、Sentinel集成等
通过全局异常处理,我们实现了:
- ✅ 代码整洁:Controller层只有业务逻辑
- ✅ 统一格式:所有异常响应格式一致
- ✅ 易于维护:异常处理逻辑集中管理
- ✅ 便于监控:异常日志完整记录
今日思考题
在你的项目中,有没有遇到过以下情况:
- 某个第三方接口频繁超时,需要特殊处理
- 不同微服务之间的异常需要统一处理
- 需要根据用户类型返回不同的错误信息
你是如何解决这些问题的?欢迎在评论区分享你的经验!
下期预告:明天我们将探讨《SpringBoot配置文件:一个注解搞定多环境配置!》,学习如何优雅管理不同环境的配置。
互动时间:你在异常处理中遇到过哪些"坑"?或者有哪些更好的实践建议?
资源获取 :关注公众号:小坏睡说Java 回复"异常处理源码",获取本文所有示例代码及完整项目。