feign 根据环境动态指定服务名

31 篇文章 1 订阅

分布式系统中,网关层或应用层调用后端的微服务,大家普遍使用SpringCloud Feign去调用,过程简单方便。

开发环境和测试环境共用一套nacos,通过服务提供方的serviceId加环境后缀作为区分,比如基础信息服务其开发环境serviceId为 baseinfo-dev,测试环境为 baseinfo-test。每次服务提供方发布的时候,会根据发布环境,手动的更改serviceId。

消费方feign调用时,直接通过

@FeignClient(name = "baseinfo-dev")

来进行调用,因为 feignClient 的 name 是直接写死在代码里的,导致每次发布到测试环境时,要手动改name,比如把 baseinfo-dev 改成 baseinfo-test,这种改法在服务比较少的情况下,还可以接受,一旦服务一多,就容易改漏,导致本来该调用测试环境的服务提供方,结果跑去调用开发环境的提供方。

方案一:通过feign拦截器+url改造

Feign的拦截器RequestInterceptor

SpringCloud的微服务使用Feign进行服务间调用的时候可以使用RequestInterceptor统一拦截请求来完成设置header等相关请求。

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,成本比较大。

方案二:重写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版本。

方案三:使用FeignClientBuilder

这个类的作用如下:

通过它可以手动编码的方式创建 feign client,来代替 @FeignClient 注解。

FeignClientBuilder提供了forType静态方法用于创建Builder。Builder的构造器创建了FeignClientFactoryBean,其build方法使用FeignClientFactoryBean的getTarget()来创建目标feign client

FeignClientBuilder源码,2.2 版本

package org.springframework.cloud.openfeign;

import feign.Feign;
import feign.hystrix.FallbackFactory;

import org.springframework.context.ApplicationContext;

/**
 * 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
 * @author Matt King
 * @author Sam Kruglov
 */
public class FeignClientBuilder {

	private final ApplicationContext applicationContext;

	public FeignClientBuilder(final ApplicationContext applicationContext) {
		this.applicationContext = applicationContext;
	}

	public <T> Builder<T> forType(final Class<T> type, final String name) {
		return new Builder<>(this.applicationContext, type, name);
	}

	/**
	 * Builder of feign targets.
	 *
	 * @param <T> type of target
	 */
	public static final class Builder<T> {

		private FeignClientFactoryBean feignClientFactoryBean;

		private Builder(final ApplicationContext applicationContext, final Class<T> type,
				final String name) {
			this.feignClientFactoryBean = new FeignClientFactoryBean();

			this.feignClientFactoryBean.setApplicationContext(applicationContext);
			this.feignClientFactoryBean.setType(type);
			this.feignClientFactoryBean.setName(FeignClientsRegistrar.getName(name));
			this.feignClientFactoryBean.setContextId(FeignClientsRegistrar.getName(name));
			this.feignClientFactoryBean.setInheritParentContext(true);
			// preset default values - these values resemble the default values on the
			// FeignClient annotation
			this.url("").path("").decode404(false);
		}

		public Builder<T> url(final String url) {
			this.feignClientFactoryBean.setUrl(FeignClientsRegistrar.getUrl(url));
			return this;
		}

		/**
		 * Applies a {@link FeignBuilderCustomizer} to the underlying
		 * {@link Feign.Builder}. May be called multiple times.
		 * @param customizer applied in the same order as supplied here after applying
		 * customizers found in the context.
		 * @return the {@link Builder} with the customizer added
		 */
		public Builder<T> customize(final FeignBuilderCustomizer customizer) {
			this.feignClientFactoryBean.addCustomizer(customizer);
			return this;
		}

		public Builder<T> contextId(final String contextId) {
			this.feignClientFactoryBean.setContextId(contextId);
			return this;
		}

		public Builder<T> path(final String path) {
			this.feignClientFactoryBean.setPath(FeignClientsRegistrar.getPath(path));
			return this;
		}

		public Builder<T> decode404(final boolean decode404) {
			this.feignClientFactoryBean.setDecode404(decode404);
			return this;
		}

		public Builder<T> inheritParentContext(final boolean inheritParentContext) {
			this.feignClientFactoryBean.setInheritParentContext(inheritParentContext);
			return this;
		}

		public Builder<T> fallback(final Class<? extends T> fallback) {
			FeignClientsRegistrar.validateFallback(fallback);
			this.feignClientFactoryBean.setFallback(fallback);
			return this;
		}

		public Builder<T> fallbackFactory(
				final Class<? extends FallbackFactory<? extends T>> fallbackFactory) {
			FeignClientsRegistrar.validateFallbackFactory(fallbackFactory);
			this.feignClientFactoryBean.setFallbackFactory(fallbackFactory);
			return this;
		}

		/**
		 * @return the created Feign client
		 */
		public T build() {
			return this.feignClientFactoryBean.getTarget();
		}

	}

}

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、例子

//远端接口
@FeignClient(value = "User_Test")
public interface RemoteApi
{
    @RequestMapping("/api/info")
    String info(String gid);
}


//使用方式
@RestController
@RequestMapping(value = "/query")
public class UserQueryHandler {

    @Autowired
    RemoteApi api;

    @RequestMapping("/userinfo")
    public String query(@RequestParam(value = "gid") String gid)
    {
        return api.info(gid);
    }
}

使用 DynamicFeignClientFactory 后

//远端接口定义
//已经没有了@FeignClient注解

public interface RemoteApi
{
    @RequestMapping("/api/info")
    String info(String gid);
}


//使用方式

@RestController
@RequestMapping(value = "/query")
public class UserQueryHandler {

    @Autowired
    DynamicFeignClientFactory<RemoteApi> client;

    @RequestMapping("/userinfo")
    public String query(@RequestParam(value = "gid") String gid)
    {
        //获取所需要的对应的服务名称
        //伪代码,自己的数据索引服务
        //定位数据的服务群组
        String service_location = GetUidLocation(gid);  
 
        RemoteApi api = client.GetFeignClient(RemoteApi.class, service_location);
        return api.info(gid);
    }
}

方案四:feignClient注入到spring之前,修改FeignClientFactoryBean

实现核心逻辑:在feignClient注入到spring容器之前,变更name

通过spring-cloud-starter-openfeign的源码,会知道openfeign通过FeignClientFactoryBean中的getObject()生成具体的客户端。因此我们在getObject托管给spring之前,把name换掉

1、在API定义一个特殊变量来占位

env为特殊变量占位符

@FeignClient(name = "feign-provider-env",path = EchoService.INTERFACE_NAME)
public interface EchoFeignClient extends EchoService {
}

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

转:https://segmentfault.com/a/1190000040353032

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值