前段时间和朋友聊天,他说他部门老大给他提了一个需求,这个需求的背景是这样,他们开发环境和测试环境共用一套eureka,服务提供方的serviceId加环境后缀作为区分,比如用户服务其开发环境serviceId为user_dev,测试环境为user_test。每次服务提供方发布的时候,会根据环境变量,自动变更serviceId。
消费方feign调用时,直接通过
@FeignClient(name = "user_dev")
来进行调用,因为他们是直接把feignClient的name直接写死在代码里,导致他们每次发版到测试环境时,要手动改name,比如把user_dev改成user_test,这种改法在服务比较少的情况下,还可以接受,一旦服务一多,就容易改漏,导致本来该调用测试环境的服务提供方,结果跑去调用开发环境的提供方。
他们的老大给他提的需求是,消费端调用需要自动根据环境调用到相应环境的服务提供方。
下面就介绍朋友通过百度搜索出来的几种方案,以及后面我帮朋友实现的另一种方案
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替换成具体环境
@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("//")) { 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
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> 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实现代替缺省的实现
@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版本相对比较低,后面基于系统稳定性的考虑,就没有贸然升级springcloud版本。因此这个方案朋友也没采纳
该方案仍然由博主无级程序员提供,下方链接是他实现该方案的链接
https://blog.csdn.net/weixin_45357522/article/details/106745468
这个类的作用如下
/** * 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实现类
@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提供,下方链接为他实现该方案的链接 https://my.oschina.net/kaster/blog/4694238
实现核心逻辑:在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,是为了让你更快知道我是怎么实现,他的回答是那还不如你直接告诉我怎么实现就好。我竟然无言以对。
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route