前言
前段時間和朋友聊天,他說他部門老大給他提了一個需求,這個需求的背景是這樣,他們開發環境和測試環境共用一套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
使用自定義的Targeter實現代替預設的實現
@Bean public RouteTargeter getRouteTargeter(Environment environment) { return new RouteTargeter(environment); }
該方案適用於spring-cloud-starter-openfeign為3。0版本以上,3。0版本以下得額外加
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
2、編寫API實現類
@Componentpublic class BarFeignClient { @Autowired private DynamicFeignClientFactory
本來朋友打算使用這種方案了,最後沒采納,原因後面會講。
該方案由博主
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