• 聊聊如何根据环境动态指定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替换成具体环境

    @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

    方案二:重写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> 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();
    
    		}
    
    	}
    }
    
    
    1. 使用自定义的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

    方案三:使用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实现类

    @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之前,修改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

  • 相关阅读:
    Spark SQL学习笔记
    《空空》陈粒
    支持向量机
    p.Value越显著,X变量越重要嘛?
    回归的武林绝学
    Neural Collaborative Filtering论文笔记
    make 学习笔记
    『并发包入坑指北』之阻塞队列
    线程池中你不容错过的一些细节
    利用策略模式优化过多 if else 代码
  • 原文地址:https://www.cnblogs.com/linyb-geek/p/15015779.html
Copyright © 2020-2023  润新知