欢迎光临
我们一直在努力

Spring Boot 3.x ResponseEntity自定义状态码被覆盖问题详解

目录

  • Spring Boot 3.x ResponseEntity自定义状态码被覆盖问题详解
    • 一、问题背景
    • 二、问题现象
      • 常见问题场景:
      • 典型问题:
    • 三、根本原因分析
      • 1. **响应处理链中的状态码覆盖点**
      • 2. **Spring Boot 3.x的变化**
      • 3. **常见覆盖原因**
    • 四、详细解决方案
      • 方案1:控制器层解决方案
        • 1.1 确保正确的ResponseEntity使用
      • 方案2:全局异常处理器配置
        • 2.1 基础异常处理器
        • 2.2 细粒度异常处理器
      • 方案3:响应包装器配置
        • 3.1 ResponseBodyAdvice实现
        • 3.2 过滤器中的响应包装
      • 方案4:Spring Security配置
        • 4.1 自定义认证/授权处理器
        • 4.2 安全过滤器配置
      • 方案5:Servlet容器配置
        • 5.1 Tomcat错误页面配置
      • 方案6:测试和验证
        • 6.1 状态码测试工具
        • 6.2 状态码调试端点
    • 五、最佳实践总结
      • 1. **状态码设置优先级**
      • 2. **避免状态码覆盖的建议**
      • 3. **配置管理**
      • 4. **监控和告警**
      • 5. **测试策略**
    • 六、常见问题FAQ
      • Q1: 为什么我的404变成了200?
      • Q2: Spring Security如何影响状态码?
      • Q3: 如何调试状态码被覆盖的问题?
      • Q4: 响应包装器如何处理状态码?
      • Q5: 如何确保状态码的一致性?

Spring Boot 3.x ResponseEntity自定义状态码被覆盖问题详解

一、问题背景

