因為一個bug,我掀開了openfeign的神秘面紗

張哥說技術發表於2024-02-19

來源:think123

報錯

最近專案中訪問一個外部api報錯了,報錯資訊如下

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

看著像是證書問題,這個時候我首先想到的是百度下,看看怎麼解決。

解決方案

百度告訴我說如果你open-feign中使用的是http client,那麼可以透過下面的配置來讓跳過SSL驗證

feign:
  httpclient:
    disable-ssl-validation: false

結果還是報同樣的錯誤。於是我又百度,又重新找了一個解決方法,這次的方案是讓我自己重寫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(nullnew TrustManager[]{tm}, null);


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

}
   
}

這把我感覺要起飛了, 一切盡在掌握中,重新deploy,開啟postman,測試測試我的介面。

測試後感覺好了但是看日誌又沒有完全好。這個介面倒是不報錯了,但是我呼叫內部服務給我報錯了,比如我這裡的內部服務名稱叫做

pro-file, 就現在它沒法根據我這個pro-file名字找到對應的IP了,從而導致我這個服務使用不了了。

百度誤我!

求人不如求己

此刻我自信的開啟了IDEA, 輸入了類名 FeignAutoConfiguration , Spring Cloud關於某個元件的自動注入類大多是XXXConfiguration, 所以按照這麼找準沒錯。

然後我有自信的把斷點打在了這個部分 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);
 }

}

重新啟動專案,好傢伙斷點沒進來呀。沒進來的原因大機率可能是不滿足條件,我趕緊看看這裡對應的Conditional, 發現了我的程式碼中沒有

設定feign.httpclient.enabled屬性的值, 而且這裡也沒有設定havingValue, 根據原始碼可以知道, 如果沒有設定havingValue, 那麼這個屬性的值會被和false進行比較

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

搞半天這個Configurtion相當於沒起作用。

好好好,這麼玩是吧。

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

@ConditionalOnClass(Feign.class)
@ConditionalOnBean(
{ LoadBalancerClient.classLoadBalancerClientFactory.class })
@AutoConfigureBefore(FeignAutoConfiguration.class)
@AutoConfigureAfter(
{ BlockingLoadBalancerClientAutoConfiguration.classLoadBalancerAutoConfiguration.class })
@EnableConfigurationProperties(FeignHttpClientProperties.class)
@Configuration(proxyBeanMethods 
false)
// Order is important here, last should be the default, first should be optional
// see
// 
@Import({ HttpClientFeignLoadBalancerConfiguration.classOkHttpFeignLoadBalancerConfiguration.class,
  HttpClient5FeignLoadBalancerConfiguration.classDefaultFeignLoadBalancerConfiguration.class })
public class FeignLoadBalancerAutoConfiguration 
{

}

因為我們專案是採用springcloud alibaba進行開發,所以引入了spring-cloud-loadbalancer這個包,因此這個這個配置類就會生效,由於我們沒有配置使用httpclient,同樣也未使用okhttp,所以生效的配置類只有一個,那就是 DefaultFeignLoadBalancerConfiguration

這個配置類中retryClient會被載入,因為我們引入了spring-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(nullnull), loadBalancerClient,
   loadBalancedRetryFactory, loadBalancerClientFactory);
}

這也是為什麼上面我們自己配置了自己的Client後,訪問其他spring cloud服務會找不到地址,這是因為預設的client不會去透過LoadBalancer去獲取服務地址。

小插曲

期間debug的時候,還發現最終的Client的SeataFeignClient,我一看才發現某個公共包引入了Seata,但是沒有使用Seata功能,然後Seata會把我們最終使用的FeignClient在給封裝一次,所以後面我就把seata從專案中移除了。

解決方案

既然問題找到了,那麼就好修改了,修改方式有兩種,一種是建立自己的RetryableFeignBlockingLoadBalancerClient, 就把上面的程式碼拿過來抄一遍,只是自己指定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(nullnew TrustManager[]{tm}, null);


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

方案二

另一種方案就是啟用httpclient,並且禁用ssl驗證,配置如下

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

自此這個問題解決了,當然在使用中更加傾向使用方案二,因為Feign預設的Client採用的是HttpURLConnection,它沒有連線池,當然你也可以使用okhttp。

寫到最後

這個問題看起來簡單,但是排查起來還是頗費心思,很多細節隱藏到了框架之下,所以我想看原始碼還是有好處的,因為網上的文章別人的情況可能和你不一樣,與其遨遊在各個文章裡面,還不如debug一把。


來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024923/viewspace-3006752/,如需轉載,請註明出處,否則將追究法律責任。

相關文章