Spring Boot 4.0 整合Spring Security 7 后的统一异常处理指南

Spring Boot 4.0 + Security 7 统一异常处理指南

本文基于 Spring Boot 4.0 + Spring Security 7 编写,涵盖异常处理的完整架构和实践方案。

1. 异常处理架构概述

在 Spring Security 框架中,异常处理是一个复杂但至关重要的部分。理解其工作原理是实现统一异常处理的基础。

1.1 Spring Security Filter 链

Spring Security 通过一系列 Filter 组成的链来保护应用程序。ExceptionTranslationFilter 是其中的核心组件之一,位于 Filter 链的第 9 位。

复制代码
Filter Chain 执行顺序:
┌─────────────────────────────────────────────────┐
│  1. ChannelFilter                                │
│  2. SecurityContextPersistenceFilter             │
│  3. SessionManagementFilter                      │
│  4. ConcurrentSessionFilter                      │
│  5. UsernamePasswordAuthenticationFilter         │
│  6. DefaultLoginPageGeneratingFilter             │
│  7. DefaultLogoutPageGeneratingFilter            │
│  8. RequestMatcherAuthorizationFilter            │
│  9. ExceptionTranslationFilter  ← 核心异常处理器  │
│ 10. FilterSecurityInterceptor                    │
│ 11. ...其他自定义过滤器...                        │
│           ↓                                      │
│      Controller 执行                              │
│           ↓                                      │
│   finally: 异常被 ExceptionTranslationFilter 捕获 │
└─────────────────────────────────────────────────┘

1.2 异常处理流程图

复制代码
                    ┌─────────────────────────────┐
                    │      HTTP 请求到达          │
                    └─────────────┬───────────────┘
                                  ↓
                    ┌─────────────────────────────┐
                    │   Security Filter Chain     │
                    │   (ExceptionTranslationFilter)│
                    └─────────────┬───────────────┘
                                  ↓
                    ┌─────────────────────────────┐
                    │       Controller 执行        │
                    └─────────────┬───────────────┘
                                  ↓
                    ┌─────────────────────────────┐
                    │  异常发生 (try-catch-finally)│
                    └─────────────┬───────────────┘
                                  ↓
              ┌───────────────────┴───────────────────┐
              ↓                                       ↓
    ┌─────────────────────┐             ┌─────────────────────┐
    │ AuthenticationException │           │ AccessDeniedException│
    │  → authenticationEntryPoint │       │  → accessDeniedHandler│
    └──────────┬──────────────┘             └──────────┬──────────────┘
               ↓                                        ↓
    ┌─────────────────────────┐           ┌─────────────────────────┐
    │ 返回 401 Unauthorized   │           │ 返回 403 Forbidden      │
    └─────────────────────────┘           └─────────────────────────┘

2. ExceptionTranslationFilter 工作原理

2.1 核心源码解析

ExceptionTranslationFilter 是 Spring Security 中负责异常转换和处理的过滤器。其核心工作流程如下:

java 复制代码
public class ExceptionTranslationFilter extends GenericFilterBean {

    private final AuthenticationEntryPoint authenticationEntryPoint;
    private final AccessDeniedHandler accessDeniedHandler;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            // 执行 FilterChain,所有后续 Filter 和 Controller 都在这里执行
            chain.doFilter(request, response);
        } catch (IOException | ServletException ex) {
            // 直接重新抛出 IO 异常
            throw ex;
        } catch (Exception ex) {
            // 捕获所有其他异常并进行转换处理
            handleSpringSecurityException(request, response, ex);
        }
    }

    private void handleSpringSecurityException(HttpServletRequest request,
                                               HttpServletResponse response,
                                               Exception exception) throws IOException, ServletException {
        // 判断异常类型并进行相应处理
        if (exception instanceof AuthenticationException) {
            // 认证异常 → authenticationEntryPoint
            authenticationEntryPoint.commence(request, response, (AuthenticationException) exception);
        } else if (exception instanceof AccessDeniedException) {
            // 授权异常 → accessDeniedHandler
            accessDeniedHandler.handle(request, response, (AccessDeniedException) exception);
        } else {
            // 其他异常重新抛出
            if (exception instanceof RuntimeException) {
                throw (RuntimeException) exception;
            }
            throw new RuntimeException(exception);
        }
    }
}

