一、引言
你是否厌倦了这样的代码?
public User getUserById(Long id) {
try {
User user = userRepository.findById(id);
if (user == null) {
throw new RuntimeException("用户不存在");
}
return user;
} catch (Exception e) {
log.error("查询用户失败", e);
throw new RuntimeException("系统错误");
}
}
满屏的 try-catch 不仅丑陋,而且分散了业务逻辑的注意力。今天我要分享三种优雅的异常处理方式,让你的代码焕然一新。
二、方案一:@ControllerAdvice + @ExceptionHandler
2.1 全局异常统一处理
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(BusinessException.class)
public ResponseEntity<Result<Void>> handleBusinessException(BusinessException e) {
log.warn("业务异常: {}", e.getMessage());
return ResponseEntity.badRequest()
.body(Result.fail(e.getCode(), e.getMessage()));
}
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<Result<Void>> handleResourceNotFoundException(ResourceNotFoundException e) {
log.warn("资源不存在: {}", e.getMessage());
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Result.fail(404, e.getMessage()));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e) {
log.error("系统异常", e);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.fail(500, "系统繁忙,请稍后重试"));
}
}
2.2 统一响应封装
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(200, "success", data);
}
public static <T> Result<T> success() {
return new Result<>(200, "success", null);
}
public static <T> Result<T> fail(int code, String message) {
return new Result<>(code, message, null);
}
}
2.3 自定义业务异常
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(String message) {
super(message);
this.code = 400;
}
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
public class ResourceNotFoundException extends BusinessException {
public ResourceNotFoundException(String message) {
super(404, message);
}
}
public class ValidationException extends BusinessException {
public ValidationException(String message) {
super(400, message);
}
}
2.4 业务代码零 try-catch
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/{id}")
public Result<User> getUser(@PathVariable Long id) {
User user = userService.getUserById(id);
return Result.success(user);
}
@PostMapping
public Result<User> createUser(@Valid @RequestBody UserCreateRequest request) {
User user = userService.createUser(request);
return Result.success(user);
}
}
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new ResourceNotFoundException("用户不存在: " + id));
}
public User createUser(UserCreateRequest request) {
if (userRepository.existsByEmail(request.getEmail())) {
throw new ValidationException("邮箱已被注册");
}
User user = new User();
user.setEmail(request.getEmail());
user.setName(request.getName());
return userRepository.save(user);
}
}
2.5 重要限制
⚠️ @ControllerAdvice 的局限性:
- 仅捕获 Controller 层抛出的异常
- 无法捕获 Filter 中的异常
- 无法捕获 Interceptor 中的异常
- 无法捕获异步线程中的异常
解决方案:
- Filter 异常:在
doFilter中手动捕获或使用ErrorController - 异步异常:实现
AsyncUncaughtExceptionHandler
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return (ex, method, params) -> {
log.error("异步任务异常: {}", method.getName(), ex);
};
}
}
三、方案二:Vavr Try
3.1 函数式异常处理
Maven 依赖:
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.4</version>
</dependency>
3.2 基础用法
import io.vavr.control.Try;
public class UserService {
public User getUserById(Long id) {
return Try.of(() -> userRepository.findById(id))
.flatMap(opt -> opt.map(Try::success)
.orElse(Try.failure(new ResourceNotFoundException("用户不存在: " + id))))
.recover(ex -> {
log.warn("查询用户失败: {}", ex.getMessage());
return null;
})
.get();
}
}
另一种简洁写法(不需要自定义异常):
public User getUserById(Long id) {
return Try.of(() -> userRepository.findById(id))
.map(opt -> opt.orElse(null))
.getOrElse(null);
}
3.3 完整 API 链
public String processOrder(Long orderId) {
return Try.of(() -> orderRepository.findById(orderId))
// 转换结果
.map(order -> {
order.setStatus("PROCESSING");
return orderRepository.save(order);
})
// 映射到 DTO
.map(order -> OrderDTO.fromEntity(order))
// 转换为 JSON
.map(dto -> objectMapper.writeValueAsString(dto))
// 捕获特定异常并恢复
.recover(EntityNotFoundException.class, ex -> {
log.warn("订单不存在: {}", orderId);
return "{\"error\": \"订单不存在\"}";
})
// 捕获所有异常并恢复
.recover(ex -> {
log.error("处理订单失败", ex);
return "{\"error\": \"系统错误\"}";
})
// 获取最终结果,如果失败则返回默认值
.getOrElse("{\"error\": \"未知错误\"}");
}
3.4 recover vs recoverWith
// recover: 返回普通值
Try.of(() -> riskyOperation())
.recover(ex -> fallbackValue)
.get();
// recoverWith: 返回另一个 Try
Try.of(() -> primaryOperation())
.recoverWith(ex -> Try.of(() -> fallbackOperation()))
.get();
3.5 链式调用示例
public String fetchUserProfile(Long userId) {
return Try.of(() -> userRepository.findById(userId))
.flatMap(userOpt -> userOpt
.map(user -> Try.success(user))
.orElse(Try.failure(new ResourceNotFoundException("用户不存在"))))
.map(user -> {
UserProfile profile = profileRepository.findByUserId(user.getId());
return profile != null ? profile : createDefaultProfile(user);
})
.map(this::convertToJson)
.recoverWith(ResourceNotFoundException.class, ex ->
Try.success("{\"profile\": \"default\"}"))
.recover(ex -> {
log.error("获取用户资料失败", ex);
return "{\"error\": \"获取失败\"}";
})
.get();
}
四、方案三:自定义 AOP
4.1 @AfterThrowing 用于日志记录
@Aspect
@Component
public class ExceptionLoggingAspect {
private static final Logger log = LoggerFactory.getLogger(ExceptionLoggingAspect.class);
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex")
public void logException(Exception ex) {
log.error("服务层异常", ex);
}
@AfterThrowing(pointcut = "execution(* com.example.controller.*.*(..))", throwing = "ex")
public void logControllerException(Exception ex) {
log.warn("控制器层异常: {}", ex.getMessage());
}
}
4.2 @Around 用于异常兜底
@Aspect
@Component
public class ExceptionHandlingAspect {
private static final Logger log = LoggerFactory.getLogger(ExceptionHandlingAspect.class);
@Around("execution(* com.example.service.external.*.*(..))")
public Object handleExternalServiceException(ProceedingJoinPoint joinPoint) {
try {
return joinPoint.proceed();
} catch (Exception e) {
String methodName = joinPoint.getSignature().getName();
log.error("外部服务调用失败: {}", methodName, e);
// 返回默认值或降级结果
Class<?> returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType();
if (returnType == String.class) {
return "default";
} else if (returnType.isPrimitive()) {
return 0;
}
return null;
}
}
@Around("@annotation(retryOnFailure)")
public Object handleRetry(ProceedingJoinPoint joinPoint, RetryOnFailure retryOnFailure) throws Throwable {
int maxRetries = retryOnFailure.maxRetries();
long delay = retryOnFailure.delay();
int attempts = 0;
while (attempts < maxRetries) {
try {
return joinPoint.proceed();
} catch (Exception e) {
attempts++;
log.warn("重试第 {} 次,方法: {}", attempts, joinPoint.getSignature().getName());
if (attempts >= maxRetries) {
throw e;
}
Thread.sleep(delay * attempts);
}
}
return null;
}
}
4.3 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RetryOnFailure {
int maxRetries() default 3;
long delay() default 1000;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface LogException {
boolean logStackTrace() default true;
}
@Aspect
@Component
public class LogExceptionAspect {
private static final Logger log = LoggerFactory.getLogger(LogExceptionAspect.class);
@Around("@annotation(logException)")
public Object logException(ProceedingJoinPoint joinPoint, LogException logException) throws Throwable {
try {
return joinPoint.proceed();
} catch (Exception e) {
if (logException.logStackTrace()) {
log.error("异常日志: {}", joinPoint.getSignature().getName(), e);
} else {
log.error("异常日志: {} - {}", joinPoint.getSignature().getName(), e.getMessage());
}
throw e;
}
}
}
4.4 使用示例
@Service
public class ExternalServiceClient {
@RetryOnFailure(maxRetries = 3, delay = 2000)
public String fetchData(String url) {
// 调用外部 API
return restTemplate.getForObject(url, String.class);
}
@LogException(logStackTrace = false)
public void processEvent(Event event) {
// 处理事件
}
}
五、三种方案对比
5.1 适用场景对比
| 方案 | 适用层级 | 可恢复性 | 侵入性 | 学习成本 | 典型场景 |
|---|---|---|---|---|---|
| @ControllerAdvice | Controller | 高 | 低 | 低 | REST API 全局异常处理 |
| Vavr Try | Service / 工具 | 高 | 中 | 中 | 需要降级处理的业务逻辑 |
| AOP | 任意层级 | 中 | 低 | 中 | 统一日志、重试、监控 |
5.2 决策树
┌──────────────────────┐
│ 异常处理需求 │
└──────────┬───────────┘
│
┌────────────────┼────────────────┐
│ │ │
┌─────────▼─────────┐ ┌────▼─────┐ ┌───────▼───────┐
│ Controller 层异常 │ │ 需要降级 │ │ 需要统一日志 │
│ 统一返回格式 │ │ 或恢复 │ │ 或重试 │
└─────────┬─────────┘ └────┬─────┘ └───────┬───────┘
│ │ │
┌─────────▼─────────┐ ┌────▼─────┐ ┌───────▼───────┐
│ @ControllerAdvice │ │ Vavr Try │ │ AOP 切面 │
└───────────────────┘ └──────────┘ └───────────────┘
5.3 组合使用策略
// 策略:三层防护
@RestController
@RequestMapping("/api")
public class ApiController {
@Autowired
private BusinessService businessService;
@GetMapping("/data")
public Result<DataDTO> getData() {
// 第一层:Controller 返回统一格式(@ControllerAdvice 兜底)
DataDTO data = businessService.process();
return Result.success(data);
}
}
@Service
public class BusinessService {
@Autowired
private ExternalClient externalClient;
public DataDTO process() {
// 第二层:业务层降级处理(Vavr Try)
String rawData = Try.of(() -> externalClient.fetch())
.recover(ex -> getFallbackData())
.get();
return parseData(rawData);
}
}
@Service
public class ExternalClient {
@RetryOnFailure(maxRetries = 3) // 第三层:底层重试(AOP)
@LogException
public String fetch() {
return restTemplate.getForObject(url, String.class);
}
}
六、最佳实践
6.1 异常分类处理
public class ExceptionHierarchy {
// RuntimeException
// ├── BusinessException
// │ ├── ValidationException
// │ ├── ResourceNotFoundException
// │ └── AuthorizationException
// └── SystemException
// ├── DatabaseException
// └── ExternalServiceException
}
6.2 不要吞掉异常
// ❌ 错误:吞掉异常,无法追踪
try {
riskyOperation();
} catch (Exception e) {
// 什么都不做
}
// ✅ 正确:至少记录日志
try {
riskyOperation();
} catch (Exception e) {
log.error("操作失败", e);
throw new BusinessException("操作失败");
}
6.3 异常信息要明确
// ❌ 错误:信息模糊
throw new RuntimeException("操作失败");
// ✅ 正确:包含上下文信息
throw new ResourceNotFoundException("用户不存在: userId=" + userId);
6.4 生产环境隐藏堆栈
@ExceptionHandler(Exception.class)
public ResponseEntity<Result<Void>> handleException(Exception e) {
log.error("系统异常", e);
// 生产环境:隐藏堆栈信息
if (isProduction()) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.fail(500, "系统繁忙,请稍后重试"));
}
// 开发环境:返回详细信息
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Result.fail(500, e.getMessage()));
}
七、总结
选择建议
| 场景 | 推荐方案 |
|---|---|
| REST API 全局异常处理 | @ControllerAdvice |
| 需要降级/恢复的业务逻辑 | Vavr Try |
| 统一日志/重试/监控 | AOP |
| 复杂场景 | 组合使用 |
核心原则
- 异常处理职责分离:Controller 负责格式,Service 负责业务,AOP 负责横切关注点
- 优雅降级优于硬报错:能用默认值就不用抛异常
- 日志优先于返回:即使返回友好提示,也要记录完整堆栈
- 异常分类清晰:业务异常和系统异常分开处理
💡 互动话题:你在项目中使用哪种异常处理方式?有没有遇到过什么坑?欢迎在评论区分享你的经验!
