因?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ù)名稱叫做
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一把。
