Appearance
异常处理机制
Spring MVC 提供了多种方式来处理应用程序中发生的异常,使开发者能够以一致、优雅的方式管理错误情况。本文将详细介绍 Spring MVC 中的异常处理机制。
异常处理概述
在 Web 应用中,异常处理通常需要满足以下需求:
- 将技术性异常转换为用户友好的错误信息
- 根据不同的异常类型展示不同的错误页面
- 保持代码的整洁,将业务逻辑与错误处理分离
- 统一处理日志记录、监控等横切关注点
Spring MVC 提供了多种异常处理方式,从简单到复杂,能够满足各种场景的需求。
HandlerExceptionResolver 接口
Spring MVC 中的异常处理核心机制是 HandlerExceptionResolver
接口,它负责将处理器(Controller)执行过程中抛出的异常解析为具体的 ModelAndView 用于渲染。
java
public interface HandlerExceptionResolver {
ModelAndView resolveException(
HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex);
}
Spring MVC 内置了多个 HandlerExceptionResolver 实现:
- SimpleMappingExceptionResolver:将异常类名映射到视图名
- DefaultHandlerExceptionResolver:处理 Spring MVC 内部异常
- ResponseStatusExceptionResolver:处理带有 @ResponseStatus 注解的异常
- ExceptionHandlerExceptionResolver:处理 @ExceptionHandler 注解方法
这些解析器按优先级顺序组成一个链,当异常发生时,会按顺序尝试使用每个解析器处理异常,直到有一个解析器返回非空的 ModelAndView。
异常处理方式
1. 使用 @ExceptionHandler 注解
最灵活的异常处理方式是使用 @ExceptionHandler
注解在控制器中定义异常处理方法。
控制器级别的异常处理
java
@Controller
public class ProductController {
@GetMapping("/products/{id}")
public String getProduct(@PathVariable Long id) {
Product product = productService.findById(id);
if (product == null) {
throw new ProductNotFoundException(id);
}
return "product/detail";
}
@ExceptionHandler(ProductNotFoundException.class)
public ModelAndView handleProductNotFoundException(ProductNotFoundException ex) {
ModelAndView mav = new ModelAndView("error/product-not-found");
mav.addObject("productId", ex.getProductId());
mav.addObject("message", ex.getMessage());
return mav;
}
@ExceptionHandler(Exception.class)
public ModelAndView handleGeneralException(Exception ex) {
ModelAndView mav = new ModelAndView("error/general");
mav.addObject("exception", ex);
return mav;
}
}
在上面的例子中:
- 特定的
ProductNotFoundException
会被第一个处理器方法处理 - 其他所有异常会被通用的处理器方法处理
@ExceptionHandler 方法的返回类型
@ExceptionHandler
方法可以有多种返回类型:
ModelAndView:指定视图和模型数据
java@ExceptionHandler(Exception.class) public ModelAndView handleException(Exception ex) { ModelAndView mav = new ModelAndView("error"); mav.addObject("message", ex.getMessage()); return mav; }
String:视图名称
java@ExceptionHandler(Exception.class) public String handleException(Exception ex, Model model) { model.addAttribute("message", ex.getMessage()); return "error"; }
ResponseEntity:控制响应状态和响应体
java@ExceptionHandler(ProductNotFoundException.class) public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException ex) { ErrorResponse error = new ErrorResponse("Product not found", ex.getProductId()); return new ResponseEntity<>(error, HttpStatus.NOT_FOUND); }
void:用于直接访问 response 对象
java@ExceptionHandler(Exception.class) public void handleException(Exception ex, HttpServletResponse response) throws IOException { response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); }
@ResponseBody 或 Rest 控制器中返回任意对象:序列化为响应体
java@ExceptionHandler(ProductNotFoundException.class) @ResponseBody public ErrorResponse handleProductNotFoundException(ProductNotFoundException ex) { return new ErrorResponse("Product not found", ex.getProductId()); }
2. 全局异常处理(@ControllerAdvice)
控制器内的 @ExceptionHandler
只能处理该控制器抛出的异常。如果需要跨控制器处理异常,可以使用 @ControllerAdvice
或 @RestControllerAdvice
定义全局异常处理类。
java
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(ProductNotFoundException.class)
public ModelAndView handleProductNotFoundException(ProductNotFoundException ex) {
logger.error("Handling ProductNotFoundException: {}", ex.getMessage());
ModelAndView mav = new ModelAndView("error/product-not-found");
mav.addObject("productId", ex.getProductId());
mav.addObject("message", ex.getMessage());
return mav;
}
@ExceptionHandler(Exception.class)
public ModelAndView handleGeneralException(Exception ex) {
logger.error("Handling general exception: ", ex);
ModelAndView mav = new ModelAndView("error/general");
mav.addObject("exception", ex);
return mav;
}
}
通过 @ControllerAdvice
注解,可以将异常处理方法应用于所有控制器。此外,可以通过属性限制处理范围:
java
// 指定处理的包
@ControllerAdvice(basePackages = "com.example.web.controllers")
// 指定处理的注解
@ControllerAdvice(annotations = RestController.class)
// 指定处理的类
@ControllerAdvice(assignableTypes = {ProductController.class, OrderController.class})
对于 RESTful API,可以使用 @RestControllerAdvice
,它相当于 @ControllerAdvice
+ @ResponseBody
:
java
@RestControllerAdvice
public class GlobalRestExceptionHandler {
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleProductNotFoundException(ProductNotFoundException ex) {
return new ErrorResponse("Product not found", ex.getProductId());
}
@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ValidationErrorResponse handleValidationException(ValidationException ex) {
ValidationErrorResponse response = new ValidationErrorResponse();
response.setMessage("Validation failed");
response.setErrors(ex.getErrors());
return response;
}
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleException(Exception ex) {
return new ErrorResponse("Internal server error", null);
}
}
3. @ResponseStatus 注解
可以使用 @ResponseStatus
注解来指定异常类应该返回的 HTTP 状态码。可以直接在自定义异常类上使用此注解:
java
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "Product not found")
public class ProductNotFoundException extends RuntimeException {
private Long productId;
public ProductNotFoundException(Long productId) {
super("Product not found with id: " + productId);
this.productId = productId;
}
public Long getProductId() {
return productId;
}
}
也可以在 @ExceptionHandler
方法上使用:
java
@ExceptionHandler(ProductNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public String handleProductNotFoundException(ProductNotFoundException ex, Model model) {
model.addAttribute("productId", ex.getProductId());
model.addAttribute("message", ex.getMessage());
return "error/product-not-found";
}
4. 使用 SimpleMappingExceptionResolver
对于传统的 Spring MVC 应用,可以配置 SimpleMappingExceptionResolver
将异常类映射到错误视图:
java
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
public SimpleMappingExceptionResolver exceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("ProductNotFoundException", "error/product-not-found");
mappings.setProperty("DataAccessException", "error/database");
mappings.setProperty("SecurityException", "error/forbidden");
resolver.setExceptionMappings(mappings);
resolver.setDefaultErrorView("error/general");
resolver.setExceptionAttribute("exception");
resolver.setWarnLogCategory("example.MvcLogger");
return resolver;
}
}
XML 配置方式:
xml
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="ProductNotFoundException">error/product-not-found</prop>
<prop key="DataAccessException">error/database</prop>
<prop key="SecurityException">error/forbidden</prop>
</props>
</property>
<property name="defaultErrorView" value="error/general"/>
<property name="exceptionAttribute" value="exception"/>
<property name="warnLogCategory" value="example.MvcLogger"/>
</bean>
5. 自定义 HandlerExceptionResolver
对于更复杂的场景,可以实现自定义的 HandlerExceptionResolver
:
java
@Component
public class CustomHandlerExceptionResolver implements HandlerExceptionResolver, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CustomHandlerExceptionResolver.class);
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
logger.error("Handling exception: ", ex);
if (ex instanceof ProductNotFoundException) {
ModelAndView mav = new ModelAndView("error/product-not-found");
mav.addObject("productId", ((ProductNotFoundException) ex).getProductId());
mav.addObject("message", ex.getMessage());
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
return mav;
}
if (ex instanceof AccessDeniedException) {
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
return new ModelAndView("error/forbidden");
}
// 返回 null 让其他解析器继续处理
return null;
}
@Override
public int getOrder() {
// 设置优先级,值越小优先级越高
return Ordered.HIGHEST_PRECEDENCE;
}
}
Spring Boot 异常处理
Spring Boot 提供了更便捷的异常处理机制,主要通过以下方式:
1. 默认错误页面
Spring Boot 自动配置了一个默认的错误处理器 BasicErrorController
,它会:
- 处理所有未被其他异常处理器捕获的异常
- 针对浏览器请求返回 HTML 错误页面
- 针对 API 请求返回 JSON 错误响应
默认情况下,错误页面包含以下信息:
- 时间戳
- 状态码
- 错误信息
- 异常类型
- 异常消息
- 错误路径
2. 自定义错误页面
可以通过以下方式自定义 Spring Boot 的错误页面:
静态 HTML 错误页面
在 src/main/resources/static/error/
目录下创建对应状态码的 HTML 文件:
404.html
:处理 404 错误500.html
:处理 500 错误4xx.html
:处理所有 4xx 错误(如果没有更具体的页面)5xx.html
:处理所有 5xx 错误(如果没有更具体的页面)
模板错误页面
也可以在 src/main/resources/templates/error/
目录下创建模板文件,如 Thymeleaf 模板:
404.html
500.html
error.html
:通用错误页面
这些模板可以访问以下错误属性:
html
<h1>Error Page</h1>
<p>Error occurred: <span th:text="${status}">Status</span></p>
<p>Message: <span th:text="${message}">Message</span></p>
<p>Time: <span th:text="${timestamp}">Timestamp</span></p>
<p>Exception: <span th:text="${exception}">Exception</span></p>
<p>Trace: <span th:text="${trace}">Trace</span></p>
<p>Path: <span th:text="${path}">Path</span></p>
3. 自定义 ErrorController
可以通过继承 AbstractErrorController
或实现 ErrorController
接口来自定义错误控制器:
java
@Controller
public class CustomErrorController extends AbstractErrorController {
private static final String ERROR_PATH = "/error";
public CustomErrorController(ErrorAttributes errorAttributes) {
super(errorAttributes);
}
@RequestMapping(ERROR_PATH)
public String handleError(HttpServletRequest request, Model model) {
HttpStatus status = getStatus(request);
ErrorAttributes errorAttributes = getErrorAttributes();
Map<String, Object> attributes = errorAttributes.getErrorAttributes(
new ServletWebRequest(request),
ErrorAttributeOptions.of(
ErrorAttributeOptions.Include.EXCEPTION,
ErrorAttributeOptions.Include.MESSAGE,
ErrorAttributeOptions.Include.STACK_TRACE
)
);
model.addAllAttributes(attributes);
if (status == HttpStatus.NOT_FOUND) {
return "custom-error/404";
}
if (status.is5xxServerError()) {
return "custom-error/500";
}
return "custom-error/error";
}
@Override
public String getErrorPath() {
return ERROR_PATH;
}
}
4. 自定义 ErrorAttributes
可以通过自定义 ErrorAttributes
来修改默认的错误信息:
java
@Component
public class CustomErrorAttributes extends DefaultErrorAttributes {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest,
ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
// 添加自定义属性
errorAttributes.put("application", "My Application");
errorAttributes.put("version", "1.0.0");
errorAttributes.put("supportContact", "support@example.com");
// 移除不想暴露的属性
errorAttributes.remove("trace");
// 获取原始异常
Throwable error = getError(webRequest);
if (error instanceof ProductNotFoundException) {
errorAttributes.put("productId", ((ProductNotFoundException) error).getProductId());
}
return errorAttributes;
}
}
实际应用示例
典型的全局异常处理配置
java
@ControllerAdvice
public class GlobalExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
// 处理资源未找到异常
@ExceptionHandler({
ResourceNotFoundException.class,
NoSuchElementException.class,
EmptyResultDataAccessException.class
})
public ModelAndView handleResourceNotFoundException(Exception ex) {
logger.warn("Resource not found: {}", ex.getMessage());
ModelAndView mav = new ModelAndView("error/not-found");
mav.addObject("message", ex.getMessage());
mav.setStatus(HttpStatus.NOT_FOUND);
return mav;
}
// 处理验证异常
@ExceptionHandler({
MethodArgumentNotValidException.class,
BindException.class,
ValidationException.class
})
public ModelAndView handleValidationException(Exception ex, WebRequest request) {
logger.warn("Validation error: {}", ex.getMessage());
ModelAndView mav = new ModelAndView("error/validation");
if (ex instanceof MethodArgumentNotValidException) {
List<FieldError> fieldErrors = ((MethodArgumentNotValidException) ex).getBindingResult().getFieldErrors();
mav.addObject("fieldErrors", fieldErrors);
} else if (ex instanceof BindException) {
List<FieldError> fieldErrors = ((BindException) ex).getBindingResult().getFieldErrors();
mav.addObject("fieldErrors", fieldErrors);
}
mav.addObject("message", "Validation failed");
mav.setStatus(HttpStatus.BAD_REQUEST);
return mav;
}
// 处理权限异常
@ExceptionHandler({
AccessDeniedException.class,
SecurityException.class
})
public ModelAndView handleSecurityException(Exception ex) {
logger.warn("Security exception: {}", ex.getMessage());
ModelAndView mav = new ModelAndView("error/forbidden");
mav.addObject("message", "Access denied");
mav.setStatus(HttpStatus.FORBIDDEN);
return mav;
}
// 处理数据库异常
@ExceptionHandler({
DataAccessException.class,
DataIntegrityViolationException.class,
SQLException.class
})
public ModelAndView handleDatabaseException(Exception ex) {
logger.error("Database error: ", ex);
ModelAndView mav = new ModelAndView("error/database");
mav.addObject("message", "A database error occurred");
mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
return mav;
}
// 处理未知的 RuntimeException
@ExceptionHandler(RuntimeException.class)
public ModelAndView handleRuntimeException(RuntimeException ex) {
logger.error("Runtime exception: ", ex);
ModelAndView mav = new ModelAndView("error/general");
mav.addObject("exception", ex);
mav.addObject("message", "An unexpected error occurred");
mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
return mav;
}
// 处理所有其他异常
@ExceptionHandler(Exception.class)
public ModelAndView handleException(Exception ex) {
logger.error("Unhandled exception: ", ex);
ModelAndView mav = new ModelAndView("error/general");
mav.addObject("exception", ex);
mav.addObject("message", "An unexpected error occurred");
mav.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
return mav;
}
}
RESTful API 异常处理
对于 RESTful API,我们通常需要返回 JSON 格式的错误信息,而不是视图页面。以下是一个适用于 RESTful API 的异常处理器示例:
java
@RestControllerAdvice
public class RestApiExceptionHandler {
private static final Logger logger = LoggerFactory.getLogger(RestApiExceptionHandler.class);
// 标准错误响应结构
@Data
@AllArgsConstructor
public static class ApiError {
private HttpStatus status;
private String message;
private List<String> errors;
private LocalDateTime timestamp;
public ApiError(HttpStatus status, String message, List<String> errors) {
this.status = status;
this.message = message;
this.errors = errors;
this.timestamp = LocalDateTime.now();
}
public ApiError(HttpStatus status, String message, String error) {
this(status, message, Collections.singletonList(error));
}
}
// 处理资源未找到异常
@ExceptionHandler({
ResourceNotFoundException.class,
NoSuchElementException.class,
EmptyResultDataAccessException.class
})
@ResponseStatus(HttpStatus.NOT_FOUND)
public ApiError handleNotFoundException(Exception ex) {
logger.warn("Resource not found: {}", ex.getMessage());
return new ApiError(HttpStatus.NOT_FOUND, "Resource not found", ex.getMessage());
}
// 处理验证异常
@ExceptionHandler({
MethodArgumentNotValidException.class,
BindException.class,
ValidationException.class,
ConstraintViolationException.class
})
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiError handleValidationException(Exception ex) {
logger.warn("Validation error: {}", ex.getMessage());
List<String> errors = new ArrayList<>();
if (ex instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException validationEx = (MethodArgumentNotValidException) ex;
validationEx.getBindingResult().getFieldErrors().forEach(error ->
errors.add(error.getField() + ": " + error.getDefaultMessage())
);
validationEx.getBindingResult().getGlobalErrors().forEach(error ->
errors.add(error.getObjectName() + ": " + error.getDefaultMessage())
);
} else if (ex instanceof BindException) {
BindException bindEx = (BindException) ex;
bindEx.getBindingResult().getFieldErrors().forEach(error ->
errors.add(error.getField() + ": " + error.getDefaultMessage())
);
bindEx.getBindingResult().getGlobalErrors().forEach(error ->
errors.add(error.getObjectName() + ": " + error.getDefaultMessage())
);
} else if (ex instanceof ConstraintViolationException) {
ConstraintViolationException constraintEx = (ConstraintViolationException) ex;
constraintEx.getConstraintViolations().forEach(violation ->
errors.add(violation.getPropertyPath() + ": " + violation.getMessage())
);
} else {
errors.add(ex.getMessage());
}
return new ApiError(HttpStatus.BAD_REQUEST, "Validation failed", errors);
}
// 处理业务逻辑异常
@ExceptionHandler(BusinessException.class)
@ResponseStatus(HttpStatus.UNPROCESSABLE_ENTITY)
public ApiError handleBusinessException(BusinessException ex) {
logger.warn("Business logic exception: {}", ex.getMessage());
return new ApiError(HttpStatus.UNPROCESSABLE_ENTITY, "Business logic error", ex.getMessage());
}
// 处理权限异常
@ExceptionHandler({
AccessDeniedException.class,
SecurityException.class
})
@ResponseStatus(HttpStatus.FORBIDDEN)
public ApiError handleSecurityException(Exception ex) {
logger.warn("Security exception: {}", ex.getMessage());
return new ApiError(HttpStatus.FORBIDDEN, "Access denied", "You do not have permission to access this resource");
}
// 处理数据库相关异常
@ExceptionHandler({
DataAccessException.class,
DataIntegrityViolationException.class,
SQLException.class
})
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleDatabaseException(Exception ex) {
logger.error("Database error:", ex);
return new ApiError(HttpStatus.INTERNAL_SERVER_ERROR, "Database error", "A database error has occurred");
}
// 处理所有未知异常
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiError handleException(Exception ex) {
logger.error("Unhandled exception:", ex);
return new ApiError(
HttpStatus.INTERNAL_SERVER_ERROR,
"An unexpected error occurred",
"Please contact the system administrator"
);
}
}
自定义异常类
为了更好地组织代码和表达业务含义,通常会创建自定义异常类:
java
// 资源未找到异常
public class ResourceNotFoundException extends RuntimeException {
public ResourceNotFoundException(String message) {
super(message);
}
public ResourceNotFoundException(String resourceName, String fieldName, Object fieldValue) {
super(String.format("%s not found with %s: '%s'", resourceName, fieldName, fieldValue));
}
}
// 业务逻辑异常
public class BusinessException extends RuntimeException {
private String errorCode;
public BusinessException(String message) {
super(message);
}
public BusinessException(String errorCode, String message) {
super(message);
this.errorCode = errorCode;
}
public String getErrorCode() {
return errorCode;
}
}
异常处理最佳实践
分层异常处理
- 在持久层捕获并转换数据库异常为应用异常
- 在服务层处理业务逻辑异常
- 在控制器层处理Web相关异常
使用具有描述性的异常类
- 创建表达明确业务含义的自定义异常
- 提供足够的上下文信息
统一的错误响应格式
- 对所有API错误使用一致的响应结构
- 包含状态码、错误消息、详细错误列表和时间戳
错误日志记录
- 根据异常类型使用不同的日志级别
- 记录足够的上下文信息以便调试
错误文档化
- 在API文档中记录可能的错误响应
- 使用Swagger/OpenAPI注解记录错误响应
异常处理配置示例
在Spring Boot应用程序中,可以添加以下配置来自定义异常处理行为:
java
@Configuration
public class WebExceptionConfig {
// 注册一个自定义的HandlerExceptionResolver
@Bean
public HandlerExceptionResolver customExceptionResolver() {
SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty(SQLException.class.getName(), "error/database");
mappings.setProperty(AccessDeniedException.class.getName(), "error/forbidden");
mappings.setProperty(Exception.class.getName(), "error/general");
resolver.setExceptionMappings(mappings);
resolver.setDefaultErrorView("error/default");
resolver.setExceptionAttribute("exception");
resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
return resolver;
}
// 自定义错误属性
@Bean
public ErrorAttributes errorAttributes() {
return new DefaultErrorAttributes() {
@Override
public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
// 添加自定义属性
errorAttributes.put("app", "My Spring App");
errorAttributes.put("timestamp", LocalDateTime.now());
// 移除敏感信息
if (!options.isIncluded(Include.STACK_TRACE)) {
errorAttributes.remove("trace");
}
return errorAttributes;
}
};
}
}
通过合理使用 Spring MVC 的异常处理机制,可以构建出健壮、用户友好且易于维护的 Web 应用程序。
Spring Boot 异常处理
Spring Boot 提供了额外的异常处理功能,使其更容易配置和使用:
- 默认错误处理:Spring Boot 默认提供
/error
映射和基本错误页面 - 自定义错误页面:通过在
/templates/error/
目录下创建404.html
、500.html
等文件 - ErrorController 自定义:通过实现
ErrorController
接口自定义错误处理
java
@Component
public class CustomErrorController implements ErrorController {
@RequestMapping("/error")
public String handleError(HttpServletRequest request) {
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
if (status != null) {
int statusCode = Integer.parseInt(status.toString());
if (statusCode == HttpStatus.NOT_FOUND.value()) {
return "error/404";
} else if (statusCode == HttpStatus.INTERNAL_SERVER_ERROR.value()) {
return "error/500";
}
}
return "error/default";
}
}
Spring Boot 的 Actuator 模块还提供了 /actuator/health
、/actuator/info
等端点,可以监控应用程序状态和提供健康信息,有助于诊断生产环境的问题。