前言
在紧张的开发工作中,总能遇到一些奇怪的问题。今天的主角是RestTemplateBuilder。
问题描述
由于某些原因,我需要一个不检查HTTPS证书的RestTemplate。但是不管我怎么搞,就是依然会被检查到证书而抛出请求异常!在构建RestTemplate时,我使用了RestTemplateBuilder. (以下代码不涉及公司业务,属于纯技术代码)
package com.evan.demo.nacos.config.demos;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.NoopHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.ssl.SSLContextBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.util.Objects;
@Configuration
public class RestTemplateConfig {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Primary
@Bean
public RestTemplate restTemplate() {
return newRestTemplateBuilder(null)
.build();
}
/**
* 不检查https的SSL证书
*/
@Bean("unsafeRestTemplate")
public RestTemplate unsafeRestTemplate() {
HttpComponentsClientHttpRequestFactory unsafeRequestFactory = new HttpComponentsClientHttpRequestFactory();
unsafeRequestFactory.setHttpClient(unsafeHttpClient());
return newRestTemplateBuilder(unsafeRequestFactory)
.builder();
}
private static RestTemplateBuilder newRestTemplateBuilder(HttpComponentsClientHttpRequestFactory unsafeRequestFactory) {
RestTemplateBuilder builder = new RestTemplateBuilder()
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(50));
if (Objects.nonNull(unsafeRequestFactory)) {
builder.requestFactory(() -> unsafeRequestFactory);
}
return builder;
}
private HttpClient unsafeHttpClient() {
SSLContext unsafeSSLContext;
try {
unsafeSSLContext = SSLContextBuilder.create()
.setProtocol(SSLConnectionSocketFactory.TLS)
.loadTrustMaterial(new TrustAllStrategy())
.build();
} catch (NoSuchAlgorithmException | KeyManagementException | KeyStoreException e) {
logger.error(e.getMessage(), e);
throw new RuntimeException("unsafe SSLContext create error.");
}
return HttpClientBuilder.create()
.setSSLContext(unsafeSSLContext)
.setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE)
.build();
}
}
大致就是,有两个RestTemplate,其中一个正常校验证书,另一个不用。不需要校验证书的,通过提供一个不检查Https证书的ClientHttpRequestFactory实现。为了复用超时设置等公共信息,所以就抽取了个方法,并且判空factory参数再设置到builder中。
问题解决
由于一时间想不到办法,所以我试了一下,直接使用 public RestTemplate.RestTemplate (ClientHttpRequestFactory requestFactory)。这种方式则正常了。但是超时时间,则需要通过HttpComponentsClientHttpRequestFactory来设置了。
但这个问题还是在脑海中萦绕,为什么使用builder就不行呢?于是开始打断点。发现以下代码很诡异:
if (Objects.nonNull(unsafeRequestFactory)) {
builder.requestFactory(() -> unsafeRequestFactory);
}
return builder;
当跟着断点进入到builder.requestFactory中时,里面创建了个新的builder,这个新的factory也确实设置进去了。但是出来的时候,factory没了!?!! 这个时候,相信脑子转得快的同学应该已经知道原因了。但是当时由于太想了解这个问题,一时间竟然没反应过来。又继续断点了好几次。。。
好了,公布原因:因为builder的每个方法都是重新创建了一个新的builder。当我们使用流式调用时,不会出现问题,因为每个调用都是对于新builder的调用。但是,当我们存在非流式调用的情况时,问题就发生了。这个调用则不会实际生效。
要解决这个问题也比较简单,只要在非流式调用后,给变量重新赋值就好了。像上面的代码的话,这样处理一下就可以了:
builder = builder.requestFactory(() -> unsafeRequestFactory);
总结
方案一: 直接创建RestTemplate,然后通过HttpComponentsClientHttpRequestFactory来设置超时时间。
方案二:像上面那样,在非流式调用时,用返回值再次对builder变量赋值。
后记
由于大家都习惯流式调用,一般情况下倒也不会出现问题。但是当出现非流式调用时,大家对于builder的使用习惯应该大多都是直接调用的,而不会再次给变量赋值。所以提醒大家在设计builder的时候,这可能不是一个好的实现。