java - Spring RestTemplate拦截器不执行请求

标签 java spring resttemplate interceptor

当我配置 RestTemplate 使用 HttpClient 时,我的拦截器仅第一次执行,第二次执行时它会挂起,在下面的这个 block 中。没有异常(exception),我不知道为什么! 如果我删除 httpClient 就没有问题了。 (我的拦截器意图是捕获 401 未授权状态以刷新访问 token )

if (url.contains("/auth") || url.contains("/custcare-common")) {
    return execution.execute(request, body);
}

完整代码如下:

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;
    }
}

最后,我通过设置 DefaultMaxPerRoute 解决了这个问题,但我仍然对此感到困惑。 :D

@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
    PoolingHttpClientConnectionManager result = new PoolingHttpClientConnectionManager();
    result.setMaxTotal(100);
    result.setDefaultMaxPerRoute(100);
    return result;
}

最佳答案

这是一个老问题,但由于人们仍然通过他们选择的搜索引擎来到这里,并且对于为什么更改“有效”感到困惑,这可能会节省一些时间:

如果响应代码为 401,则问题源于原始 http 响应未在您的自定义拦截器类中使用。

增加连接池容量只会隐藏问题(并且可能“修复”应用程序运行的问题),但实际上并不会将连接释放回池并解决问题。

在您的拦截器类中,401 响应永远不会被消耗,仅用于 StatusCode 检查。此响应会阻塞池的一个连接。 401 响应需要手动使用,因为它从未被传递。

如果响应有 401 状态,您将执行一个新请求,而原始 401 响应只会永远阻止连接。这就是为什么在一定数量的 401 请求(默认为 5)之后不再有可用连接的原因。由于在这 5 401 个请求之后,ConnectionPool 中不再有可用的连接,因此线程会挂起,并且执行调用永远不会完成,因为它正在等待可用的空闲连接。

需要改变吗?一行代码可以让我们避免头痛和解决方法,在 401 检查后(或任何时候未返回/使用响应时)关闭响应:

public class RestTemplateHeaderInterceptor implements ClientHttpRequestInterceptor {

@Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
        throws IOException {
        // ... omitted ...

        // refresh token when expire
        // THIS RESPONSE IS NEVER HANDLED
        ClientHttpResponse response = execution.execute(request, body);
        if (response.getStatusCode() == HttpStatus.UNAUTHORIZED) {
            // Close response only needed for the status code and refresh
            response.close();
            // Continue on with your code.
            Object refreshToken = session.getAttribute(AppConstants.Token.REFRESH_TOKEN);
            if (refreshToken != null) {
                // ... omitted ...
                    return execution.execute(request, body);
                }
            }
        }
    }

    return execution.execute(request, body);
}

在使用 HttpEntites 时,还有一种方便的 HttpEntities 方法:org.apache.http.util.EntityUtils#consume。

请注意,此代码中针对 401 所做的拦截器中的重新请求不会通过添加到 httpclient 拦截器列表中的其他拦截器传递。

关于java - Spring RestTemplate拦截器不执行请求,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59244512/

相关文章:

spring - 在 Junit 测试中使用来自 WEB-INF 的 i18n 资源

java - 属性值的自定义类型转换

java - 当我尝试在 ubuntu 上启动 Scala 时抛出异常

mysql - Liquibase 不在 MySQL/MariaDB 上执行 SQL,而是在 Spring Boot 中执行 H2

rest - Http Post 请求导致流结束

java - 如何模拟带有客户端或服务器错误的 RestTemplate?

java - 如何使用 RestTemplate 禁用编码

java - 带有样式 block 的 JTextField

java - 将 sql 翻译添加到 criteriaBuilder JPQL

java - 自定义注释中的 Spring 表达式语言