Spring Boot 3.x ResponseEntity自定义状态码被覆盖问题详解
·
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)