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

一、问题背景

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

  1. 全局异常处理器覆盖了控制器返回的状态码
  2. 过滤器或拦截器修改了响应状态码
  3. Spring Security处理了认证/授权异常
  4. Spring Boot的自动错误处理机制介入
  5. 响应包装器修改了状态码

二、问题现象

常见问题场景:

// 控制器返回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

典型问题:

  1. 自定义404被覆盖为200
  2. 业务异常状态码被覆盖为500
  3. 认证失败状态码不正确
  4. 响应被统一包装后状态码丢失

三、根本原因分析

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:

  1. 添加日志记录每个组件的状态码设置
  2. 使用调试端点查看状态信息
  3. 检查过滤器链和拦截器链
  4. 查看Spring Boot的自动配置

Q4: 响应包装器如何处理状态码?

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

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

A:

  1. 制定状态码使用规范
  2. 使用枚举定义状态码
  3. 编写状态码测试用例
  4. 使用代码审查确保规范执行

通过以上解决方案,可以有效解决Spring Boot 3.x中ResponseEntity自定义状态码被覆盖的问题。关键是要理解Spring MVC的处理流程,合理配置各个组件,确保状态码在正确的时机被设置且不被意外覆盖。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