2.2 为什么 Controller 异常会被捕获?

这是 Spring Security 的设计机制。ExceptionTranslationFilter 的 doFilter 方法使用 try-catch 包装了整个 FilterChain 的执行:

java 复制代码
try {
    chain.doFilter(request, response);  // ← Controller 在这里执行
} catch (Exception ex) {
    handleSpringSecurityException(request, response, ex);  // ← 捕获所有异常
}

这意味着:

  • 所有通过 FilterChain 的请求抛出的异常都会被捕获
  • 包括 Controller 层、业务层、数据访问层
  • 但只处理 Security 相关的异常类型

3. Spring MVC 异常处理机制

3.1 ResponseEntityExceptionHandler

ResponseEntityExceptionHandler 是 Spring MVC 提供的抽象类,用于统一处理 Spring MVC 内部抛出的标准异常。

3.1.1 可处理的异常类型
异常类 HTTP 状态码 触发场景
NoResourceFoundException 404 资源不存在
HttpRequestMethodNotSupportedException 405 请求方法不支持
HttpMediaTypeNotSupportedException 415 媒体类型不支持
HttpMediaTypeNotAcceptableException 406 媒体类型不可接受
MissingPathVariableException 500 路径变量缺失
MissingServletRequestParameterException 400 请求参数缺失
TypeMismatchException 400 类型不匹配
ConversionNotSupportedException 500 转换不支持
MethodArgumentNotValidException 400 参数校验失败
3.1.2 基本用法
java 复制代码
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
                                                             Object body,
                                                             HttpHeaders headers,
                                                             HttpStatus status,
                                                             WebRequest request) {
        // 自定义异常处理逻辑
        Map<String, Object> error = new HashMap<>();
        error.put("code", status.value());
        error.put("message", ex.getMessage());
        error.put("timestamp", System.currentTimeMillis());
        error.put("path", request.getDescription(false).replace("uri=", ""));

        return ResponseEntity.status(status).body(error);
    }
}
3.1.3 自定义异常处理示例
java 复制代码
@RestControllerAdvice
public class CustomExceptionHandler extends ResponseEntityExceptionHandler {

    /**
     * 处理 404 资源不存在异常
     */
    @Override
    protected ResponseEntity<Object> handleNoResourceFoundException(NoResourceFoundException ex,
                                                                      HttpHeaders headers,
                                                                      HttpStatus status,
                                                                      WebRequest request) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(Map.of(
                    "code", 404,
                    "type", "/errors/not-found",
                    "title", "Resource Not Found",
                    "detail", "The requested resource was not found: " + ex.getPath(),
                    "path", request.getDescription(false).replace("uri=", "")
                ));
    }

    /**
     * 处理 405 请求方法不支持异常
     */
    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(
            HttpRequestMethodNotSupportedException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
                .body(Map.of(
                    "code", 405,
                    "type", "/errors/method-not-allowed",
                    "title", "Method Not Allowed",
                    "detail", "Request method '" + ex.getMethod() + "' is not supported",
                    "supportedMethods", ex.getSupportedHttpMethods()
                ));
    }

    /**
     * 处理 400 参数校验失败异常
     */
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        List<Map<String, String>> errors = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> Map.of(
                    "field", error.getField(),
                    "message", error.getDefaultMessage(),
                    "rejectedValue", String.valueOf(error.getRejectedValue())
                ))
                .toList();

        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Map.of(
                    "code", 400,
                    "type", "/errors/validation-failed",
                    "title", "Validation Failed",
                    "errors", errors
                ));
    }

    /**
     * 处理类型转换异常
     */
    @Override
    protected ResponseEntity<Object> handleTypeMismatch(
            TypeMismatchException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(Map.of(
                    "code", 400,
                    "type", "/errors/type-mismatch",
                    "title", "Type Mismatch",
                    "detail", String.format("Failed to convert value '%s' for '%s' to type '%s'",
                            ex.getValue(), ex.getPropertyName(), ex.getRequiredType()),
                    "path", request.getDescription(false).replace("uri=", "")
                ));
    }
}

3.2 Problem Details (RFC 7807)

Spring Boot 3.x 引入了对 RFC 7807 Problem Details 标准的支持。

3.2.1 配置方式
yaml 复制代码
server:
  error:
    include-message: always
    include-binding-errors: always
    problemdetails:
      enabled: true  # 启用 RFC 7807 格式
3.2.2 响应格式对比

启用 Problem Details (enabled: true):

json 复制代码
{
  "type": "https://example.com/problems/resource-not-found",
  "title": "Not Found",
  "status": 404,
  "detail": "Resource not found with id: 123",
  "instance": "/api/users/123"
}

禁用 Problem Details (enabled: false):

json 复制代码
{
  "error": "Not Found",
  "message": "Resource not found with id: 123",
  "path": "/api/users/123",
  "status": 404,
  "timestamp": "2025-12-30T10:00:00.000+00:00"
}
3.2.3 自定义 Problem Details
java 复制代码
@RestControllerAdvice
public class ProblemDetailsExceptionHandler extends ResponseEntityExceptionHandler {

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
                                                             Object body,
                                                             HttpHeaders headers,
                                                             HttpStatus status,
                                                             WebRequest request) {
        // 构建自定义 RFC 7807 格式响应
        Map<String, Object> problem = new LinkedHashMap<>();
        problem.put("type", "/errors/" + status.value());
        problem.put("title", status.getReasonPhrase());
        problem.put("status", status.value());
        problem.put("detail", ex.getMessage());
        problem.put("instance", request.getDescription(false).replace("uri=", ""));
        problem.put("timestamp", System.currentTimeMillis());

        // 添加额外信息
        if (ex instanceof BindingException) {
            problem.put("errors", ((BindingException) ex).getErrors());
        }

        return ResponseEntity.status(status)
                .contentType(MediaType.APPLICATION_PROBLEM_JSON)
                .body(problem);
    }
}

4. Spring Security 异常处理

4.1 配置 Security 异常处理器

java 复制代码
@Configuration
@EnableWebSecurity
public class SecurityExceptionConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authz -> authz
                .requestMatchers("/public/**").permitAll()
                .anyRequest().authenticated()
            )
            .exceptionHandling(exception -> exception
                .authenticationEntryPoint(authenticationEntryPoint())
                .accessDeniedHandler(accessDeniedHandler())
            );

        return http.build();
    }

    /**
     * 认证入口点:处理未认证访问
     */
    @Bean
    public AuthenticationEntryPoint authenticationEntryPoint() {
        return (request, response, authException) -> {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

            Map<String, Object> error = new HashMap<>();
            error.put("timestamp", System.currentTimeMillis());
            error.put("status", 401);
            error.put("error", "Unauthorized");
            error.put("message", "Authentication required: " + authException.getMessage());
            error.put("path", request.getRequestURI());

            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), error);
        };
    }

    /**
     * 访问拒绝处理器:处理权限不足
     */
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        return (request, response, accessDeniedException) -> {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);

            Map<String, Object> error = new HashMap<>();
            error.put("timestamp", System.currentTimeMillis());
            error.put("status", 403);
            error.put("error", "Forbidden");
            error.put("message", "Access denied: " + accessDeniedException.getMessage());
            error.put("path", request.getRequestURI());

            ObjectMapper mapper = new ObjectMapper();
            mapper.writeValue(response.getOutputStream(), error);
        };
    }
}

4.2 自定义 Security 异常处理类

java 复制代码
@Component
public class UocAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private final ObjectMapper objectMapper;

    public UocAuthenticationEntryPoint(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void commence(HttpServletRequest request,
                         HttpServletResponse response,
                         AuthenticationException authException) throws IOException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpServletResponse.SC_UNAUTHORIZED)
                .error("Unauthorized")
                .message(authException.getMessage())
                .path(request.getRequestURI())
                .build();

        objectMapper.writeValue(response.getOutputStream(), error);
    }
}

@Component
public class UocAccessDeniedHandler implements AccessDeniedHandler {

    private final ObjectMapper objectMapper;

    public UocAccessDeniedHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    @Override
    public void handle(HttpServletRequest request,
                       HttpServletResponse response,
                       AccessDeniedException accessDeniedException) throws IOException {

        response.setContentType("application/json;charset=UTF-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpServletResponse.SC_FORBIDDEN)
                .error("Forbidden")
                .message("Access denied: " + accessDeniedException.getMessage())
                .path(request.getRequestURI())
                .build();

        objectMapper.writeValue(response.getOutputStream(), error);
    }
}

4.3 统一响应格式

java 复制代码
/**
 * 统一 API 响应对象
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {

    private boolean success;
    private String message;
    private T data;
    private ApiError error;

    public static <T> ApiResponse<T> success(T data) {
        return ApiResponse.<T>builder()
                .success(true)
                .data(data)
                .build();
    }

    public static <T> ApiResponse<T> success() {
        return ApiResponse.<T>builder()
                .success(true)
                .build();
    }

    public static <T> ApiResponse<T> error(String message) {
        return ApiResponse.<T>builder()
                .success(false)
                .message(message)
                .build();
    }
}

/**
 * 统一错误对象
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiError {
    private LocalDateTime timestamp;
    private int status;
    private String error;
    private String message;
    private String path;
    private String type;
    private List<FieldError> fieldErrors;

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class FieldError {
        private String field;
        private String message;
        private Object rejectedValue;
    }
}

5. 完整异常处理架构

5.1 分层异常处理策略

复制代码
┌─────────────────────────────────────────────────────────────┐
│                     Controller 层                            │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ @ExceptionHandler + @ControllerAdvice                   ││
│  │ 处理业务异常、自定义异常                                  ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  Spring MVC 内部                             │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ ResponseEntityExceptionHandler + ProblemDetails         ││
│  │ 处理 404、405、400、参数校验等 MVC 内部异常               ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  Spring Security                             │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ ExceptionTranslationFilter                              ││
│  │ authenticationEntryPoint (401)                          ││
│  │ accessDeniedHandler (403)                               ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘
                              ↓
┌─────────────────────────────────────────────────────────────┐
│                  Spring Boot 默认                             │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ /error 端点 (兜底处理)                                   ││
│  └─────────────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────────────┘

5.2 完整实现示例

java 复制代码
/**
 * 统一异常处理器
 */
@RestControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    private final ObjectMapper objectMapper;

    public GlobalExceptionHandler(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
    }

    // ==================== 业务异常处理 ====================

    /**
     * 处理自定义业务异常
     */
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ApiResponse<Void>> handleBusinessException(BusinessException ex) {
        log.warn("业务异常: {}", ex.getMessage());

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error("Business Error")
                .message(ex.getMessage())
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message(ex.getMessage())
                .error(error)
                .build();

        return ResponseEntity.badRequest().body(response);
    }

    /**
     * 处理资源不存在异常
     */
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ApiResponse<Void>> handleResourceNotFoundException(ResourceNotFoundException ex) {
        log.warn("资源不存在: {}", ex.getMessage());

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.NOT_FOUND.value())
                .error("Not Found")
                .message(ex.getMessage())
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message(ex.getMessage())
                .error(error)
                .build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    /**
     * 处理认证异常
     */
    @ExceptionHandler({AuthenticationException.class})
    public ResponseEntity<ApiResponse<Void>> handleAuthenticationException(AuthenticationException ex) {
        log.warn("认证异常: {}", ex.getMessage());

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.UNAUTHORIZED.value())
                .error("Unauthorized")
                .message("Authentication required")
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message("Authentication required")
                .error(error)
                .build();

        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response);
    }

    /**
     * 处理授权异常
     */
    @ExceptionHandler({AccessDeniedException.class})
    public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(AccessDeniedException ex) {
        log.warn("授权异常: {}", ex.getMessage());

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.FORBIDDEN.value())
                .error("Forbidden")
                .message("Access denied: " + ex.getMessage())
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message("Access denied")
                .error(error)
                .build();

        return ResponseEntity.status(HttpStatus.FORBIDDEN).body(response);
    }

    // ==================== Spring MVC 异常处理 ====================

    /**
     * 处理参数校验异常
     */
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        List<ApiError.FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> ApiError.FieldError.builder()
                        .field(error.getField())
                        .message(error.getDefaultMessage())
                        .rejectedValue(error.getRejectedValue())
                        .build())
                .toList();

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error("Validation Failed")
                .message("Request parameter validation failed")
                .fieldErrors(fieldErrors)
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message("Validation failed")
                .error(error)
                .build();

        return ResponseEntity.badRequest().body(response);
    }

    /**
     * 处理绑定异常
     */
    @Override
    protected ResponseEntity<Object> handleBindException(
            BindException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        List<ApiError.FieldError> fieldErrors = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> ApiError.FieldError.builder()
                        .field(error.getField())
                        .message(error.getDefaultMessage())
                        .rejectedValue(error.getRejectedValue())
                        .build())
                .toList();

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.BAD_REQUEST.value())
                .error("Binding Failed")
                .message("Request binding failed")
                .fieldErrors(fieldErrors)
                .build();

        return ResponseEntity.badRequest().body(ApiResponse.<Void>builder()
                .success(false)
                .message("Binding failed")
                .error(error)
                .build());
    }

    /**
     * 处理 404 异常
     */
    @Override
    protected ResponseEntity<Object> handleNoResourceFoundException(
            NoResourceFoundException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.NOT_FOUND.value())
                .error("Not Found")
                .message("Resource not found: " + ex.getPath())
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message("Resource not found")
                .error(error)
                .build();

        return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response);
    }

    // ==================== 兜底处理 ====================

    /**
     * 处理所有其他未捕获异常
     */
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ApiResponse<Void>> handleGenericException(Exception ex) {
        log.error("未捕获的异常", ex);

        ApiError error = ApiError.builder()
                .timestamp(LocalDateTime.now())
                .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
                .error("Internal Server Error")
                .message("An unexpected error occurred")
                .build();

        ApiResponse<Void> response = ApiResponse.<Void>builder()
                .success(false)
                .message("Internal server error")
                .error(error)
                .build();

        return ResponseEntity.internalServerError().body(response);
    }
}

5.3 配置文件

yaml 复制代码
server:
  port: 8080
  error:
    include-message: always
    include-binding-errors: always
    include-stacktrace: never
    problemdetails:
      enabled: true

spring:
  mvc:
    problemdetails:
      enabled: true

# 日志配置
logging:
  level:
    root: INFO
    com.example: DEBUG
    org.springframework.security: DEBUG

6. 常见问题与解决方案

6.1 问题:Controller 异常信息被 ExceptionTranslationFilter 拦截

问题描述 :Controller 中抛出的异常没有进入 @ExceptionHandler,反而被 Security 处理了。

原因 :ExceptionTranslationFilter 捕获了 FilterChain 中的所有异常,但只会处理 AuthenticationException 和 AccessDeniedException。

解决方案:确保异常类型正确

  • 业务异常 → @ExceptionHandler 处理
  • 认证异常 → ExceptionTranslationFilter → authenticationEntryPoint
  • 授权异常 → ExceptionTranslationFilter → accessDeniedHandler

6.2 问题:异常处理优先级混乱

解决方案:理解异常处理优先级

  1. @ExceptionHandler (最高优先级)
  2. ResponseEntityExceptionHandler
  3. ExceptionTranslationFilter (仅限 Security 异常)
  4. Spring Boot /error 端点 (兜底)

6.3 问题:返回格式不一致

解决方案:统一响应格式

java 复制代码
@RestControllerAdvice
public class UnifiedExceptionHandler {

    private static final String DEFAULT_ERROR_TYPE = "/errors/internal-error";

    @ExceptionHandler(Exception.class)
    public ResponseEntity<Map<String, Object>> handleException(HttpServletRequest request, Exception ex) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", System.currentTimeMillis());
        body.put("status", getStatus(request).value());
        body.put("error", getStatus(request).getReasonPhrase());
        body.put("message", ex.getMessage());
        body.put("path", request.getRequestURI());
        body.put("type", DEFAULT_ERROR_TYPE);

        return new ResponseEntity<>(body, getStatus(request));
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("jakarta.servlet.error.status_code");
        if (statusCode != null) {
            return HttpStatus.valueOf(statusCode);
        }
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
}

7. 总结

组件 作用 处理异常类型
@ExceptionHandler 处理业务自定义异常 BusinessException 等
ResponseEntityExceptionHandler 处理 Spring MVC 内部异常 404、405、400 等
ProblemDetailsExceptionHandler RFC 7807 格式响应 需配置 enabled: true
ExceptionTranslationFilter Security 异常转换 AuthenticationException, AccessDeniedException
/error 端点 兜底处理 所有未处理异常

最佳实践:

  1. 业务异常使用自定义异常类 + @ExceptionHandler
  2. Security 异常配置 authenticationEntryPoint 和 accessDeniedHandler
  3. 统一响应格式,避免格式混用
  4. 合理配置 problemdetails.enabled
  5. 做好日志记录,便于问题排查
相关推荐
软件技术NINI15 分钟前
基于SpringBoot和Vue的摇滚乐鉴赏网站
vue.js·spring boot·后端·spring·html·毕业设计·毕设
Tsuki_tl32 分钟前
【总结】Redis五大数据类型应用场景及基本命令
数据库·redis·后端·缓存·缓存中间件·redis命令·redis 五大数据类型
后台模板学习34 分钟前
Go Web开发中任务调度框架的源码解析与高可用实现方案
开发语言·后端·golang
系统集成-鹰翔38 分钟前
ERP对接升级提示数据库迁移失败:SQLite逐版本事务到底回滚到哪一步
javascript·数据库·经验分享·后端
IT_陈寒38 分钟前
深入理解SpringBoot:核心原理与最佳实践
前端·人工智能·后端
学长毕设40 分钟前
基于SpringBoot的校园礼服租赁系统的设计与实现
java·spring boot·后端
计算机毕设定制辅导-无忧学长1 小时前
《基于物品协同过滤的闲置书换购微信小程序设计开发》
java·vue.js·spring boot·微信小程序·uniapp·闲置书换购微信小程序·物品协同过滤算法
Best_Liu~1 小时前
SpringBoot全局日期格式化
java·spring boot·后端
小蒜学长1 小时前
基于Django+Vue喵汪联盟宠物领养系统的设计与实现(代码+数据库+LW)
后端·python·django·宠物领养系统·流程规范化
RuoyiOffice1 小时前
SpringBoot3 + openGauss:RuoYi Office 登录、流程发布与用印填报适配实战
java·数据库·spring boot·opengauss·信创