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 的设计机制。ExceptionTranslationFilterdoFilter 方法使用 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 中的所有异常,但只会处理 AuthenticationExceptionAccessDeniedException

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

  • 业务异常 → @ExceptionHandler 处理
  • 认证异常 → ExceptionTranslationFilterauthenticationEntryPoint
  • 授权异常 → ExceptionTranslationFilteraccessDeniedHandler

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 异常配置 authenticationEntryPointaccessDeniedHandler
  3. 统一响应格式,避免格式混用
  4. 合理配置 problemdetails.enabled
  5. 做好日志记录,便于问题排查
相关推荐
学博成3 小时前
在 Spring Boot 中使用 Kafka 并保证顺序性(Topic 分区为 100)的完整案例
spring boot·kafka
無欲無为4 小时前
Spring Boot 整合 RabbitMQ 详细指南:从入门到实战
spring boot·rabbitmq·java-rabbitmq
掘根4 小时前
【消息队列项目】客户端四大模块实现
开发语言·后端·ruby
NAGNIP10 小时前
多个 GitHub 账户SSH 密钥配置全攻略
后端
NAGNIP10 小时前
Windows命令行代码自动补全详细步骤
后端
追逐时光者11 小时前
精选 8 款 .NET 开源、前后端分离的快速开发框架,提高开发生产效率!
后端·.net
用户479492835691512 小时前
性能提升 4000%!我是如何解决 运营看板 不能跨库&跨库查询慢这个难题的
数据库·后端·postgresql
老华带你飞12 小时前
农产品销售管理|基于java + vue农产品销售管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
短剑重铸之日13 小时前
SpringBoot声明式事务的源码解析
java·后端·spring·springboot