0

聊聊如何根据环境动态指定feign调用服务名

 2 years ago
source link: https://my.oschina.net/u/4494662/blog/5132674
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

聊聊如何根据环境动态指定feign调用服务名 - linyb极客之路的个人空间 - OSCHINA - 中文开源技术交流社区

前段时间和朋友聊天,他说他部门老大给他提了一个需求,这个需求的背景是这样,他们开发环境和测试环境共用一套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();

		}

	}
}

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

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK