RestTemplate配置HttpClient后拦截器仅首次执行后续挂起问题
问题场景
我遇到过完全一致的问题:当RestTemplate配置使用HttpClient作为底层客户端时,自定义的RestTemplateHeaderInterceptor拦截器仅首次请求正常执行,后续请求会卡在这段代码处无响应也不抛出异常:
if (url.contains("/auth") || url.contains("/custcare-common")) { return execution.execute(request, body); }
移除HttpClient配置,改用RestTemplate默认的客户端实现时,问题完全消失。这个拦截器的核心作用是捕获401未授权状态,自动刷新访问令牌。
相关代码片段
自定义拦截器完整代码
public class RestTemplateHeaderInterceptor implements ClientHttpRequestInterceptor { @Override public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { String url = request.getURI().getPath(); if (url.contains("/auth") || url.contains("/custcare-common")) { return execution.execute(request, body); } HttpSession session = SessionBean.getSession(); if (session != null) { // check header does not has token then add to header if (request.getHeaders().isEmpty() || !request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { // add jwt token to header Object accessToken = session.getAttribute(AppConstants.Token.ACCESS_TOKEN); if (accessToken != null) { request.getHeaders().add(HttpHeaders.AUTHORIZATION, SecurityConstants.TOKEN_PREFIX + accessToken); } } // refresh token when expire ClientHttpResponse response = execution.execute(request, body); if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) { Object refreshToken = session.getAttribute(AppConstants.Token.REFRESH_TOKEN); if (refreshToken != null) { TokenData newTokenData = getNewToken(refreshToken.toString()); if (newTokenData != null) { session.setAttribute(AppConstants.Token.ACCESS_TOKEN, newTokenData.getAccessToken()); session.setAttribute(AppConstants.Token.REFRESH_TOKEN, newTokenData.getRefreshToken()); request.getHeaders().set(HttpHeaders.AUTHORIZATION, SecurityConstants.TOKEN_PREFIX + newTokenData.getAccessToken()); return execution.execute(request, body); } } } } return execution.execute(request, body); } private TokenData getNewToken(String refreshToken) { TokenData tokenData = null; RefreshTokenRequest request = new RefreshTokenRequest(); request.setRefreshToken(refreshToken); MessagesResponse<TokenData> response = RestUtil.post(RestUtil.getCcApiUrl("cc-api.authen.refresh-token"), request, new ParameterizedTypeReference<MessagesResponse<TokenData>>() {}); if (response != null) { tokenData = response.getData(); } return tokenData; } }
初始RestTemplate配置(有问题版本)
@Configuration public class RestTemplateConfig { @Bean public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() { PoolingHttpClientConnectionManager result = new PoolingHttpClientConnectionManager(); result.setMaxTotal(20); return result; } @Bean public RequestConfig requestConfig() { RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(60000).setConnectTimeout(60000) .setSocketTimeout(60000).build(); return requestConfig; } @Bean public CloseableHttpClient httpClient() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { TrustStrategy trustStrategy = new TrustStrategy() { @Override public boolean isTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { return true; } }; SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, trustStrategy).build(); SSLConnectionSocketFactory csf = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier()); CloseableHttpClient result = HttpClientBuilder.create() .setConnectionManager(poolingHttpClientConnectionManager()) .setDefaultRequestConfig(requestConfig()) .setSSLSocketFactory(csf) .build(); return result; } @Bean @Primary public RestTemplate restTemplate() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException { HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient()); RestTemplate restTemplate = new RestTemplate(requestFactory); restTemplate.setErrorHandler(new RestResponseErrorHandler()); List<ClientHttpRequestInterceptor> interceptors = restTemplate.getInterceptors(); if (interceptors.isEmpty()) { interceptors = new ArrayList<>(); } interceptors.add(new RestTemplateHeaderInterceptor()); restTemplate.setInterceptors(interceptors); return restTemplate; } }
问题根源分析
问题出在PoolingHttpClientConnectionManager的连接池配置上:初始配置只设置了setMaxTotal(20),但没有设置defaultMaxPerRoute。HttpClient的连接池默认的defaultMaxPerRoute值是2,这意味着针对同一个目标路由(比如你的/auth或/custcare-common接口所在的域名),最多只能同时建立2个连接。
当你的拦截器在处理401时,会发起刷新令牌的请求(调用getNewToken方法,而这个方法内部又使用了同一个RestTemplate),这会占用连接池中的路由连接数。当后续请求量上来后,路由连接被耗尽,新的请求就会在等待获取连接时挂起,没有超时也没有异常(因为你设置的connectionRequestTimeout是60秒,可能还没到超时时间)。
解决方案
修改PoolingHttpClientConnectionManager的配置,同时设置setMaxTotal和defaultMaxPerRoute,确保单个路由的连接数足够支撑你的请求量:
@Bean public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() { PoolingHttpClientConnectionManager result = new PoolingHttpClientConnectionManager(); result.setMaxTotal(100); // 全局最大连接数 result.setDefaultMaxPerRoute(100); // 单个路由的最大连接数 return result; }
调整这两个参数后,连接池不会因为单个路由的连接耗尽而导致请求挂起,拦截器就能正常处理所有后续请求了。
内容的提问来源于stack exchange,提问作者phucvm8x

