从Controller到RestController:Spring MVC异常处理返回值那些坑,一个@ResponseBody就搞定?

张开发
2026/4/16 18:08:21 15 分钟阅读

分享文章

从Controller到RestController:Spring MVC异常处理返回值那些坑,一个@ResponseBody就搞定?
Spring MVC异常处理返回值陷阱ResponseBody的隐藏规则解析在传统Spring MVC向RESTful架构转型的过程中异常处理返回值类型的自动判断机制常常成为开发者的认知盲区。你是否遇到过这样的场景同一个异常处理方法在Controller中返回了JSON而在另一个地方却变成了视图跳转本文将揭示Spring异常处理中返回值类型判定的底层逻辑特别是ResponseBody注解在不同上下文中的差异化表现。1. 异常处理返回值的行为分化现象当项目同时存在视图渲染和API接口时异常处理方法的返回值行为会出现令人困惑的分化。我们通过两组对照实验来观察这种差异实验组A传统Controller中的异常处理Controller public class TraditionalController { ExceptionHandler(IllegalArgumentException.class) public MapString, Object handleIllegalArgument(IllegalArgumentException ex) { return Map.of(error, ex.getMessage()); // 返回值类型 } }实验组BRestController中的异常处理RestController public class ApiController { ExceptionHandler(IllegalArgumentException.class) public MapString, Object handleIllegalArgument(IllegalArgumentException ex) { return Map.of(error, ex.getMessage()); // 返回值类型 } }在未显式声明ResponseBody的情况下两组代码的返回值处理方式截然不同控制器类型默认返回值处理方式需要显式ResponseBodyController视图解析是RestController消息转换器否这种差异源于Spring对两类控制器的不同设计定位。RestController本质上是Controller和ResponseBody的组合注解其类级别已经隐含了响应体转换的语义。2. 注解组合的矩阵效应异常处理的返回值行为实际上是由注解组合形成的矩阵决定的。我们通过分解各注解的元数据影响可以得到完整的决策逻辑2.1 局部异常处理 (ExceptionHandler)在控制器内部的异常处理方法中返回值处理遵循以下优先级方法级别的ResponseBody注解类级别的RestController注解默认的视图解析行为// 案例1显式声明优先 Controller public class HybridController { ExceptionHandler(SQLException.class) ResponseBody // 强制JSON响应 public ErrorResult handleSQLException(SQLException ex) { return new ErrorResult(ex.getErrorCode(), ex.getMessage()); } } // 案例2类级别注解影响 RestController public class PureApiController { ExceptionHandler(IOException.class) // 自动JSON响应 public ErrorResult handleIOException(IOException ex) { return new ErrorResult(500, ex.getMessage()); } }2.2 全局异常处理 (ControllerAdvice)全局异常处理的规则更为复杂涉及三种注解组合ControllerAdvice ExceptionHandler默认视图解析需要显式添加ResponseBodyRestControllerAdvice ExceptionHandler自动JSON响应等效于ControllerAdvice ResponseBody混合场景下的优先级局部ExceptionHandler优先于全局处理更具体的异常类型匹配优先// 全局JSON异常处理器 RestControllerAdvice public class GlobalApiExceptionHandler { ExceptionHandler(DataAccessException.class) ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) public ErrorResult handleDataAccess(DataAccessException ex) { // 自动应用ResponseBody语义 } } // 传统全局异常处理器 ControllerAdvice public class GlobalViewExceptionHandler { ExceptionHandler(AuthenticationException.class) public String handleAuthException(AuthenticationException ex) { return error/login; // 视图解析 } }3. 混合架构下的统一处理方案对于同时包含传统视图和REST API的项目推荐采用分层异常处理策略3.1 基础异常结构设计首先定义统一的错误响应体public record ErrorResponse( int status, String code, String message, Instant timestamp, String path ) { public static ErrorResponse of(HttpStatus status, String code, String message, String path) { return new ErrorResponse(status.value(), code, message, Instant.now(), path); } }3.2 双轨制异常处理器配置// API异常处理器处理所有API控制器的异常 RestControllerAdvice(basePackages com.example.api) public class ApiExceptionHandler { ExceptionHandler(BusinessException.class) public ResponseEntityErrorResponse handleBusinessException( BusinessException ex, WebRequest request) { return ResponseEntity.badRequest() .body(ErrorResponse.of( HttpStatus.BAD_REQUEST, ex.getErrorCode(), ex.getMessage(), request.getDescription(false))); } } // 视图异常处理器处理传统控制器的异常 ControllerAdvice(basePackages com.example.web) public class WebExceptionHandler { ExceptionHandler(BusinessException.class) public ModelAndView handleBusinessException(BusinessException ex) { ModelAndView mav new ModelAndView(error/business); mav.addObject(error, ex); return mav; } }3.3 异常转换中间件对于需要统一日志记录和监控的场景可以增加异常拦截器ControllerAdvice public class ExceptionMonitoringAdvice { ExceptionHandler(Exception.class) public void handleAllExceptions(Exception ex, HttpServletRequest request) { log.error(Request {} failed: {}, request.getRequestURI(), ex.getMessage()); metrics.increment(server.errors); // 继续传播异常 throw ex; } }4. 实战中的典型陷阱与解决方案4.1 陷阱1注解继承失效当基类包含ExceptionHandler方法时子类的注解组合可能改变行为RestController public class SubController extends BaseController { // 继承的异常处理方法可能意外变成JSON响应 } // 解决方案明确声明处理方式 public abstract class BaseController { ExceptionHandler(ValidationException.class) ResponseBody // 显式声明 public ErrorResult handleValidation(ValidationException ex) { // ... } }4.2 陷阱2响应状态码丢失默认情况下成功的异常处理会返回200状态码RestControllerAdvice public class ProblematicHandler { ExceptionHandler(ResourceNotFoundException.class) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { return new ErrorResponse(404, NOT_FOUND, ex.getMessage()); // 实际HTTP状态仍是200 } } // 正确做法使用ResponseStatus ResponseStatus(HttpStatus.NOT_FOUND) ExceptionHandler(ResourceNotFoundException.class) public ErrorResponse handleNotFound(ResourceNotFoundException ex) { // ... }4.3 陷阱3内容协商冲突当同时配置了多种消息转换器时// 可能导致意外的XML响应 RestController public class UnpredictableController { ExceptionHandler(IllegalStateException.class) public ErrorResult handleStateError(IllegalStateException ex) { return new ErrorResult(STATE_ERROR, ex.getMessage()); } } // 解决方案强制JSON响应 ExceptionHandler(IllegalStateException.class) public ResponseBody ErrorResult handleStateError(IllegalStateException ex) { // ... }5. 进阶自定义异常解析策略对于需要更精细控制的场景可以实现HandlerExceptionResolverpublic class HybridExceptionResolver implements HandlerExceptionResolver { Override public ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { if (isApiRequest(request)) { // API请求的JSON响应 response.setContentType(MediaType.APPLICATION_JSON_VALUE); writeJsonResponse(response, convertToError(ex)); return new ModelAndView(); } else { // 传统视图渲染 ModelAndView mav new ModelAndView(error/custom); mav.addObject(exception, ex); return mav; } } private boolean isApiRequest(HttpServletRequest request) { return request.getRequestURI().startsWith(/api/); } }在配置类中注册该解析器Configuration public class ExceptionConfig implements WebMvcConfigurer { Override public void configureHandlerExceptionResolvers( ListHandlerExceptionResolver resolvers) { resolvers.add(0, new HybridExceptionResolver()); } }这种方案虽然需要更多编码但提供了最大的灵活性和控制力特别适合遗留系统改造场景。

更多文章