前言
前段時間和朋友聊天,他說他部門老大給他提了一個需求信姓,這個需求的背景是這樣,他們開發(fā)環(huán)境和測試環(huán)境共用一套eureka桐愉,服務提供方的serviceId加環(huán)境后綴作為區(qū)分财破,比如用戶服務其開發(fā)環(huán)境serviceId為user_dev,測試環(huán)境為user_test。每次服務提供方發(fā)布的時候从诲,會根據(jù)環(huán)境變量左痢,自動變更serviceId。
消費方feign調用時系洛,直接通過
@FeignClient(name = "user_dev")
來進行調用俊性,因為他們是直接把feignClient的name直接寫死在代碼里,導致他們每次發(fā)版到測試環(huán)境時描扯,要手動改name定页,比如把user_dev改成user_test,這種改法在服務比較少的情況下绽诚,還可以接受典徊,一旦服務一多,就容易改漏恩够,導致本來該調用測試環(huán)境的服務提供方卒落,結果跑去調用開發(fā)環(huán)境的提供方。
他們的老大給他提的需求是蜂桶,消費端調用需要自動根據(jù)環(huán)境調用到相應環(huán)境的服務提供方儡毕。
下面就介紹朋友通過百度搜索出來的幾種方案,以及后面我?guī)团笥褜崿F(xiàn)的另一種方案
方案一:通過feign攔截器+url改造
1扑媚、在API的URI上做一下特殊標記
@FeignClient(name = "feign-provider")
public interface FooFeignClient {
@GetMapping(value = "http://feign-provider-$env/foo/{username}")
String foo(@PathVariable("username") String username);
}
這邊指定的URI有兩點需要注意的地方
一是前面“//”腰湾,這個是由于feign
template不允許URI有“http://"開頭雷恃,所以我們用“//”標記為后面緊跟著服務名稱,而不是普通的URI二是“$env”费坊,這個是后面要替換成具體的環(huán)境
2倒槐、在RequestInterceptor中查找到特殊的變量標記,把
$env替換成具體環(huán)境
@Configuration
public 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("http://")) {
url = "http:" + url;
template.target(url);
template.uri("");
}
}
private CharSequence route(RequestTemplate template) {
// TODO 你的路由算法在這里
return environment.getProperty("feign.env");
}
};
}
}
這種方案是可以實現(xiàn)葵萎,但是朋友沒有采納导犹,因為朋友的項目已經(jīng)是上線的項目,通過改造url羡忘,成本比較大。就放棄了
該方案由博主無級程序員提供磕昼,下方鏈接是他實現(xiàn)該方案的鏈接
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為基礎漫雕,實現(xiàn)Targeter
public class RouteTargeter implements Targeter {
private Environment environment;
public RouteTargeter(Environment environment){
this.environment = environment;
}
/**
* 服務名以本字符串結尾的,會被置換為實現(xiàn)定位到環(huán)境
*/
public static final String CLUSTER_ID_SUFFIX = "env";
@Override
public <T> T target(FeignClientFactoryBean factory, Builder feign, FeignContext context,
HardCodedTarget<T> target) {
return feign.target(new RouteTarget<>(target));
}
public static class RouteTarget<T> implements Target<T> {
Logger log = LoggerFactory.getLogger(getClass());
private Target<T> realTarget;
public RouteTarget(Target<T> realTarget) {
super();
this.realTarget = realTarget;
}
@Override
public Class<T> 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();
}
}
}
3峰鄙、 使用自定義的Targeter實現(xiàn)代替缺省的實現(xiàn)
@Bean
public RouteTargeter getRouteTargeter(Environment environment) {
return new RouteTargeter(environment);
}
該方案適用于spring-cloud-starter-openfeign為3.0版本以上浸间,3.0版本以下得額外加
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
Targeter 這個接口在3.0之前的包是屬于package范圍,因此沒法直接繼承吟榴。朋友的springcloud版本相對比較低魁蒜,后面基于系統(tǒng)穩(wěn)定性的考慮,就沒有貿然升級springcloud版本吩翻。因此這個方案朋友也沒采納
該方案仍然由博主無級程序員提供兜看,下方鏈接是他實現(xiàn)該方案的鏈接
https://blog.csdn.net/weixin_45357522/article/details/106745468
方案三:使用FeignClientBuilder
這個類的作用如下
/**
* A builder for creating Feign clients without using the {@link FeignClient} annotation.
* <p>
* 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工廠類
@Component
public class DynamicFeignClientFactory<T> {
private FeignClientBuilder feignClientBuilder;
public DynamicFeignClientFactory(ApplicationContext appContext) {
this.feignClientBuilder = new FeignClientBuilder(appContext);
}
public T getFeignClient(final Class<T> type, String serviceId) {
return this.feignClientBuilder.forType(type, serviceId).build();
}
}
2细移、編寫API實現(xiàn)類
@Component
public class BarFeignClient {
@Autowired
private DynamicFeignClientFactory<BarService> 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提供,下方鏈接為他實現(xiàn)該方案的鏈接
https://my.oschina.net/kaster/blog/4694238
方案四:feignClient注入到spring之前碗殷,修改FeignClientFactoryBean
實現(xiàn)核心邏輯:在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,是為了讓你更快知道我是怎么實現(xiàn)你虹,他的回答是那還不如你直接告訴我怎么實現(xiàn)就好绘搞。我竟然無言以對。
demo鏈接
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route