You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

RestTemplate配置HttpClient后拦截器仅首次执行后续挂起问题

问题排查与解决: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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.14 09:03:38