在Spring Boot 3.x中,使用ResponseEntity自定义HTTP状态码时,经常遇到状态码被意外覆盖的问题。这通常发生在以下场景:

  • 全局异常处理器覆盖了控制器返回的状态码
  • 过滤器或拦截器修改了响应状态码
  • Spring Security处理了认证/授权异常
  • Spring Boot的自动错误处理机制介入
  • 响应包装器修改了状态码
  • 二、问题现象

    常见问题场景:

    // 控制器返回404
    @GetMapping("/user/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
    User user = userService.findById(id);
    if (user == null) {
    return ResponseEntity.status(HttpStatus.NOT_FOUND).build(); // 期望404
    }
    return ResponseEntity.ok(user);
    }

    // 但实际客户端收到的是200或500

    典型问题:

  • 自定义404被覆盖为200
  • 业务异常状态码被覆盖为500
  • 认证失败状态码不正确
  • 响应被统一包装后状态码丢失
  • 三、根本原因分析

    1. 响应处理链中的状态码覆盖点

    Controller → 拦截器 → 过滤器 → 异常处理器 → 响应包装器 → 客户端
    ↓ ↓ ↓ ↓ ↓
    设置状态码 可能修改 可能修改 可能覆盖 可能修改

    2. Spring Boot 3.x的变化

    • 基于Jakarta EE 9+
    • Spring Framework 6的新特性
    • 更严格的响应处理机制
    • 自动错误处理的改进

    3. 常见覆盖原因

    • @ControllerAdvice中的异常处理器
    • ResponseBodyAdvice实现类
    • Spring Security过滤器链
    • Servlet过滤器
    • 第三方库的拦截器

    四、详细解决方案

    方案1:控制器层解决方案

    1.1 确保正确的ResponseEntity使用

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.*;

    @RestController
    @RequestMapping("/api/users")
    public class UserController {

    /**
    * 方法1:明确设置状态码
    */

    @GetMapping("/{id}")
    public ResponseEntity<User> getUser(@PathVariable Long id) {
    try {
    User user = userService.findById(id);

    if (user == null) {
    // 明确返回404
    return ResponseEntity
    .status(HttpStatus.NOT_FOUND)
    .header("X-Custom-Status", "USER_NOT_FOUND")
    .build();
    }

    return ResponseEntity.ok(user);

    } catch (Exception e) {
    // 返回500并包含错误信息
    return ResponseEntity
    .status(HttpStatus.INTERNAL_SERVER_ERROR)
    .body(new ErrorResponse("服务器内部错误", e.getMessage()));
    }
    }

    /**
    * 方法2:使用ResponseEntity的便捷方法
    */

    @PostMapping
    public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest request) {
    try {
    User user = userService.create(request);

    // 201 Created
    return ResponseEntity
    .created(URI.create("/api/users/" + user.getId()))
    .body(user);

    } catch (ValidationException e) {
    // 400 Bad Request
    return ResponseEntity
    .badRequest()
    .body(new ErrorResponse("验证失败", e.getErrors()));

    } catch (ConflictException e) {
    // 409 Conflict
    return ResponseEntity
    .status(HttpStatus.CONFLICT)
    .body(new ErrorResponse("资源冲突", e.getMessage()));
    }
    }

    /**
    * 方法3:返回Void并设置响应状态
    */

    @DeleteMapping("/{id}")
    @ResponseStatus(HttpStatus.NO_CONTENT) // 明确设置状态码
    public void deleteUser(@PathVariable Long id) {
    userService.delete(id);
    }

    /**
    * 方法4:自定义响应头防止覆盖
    */

    @GetMapping("/search")
    public ResponseEntity<List<User>> searchUsers(@RequestParam String keyword) {
    List<User> users = userService.search(keyword);

    if (users.isEmpty()) {
    // 添加自定义头,用于后续处理识别
    HttpHeaders headers = new HttpHeaders();
    headers.add("X-Custom-Empty-Response", "true");

    return ResponseEntity
    .status(HttpStatus.OK) // 返回200但添加标记
    .headers(headers)
    .body(users);
    }

    return ResponseEntity.ok(users);
    }

    // 辅助类
    @Data
    @AllArgsConstructor
    static class ErrorResponse {
    private String error;
    private String message;
    private Map<String, Object> errors;

    public ErrorResponse(String error, String message) {
    this.error = error;
    this.message = message;
    }
    }
    }

    方案2:全局异常处理器配置

    2.1 基础异常处理器

    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    import org.springframework.web.context.request.WebRequest;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

    /**
    * 全局异常处理器
    * 注意:避免覆盖控制器设置的正常响应状态码
    */

    @RestControllerAdvice
    @Order(Ordered.LOWEST_PRECEDENCE) // 设置为最低优先级,避免覆盖控制器返回
    public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    /**
    * 处理自定义业务异常
    * 确保返回正确的状态码,不覆盖控制器的正常返回
    */

    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<ErrorResponse> handleBusinessException(
    BusinessException ex, WebRequest request
    ) {
    // 从异常中获取状态码
    HttpStatus status = ex.getStatus() != null ?
    ex.getStatus() : HttpStatus.BAD_REQUEST;

    ErrorResponse error = ErrorResponse.builder()
    .timestamp(LocalDateTime.now())
    .status(status.value())
    .error(status.getReasonPhrase())
    .message(ex.getMessage())
    .path(getRequestPath(request))
    .build();

    // 明确设置状态码,确保不被覆盖
    return ResponseEntity
    .status(status)
    .body(error);
    }

    /**
    * 处理资源不存在异常
    */

    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleResourceNotFound(
    ResourceNotFoundException ex, WebRequest request
    ) {
    return buildErrorResponse(
    HttpStatus.NOT_FOUND,
    ex.getMessage(),
    request
    );
    }

    /**
    * 处理所有未捕获的异常
    * 注意:这个处理器可能会覆盖控制器的返回
    */

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponse> handleAllUncaughtException(
    Exception ex, WebRequest request
    ) {
    // 检查异常是否已经被处理过
    if (isAlreadyHandled(request)) {
    // 如果已经处理过,返回null让Spring使用原始响应
    return null;
    }

    // 记录异常但不覆盖控制器的成功响应
    return buildErrorResponse(
    HttpStatus.INTERNAL_SERVER_ERROR,
    "服务器内部错误",
    request
    );
    }

    /**
    * 重写ResponseEntityExceptionHandler的方法
    * 确保不会覆盖控制器设置的响应
    */

    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
    Exception ex, Object body, HttpHeaders headers,
    HttpStatusCode status, WebRequest request
    ) {
    // 检查是否应该处理这个异常
    if (shouldSkipExceptionHandling(request)) {
    // 返回null让Spring使用原始响应
    return null;
    }

    // 构建错误响应
    ErrorResponse error = ErrorResponse.builder()
    .timestamp(LocalDateTime.now())
    .status(status.value())
    .error(ex.getClass().getSimpleName())
    .message(ex.getMessage())
    .path(getRequestPath(request))
    .build();

    return new ResponseEntity<>(error, headers, status);
    }

    /**
    * 构建统一的错误响应
    */

    private ResponseEntity<ErrorResponse> buildErrorResponse(
    HttpStatus status, String message, WebRequest request
    ) {
    ErrorResponse error = ErrorResponse.builder()
    .timestamp(LocalDateTime.now())
    .status(status.value())
    .error(status.getReasonPhrase())
    .message(message)
    .path(getRequestPath(request))
    .build();

    return ResponseEntity.status(status).body(error);
    }

    /**
    * 获取请求路径
    */

    private String getRequestPath(WebRequest request) {
    if (request instanceof ServletWebRequest) {
    HttpServletRequest servletRequest =
    ((ServletWebRequest) request).getRequest();
    return servletRequest.getRequestURI();
    }
    return "unknown";
    }

    /**
    * 检查异常是否已经被处理
    */

    private boolean isAlreadyHandled(WebRequest request) {
    return request.getAttribute(
    HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE,
    WebRequest.SCOPE_REQUEST
    ) != null;
    }

    /**
    * 检查是否应该跳过异常处理
    */

    private boolean shouldSkipExceptionHandling(WebRequest request) {
    // 检查请求属性,判断是否已经有响应
    Integer status = (Integer) request.getAttribute(
    "javax.servlet.error.status_code",
    WebRequest.SCOPE_REQUEST
    );

    // 如果已经有状态码,说明已经有响应了
    return status != null && status >= 200 && status < 300;
    }

    // 业务异常类
    static class BusinessException extends RuntimeException {
    private HttpStatus status;

    public BusinessException(String message) {
    super(message);
    }

    public BusinessException(String message, HttpStatus status) {
    super(message);
    this.status = status;
    }

    public HttpStatus getStatus() {
    return status;
    }
    }

    static class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
    super(message);
    }
    }

    // 错误响应构建器
    @Data
    @Builder
    static class ErrorResponse {
    @Builder.Default
    private LocalDateTime timestamp = LocalDateTime.now();
    private int status;
    private String error;
    private String message;
    private String path;
    @Singular
    private Map<String, Object> details;
    }
    }

    2.2 细粒度异常处理器

    import org.springframework.core.annotation.Order;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseStatus;
    import org.springframework.web.context.request.WebRequest;

    /**
    * 细粒度异常处理器
    * 为不同包下的控制器提供不同的异常处理
    */

    @ControllerAdvice(basePackages = "com.example.api.v1")
    @Order(1) // 高优先级
    public class V1ApiExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleValidationException(
    ValidationException ex, WebRequest request
    ) {
    return ErrorResponse.builder()
    .status(HttpStatus.BAD_REQUEST.value())
    .error("Validation Failed")
    .message(ex.getMessage())
    .details(ex.getErrors())
    .build();
    }
    }

    @ControllerAdvice(basePackages = "com.example.api.v2")
    @Order(2)
    public class V2ApiExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidationExceptionV2(
    ValidationException ex, WebRequest request
    ) {
    // V2版本返回更详细的错误信息
    ErrorResponse error = ErrorResponse.builder()
    .status(HttpStatus.UNPROCESSABLE_ENTITY.value()) // 422
    .error("Validation Failed")
    .message("请求参数验证失败")
    .details(ex.getErrors())
    .build();

    return ResponseEntity
    .status(HttpStatus.UNPROCESSABLE_ENTITY)
    .body(error);
    }
    }

    /**
    * 通用API异常处理器
    */

    @ControllerAdvice
    @Order(Ordered.LOWEST_PRECEDENCE) // 最低优先级
    public class CommonApiExceptionHandler {

    /**
    * 最后兜底的异常处理
    * 注意:这个处理器不设置状态码,避免覆盖
    */

    @ExceptionHandler(Exception.class)
    public Object handleRemainingExceptions(Exception ex, WebRequest request) {
    // 检查响应是否已经提交
    if (isResponseCommitted(request)) {
    return null; // 响应已提交,不再处理
    }

    // 返回错误信息,但不强制设置状态码
    return ErrorResponse.builder()
    .status(HttpStatus.INTERNAL_SERVER_ERROR.value())
    .error("Internal Server Error")
    .message(ex.getMessage())
    .build();
    }

    private boolean isResponseCommitted(WebRequest request) {
    if (request instanceof ServletWebRequest) {
    ServletWebRequest servletRequest = (ServletWebRequest) request;
    return servletRequest.getResponse().isCommitted();
    }
    return false;
    }
    }

    方案3:响应包装器配置

    3.1 ResponseBodyAdvice实现

    import org.springframework.core.MethodParameter;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.MediaType;
    import org.springframework.http.ResponseEntity;
    import org.springframework.http.converter.HttpMessageConverter;
    import org.springframework.http.server.ServerHttpRequest;
    import org.springframework.http.server.ServerHttpResponse;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

    /**
    * 响应体包装器
    * 统一包装响应,注意不要覆盖原有的状态码
    */

    @ControllerAdvice
    public class GlobalResponseWrapper implements ResponseBodyAdvice<Object> {

    /**
    * 判断是否支持包装
    * 注意:避免包装ResponseEntity和特定类型的响应
    */

    @Override
    public boolean supports(MethodParameter returnType,
    Class<? extends HttpMessageConverter<?>> converterType) {
    // 不包装以下类型:
    // 1. 已经是ResponseEntity
    // 2. 返回void的方法
    // 3. 特定的注解标记的方法

    boolean isResponseEntity =
    ResponseEntity.class.isAssignableFrom(returnType.getParameterType());

    boolean isVoid =
    void.class.equals(returnType.getParameterType());

    boolean hasSkipAnnotation =
    returnType.hasMethodAnnotation(SkipWrapper.class);

    // 只包装需要包装的类型
    return !isResponseEntity && !isVoid && !hasSkipAnnotation;
    }

    /**
    * 包装响应体
    * 注意:这里不能修改状态码,只能包装响应体
    */

    @Override
    public Object beforeBodyWrite(Object body,
    MethodParameter returnType,
    MediaType selectedContentType,
    Class<? extends HttpMessageConverter<?>> selectedConverterType,
    ServerHttpRequest request,
    ServerHttpResponse response) {

    // 如果响应已经被设置为错误状态码,保持原样
    if (isErrorResponse(response.getStatusCode())) {
    return body;
    }

    // 检查是否应该包装
    if (shouldWrap(body, response)) {
    return ApiResponse.success(body);
    }

    return body;
    }

    /**
    * 判断是否为错误响应
    */

    private boolean isErrorResponse(HttpStatusCode statusCode) {
    if (statusCode == null) {
    return false;
    }
    int status = statusCode.value();
    return status >= 400;
    }

    /**
    * 判断是否应该包装响应
    */

    private boolean shouldWrap(Object body, ServerHttpResponse response) {
    // 不包装以下情况:
    // 1. 已经是ApiResponse类型
    // 2. 响应头中有特定标记
    // 3. 特定内容类型

    if (body instanceof ApiResponse) {
    return false;
    }

    if (response.getHeaders().containsKey("X-No-Wrapper")) {
    return false;
    }

    return true;
    }

    /**
    * 统一的API响应格式
    */

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class ApiResponse<T> {
    private boolean success;
    private int code;
    private String message;
    private T data;
    private LocalDateTime timestamp;

    public static <T> ApiResponse<T> success(T data) {
    return new ApiResponse<>(
    true,
    200,
    "Success",
    data,
    LocalDateTime.now()
    );
    }

    public static <T> ApiResponse<T> error(int code, String message) {
    return new ApiResponse<>(
    false,
    code,
    message,
    null,
    LocalDateTime.now()
    );
    }
    }

    /**
    * 跳过包装的注解
    */

    @Target(ElementType.METHOD)
    @Retention(RetentionPolicy.RUNTIME)
    public @interface SkipWrapper {
    }
    }

    3.2 过滤器中的响应包装

    import jakarta.servlet.*;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import jakarta.servlet.http.HttpServletResponseWrapper;
    import java.io.IOException;
    import java.io.PrintWriter;
    import java.io.StringWriter;

    /**
    * 响应包装过滤器
    * 在过滤器层面包装响应,注意状态码处理
    */

    @Component
    @Order(Ordered.LOWEST_PRECEDENCE 100) // 非常低的优先级
    public class ResponseWrapperFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request,
    ServletResponse response,
    FilterChain chain) throws IOException, ServletException {

    if (!(request instanceof HttpServletRequest) ||
    !(response instanceof HttpServletResponse)) {
    chain.doFilter(request, response);
    return;
    }

    HttpServletRequest httpRequest = (HttpServletRequest) request;
    HttpServletResponse httpResponse = (HttpServletResponse) response;

    // 创建可缓存响应的包装器
    ResponseCachingWrapper wrapper = new ResponseCachingWrapper(httpResponse);

    try {
    chain.doFilter(request, wrapper);

    // 检查状态码
    int status = wrapper.getStatus();
    byte[] content = wrapper.getContentAsByteArray();

    // 根据状态码决定是否包装
    if (shouldWrapResponse(status, httpRequest)) {
    wrapResponse(httpResponse, status, content);
    } else {
    // 直接写入原始响应
    httpResponse.setStatus(status);
    httpResponse.getOutputStream().write(content);
    }

    } catch (Exception e) {
    handleFilterException(e, httpRequest, httpResponse);
    }
    }

    /**
    * 判断是否应该包装响应
    */

    private boolean shouldWrapResponse(int status, HttpServletRequest request) {
    String contentType = request.getContentType();
    String requestURI = request.getRequestURI();

    // 不包装的情况:
    // 1. 错误状态码(4xx, 5xx)
    // 2. 非JSON响应
    // 3. 特定路径

    if (status >= 400) {
    return false; // 错误响应不包装,保持原状态码
    }

    if (contentType == null || !contentType.contains("application/json")) {
    return false;
    }

    // 排除静态资源等
    if (requestURI.startsWith("/swagger") ||
    requestURI.startsWith("/v3/api-docs") ||
    requestURI.startsWith("/actuator")) {
    return false;
    }

    return true;
    }

    /**
    * 包装响应
    */

    private void wrapResponse(HttpServletResponse response,
    int originalStatus,
    byte[] originalContent) throws IOException {

    // 确保状态码不被覆盖
    response.setStatus(originalStatus);

    // 构建包装后的响应
    ApiResponse<?> wrappedResponse = ApiResponse.success(
    parseOriginalContent(originalContent)
    );

    // 写入包装后的响应
    response.setContentType("application/json;charset=UTF-8");
    ObjectMapper mapper = new ObjectMapper();
    String json = mapper.writeValueAsString(wrappedResponse);
    response.getWriter().write(json);
    }

    /**
    * 解析原始响应内容
    */

    private Object parseOriginalContent(byte[] content) {
    if (content == null || content.length == 0) {
    return null;
    }

    try {
    ObjectMapper mapper = new ObjectMapper();
    return mapper.readValue(content, Object.class);
    } catch (Exception e) {
    // 如果解析失败,返回原始字符串
    return new String(content, StandardCharsets.UTF_8);
    }
    }

    /**
    * 处理过滤器异常
    */

    private void handleFilterException(Exception e,
    HttpServletRequest request,
    HttpServletResponse response) throws IOException {

    response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
    response.setContentType("application/json");

    ErrorResponse error = ErrorResponse.builder()
    .status(HttpServletResponse.SC_INTERNAL_SERVER_ERROR)
    .error("Filter Error")
    .message("响应包装失败: " + e.getMessage())
    .path(request.getRequestURI())
    .build();

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(error));
    }

    /**
    * 可缓存响应的包装器
    */

    static class ResponseCachingWrapper extends HttpServletResponseWrapper {
    private final ByteArrayOutputStream content = new ByteArrayOutputStream();
    private final PrintWriter writer = new PrintWriter(content);
    private int status = HttpServletResponse.SC_OK;

    public ResponseCachingWrapper(HttpServletResponse response) {
    super(response);
    }

    @Override
    public void setStatus(int sc) {
    this.status = sc;
    super.setStatus(sc);
    }

    @Override
    public void setStatus(int sc, String sm) {
    this.status = sc;
    super.setStatus(sc, sm);
    }

    @Override
    public void sendError(int sc) throws IOException {
    this.status = sc;
    super.sendError(sc);
    }

    @Override
    public void sendError(int sc, String msg) throws IOException {
    this.status = sc;
    super.sendError(sc, msg);
    }

    @Override
    public ServletOutputStream getOutputStream() {
    return new ServletOutputStream() {
    @Override
    public boolean isReady() {
    return true;
    }

    @Override
    public void setWriteListener(WriteListener writeListener) {
    // 不需要实现
    }

    @Override
    public void write(int b) throws IOException {
    content.write(b);
    }
    };
    }

    @Override
    public PrintWriter getWriter() {
    return writer;
    }

    public int getStatus() {
    return status;
    }

    public byte[] getContentAsByteArray() {
    writer.flush();
    return content.toByteArray();
    }
    }
    }

    方案4:Spring Security配置

    4.1 自定义认证/授权处理器

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.web.AuthenticationEntryPoint;
    import org.springframework.security.web.access.AccessDeniedHandler;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

    /**
    * Spring Security配置
    * 确保认证/授权错误返回正确的状态码
    */

    @Configuration
    @EnableWebSecurity
    public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
    .csrf(csrf -> csrf.disable())

    // 认证配置
    .formLogin(form -> form
    .successHandler(authenticationSuccessHandler())
    .failureHandler(authenticationFailureHandler())
    )

    // 异常处理
    .exceptionHandling(exception -> exception
    .authenticationEntryPoint(customAuthenticationEntryPoint())
    .accessDeniedHandler(customAccessDeniedHandler())
    )

    // 授权配置
    .authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/public/**").permitAll()
    .requestMatchers("/api/admin/**").hasRole("ADMIN")
    .anyRequest().authenticated()
    );

    return http.build();
    }

    /**
    * 自定义认证成功处理器
    */

    @Bean
    public AuthenticationSuccessHandler authenticationSuccessHandler() {
    return (request, response, authentication) -> {
    // 返回200,而不是默认的302重定向
    response.setStatus(HttpStatus.OK.value());
    response.setContentType("application/json");

    Map<String, Object> body = Map.of(
    "success", true,
    "message", "认证成功",
    "user", authentication.getName(),
    "timestamp", LocalDateTime.now()
    );

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(body));
    };
    }

    /**
    * 自定义认证失败处理器
    */

    @Bean
    public AuthenticationFailureHandler authenticationFailureHandler() {
    return (request, response, exception) -> {
    // 返回401,而不是默认的302重定向
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType("application/json");

    Map<String, Object> body = Map.of(
    "success", false,
    "error", "Authentication Failed",
    "message", exception.getMessage(),
    "timestamp", LocalDateTime.now()
    );

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(body));
    };
    }

    /**
    * 自定义认证入口点
    * 处理未认证的请求
    */

    @Bean
    public AuthenticationEntryPoint customAuthenticationEntryPoint() {
    return (request, response, authException) -> {
    // 明确设置401状态码
    response.setStatus(HttpStatus.UNAUTHORIZED.value());
    response.setContentType("application/json");

    ErrorResponse error = ErrorResponse.builder()
    .status(HttpStatus.UNAUTHORIZED.value())
    .error("Unauthorized")
    .message("需要认证才能访问此资源")
    .path(request.getRequestURI())
    .build();

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(error));
    };
    }

    /**
    * 自定义访问拒绝处理器
    * 处理已认证但权限不足的请求
    */

    @Bean
    public AccessDeniedHandler customAccessDeniedHandler() {
    return (request, response, accessDeniedException) -> {
    // 明确设置403状态码
    response.setStatus(HttpStatus.FORBIDDEN.value());
    response.setContentType("application/json");

    ErrorResponse error = ErrorResponse.builder()
    .status(HttpStatus.FORBIDDEN.value())
    .error("Forbidden")
    .message("没有权限访问此资源")
    .path(request.getRequestURI())
    .build();

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(error));
    };
    }

    /**
    * 自定义注销成功处理器
    */

    @Bean
    public LogoutSuccessHandler customLogoutSuccessHandler() {
    return (request, response, authentication) -> {
    // 返回200,而不是默认的302重定向
    response.setStatus(HttpStatus.OK.value());
    response.setContentType("application/json");

    Map<String, Object> body = Map.of(
    "success", true,
    "message", "注销成功",
    "timestamp", LocalDateTime.now()
    );

    ObjectMapper mapper = new ObjectMapper();
    response.getWriter().write(mapper.writeValueAsString(body));
    };
    }
    }

    4.2 安全过滤器配置

    import jakarta.servlet.FilterChain;
    import jakarta.servlet.ServletException;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    import org.springframework.web.filter.OncePerRequestFilter;
    import java.io.IOException;

    /**
    * 自定义安全过滤器
    * 在Spring Security过滤器链中正确处理状态码
    */

    @Component
    @Order(1)
    public class CustomSecurityFilter extends OncePerRequestFilter {

    private static final List<String> EXCLUDED_PATHS = Arrays.asList(
    "/actuator/**",
    "/swagger-ui/**",
    "/v3/api-docs/**"
    );

    @Override
    protected void doFilterInternal(HttpServletRequest request,
    HttpServletResponse response,
    FilterChain filterChain) throws ServletException, IOException {

    // 检查是否应该跳过此过滤器
    if (shouldNotFilter(request)) {
    filterChain.doFilter(request, response);
    return;
    }

    // 保存原始状态码
    ResponseStatusHolder statusHolder = new ResponseStatusHolder();

    // 创建响应包装器来捕获状态码
    HttpServletResponse wrappedResponse = new HttpServletResponseWrapper(response) {
    @Override
    public void setStatus(int sc) {
    statusHolder.setStatusCode(sc);
    super.setStatus(sc);
    }

    @Override
    public void sendError(int sc) throws IOException {
    statusHolder.setStatusCode(sc);
    super.sendError(sc);
    }

    @Override
    public void sendError(int sc, String msg) throws IOException {
    statusHolder.setStatusCode(sc);
    super.sendError(sc, msg);
    }
    };

    try {
    filterChain.doFilter(request, wrappedResponse);

    // 检查状态码
    if (statusHolder.getStatusCode() != null) {
    // 确保状态码不被后续过滤器覆盖
    ensureStatusCode(response, statusHolder.getStatusCode());
    }

    } catch (Exception e) {
    handleSecurityFilterException(e, request, response, statusHolder);
    }
    }

    /**
    * 确保状态码不被覆盖
    */

    private void ensureStatusCode(HttpServletResponse response, Integer desiredStatusCode) {
    if (response.getStatus() != desiredStatusCode) {
    // 如果状态码被修改,尝试恢复
    try {
    response.setStatus(desiredStatusCode);
    } catch (IllegalStateException e) {
    // 响应已提交,无法修改状态码
    logger.warn("无法修改已提交的响应状态码: " + e.getMessage());
    }
    }
    }

    /**
    * 处理过滤器异常
    */

    private void handleSecurityFilterException(Exception e,
    HttpServletRequest request,
    HttpServletResponse response,
    ResponseStatusHolder statusHolder) throws IOException {

    // 如果还没有设置状态码,设置为500
    if (statusHolder.getStatusCode() == null) {
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    }

    // 记录日志但不覆盖现有的错误响应
    logger.error("安全过滤器异常: " + e.getMessage(), e);
    }

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) {
    String requestURI = request.getRequestURI();

    return EXCLUDED_PATHS.stream()
    .anyMatch(pattern -> new AntPathRequestMatcher(pattern).matches(request));
    }

    /**
    * 响应状态码持有器
    */

    static class ResponseStatusHolder {
    private Integer statusCode;

    public Integer getStatusCode() {
    return statusCode;
    }

    public void setStatusCode(Integer statusCode) {
    this.statusCode = statusCode;
    }
    }
    }

    方案5:Servlet容器配置

    5.1 Tomcat错误页面配置

    import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
    import org.springframework.boot.web.server.ErrorPage;
    import org.springframework.boot.web.server.ErrorPageRegistrar;
    import org.springframework.boot.web.server.ErrorPageRegistry;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.HttpStatus;

    /**
    * Servlet容器错误页面配置
    * 防止容器覆盖应用设置的状态码
    */

    @Configuration
    public class ServletContainerConfig {

    /**
    * 配置Tomcat
    */

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
    TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();

    // 添加错误页面
    factory.addErrorPages(
    new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"),
    new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500"),
    new ErrorPage(Exception.class, "/error/generic")
    );

    // 配置连接器
    factory.addConnectorCustomizers(connector -> {
    // 禁用Tomcat的错误页面,让应用处理
    connector.setProperty("errorReportValveClass",
    "org.apache.catalina.valves.ErrorReportValve");
    });

    return factory;
    }

    /**
    * 自定义错误页面注册器
    */

    @Bean
    public ErrorPageRegistrar errorPageRegistrar() {
    return new CustomErrorPageRegistrar();
    }

    static class CustomErrorPageRegistrar implements ErrorPageRegistrar {
    @Override
    public void registerErrorPages(ErrorPageRegistry registry) {
    // 注册错误页面,但设置skip=true避免覆盖状态码
    registry.addErrorPages(
    new ErrorPage(HttpStatus.NOT_FOUND, "/error/404"),
    new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500")
    );

    // 添加自定义错误页面处理器
    if (registry instanceof ConfigurableServletWebServerFactory) {
    ((ConfigurableServletWebServerFactory) registry)
    .addInitializers(new CustomErrorPageInitializer());
    }
    }
    }

    /**
    * 自定义错误页面初始化器
    */

    static class CustomErrorPageInitializer implements ServletContextInitializer {
    @Override
    public void onStartup(ServletContext servletContext) {
    // 配置错误页面,但不覆盖状态码
    servletContext.setInitParameter(
    "org.apache.catalina.core.DEFAULT_IS_ERROR_PAGE", "false");

    // 禁用Tomcat的默认错误报告
    servletContext.setInitParameter(
    "org.apache.catalina.connector.RECYCLE_FACADES", "true");
    }
    }
    }

    /**
    * 自定义错误控制器
    * 处理容器的错误页面请求
    */

    @RestController
    @RequestMapping("/error")
    public class CustomErrorController {

    @GetMapping("/404")
    public ResponseEntity<ErrorResponse> handle404(HttpServletRequest request) {
    // 明确返回404状态码
    ErrorResponse error = ErrorResponse.builder()
    .status(404)
    .error("Not Found")
    .message("请求的资源不存在")
    .path(getRequestPath(request))
    .build();

    return ResponseEntity.status(404).body(error);
    }

    @GetMapping("/500")
    public ResponseEntity<ErrorResponse> handle500(HttpServletRequest request) {
    ErrorResponse error = ErrorResponse.builder()
    .status(500)
    .error("Internal Server Error")
    .message("服务器内部错误")
    .path(getRequestPath(request))
    .build();

    return ResponseEntity.status(500).body(error);
    }

    @GetMapping("/generic")
    public ResponseEntity<ErrorResponse> handleGeneric(HttpServletRequest request) {
    // 从请求属性中获取状态码
    Integer statusCode = (Integer) request.getAttribute(
    "jakarta.servlet.error.status_code");

    if (statusCode == null) {
    statusCode = 500;
    }

    String message = (String) request.getAttribute(
    "jakarta.servlet.error.message");

    ErrorResponse error = ErrorResponse.builder()
    .status(statusCode)
    .error(HttpStatus.valueOf(statusCode).getReasonPhrase())
    .message(message != null ? message : "未知错误")
    .path(getRequestPath(request))
    .build();

    return ResponseEntity.status(statusCode).body(error);
    }

    private String getRequestPath(HttpServletRequest request) {
    return (String) request.getAttribute(
    "jakarta.servlet.error.request_uri");
    }
    }

    方案6:测试和验证

    6.1 状态码测试工具

    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.test.web.servlet.MockMvc;
    import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

    @SpringBootTest
    @AutoConfigureMockMvc
    public class StatusCodeTest {

    @Autowired
    private MockMvc mockMvc;

    /**
    * 测试自定义404状态码
    */

    @Test
    public void testCustomNotFound() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api/users/999999"))
    .andExpect(status().isNotFound()) // 期望404
    .andExpect(jsonPath("$.status").value(404))
    .andExpect(jsonPath("$.error").value("Not Found"));
    }

    /**
    * 测试成功响应状态码
    */

    @Test
    public void testSuccessStatusCode() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api/users/1"))
    .andExpect(status().isOk()) // 期望200
    .andExpect(jsonPath("$.id").exists());
    }

    /**
    * 测试认证失败状态码
    */

    @Test
    public void testAuthenticationFailure() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/api/login")
    .contentType("application/json")
    .content("{\\"username\\":\\"wrong\\",\\"password\\":\\"wrong\\"}"))
    .andExpect(status().isUnauthorized()) // 期望401
    .andExpect(jsonPath("$.error").value("Authentication Failed"));
    }

    /**
    * 测试权限不足状态码
    */

    @Test
    public void testAccessDenied() throws Exception {
    // 使用普通用户访问管理员接口
    mockMvc.perform(MockMvcRequestBuilders.get("/api/admin/users")
    .header("Authorization", "Bearer user_token"))
    .andExpect(status().isForbidden()) // 期望403
    .andExpect(jsonPath("$.error").value("Forbidden"));
    }

    /**
    * 测试验证失败状态码
    */

    @Test
    public void testValidationFailure() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/api/users")
    .contentType("application/json")
    .content("{\\"name\\":\\"\\",\\"email\\":\\"invalid\\"}"))
    .andExpect(status().isBadRequest()) // 期望400
    .andExpect(jsonPath("$.error").value("Validation Failed"));
    }

    /**
    * 测试冲突状态码
    */

    @Test
    public void testConflictStatusCode() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.post("/api/users")
    .contentType("application/json")
    .content("{\\"name\\":\\"test\\",\\"email\\":\\"existing@example.com\\"}"))
    .andExpect(status().isConflict()) // 期望409
    .andExpect(jsonPath("$.error").value("Conflict"));
    }

    /**
    * 测试响应包装器不影响状态码
    */

    @Test
    public void testResponseWrapperWithStatusCode() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api/wrapped/users"))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.success").value(true))
    .andExpect(jsonPath("$.code").value(200))
    .andExpect(jsonPath("$.data").exists());
    }

    /**
    * 测试错误响应不被包装
    */

    @Test
    public void testErrorResponseNotWrapped() throws Exception {
    mockMvc.perform(MockMvcRequestBuilders.get("/api/wrapped/error"))
    .andExpect(status().isNotFound())
    .andExpect(jsonPath("$.success").doesNotExist()) // 不应该有success字段
    .andExpect(jsonPath("$.error").exists()); // 应该有error字段
    }
    }

    6.2 状态码调试端点

    import jakarta.servlet.http.HttpServletRequest;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import java.util.Enumeration;
    import java.util.HashMap;
    import java.util.Map;

    /**
    * 状态码调试端点
    */

    @RestController
    @RequestMapping("/debug/status")
    public class StatusCodeDebugController {

    /**
    * 显示当前请求的响应状态信息
    */

    @GetMapping("/info")
    public Map<String, Object> getStatusInfo(HttpServletRequest request) {
    Map<String, Object> info = new HashMap<>();

    // 请求信息
    info.put("method", request.getMethod());
    info.put("uri", request.getRequestURI());
    info.put("contextPath", request.getContextPath());
    info.put("servletPath", request.getServletPath());

    // 请求头
    Map<String, String> headers = new HashMap<>();
    Enumeration<String> headerNames = request.getHeaderNames();
    while (headerNames.hasMoreElements()) {
    String name = headerNames.nextElement();
    headers.put(name, request.getHeader(name));
    }
    info.put("headers", headers);

    // 响应状态信息
    Integer status = (Integer) request.getAttribute(
    "jakarta.servlet.error.status_code");
    info.put("errorStatusCode", status);

    String errorMessage = (String) request.getAttribute(
    "jakarta.servlet.error.message");
    info.put("errorMessage", errorMessage);

    // 检查是否已经被处理
    Boolean handled = (Boolean) request.getAttribute(
    "org.springframework.web.servlet.HandlerMapping.bestMatchingPattern");
    info.put("handledBySpringMVC", handled != null);

    return info;
    }

    /**
    * 测试不同状态码的响应
    */

    @GetMapping("/test/{code}")
    public ResponseEntity<Map<String, Object>> testStatusCode(
    @PathVariable int code, HttpServletRequest request) {

    try {
    HttpStatus httpStatus = HttpStatus.valueOf(code);

    Map<String, Object> body = new HashMap<>();
    body.put("requestedCode", code);
    body.put("httpStatus", httpStatus.name());
    body.put("message", "测试状态码: " + code);
    body.put("timestamp", LocalDateTime.now());
    body.put("path", request.getRequestURI());

    return ResponseEntity.status(httpStatus).body(body);

    } catch (IllegalArgumentException e) {
    // 无效的状态码
    Map<String, Object> body = new HashMap<>();
    body.put("error", "Invalid Status Code");
    body.put("message", "无效的HTTP状态码: " + code);
    body.put("validRange", "100-599");

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

    /**
    * 检查状态码是否会被覆盖
    */

    @GetMapping("/check-override")
    public ResponseEntity<Map<String, Object>> checkStatusCodeOverride() {
    // 设置一个非标准状态码
    int customStatusCode = 418; // I'm a teapot

    Map<String, Object> body = new HashMap<>();
    body.put("customStatusCode", customStatusCode);
    body.put("message", "这个状态码应该被正确返回");
    body.put("test", "检查状态码是否会被覆盖");

    return ResponseEntity.status(customStatusCode).body(body);
    }
    }

    五、最佳实践总结

    1. 状态码设置优先级

    1. 控制器直接返回ResponseEntity
    2. @ResponseStatus注解
    3. 异常处理器设置
    4. 过滤器/拦截器修改
    5. Spring Security处理器
    6. 容器错误页面

    2. 避免状态码覆盖的建议

    • 明确设置:在控制器中明确设置状态码
    • 避免冲突:确保不同组件之间状态码设置不冲突
    • 检查顺序:了解组件执行顺序,避免后面的覆盖前面的
    • 使用属性:通过请求属性传递状态码信息
    • 日志记录:记录状态码变化,便于调试

    3. 配置管理

    # application.yml
    server:
    error:
    # 禁用默认错误页面
    whitelabel:
    enabled: false

    # 自定义错误路径
    path: /error

    # 是否包含堆栈信息
    include-stacktrace: never

    # 是否包含消息
    include-message: always

    # 是否包含绑定错误
    include-binding-errors: never

    spring:
    mvc:
    # 配置静态资源处理
    static-path-pattern: /static/**

    # 抛出异常时不设置状态码
    throw-exception-if-no-handler-found: false

    web:
    resources:
    # 添加默认静态资源位置
    static-locations: classpath:/static/

    4. 监控和告警

    • 监控异常状态码比例
    • 设置状态码异常的告警
    • 记录状态码变更日志
    • 定期审计状态码使用

    5. 测试策略

    • 单元测试:测试控制器状态码
    • 集成测试:测试完整请求链
    • 安全测试:测试认证/授权状态码
    • 性能测试:测试异常情况下的状态码

    六、常见问题FAQ

    Q1: 为什么我的404变成了200?

    A: 可能是被异常处理器、响应包装器或过滤器覆盖了。检查组件执行顺序和条件判断。

    Q2: Spring Security如何影响状态码?

    A: Spring Security有自己的认证/授权处理器,可能会覆盖应用设置的状态码。需要配置自定义处理器。

    Q3: 如何调试状态码被覆盖的问题?

    A:

  • 添加日志记录每个组件的状态码设置
  • 使用调试端点查看状态信息
  • 检查过滤器链和拦截器链
  • 查看Spring Boot的自动配置
  • Q4: 响应包装器如何处理状态码?

    A: 响应包装器应该只包装响应体,不修改状态码。错误响应不应该被包装。

    Q5: 如何确保状态码的一致性?

    A:

  • 制定状态码使用规范
  • 使用枚举定义状态码
  • 编写状态码测试用例
  • 使用代码审查确保规范执行
  • 通过以上解决方案,可以有效解决Spring Boot 3.x中ResponseEntity自定义状态码被覆盖的问题。关键是要理解Spring MVC的处理流程,合理配置各个组件,确保状态码在正确的时机被设置且不被意外覆盖。

    赞(0)
    未经允许不得转载:171主机测评 » Spring Boot 3.x ResponseEntity自定义状态码被覆盖问题详解
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址