异常处理
⚠️ Java 与 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:错误倾向于抛异常,不返回特殊值
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 中
Exception和Error都实现了Throwable,你可以 catch 任何东西。Java 中Error通常不可恢复(比如内存溢出),不需要 catch。⚠️ Checked Exception 是 Java 独有的。如果一个方法声明了
throws IOException,调用者必须处理它(try-catch 或继续往上抛)。PHP 中所有异常都是"非受检"的,可以不处理。Spring Boot 项目中几乎不用 Checked Exception 做业务异常,主要因为它会让方法签名变得冗长。
全局异常处理
@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 判断。
自定义业务异常
// 自定义异常(不需要继承 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。
状态码控制
@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. 事务中的异常会导致回滚
@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() 不需要在方法签名上声明
// 不用写 throws RuntimeException
public void doSomething() {
throw new RuntimeException("出错了");
}这是 Runtime 异常(非受检)和 Checked Exception 的区别。业务异常通常继承
RuntimeException,省去了方法签名上的throws声明。
4. 不要用异常做流程控制
// ❌ 不推荐:用异常控制业务流程
try {
return userService.findById(id);
} catch (ResourceNotFoundException e) {
return createDefaultUser();
}
// ✅ 推荐:用 Optional 表达"可能找不到"
return userService.findById(id)
.orElseGet(() -> createDefaultUser());