You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.20 00:27:07