Spring Boot MVC与REST API混合应用错误处理优化方案咨询
问题描述
我正在为同时包含MVC与REST API的Spring Boot应用确定最优错误处理方案:/admin路径提供Thymeleaf模板页面,其余路径为REST API接口。
当前项目配置
- Thymeleaf错误模板位于
/src/main/resources/templates/error/{id}.html,已定义400、401、403、404、500状态模板; - 默认Thymeleaf错误模板为
/src/main/resources/templates/error.html; - HttpSecurity配置:
@Override protected void configure(HttpSecurity http) throws Exception { (...) http .anonymous(); http .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase())) .accessDeniedHandler(new AccessDeniedHandlerImpl()); (...) }
- 适配MVC与REST的全局异常处理器:
@ControllerAdvice public class ExceptionHandlers { private final BasicErrorController basicErrorController; public ExceptionHandlers(BasicErrorController basicErrorController) { this.basicErrorController = basicErrorController; } @ExceptionHandler(Exception.class) public Object handleAllExceptions(Exception e, HttpServletRequest request, HttpServletResponse response) { return handle(e, request, response, HttpStatus.INTERNAL_SERVER_ERROR); } @ExceptionHandler(EntityNotFoundException.class) public Object handleEntityNotFoundException(EntityNotFoundException e, HttpServletRequest request, HttpServletResponse response) { return handle(e, request, response, HttpStatus.NOT_FOUND, I18nCodes.ENTITY_NOT_FOUND); } /** * 排除所有继承自AccessDeniedException的异常,不进行自定义处理 */ @ExceptionHandler(AccessDeniedException.class) public Object handleAccessDeniedException(AccessDeniedException e, HttpServletRequest request, HttpServletResponse response) { throw e; } /** * 排除所有继承自AppBaseException的异常,不进行自定义处理 */ @ExceptionHandler(AppBaseException.class) public Object handleAppBaseException(AppBaseException e, HttpServletRequest request, HttpServletResponse response) { throw e; } private Object handle(Exception e, HttpServletRequest request, HttpServletResponse response, HttpStatus status) { return handle(e, request, response, HttpStatus.INTERNAL_SERVER_ERROR, I18nCodes.getCodeByStatus(status)); } private Object handle(Exception e, HttpServletRequest request, HttpServletResponse response, HttpStatus status, String message) { String header = request.getHeader("Accept"); if (header != null && header.contains("text/html")) { setErrorCode(request, response, status); return basicErrorController.errorHtml(request, response); } return createJsonResponse(message, status, request.getRequestURI()); } private ResponseEntity<ErrorResponseDTO> createJsonResponse(String message, HttpStatus status, String path) { ErrorResponseDTO errorResponseDTO = new ErrorResponseDTO() .setTimestamp(new Timestamp(System.currentTimeMillis())) .setStatus(status.value()) .setMessage(message) .setPath(path) .setError(status.name().toLowerCase()); HttpHeaders httpHeaders = new HttpHeaders(); httpHeaders.setContentType(MediaType.APPLICATION_JSON); return ResponseEntity.status(status).headers(httpHeaders).body(errorResponseDTO); } private void setErrorCode(HttpServletRequest request, HttpServletResponse response, HttpStatus httpStatus) { request.setAttribute(RequestDispatcher.ERROR_STATUS_CODE, httpStatus.value()); response.setStatus(httpStatus.value()); } }
- 带
@ResponseStatus的自定义异常示例:
@ResponseStatus(value = HttpStatus.BAD_REQUEST) public class AccountInfoException extends AppBaseException { protected AccountInfoException(String message) { super(message); } protected AccountInfoException(String message, Throwable cause) { super(message, cause); } public static AccountInfoException emailAlreadyExists() { return new AccountInfoException(I18nCodes.EMAIL_EXIST); } public static AccountInfoException accountNotFound() { return new AccountInfoException(I18nCodes.ACCOUNT_NOT_FOUND); } }
遇到的问题
未添加全局异常处理器时,错误处理符合预期:未认证返回401,权限不足返回403;带@ResponseStatus的自定义异常能正确映射状态码与消息,且跳转至对应Thymeleaf错误页面。
但添加处理Exception.class的全局处理器后,出现以下问题:
- 无法获取自定义异常的
@ResponseStatus状态码; - Spring Security认证入口与权限拒绝处理器失效,未认证与权限不足均被视为
AccessDeniedException; - 丢失部分默认异常映射(如
LockedException应返回401)。
目前临时方案为重抛特定异常,现寻求更合理的永久解决方案。
最优解决方案
1. 调整全局异常处理器的范围,避免覆盖核心逻辑
Spring异常处理遵循具体异常优先于通用异常的规则,且Spring Security的异常处理逻辑在DispatcherServlet之前执行。因此需要:
- 移除
@ExceptionHandler(Exception.class)的通用处理,只针对业务特定异常编写处理逻辑; - 保留对
AccessDeniedException和AppBaseException的重抛逻辑,让Spring Security和默认机制处理这些异常。
修改后的全局异常处理器核心代码:
@ControllerAdvice public class ExceptionHandlers { private final BasicErrorController basicErrorController; public ExceptionHandlers(BasicErrorController basicErrorController) { this.basicErrorController = basicErrorController; } // 仅处理业务特定异常,不再覆盖通用Exception @ExceptionHandler(EntityNotFoundException.class) public Object handleEntityNotFoundException(EntityNotFoundException e, HttpServletRequest request, HttpServletResponse response) { return handle(e, request, response, HttpStatus.NOT_FOUND, I18nCodes.ENTITY_NOT_FOUND); } @ExceptionHandler(AccessDeniedException.class) public void handleAccessDeniedException(AccessDeniedException e) throws AccessDeniedException { throw e; // 交给Spring Security的accessDeniedHandler处理 } @ExceptionHandler(AppBaseException.class) public void handleAppBaseException(AppBaseException e) throws AppBaseException { throw e; // 交给默认的@ResponseStatus处理机制 } // 新增其他需要自定义处理的业务异常... private Object handle(Exception e, HttpServletRequest request, HttpServletResponse response, HttpStatus status, String message) { String header = request.getHeader("Accept"); if (header != null && header.contains("text/html")) { setErrorCode(request, response, status); return basicErrorController.errorHtml(request, response); } return createJsonResponse(message, status, request.getRequestURI()); } // 保留createJsonResponse和setErrorCode方法... }
2. 复用Spring Boot默认错误处理机制
Spring Boot的BasicErrorController已默认支持根据Accept头返回HTML或JSON格式的错误响应,无需自行处理通用异常:
- 确保Thymeleaf错误模板路径正确,Spring Boot会自动根据状态码匹配
error/{id}.html模板; - 自定义异常的
@ResponseStatus注解会被默认机制识别,自动映射对应状态码并返回合适的响应格式。
3. 确保Spring Security异常处理逻辑生效
保持HttpSecurity配置中的异常处理逻辑不变,确保:
- 认证入口点(
authenticationEntryPoint)处理未认证请求,返回401; - 权限拒绝处理器(
accessDeniedHandler)处理权限不足请求,返回403; - Spring Security特定异常(如
LockedException)会由认证入口点自动处理,返回401。
4. 统一业务异常的响应格式
对于需要自定义响应的业务异常,在@ExceptionHandler中根据Accept头分支处理:
- HTML响应:设置请求状态属性后调用
BasicErrorController.errorHtml,复用已有的Thymeleaf模板; - JSON响应:返回统一的
ErrorResponseDTO实体,保证格式一致性。
5. 验证关键场景
测试以下核心场景,确保逻辑正常:
- 访问
/admin路径未认证:返回401的Thymeleaf模板; - 访问REST API未认证:返回401的JSON响应;
- 触发
AccountInfoException:返回400的对应模板或JSON; - 触发
LockedException:返回401的对应模板或JSON; - 权限不足访问资源:返回403的对应模板或JSON。
内容的提问来源于stack exchange,提问作者Zdrzalik Przemyslaw
相关产品推荐
相关产品推荐

