记RestTemplateBuilder奇诡的坑

前言

在紧张的开发工作中,总能遇到一些奇怪的问题。今天的主角是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的时候,这可能不是一个好的实现。

你可能感兴趣的:(springboot,http)