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 问题:异常处理优先级混乱
解决方案:理解异常处理优先级
@ExceptionHandler(最高优先级)ResponseEntityExceptionHandlerExceptionTranslationFilter(仅限 Security 异常)- 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 端点 |
兜底处理 | 所有未处理异常 |
最佳实践:
- 业务异常使用自定义异常类 +
@ExceptionHandler - Security 异常配置
authenticationEntryPoint和accessDeniedHandler - 统一响应格式,避免格式混用
- 合理配置
problemdetails.enabled - 做好日志记录,便于问题排查