Spring Boot 中异常捕获与处理指南

221 阅读2分钟

目录

  1. 背景
  2. Java 异常分类
  3. 示例场景
  4. 是否需要显式捕获异常?
  5. 是否需要显式往外抛出异常?
  6. 哪些异常需要捕获?
  7. 最佳实践
  8. 总结

1. 背景

在 Spring Boot 开发中,异常处理是确保应用程序健壮性和用户体验的关键部分。本文档通过示例代码,讲解异常捕获和抛出的原则。


2. Java 异常分类

异常类型描述是否必须捕获例子
Checked Exception继承自 Exception(但不是 RuntimeException),编译时强制处理是(try-catch 或 throws)IOException, SQLException
Unchecked Exception继承自 RuntimeException,运行时异常,不强制处理IllegalArgumentException, NullPointerException

3. 示例场景

3.1 服务层代码

@Service
public class OrderService {
    public String getOrderDetails(String orderId) {
        if (orderId == null) {
            throw new IllegalArgumentException("订单 ID 不能为空");
        }
        return "Order: " + orderId;
    }
}

3.2 控制器代码

@RestController
@RequestMapping("/api")
public class OrderController {
    @Autowired
    private OrderService orderService;

    @GetMapping("/orders/{id}")
    public String getOrder(@PathVariable String id) {
        return orderService.getOrderDetails(id); // 未显式捕获
    }
}

4. 是否需要显式捕获异常?

4.1 不需要显式捕获的情况

  • 默认处理:Spring Boot 返回 500:

    {"status": 500, "error": "Internal Server Error", "message": "订单 ID 不能为空"}
    
  • 全局处理:已有 @ControllerAdvice。

4.2 需要显式捕获的情况

  • 自定义响应

    @GetMapping("/orders/{id}")
    public ResponseEntity<String> getOrder(@PathVariable String id) {
        try {
            return ResponseEntity.ok(orderService.getOrderDetails(id));
        } catch (IllegalArgumentException e) {
            return ResponseEntity.badRequest().body("错误: " + e.getMessage());
        }
    }
    

5. 是否需要显式往外抛出异常?

5.1 什么时候需要抛出?

  • 未检查异常(如 IllegalArgumentException)

    • 需要抛出:表示方法无法继续执行,通知调用者问题。

    • 无需声明 throws:运行时异常自动传播。

    • 示例:

      public String getOrderDetails(String orderId) {
          if (orderId == null) {
              throw new IllegalArgumentException("订单 ID 不能为空");
          }
          return "Order: " + orderId;
      }
      
  • 受检异常(如 IOException)

    • 需要抛出并声明 throws:如果方法无法处理。

    • 示例:

      public String readFile() throws IOException {
          return Files.readString(Paths.get("file.txt"));
      }
      

5.2 什么时候不需要抛出?

  • 内部消化

    public String getOrderDetails(String orderId) {
        if (orderId == null) {
            return "默认订单";
        }
        return "Order: " + orderId;
    }
    
  • 捕获后处理

    public String getOrderDetails(String orderId) {
        try {
            return fetchOrder(orderId);
        } catch (Exception e) {
            log.error("获取失败", e);
            return null;
        }
    }
    

5.3 Spring Boot 中的建议

  • 运行时异常:抛出但无需声明 throws,交给全局处理器。
  • 受检异常:抛出并声明 throws,或在适当层级捕获。

6. 哪些异常需要捕获?

异常类型是否需要捕获?原因及建议
Checked Exception编译器强制要求。
IllegalArgumentException否(视情况)可抛出,交给全局处理;若需特定响应,则捕获。
Custom Business Exception否(推荐全局处理)抛出自定义异常,统一处理。

7. 最佳实践:全局异常处理

@RestControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler(IllegalArgumentException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public Map<String, String> handleIllegalArgument(IllegalArgumentException e) {
        return Collections.singletonMap("error", e.getMessage());
    }
}

8. 总结

  • 捕获:受检异常必须捕获,运行时异常可选。

  • 抛出

    • 需要:无法处理时抛出,运行时异常无需 throws,受检异常需声明。
    • 不需要:内部可消化或无需通知调用者时。
  • 推荐:结合全局处理,抛出语义化异常。