Spring 4.3+如何重写HEAD请求默认行为以返回Content-Length且不写数据
你说的这个痛点我太懂了——Spring 4.3+开始,@GetMapping和@RequestMapping(method = HttpMethod.GET)确实会自动支持HTTP HEAD请求,但默认逻辑实在鸡肋:它直接复用GET的处理流程,把所有响应数据都写入OutputStream,最后才丢弃响应体返回给客户端。这完全不符合HEAD请求的规范,还平白浪费了IO资源,毕竟HEAD只需要返回响应头(尤其是Content-Length)就行,根本不用输出内容。
下面给你两种实用的解决方案,帮你搞定这个问题:
方案1:单独定义HEAD请求映射,手动计算Content-Length
如果只有少数接口需要优化,你可以为这些接口单独添加HEAD请求的处理方法,只计算响应体长度并设置到响应头,完全不输出内容:
@RestController @RequestMapping("/api/data") public class DataController { // 原有的GET请求处理逻辑 @GetMapping public String getData() { return "这里是原本要返回的大量数据内容"; } // 专门处理HEAD请求 @RequestMapping(method = HttpMethod.HEAD) public void handleHeadRequest(HttpServletResponse response) throws UnsupportedEncodingException { // 复用GET方法里的内容生成逻辑(或者直接调用业务层计算预期内容长度) String expectedContent = getData(); int contentLength = expectedContent.getBytes(StandardCharsets.UTF_8).length; response.setContentLength(contentLength); // 啥也不用写,直接返回就行 } }
这种方式简单直观,对原有GET逻辑几乎没有侵入,但如果接口数量多的话,会有重复代码。
方案2:全局拦截HEAD请求,统一优化
如果项目里有大量接口需要处理HEAD请求,推荐用全局拦截器统一处理,避免重复劳动:
首先自定义一个拦截器,在请求处理后判断是否是HEAD请求,然后截断响应体,只保留响应头:
@Component public class HeadRequestOptimizerInterceptor implements HandlerInterceptor { @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { if (HttpMethod.HEAD.name().equalsIgnoreCase(request.getMethod())) { // 如果响应已经被缓存(比如用了ContentCachingResponseWrapper),直接获取内容长度 if (response instanceof ContentCachingResponseWrapper) { ContentCachingResponseWrapper cachedResponse = (ContentCachingResponseWrapper) response; int contentLength = cachedResponse.getContentAsByteArray().length; cachedResponse.setContentLength(contentLength); // 这里只提交响应头,不写回缓存的内容 cachedResponse.copyBodyToResponse(); } else { // 如果没有缓存,直接设置Content-Length(替换成实际的业务长度计算逻辑) response.setContentLength(0); } // 关闭输出流,避免后续写入操作 ServletOutputStream outputStream = response.getOutputStream(); outputStream.flush(); outputStream.close(); } } }
然后把这个拦截器注册到Spring MVC中:
@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Autowired private HeadRequestOptimizerInterceptor headRequestOptimizerInterceptor; @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(headRequestOptimizerInterceptor); } }
这种方式可以全局生效,所有HEAD请求都会被自动优化,不用逐个接口修改。
额外说下Spring的默认逻辑
Spring 4.3+之所以让@GetMapping支持HEAD请求,是因为它自动注册了HeadMethodRequestHandler这个处理器。这个处理器的逻辑是把HEAD请求委托给对应的GET处理器,等GET处理器把整个响应体写入流之后,再把响应体丢弃,只返回响应头。这就是为什么你会觉得浪费——数据其实已经写了一遍,只是没发给客户端而已,完全没达到我们想要的“不写入全部数据”的效果。
所以上面的两种方案,本质上都是绕过这个默认逻辑:要么提前计算Content-Length,不执行完整的GET写入逻辑;要么拦截后截断响应,避免不必要的IO操作。
内容的提问来源于stack exchange,提问作者hooknc

