Jersey客户端接收分块gzip响应时无法复用连接的问题
分块Gzip响应下Jersey ApacheConnector连接无法复用的解决方法
在使用Jersey 3.x/2.x搭配ApacheConnector客户端时,若服务端返回分块编码的Gzip压缩响应,会出现连接读取响应后立即关闭、无法复用的问题;但非Gzip压缩的分块响应可正常复用连接。问题触发点在ApacheConnectionClosingStrategy类的request.abort()逻辑,即便配置了连接池、长连接及复用策略也无法解决。
问题原因
Jersey的ApacheConnector默认关闭策略在处理Gzip压缩的分块响应时,会误判连接状态为未释放,从而触发abort()强制关闭连接。这是因为Gzip压缩流的包装导致连接复用的判断逻辑失效,而普通分块响应不存在这个包装层,所以可正常复用。
解决方法
方法1:自定义连接关闭策略
重写默认的ApacheConnectionClosingStrategy,针对Gzip分块响应跳过abort()操作,改为正常释放连接:
import org.apache.http.Header; import org.apache.http.HttpHeaders; import org.apache.http.HttpResponse; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.util.EntityUtils; import org.glassfish.jersey.apache.connector.ApacheClientRequest; import org.glassfish.jersey.apache.connector.ApacheConnectionClosingStrategy; import java.io.IOException; public class GzipChunkedConnectionClosingStrategy implements ApacheConnectionClosingStrategy { @Override public void close(ApacheClientRequest request, boolean forceClose) { HttpResponse response = request.getHttpResponse(); if (response == null) { if (request.getHttpRequest() != null) { request.getHttpRequest().abort(); } return; } // 判断是否为Gzip分块响应 Header contentEncoding = response.getFirstHeader(HttpHeaders.CONTENT_ENCODING); Header transferEncoding = response.getFirstHeader(HttpHeaders.TRANSFER_ENCODING); boolean isGzipChunked = contentEncoding != null && "gzip".equalsIgnoreCase(contentEncoding.getValue()) && transferEncoding != null && "chunked".equalsIgnoreCase(transferEncoding.getValue()); if (forceClose || !isGzipChunked) { // 非目标场景或强制关闭时,执行原abort逻辑 HttpRequestBase httpRequest = request.getHttpRequest(); if (httpRequest != null) { httpRequest.abort(); } } else { // Gzip分块响应:确保实体被释放,而非强制abort try { EntityUtils.consume(response.getEntity()); } catch (IOException ignored) { // 忽略释放时的异常 } } } }
然后在客户端配置中替换默认策略:
// 原连接池配置不变 PoolingHttpClientConnectionManager connectionManager2 = new PoolingHttpClientConnectionManager(); connectionManager2.setMaxTotal(100); connectionManager2.setDefaultMaxPerRoute(100); final ClientConfig clientConfig = new ClientConfig(); clientConfig.connectorProvider(new ApacheConnectorProvider()); clientConfig.property(ApacheClientProperties.CONNECTION_MANAGER, connectionManager2); clientConfig.property(ApacheClientProperties.KEEPALIVE_STRATEGY, (ConnectionKeepAliveStrategy) (response, context) -> 60000); clientConfig.property(ApacheClientProperties.REUSE_STRATEGY, (ConnectionReuseStrategy) (response, context) -> true); // 添加自定义关闭策略 clientConfig.property(ApacheClientProperties.CONNECTION_CLOSING_STRATEGY, new GzipChunkedConnectionClosingStrategy()); JERSEY_CLIENT = ClientBuilder.newClient(clientConfig);
方法2:让Apache HttpClient自动处理Gzip压缩
绕过Jersey的压缩处理逻辑,让Apache HttpClient在底层自动完成Gzip解压,使Jersey处理的是普通分块响应:
import org.apache.http.HttpEntity; import org.apache.http.HttpHeaders; import org.apache.http.HttpRequest; import org.apache.http.HttpResponse; import org.apache.http.client.HttpClient; import org.apache.http.conn.ConnectionKeepAliveStrategy; import org.apache.http.conn.ConnectionReuseStrategy; import org.apache.http.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.message.BasicHeader; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpRequestInterceptor; import org.apache.http.protocol.HttpResponseInterceptor; import org.apache.http.client.entity.GzipDecompressingEntity; PoolingHttpClientConnectionManager connectionManager2 = new PoolingHttpClientConnectionManager(); connectionManager2.setMaxTotal(100); connectionManager2.setDefaultMaxPerRoute(100); // 配置HttpClient自动处理Gzip HttpClient httpClient = HttpClientBuilder.create() .setConnectionManager(connectionManager2) .setKeepAliveStrategy((response, context) -> 60000) .setConnectionReuseStrategy((response, context) -> true) // 添加请求头:接受Gzip压缩 .addInterceptorFirst((HttpRequestInterceptor) (request, context) -> { if (!request.containsHeader(HttpHeaders.ACCEPT_ENCODING)) { request.addHeader(new BasicHeader(HttpHeaders.ACCEPT_ENCODING, "gzip")); } }) // 响应拦截:自动解压Gzip实体 .addInterceptorFirst((HttpResponseInterceptor) (response, context) -> { HttpEntity entity = response.getEntity(); if (entity != null) { Header encoding = entity.getContentEncoding(); if (encoding != null && "gzip".equalsIgnoreCase(encoding.getValue())) { response.setEntity(new GzipDecompressingEntity(entity)); } } }) .build(); final ClientConfig clientConfig = new ClientConfig(); clientConfig.connectorProvider(new ApacheConnectorProvider()); // 直接传入配置好的HttpClient clientConfig.property(ApacheClientProperties.HTTP_CLIENT, httpClient); JERSEY_CLIENT = ClientBuilder.newClient(clientConfig);
方法3:确保实体被完全消费
若使用InputStream读取响应,需确保流被完全读取并关闭,避免连接因未消费完实体被判定为不可复用:
import org.apache.commons.io.IOUtils; // 读取响应时完全消费流 try (InputStream is = response.readEntity(InputStream.class)) { IOUtils.copy(is, new ByteArrayOutputStream()); // 将流读取到缓冲区,确保完全消费 } catch (IOException e) { // 按需处理异常 }
此方法需配合其他方案使用,单独使用可能无法彻底解决问题,但能减少异常场景。
Jersey本身并非不支持分块Gzip响应,只是ApacheConnector的默认策略在该场景下存在逻辑缺陷,通过上述方法可修复连接复用问题。
内容的提问来源于stack exchange,提问作者user29495070
相关产品推荐
相关产品推荐

