聊聊如何根據環境動態指定feign呼叫服務名

前言

前段時間和朋友聊天,他說他部門老大給他提了一個需求,這個需求的背景是這樣,他們開發環境和測試環境共用一套eureka,服務提供方的serviceId加環境字尾作為區分,比如使用者服務其開發環境serviceId為user_dev,測試環境為user_test。每次服務提供方釋出的時候,會根據環境變數,自動變更serviceId。

消費方feign呼叫時,直接透過

@FeignClient(name = “user_dev”)

來進行呼叫,因為他們是直接把feignClient的name直接寫死在程式碼裡,導致他們每次發版到測試環境時,要手動改name,比如把user_dev改成user_test,這種改法在服務比較少的情況下,還可以接受,一旦服務一多,就容易改漏,導致本來該呼叫測試環境的服務提供方,結果跑去呼叫開發環境的提供方。

他們的老大給他提的需求是,消費端呼叫需要

自動

根據環境呼叫到相應環境的服務提供方。

下面就介紹朋友透過百度搜索出來的幾種方案,以及後面我幫朋友實現的另一種方案

方案一:透過feign攔截器+url改造

1、在API的URI上做一下特殊標記

@FeignClient(name = “feign-provider”)public interface FooFeignClient { @GetMapping(value = “//feign-provider-$env/foo/{username}”) String foo(@PathVariable(“username”) String username);}

這邊指定的URI有兩點需要注意的地方

一是前面“//”,這個是由於feign

template不允許URI有“http://“開頭,所以我們用“//”標記為後面緊跟著服務名稱,而不是普通的URI

二是“$env”,這個是後面要替換成具體的環境

2、在RequestInterceptor中查詢到特殊的變數標記,把

$env替換成具體環境

@Configurationpublic class InterceptorConfig { @Autowired private Environment environment; @Bean public RequestInterceptor cloudContextInterceptor() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { String url = template。url(); if (url。contains(”$env“)) { url = url。replace(”$env“, route(template)); System。out。println(url); template。uri(url); } if (url。startsWith(”//“)) { url = ”http:“ + url; template。target(url); template。uri(”“); } } private CharSequence route(RequestTemplate template) { // TODO 你的路由演算法在這裡 return environment。getProperty(”feign。env“); } }; }}

這種方案是可以實現,但是朋友沒有采納,因為朋友的專案已經是上線的專案,透過改造url,成本比較大。就放棄了

該方案由博主

無級程式設計師

提供,下方連結是他實現該方案的連結

https://blog。csdn。net/weixin_45357522/article/details/104020061

方案二:重寫RouteTargeter

1、API的URL中定義一個特殊的變數標記,形如下

@FeignClient(name = ”feign-provider-env“)public interface FooFeignClient { @GetMapping(value = ”/foo/{username}“) String foo(@PathVariable(”username“) String username);}

2、以HardCodedTarget為基礎,實現Targeter

public class RouteTargeter implements Targeter { private Environment environment; public RouteTargeter(Environment environment){ this。environment = environment; } /** * 服務名以本字串結尾的,會被置換為實現定位到環境 */ public static final String CLUSTER_ID_SUFFIX = ”env“; @Override public T target(FeignClientFactoryBean factory, Builder feign, FeignContext context, HardCodedTarget target) { return feign。target(new RouteTarget<>(target)); } public static class RouteTarget implements Target { Logger log = LoggerFactory。getLogger(getClass()); private Target realTarget; public RouteTarget(Target realTarget) { super(); this。realTarget = realTarget; } @Override public Class type() { return realTarget。type(); } @Override public String name() { return realTarget。name(); } @Override public String url() { String url = realTarget。url(); if (url。endsWith(CLUSTER_ID_SUFFIX)) { url = url。replace(CLUSTER_ID_SUFFIX, locateCusterId()); log。debug(”url changed from {} to {}“, realTarget。url(), url); } return url; } /** * @return 定位到的實際單元號 */ private String locateCusterId() { // TODO 你的路由演算法在這裡 return environment。getProperty(”feign。env“); } @Override public Request apply(RequestTemplate input) { if (input。url()。indexOf(”http“) != 0) { input。target(url()); } return input。request(); } }}

使用自定義的Targeter實現代替預設的實現

@Bean public RouteTargeter getRouteTargeter(Environment environment) { return new RouteTargeter(environment); }

該方案適用於spring-cloud-starter-openfeign為3。0版本以上,3。0版本以下得額外加

spring-milestones Spring Milestones https://repo。spring。io/milestone

Targeter 這個介面在3。0之前的包是屬於package範圍,因此沒法直接繼承。朋友的springcloud版本相對比較低,後面基於系統穩定性的考慮,就沒有貿然升級springcloud版本。因此這個方案朋友也沒采納

該方案仍然由博主

無級程式設計師

提供,下方連結是他實現該方案的連結

https://blog。csdn。net/weixin_45357522/article/details/106745468

方案三:使用FeignClientBuilder

這個類的作用如下

/** * A builder for creating Feign clients without using the {@link FeignClient} annotation。 *

* This builder builds the Feign client exactly like it would be created by using the * {@link FeignClient} annotation。 * * @author Sven Döring */

他的功效是和@FeignClient是一樣的,因此就可以透過手動編碼的方式

1、編寫一個feignClient工廠類

@Componentpublic class DynamicFeignClientFactory { private FeignClientBuilder feignClientBuilder; public DynamicFeignClientFactory(ApplicationContext appContext) { this。feignClientBuilder = new FeignClientBuilder(appContext); } public T getFeignClient(final Class type, String serviceId) { return this。feignClientBuilder。forType(type, serviceId)。build(); }}

2、編寫API實現類

@Componentpublic class BarFeignClient { @Autowired private DynamicFeignClientFactory dynamicFeignClientFactory; @Value(”${feign。env}“) private String env; public String bar(@PathVariable(”username“) String username){ BarService barService = dynamicFeignClientFactory。getFeignClient(BarService。class,getBarServiceName()); return barService。bar(username); } private String getBarServiceName(){ return ”feign-other-provider-“ + env; }}

本來朋友打算使用這種方案了,最後沒采納,原因後面會講。

該方案由博主

lotern

提供,下方連結為他實現該方案的連結

https://my。oschina。net/kaster/blog/4694238

方案四:feignClient注入到spring之前,修改FeignClientFactoryBean

實現核心邏輯

:在feignClient注入到spring容器之前,變更name

如果有看過spring-cloud-starter-openfeign的原始碼的朋友,應該就會知道openfeign透過FeignClientFactoryBean中的getObject()生成具體的客戶端。因此我們在getObject託管給spring之前,把name換掉

1、在API定義一個特殊變數來佔位

@FeignClient(name = ”feign-provider-env“,path = EchoService。INTERFACE_NAME)public interface EchoFeignClient extends EchoService {}

注:

env為特殊變數佔位符

2、透過spring後置器處理FeignClientFactoryBean的name

public class FeignClientsServiceNameAppendBeanPostProcessor implements BeanPostProcessor, ApplicationContextAware , EnvironmentAware { private ApplicationContext applicationContext; private Environment environment; private AtomicInteger atomicInteger = new AtomicInteger(); @SneakyThrows @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if(atomicInteger。getAndIncrement() == 0){ String beanNameOfFeignClientFactoryBean = ”org。springframework。cloud。openfeign。FeignClientFactoryBean“; Class beanNameClz = Class。forName(beanNameOfFeignClientFactoryBean); applicationContext。getBeansOfType(beanNameClz)。forEach((feignBeanName,beanOfFeignClientFactoryBean)->{ try { setField(beanNameClz,”name“,beanOfFeignClientFactoryBean); setField(beanNameClz,”url“,beanOfFeignClientFactoryBean); } catch (Exception e) { e。printStackTrace(); } System。out。println(feignBeanName + ”——>“ + beanOfFeignClientFactoryBean); }); } return null; } private void setField(Class clazz, String fieldName, Object obj) throws Exception{ Field field = ReflectionUtils。findField(clazz, fieldName); if(Objects。nonNull(field)){ ReflectionUtils。makeAccessible(field); Object value = field。get(obj); if(Objects。nonNull(value)){ value = value。toString()。replace(”env“,environment。getProperty(”feign。env“)); ReflectionUtils。setField(field, obj, value); } } } @Override public void setEnvironment(Environment environment) { this。environment = environment; } @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this。applicationContext = applicationContext; }}

注:

這邊不能直接用FeignClientFactoryBean。class,因為FeignClientFactoryBean這個類的許可權修飾符是default。因此得用反射。

其次只要是在bean注入到spring IOC之前提供的擴充套件點,都可以進行FeignClientFactoryBean的name替換,不一定得用BeanPostProcessor

3、使用import注入

@Retention(RetentionPolicy。RUNTIME)@Target(ElementType。TYPE)@Documented@Import(FeignClientsServiceNameAppendEnvConfig。class)public @interface EnableAppendEnv2FeignServiceName {}

4、在啟動類上加上@EnableAppendEnv2FeignServiceName

總結

後面朋友採用了第四種方案,主要這種方案相對其他三種方案改動比較小。

第四種方案朋友有個不解的地方,為啥要用import,直接在spring。factories配置自動裝配,這樣就不用在啟動類上@EnableAppendEnv2FeignServiceName

不然啟動類上一堆@Enable看著噁心,哈哈。

我給的答案是開了一個顯眼的@Enable,是為了讓你更快知道我是怎麼實現,他的回答是那還不如你直接告訴我怎麼實現就好。我竟然無言以對。

demo連結

https://github。com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route