Skip to content

异常处理


⚠️ Java 与 PHP 的异常哲学差异

php
// PHP:几乎所有 PHP 函数都可以在出错时返回 false
$result = mysqli_query($conn, $sql);
if ($result === false) { ... }

$data = json_decode($json, true);
if ($data === null && json_last_error() !== JSON_ERROR_NONE) { ... }
java
// Java:错误倾向于抛异常,不返回特殊值
String json = "{\"name\":\"foo\"}";
ObjectMapper mapper = new ObjectMapper();
try {
    User user = mapper.readValue(json, User.class);
} catch (JsonProcessingException e) {
    // JSON 解析失败时抛出异常,没有 null / false 返回值
}

历史原因:C 语言的函数返回值风格(成功返回结果,失败返回 -1/null)影响了 PHP 和大量 C 系语言。Java 从设计上选择了异常机制,认为返回值应该只用于业务结果,错误处理走专门的异常路径。


异常体系

Throwable(类比 PHP 的 Throwable)
├── Error(不可恢复,JVM 内部问题)
│   ├── OutOfMemoryError      — 内存溢出
│   ├── StackOverflowError    — 栈溢出
│   └── NoClassDefFoundError  — 类找不到

└── Exception(可恢复)
    ├── RuntimeException(运行时异常,可不用 try-catch)
    │   ├── NullPointerException      — 空指针(最常遇到)
    │   ├── IllegalArgumentException  — 非法参数
    │   ├── IndexOutOfBoundsException — 数组越界
    │   └── ... 以及其他 Spring 异常

    └── Checked Exception(受检异常,必须 try-catch)
        ├── IOException              — IO 异常
        ├── SQLException             — SQL 异常
        └── ... 以及其他

PHP 中 ExceptionError 都实现了 Throwable,你可以 catch 任何东西。Java 中 Error 通常不可恢复(比如内存溢出),不需要 catch。

⚠️ Checked Exception 是 Java 独有的。如果一个方法声明了 throws IOException,调用者必须处理它(try-catch 或继续往上抛)。PHP 中所有异常都是"非受检"的,可以不处理。

Spring Boot 项目中几乎不用 Checked Exception 做业务异常,主要因为它会让方法签名变得冗长。


全局异常处理

java
@RestControllerAdvice   // 类比 PHP 中 ExceptionHandler 中间件
public class GlobalExceptionHandler {

    // 处理业务异常(自定义异常)
    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND)
    public ApiResponse<Void> handleNotFound(ResourceNotFoundException e) {
        // 日志记录
        log.error("Resource not found: {}", e.getMessage());
        return ApiResponse.error(404, e.getMessage());
    }

    // 处理参数校验失败
    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ApiResponse<Map<String, String>> handleValidation(
            MethodArgumentNotValidException e) {

        Map<String, String> errors = new HashMap<>();
        e.getBindingResult().getFieldErrors()
            .forEach(err -> errors.put(err.getField(), err.getDefaultMessage()));

        return ApiResponse.error(400, "Validation failed")
                .setData(errors);
    }

    // 兜底:未预期的异常
    @ExceptionHandler(Exception.class)  // catch Exception 类型的全部
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ApiResponse<Void> handleUnknown(Exception e) {
        log.error("Unexpected error", e);
        return ApiResponse.error(500, "Internal server error");
    }
}

相比于 PHP 在 App\Exceptions\Handler 中写 render()report(),Java 的 @RestControllerAdvice + @ExceptionHandler 更简洁:每个方法处理一种异常类型,不需要 if-else 判断。


自定义业务异常

java
// 自定义异常(不需要继承 Exception,继承 RuntimeException 即可)
public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }

    public ResourceNotFoundException(String resource, Long id) {
        super(resource + " not found with id: " + id);
    }
}

// 使用
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
    return userRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("User", id));
    // ⬆ 一行代码完成:查找不到 → 抛异常 → 全局处理器 → 404 JSON
}

PHP 中你可能这样写:

php
$user = User::find($id);
if (!$user) {
    abort(404, 'User not found');
}

Java 中 Optional + orElseThrow 是标准模式,省去了 if 判断。如果查不到就直接抛异常,异常被全局处理器捕获后统一返回 404。


状态码控制

java
@ExceptionHandler(IllegalArgumentException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)          // 400
public ApiResponse<Void> handleBadRequest(...) { ... }

@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)            // 403
public ApiResponse<Void> handleForbidden(...) { ... }

// 或者用 ResponseEntity 动态控制状态码
@ExceptionHandler(CustomException.class)
public ResponseEntity<ApiResponse<Void>> handleCustom(CustomException e) {
    return ResponseEntity
            .status(e.getHttpStatus())           // 运行时决定状态码
            .body(ApiResponse.error(e.getCode(), e.getMessage()));
}

⚠️ 常见坑

1. 不捕获异常会导致 500 和丑陋的错误页

没配全局异常处理器时,未捕获的异常会返回 Tomcat 默认的 Whitelabel Error Page(白底黑字 HTML),API 项目应该返回 JSON。

解决方案:配 @RestControllerAdvice + 捕获 Exception.class 做兜底。

2. 事务中的异常会导致回滚

java
@Transactional
public void transfer(Long fromId, Long toId, BigDecimal amount) {
    accountRepository.deduct(fromId, amount);   // 扣钱
    // 如果下一行抛异常,上面的扣钱会自动回滚
    accountRepository.add(toId, amount);        // 加钱
}

@Transactional 默认在 RuntimeException 和 Error 时回滚。Checked Exception 不触发回滚。

3. throw new RuntimeException() 不需要在方法签名上声明

java
// 不用写 throws RuntimeException
public void doSomething() {
    throw new RuntimeException("出错了");
}

这是 Runtime 异常(非受检)和 Checked Exception 的区别。业务异常通常继承 RuntimeException,省去了方法签名上的 throws 声明。

4. 不要用异常做流程控制

java
// ❌ 不推荐:用异常控制业务流程
try {
    return userService.findById(id);
} catch (ResourceNotFoundException e) {
    return createDefaultUser();
}

// ✅ 推荐:用 Optional 表达"可能找不到"
return userService.findById(id)
    .orElseGet(() -> createDefaultUser());

面向 PHP 开发者的 Spring Boot 文档