Spring Boot 3中如何在Servlet Filter设置自定义响应?响应被覆盖
问题场景
在Spring Boot 3中编写了如下Filter类,未调用chain.doFilter(),仅尝试返回自定义错误响应,但自定义消息始终不生效,返回的是Spring Boot默认错误JSON:
@Component public class AcknowledgeFilter implements jakarta.servlet.Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { // 无效 ((HttpServletResponse) response).sendError(500, "my-error-message"); // 无效 // response.getWriter().write("my-error-message"); // response.getWriter().flush(); // 或 response.getWriter().close(); // 同样无效 response.getOutputStream().write("my-error-message".getBytes()); return; } }
返回的默认响应:
{ "timestamp": "2023-10-17T15:05:12.825+00:00", "status": 500, "error": "Internal Server Error", "path": "/api/myrequest/path" }
开启调试日志后发现,错误响应被BasicErrorController处理:
o.s.security.web.FilterChainProxy : Secured GET /api/order/last?customerNumber=123 o.a.c.c.C.[Tomcat].[localhost] : Processing ErrorPage[errorCode=0, location=/api/error] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) o.s.security.web.FilterChainProxy : Securing GET /api/error?customerNumber=123 o.s.security.web.FilterChainProxy : Secured GET /api/error?customerNumber=123 o.s.web.servlet.DispatcherServlet : "ERROR" dispatch for GET "/api/error?customerNumber=123", parameters={masked} s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest) c.a.c.c.c.EndpointLoggingInterceptor : Endpoint /api/error called s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#error(HttpServletRequest)
默认响应来源
这个默认JSON响应来自Spring Boot自动配置的BasicErrorController。当调用sendError()方法,或者响应状态码被标记为错误码时,内嵌的Tomcat容器会触发错误页面转发,将请求转发到/error路径,而Spring Boot默认注册的BasicErrorController会处理这个请求,生成包含timestamp、status、error、path的默认错误JSON。
直接用getWriter()或getOutputStream()写响应无效,是因为Tomcat在触发错误页面转发时,会忽略之前写入的响应内容,重新发起对/error路径的请求,最终返回BasicErrorController生成的响应。
解决方法
方法一:直接写响应且不触发错误页面转发
如果想在Filter中直接返回自定义响应,需要避免触发Tomcat的错误页面机制。不要调用sendError(),而是直接设置响应状态码、内容类型,然后写入响应内容,同时确保响应已提交:
@Component public class AcknowledgeFilter implements jakarta.servlet.Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletResponse httpResponse = (HttpServletResponse) response; // 设置状态码 httpResponse.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 设置内容类型为JSON(根据需要调整) httpResponse.setContentType("application/json;charset=UTF-8"); // 写入自定义响应内容 String customError = "{\"status\":500,\"message\":\"my-error-message\"}"; httpResponse.getWriter().write(customError); // 确保响应提交 httpResponse.getWriter().flush(); httpResponse.getWriter().close(); return; } }
注意:这里用setStatus()而不是sendError(),setStatus()只会设置响应状态码,不会触发Tomcat的错误页面转发,因此写入的响应内容会被正常返回。
方法二:自定义ErrorController覆盖默认行为
如果希望保留Spring Boot的错误处理机制,但自定义响应内容,可以创建自定义的ErrorController来替代BasicErrorController:
- 实现
ErrorController接口(或继承AbstractErrorController):
@RestController public class CustomErrorController implements ErrorController { private final ErrorAttributes errorAttributes; public CustomErrorController(ErrorAttributes errorAttributes) { this.errorAttributes = errorAttributes; } @RequestMapping("/error") public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request) { // 获取错误信息 Map<String, Object> errorMap = errorAttributes.getErrorAttributes(request, ErrorAttributeOptions.defaults()); // 修改或添加自定义字段 errorMap.put("message", "my-error-message"); HttpStatus status = HttpStatus.valueOf((Integer) errorMap.get("status")); return new ResponseEntity<>(errorMap, status); } }
- 若需要获取调用
sendError()时传入的自定义消息,可通过request.getAttribute(RequestDispatcher.ERROR_MESSAGE)获取:
@RequestMapping("/error") public ResponseEntity<Map<String, Object>> handleError(HttpServletRequest request) { Map<String, Object> customResponse = new HashMap<>(); Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE); String errorMessage = (String) request.getAttribute(RequestDispatcher.ERROR_MESSAGE); customResponse.put("status", statusCode); customResponse.put("message", errorMessage != null ? errorMessage : "Default error message"); customResponse.put("path", request.getRequestURI()); HttpStatus status = HttpStatus.valueOf(statusCode); return new ResponseEntity<>(customResponse, status); }
方法三:通过配置开启错误消息包含(针对sendError场景)
如果只是希望让sendError()传入的消息出现在默认响应中,可在application.properties或application.yml中添加配置:
server.error.include-message=always
这样BasicErrorController生成的响应会包含message字段,值就是调用sendError()时传入的内容。
内容的提问来源于stack exchange,提问作者Datz

