因?yàn)橐粋€(gè)bug,我還是掀開(kāi)了openfeign的神秘面紗

報(bào)錯(cuò)

最近項(xiàng)目中訪問(wèn)一個(gè)外部api報(bào)錯(cuò)了,報(bào)錯(cuò)信息如下

PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target

看著像是證書(shū)問(wèn)題,這個(gè)時(shí)候我首先想到的是百度下,看看怎么解決。

解決方案

百度告訴我說(shuō)如果你open-feign中使用的是http client,那么可以通過(guò)下面的配置來(lái)讓跳過(guò)SSL驗(yàn)證

feign:
  httpclient:
    disable-ssl-validation: false

結(jié)果還是報(bào)同樣的錯(cuò)誤。 于是我又百度,又重新找了一個(gè)解決方法,這次的方案是讓我自己重寫(xiě)Client了,具體操作如下

@Configuration
public class FeignConfiguration {

@Bean
public Client feignClient() throws NoSuchAlgorithmException, KeyManagementException {
    SSLContext ctx = SSLContext.getInstance("SSL");
    X509TrustManager tm = new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    };
    ctx.init(null, new TrustManager[]{tm}, null);


    return  new Client.Default(ctx.getSocketFactory(), (hostName, session) -> true);

}
   
}

這把我感覺(jué)要起飛了, 一切盡在掌握中,重新deploy,打開(kāi)postman,測(cè)試測(cè)試我的接口。

測(cè)試后感覺(jué)好了但是看日志又沒(méi)有完全好。 這個(gè)接口倒是不報(bào)錯(cuò)了,但是我調(diào)用內(nèi)部服務(wù)給我報(bào)錯(cuò)了,比如我這里的內(nèi)部服務(wù)名稱(chēng)叫做

pro-file, 就現(xiàn)在它沒(méi)法根據(jù)我這個(gè)pro-file名字找到對(duì)應(yīng)的IP了,從而導(dǎo)致我這個(gè)服務(wù)使用不了了。

百度誤我!

求人不如求己

此刻我自信的打開(kāi)了IDEA, 輸入了類(lèi)名 FeignAutoConfiguration , Spring Cloud關(guān)于某個(gè)組件的自動(dòng)注入類(lèi)大多是XXXConfiguration, 所以按照這么找準(zhǔn)沒(méi)錯(cuò)。

然后我有自信的把斷點(diǎn)打在了這個(gè)部分 FeignAutoConfiguration:246

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
@Conditional(HttpClient5DisabledConditions.class)
protected static class HttpClientFeignConfiguration {
    // 省略其他代碼

    @Bean
    @ConditionalOnMissingBean(Client.class)
    public Client feignClient(HttpClient httpClient) {
        return new ApacheHttpClient(httpClient);
    }

}

重新啟動(dòng)項(xiàng)目,好家伙斷點(diǎn)沒(méi)進(jìn)來(lái)呀。 沒(méi)進(jìn)來(lái)的原因大概率可能是不滿足條件,我趕緊看看這里對(duì)應(yīng)的Conditional, 發(fā)現(xiàn)了我的代碼中沒(méi)有

設(shè)置feign.httpclient.enabled屬性的值, 而且這里也沒(méi)有設(shè)置havingValue, 根據(jù)源碼可以知道, 如果沒(méi)有設(shè)置havingValue, 那么這個(gè)屬性的值會(huì)被和false進(jìn)行比較

//org.springframework.boot.autoconfigure.condition.OnPropertyCondition.Spec#isMatch
// 這里的requiredValue是havingValue
private boolean isMatch(String value, String requiredValue) {
    if (StringUtils.hasLength(requiredValue)) {
        return requiredValue.equalsIgnoreCase(value);
    }
    return !"false".equalsIgnoreCase(value);
}

搞半天這個(gè)Configurtion相當(dāng)于沒(méi)起作用。

好好好,這么玩是吧。

既然這個(gè)配置不生效,那肯定有其他配置生效,我就找找其他配置,最終我在spring-cloud-openfeign-core這個(gè)jar包的loadbalancer這個(gè)包下面找到了我想要的配置

@ConditionalOnClass(Feign.class)
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods = false)
// Order is important here, last should be the default, first should be optional
// see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
        HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration {

}

因?yàn)槲覀冺?xiàng)目是采用springcloud alibaba進(jìn)行開(kāi)發(fā),所以引入了spring-cloud-loadbalancer這個(gè)包,因此這個(gè)這個(gè)配置類(lèi)就會(huì)生效,由于我們沒(méi)有配置使用httpclient,同樣也未使用okhttp,所以生效的配置類(lèi)只有一個(gè),那就是 DefaultFeignLoadBalancerConfiguration

這個(gè)配置類(lèi)中retryClient會(huì)被加載,因?yàn)槲覀円肓藄pring-retry.

@Bean
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
@ConditionalOnBean(LoadBalancedRetryFactory.class)
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
        matchIfMissing = true)
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
        LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) {
    return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
            loadBalancedRetryFactory, loadBalancerClientFactory);
}

這也是為什么上面我們自己配置了自己的Client后,訪問(wèn)其他spring cloud服務(wù)會(huì)找不到地址,這是因?yàn)槟J(rèn)的client不會(huì)去通過(guò)LoadBalancer去獲取服務(wù)地址。

小插曲

期間debug的時(shí)候,還發(fā)現(xiàn)最終的Client的SeataFeignClient,我一看才發(fā)現(xiàn)某個(gè)公共包引入了Seata,但是沒(méi)有使用Seata功能,然后Seata會(huì)把我們最終使用的FeignClient在給封裝一次,所以后面我就把seata從項(xiàng)目中移除了。

解決方案

既然問(wèn)題找到了,那么就好修改了,修改方式有兩種,一種是創(chuàng)建自己的RetryableFeignBlockingLoadBalancerClient, 就把上面的代碼拿過(guò)來(lái)抄一遍,只是自己指定SSLContext,另一種是啟用httpclient

方案一

@Bean
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
                               LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerClientFactory loadBalancerClientFactory) throws NoSuchAlgorithmException, KeyManagementException {
    SSLContext ctx = SSLContext.getInstance("SSL");
    X509TrustManager tm = new X509TrustManager() {
        @Override
        public void checkClientTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public void checkServerTrusted(X509Certificate[] chain, String authType) {
        }
        @Override
        public X509Certificate[] getAcceptedIssuers() {
            return null;
        }
    };
    ctx.init(null, new TrustManager[]{tm}, null);


    return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(ctx.getSocketFactory(), (hostname, session) -> true), loadBalancerClient,
            loadBalancedRetryFactory, loadBalancerClientFactory);
}

方案二

另一種方案就是啟用httpclient,并且禁用ssl驗(yàn)證,配置如下

feign:
  httpclient:
    enabled: true
    disable-ssl-validation: true

自此這個(gè)問(wèn)題解決了,當(dāng)然在使用中更加傾向使用方案二,因?yàn)镕eign默認(rèn)的Client采用的是HttpURLConnection,它沒(méi)有連接池,當(dāng)然你也可以使用okhttp。

寫(xiě)到最后

這個(gè)問(wèn)題看起來(lái)簡(jiǎn)單,但是排查起來(lái)還是頗費(fèi)心思,很多細(xì)節(jié)隱藏到了框架之下,所以我想看源碼還是有好處的,因?yàn)榫W(wǎng)上的文章別人的情況可能和你不一樣,與其遨游在各個(gè)文章里面,還不如debug一把。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容