文章 585
评论 5
浏览 226854
干掉你的 try-catch:这 3 种异常处理方式优雅 10 倍

干掉你的 try-catch:这 3 种异常处理方式优雅 10 倍

一、引言

你是否厌倦了这样的代码?

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 的局限性

  1. 仅捕获 Controller 层抛出的异常
  2. 无法捕获 Filter 中的异常
  3. 无法捕获 Interceptor 中的异常
  4. 无法捕获异步线程中的异常

解决方案

  • 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 适用场景对比

方案适用层级可恢复性侵入性学习成本典型场景
@ControllerAdviceControllerREST API 全局异常处理
Vavr TryService / 工具需要降级处理的业务逻辑
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
复杂场景组合使用

核心原则

  1. 异常处理职责分离:Controller 负责格式,Service 负责业务,AOP 负责横切关注点
  2. 优雅降级优于硬报错:能用默认值就不用抛异常
  3. 日志优先于返回:即使返回友好提示,也要记录完整堆栈
  4. 异常分类清晰:业务异常和系统异常分开处理

💡 互动话题:你在项目中使用哪种异常处理方式?有没有遇到过什么坑?欢迎在评论区分享你的经验!


标题:干掉你的 try-catch:这 3 种异常处理方式优雅 10 倍
作者:jiangyi
地址:http://jiangyi.space/articles/2026/07/13/1783752450175.html
公众号:服务端技术精选

服务端开发博客:后端架构、高并发、性能优化与微服务实战教程

取消