Skip to content

异常处理机制

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 实现:

  1. SimpleMappingExceptionResolver:将异常类名映射到视图名
  2. DefaultHandlerExceptionResolver:处理 Spring MVC 内部异常
  3. ResponseStatusExceptionResolver:处理带有 @ResponseStatus 注解的异常
  4. 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 方法可以有多种返回类型:

  1. ModelAndView:指定视图和模型数据

    java
    @ExceptionHandler(Exception.class)
    public ModelAndView handleException(Exception ex) {
        ModelAndView mav = new ModelAndView("error");
        mav.addObject("message", ex.getMessage());
        return mav;
    }
  2. String:视图名称

    java
    @ExceptionHandler(Exception.class)
    public String handleException(Exception ex, Model model) {
        model.addAttribute("message", ex.getMessage());
        return "error";
    }
  3. 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);
    }
  4. void:用于直接访问 response 对象

    java
    @ExceptionHandler(Exception.class)
    public void handleException(Exception ex, HttpServletResponse response) throws IOException {
        response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    }
  5. @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;
    }
}

异常处理最佳实践

  1. 分层异常处理

    • 在持久层捕获并转换数据库异常为应用异常
    • 在服务层处理业务逻辑异常
    • 在控制器层处理Web相关异常
  2. 使用具有描述性的异常类

    • 创建表达明确业务含义的自定义异常
    • 提供足够的上下文信息
  3. 统一的错误响应格式

    • 对所有API错误使用一致的响应结构
    • 包含状态码、错误消息、详细错误列表和时间戳
  4. 错误日志记录

    • 根据异常类型使用不同的日志级别
    • 记录足够的上下文信息以便调试
  5. 错误文档化

    • 在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 提供了额外的异常处理功能,使其更容易配置和使用:

  1. 默认错误处理:Spring Boot 默认提供 /error 映射和基本错误页面
  2. 自定义错误页面:通过在 /templates/error/ 目录下创建 404.html500.html 等文件
  3. 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 等端点,可以监控应用程序状态和提供健康信息,有助于诊断生产环境的问题。