Spring Boot 3.2.x下MVC与REST混合应用的全局异常处理实现
问题描述
环境
Spring Boot 3.2.x / Spring Web 6.1.x
问题背景
应用同时包含返回JSP HTML页面的MVC控制器和REST控制器,需要实现差异化全局异常处理:
- MVC控制器触发异常时,返回错误页面
- REST端点触发异常时,返回带对应HTTP状态码的JSON/文本响应(如400、404、500等)
现有实现
已实现@ControllerAdvice类处理MVC请求异常,所有方法返回页面(如"forward:/error"),兜底处理方法如下:
@ControllerAdvice public class GlobalDefaultExceptionHandler { //...其他方法... @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String defaultInternalErrorHandler(Model model, Exception e, HttpServletRequest req) { UUID errorID = UUID.randomUUID(); log.error(errorID + " | Unhandled exception processing request to " + req.getRequestURI(), e); model.addAttribute("exception", e); model.addAttribute("errorID", errorID); return "forward:/error"; } }
该实现对MVC请求的异常处理表现正常。
需求与问题
期望REST控制器端点返回JSON格式响应及对应HTTP状态码,而非HTML内容。尝试添加@RestControllerAdvice类但未生效,REST端点抛出的异常仍会走MVC的异常处理方法返回错误页面:
@RestControllerAdvice @Slf4j public class GlobalRestExceptionHandler { @ExceptionHandler(ServletRequestBindingException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public String handle(ServletRequestBindingException e, HttpServletRequest req) { defaultInternalErrorHandler(e, req); return "Invalid Request: " + e.getMessage(); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public String defaultInternalErrorHandler(Exception e, HttpServletRequest req) { UUID errorID = UUID.randomUUID(); log.error(errorID + " | Unhandled exception processing API request to " + req.getRequestURI(), e); return "Server error: " + e; } }
咨询问题
如何实现MVC请求返回错误页面、REST端点返回REST友好响应的双重全局异常处理?
解决方案
方法1:限定异常处理器的作用范围
Spring允许通过basePackages、assignableTypes或annotations参数,指定@ControllerAdvice/@RestControllerAdvice的作用目标,让MVC和REST的异常处理器分别只处理对应控制器的异常,避免冲突。
针对REST控制器的处理器
可以通过自定义注解或包路径限定范围:
// 方式1:通过自定义注解限定 @RestControllerAdvice(annotations = RestApiController.class) @Slf4j public class GlobalRestExceptionHandler { @ExceptionHandler(ServletRequestBindingException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public Map<String, Object> handleBadRequest(ServletRequestBindingException e, HttpServletRequest req) { UUID errorID = UUID.randomUUID(); log.error(errorID + " | Invalid API request to " + req.getRequestURI(), e); return Map.of( "errorId", errorID.toString(), "message", "Invalid Request: " + e.getMessage(), "status", HttpStatus.BAD_REQUEST.value() ); } @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public Map<String, Object> handleServerError(Exception e, HttpServletRequest req) { UUID errorID = UUID.randomUUID(); log.error(errorID + " | Unhandled API exception to " + req.getRequestURI(), e); return Map.of( "errorId", errorID.toString(), "message", "Server error", "status", HttpStatus.INTERNAL_SERVER_ERROR.value() ); } } // 自定义REST控制器标识注解 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @interface RestApiController {} // 给REST控制器添加注解 @RestController @RestApiController @RequestMapping("/api") public class UserRestController { // REST接口实现... }
// 方式2:通过包路径限定 @RestControllerAdvice(basePackages = "com.yourproject.api") @Slf4j public class GlobalRestExceptionHandler { // 异常处理方法同上... }
针对MVC控制器的处理器
同样限定范围,确保只处理MVC控制器的异常:
@ControllerAdvice(basePackages = "com.yourproject.web") public class GlobalDefaultExceptionHandler { // 原有MVC异常处理方法... }
方法2:在单个处理器中根据请求类型动态响应
在同一个@ControllerAdvice中,通过请求头、路径特征判断请求类型,动态返回页面或JSON:
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) public Object handleException(Exception e, HttpServletRequest req) { UUID errorID = UUID.randomUUID(); log.error(errorID + " | Unhandled exception to " + req.getRequestURI(), e); // 判断是否为API请求:根据Accept头或路径前缀 boolean isApiRequest = req.getRequestURI().startsWith("/api") || (req.getHeader("Accept") != null && req.getHeader("Accept").contains("application/json")); if (isApiRequest) { // 返回JSON响应 return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR) .body(Map.of( "errorId", errorID.toString(), "message", "Server error", "status", HttpStatus.INTERNAL_SERVER_ERROR.value() )); } else { // 返回错误页面 ModelAndView modelAndView = new ModelAndView("forward:/error"); modelAndView.addObject("exception", e); modelAndView.addObject("errorID", errorID); return modelAndView; } } }
方法3:自定义ErrorController扩展默认错误处理
通过实现ErrorController,统一接管所有错误请求,再根据请求类型分发处理:
@Controller @RequestMapping("/error") public class CustomErrorController implements ErrorController { @GetMapping public Object handleError(HttpServletRequest req) { HttpStatus status = getRequestStatus(req); String acceptHeader = req.getHeader("Accept"); if (acceptHeader != null && acceptHeader.contains("application/json")) { // 返回JSON响应 return ResponseEntity.status(status) .body(Map.of( "status", status.value(), "error", status.getReasonPhrase(), "path", req.getRequestURI() )); } else { // 返回错误页面 ModelAndView modelAndView = new ModelAndView("error-page"); modelAndView.addObject("status", status.value()); modelAndView.addObject("error", status.getReasonPhrase()); return modelAndView; } } private HttpStatus getRequestStatus(HttpServletRequest request) { Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); if (statusCode == null) { return HttpStatus.INTERNAL_SERVER_ERROR; } try { return HttpStatus.valueOf(statusCode); } catch (Exception ex) { return HttpStatus.INTERNAL_SERVER_ERROR; } } }
关键注意事项
- 当存在多个
@ControllerAdvice时,Spring会优先选择范围更具体的处理器(如指定了包/注解的);若范围相同,则按类加载顺序选择,因此明确限定范围是避免冲突的核心。 @RestControllerAdvice本质是@ControllerAdvice + @ResponseBody,通过范围限定后,两个处理器不会互相干扰。
内容的提问来源于stack exchange,提问作者E-Riz
相关产品推荐
相关产品推荐

