1. 为什么需要Spring MVC异常处理在开发Web应用时异常处理就像给程序穿上防弹衣。想象一下用户正在提交订单突然因为参数校验失败导致服务器返回500错误页面这种体验就像在线购物时收银台突然爆炸。Spring MVC提供了从局部到全局的异常处理方案让我们能够优雅地处理各种意外情况。我见过太多新手项目直接让异常信息暴露给前端这不仅影响用户体验还可能泄露敏感信息。比如数据库连接失败时如果直接将JDBC的报错信息返回黑客就能获取数据库表结构等关键信息。合理的异常处理应该做到三点记录完整错误日志、屏蔽敏感信息、提供友好提示。2. 局部异常处理利器ExceptionHandler2.1 基础用法实战ExceptionHandler就像是给Controller配备的私人医生专门处理当前Controller的异常情况。下面这个员工查询接口就非常典型RestController public class EmployeeController { GetMapping(/employees/{id}) public Employee getEmployee(PathVariable String id) { return employeeService.findById(Integer.parseInt(id)); } ExceptionHandler(NumberFormatException.class) public ResponseEntityErrorResponse handleNumberFormat(NumberFormatException ex) { log.error(ID格式错误, ex); return ResponseEntity.badRequest() .body(new ErrorResponse(EMP_400, 员工ID必须是数字)); } }当用户传入非数字ID时系统会返回清晰的错误提示而不是晦涩的堆栈信息。我在电商项目中就遇到过因为没处理这种基础异常导致促销活动时服务器被无效请求打垮的情况。2.2 你必须知道的10个细节异常匹配规则Spring会优先匹配最具体的异常类型。比如同时定义了NullPointerException和RuntimeException处理器当发生空指针时会进入前者返回值灵活性异常处理方法可以和原方法返回不同类型。比如原方法返回String视图异常处理可以返回ResponseEntity事务边界问题如果Service层用try-catch吞掉了异常会导致Transactional事务不回滚。正确的做法是// 错误示范 try { userDao.save(user); } catch (DataIntegrityViolationException e) { log.error(保存失败, e); return false; // 事务不会回滚 } // 正确做法 Transactional public void createUser(User user) { try { userDao.save(user); } catch (DataIntegrityViolationException e) { throw new BusinessException(用户已存在, e); // 触发回滚 } }处理顺序当Controller自身和全局都有匹配的处理器时Controller内的处理器优先级更高日志记录要点一定要记录完整的异常堆栈但返回给前端的应该是简化后的信息。我推荐使用Slf4j的占位符语法log.error(查询用户[{}]失败, userId, ex); // 不要用字符串拼接3. 全局异常处理ControllerAdvice进阶3.1 从局部到全局的进化当项目有几十个Controller时在每个类里写重复的异常处理就变成了灾难。ControllerAdvice就像中央应急指挥中心可以统一处理所有控制层的异常。下面是一个处理验证失败的典型场景ControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntityErrorResponse handleValidationError( MethodArgumentNotValidException ex) { String errorMsg ex.getBindingResult().getFieldErrors().stream() .map(field - field.getField() : field.getDefaultMessage()) .collect(Collectors.joining(; )); return ResponseEntity.badRequest() .body(new ErrorResponse(VALID_400, errorMsg)); } }这种处理方式让所有参数校验失败的情况都能返回标准化的错误格式。我在金融项目中采用这种方案后前端错误处理代码减少了70%。3.2 高级配置技巧通过basePackages参数可以限定作用范围特别适合多模块项目// 只处理com.order包下的Controller ControllerAdvice(basePackages com.order) public class OrderExceptionAdvice { // 订单相关异常处理 } // 处理带RestController注解的Controller ControllerAdvice(annotations RestController.class) public class RestApiExceptionAdvice { // API专用异常处理 }性能优化提示全局异常处理器会拦截所有匹配的异常如果处理逻辑复杂会影响性能。建议对频繁发生的异常如参数校验保持处理逻辑精简耗时操作如写入错误日志可以放到异步线程处理4. RESTful API的最佳搭档RestControllerAdvice4.1 与传统方案的对比RestControllerAdvice相当于ControllerAdviceResponseBody的合体版专为API场景优化。对比下两种写法的区别// 传统方式 ControllerAdvice public class OldStyleHandler { ExceptionHandler(Exception.class) ResponseBody // 必须加这个注解 public ErrorResponse handle(Exception ex) { return new ErrorResponse(500, ex.getMessage()); } } // 现代方式 RestControllerAdvice public class ModernHandler { ExceptionHandler(Exception.class) // 自动带ResponseBody public ErrorResponse handle(Exception ex) { return new ErrorResponse(500, ex.getMessage()); } }在微服务项目中我强烈推荐使用RestControllerAdvice它能确保所有异常响应都是JSON格式避免前端收到意外的HTML错误页面。4.2 实战中的完美响应优秀的API异常响应应该包含业务错误码非HTTP状态码可读的错误信息可选的错误详情开发环境错误类型标识RestControllerAdvice public class ApiExceptionHandler { ExceptionHandler(BusinessException.class) ResponseStatus(HttpStatus.BAD_REQUEST) public ApiError handleBusinessError(BusinessException ex) { return ApiError.builder() .code(ex.getCode()) .message(ex.getMessage()) .trackingId(MDC.get(traceId)) // 链路追踪ID .details(isDevEnv() ? ex.getStackTrace() : null) .build(); } }重要经验生产环境一定要过滤掉堆栈信息等敏感内容。我曾经犯过直接返回e.getMessage()的错误结果暴露了SQL表结构。5. 异常处理架构设计心得5.1 分层处理策略经过多个项目实践我总结出这样的分层处理原则基础校验异常在全局处理器处理参数校验等基础错误业务异常定义BusinessException体系携带业务错误码系统异常对NullPointerException等意外错误统一包装特殊异常如需要重试的OptimisticLockingFailureException// 业务异常定义示例 public class PaymentException extends BusinessException { public PaymentException(String code, String message) { super(code, message); } } // 使用示例 if(balance amount) { throw new PaymentException(PAY_1001, 余额不足); }5.2 监控与告警集成异常处理不仅要考虑用户体验还要便于运维监控。我的做法是在全局处理器中添加监控埋点对关键业务异常触发告警记录错误发生时的上下文信息ExceptionHandler(Exception.class) public ResponseEntity handleGlobalError(Exception ex) { // 记录监控指标 metrics.increment(system.error); // 关键错误发送告警 if(ex instanceof DatabaseException) { alertService.sendCriticalAlert(ex); } // 保存错误快照 errorStorage.save(new ErrorSnapshot( RequestContext.getCurrentRequest(), Thread.currentThread().getStackTrace() )); return ResponseEntity.internalServerError().build(); }在日交易量上百万的系统中这套机制帮我们快速定位了多次线上问题。比如有一次Redis连接超时异常通过错误快照发现是网络闪断导致立即切换了备用集群。