preface

Chatting with a friend some time ago, he said that the boss of his department asked him a requirement. The background of this requirement is that their development environment and test environment share a set of Eureka, distinguished by serviceId of the service provider and environment suffix. For example, the development environment serviceId of the user service is user_dev and the test environment is user_test. The serviceId is automatically changed each time a service provider releases a service based on environment variables.

The consumer feign calls directly through

@FeignClient(name = "user_dev")
Copy the code

Because they write the name of feignClient directly into the code, so they have to change the name manually every time they send the version to the test environment, for example, change user_dev to user_test. This change is acceptable in the case of few services, but once there are many services, it is easy to change the name. The service provider that should have called the test environment instead calls the provider of the development environment.

Their boss gave him a requirement that the consumer call be automatically called to the service provider based on the environment.

The following is to introduce friends through Baidu search out several schemes, as well as behind me to help friends achieve another scheme

Scheme 1: Through feign interceptor + URL transformation

1. Make a special mark on the API URI

@FeignClient(name = "feign-provider")
public interface FooFeignClient {

    @GetMapping(value = "//feign-provider-$env/foo/{username}")
    String foo(@PathVariable("username") String username);
}
Copy the code

There are two caveats to the URI specified here

  • The first is preceded by “//”. This is because the Feign Template does not allow urIs to start with” http://”, so we use “//” to mark urIs immediately after service names instead of normal URIs

  • The second is “$env”, which is replaced by the specific environment

Find a special variable flag in the RequestInterceptor

$env is replaced with the specific environment

@Configuration
public class InterceptorConfig {

    @Autowired
    private Environment environment;

    @Bean
    public RequestInterceptor cloudContextInterceptor(a) {
        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 your routing algorithm is here
                return environment.getProperty("feign.env"); }}; }}Copy the code

Such a scheme could be realized, but my friend did not adopt it, because my friend’s project was already online, and it would cost a lot to change the URL. Gave up

The solution is provided by the blogger, the gradeless Programmer, and his implementation is linked below

Blog.csdn.net/weixin_4535…

Scheme 2: Rewrite RouteTargeter

1. Define a special variable tag in the API URL, like the following

@FeignClient(name = "feign-provider-env")
public interface FooFeignClient {

    @GetMapping(value = "/foo/{username}")
    String foo(@PathVariable("username") String username);
}

Copy the code

2, based on HardCodedTarget, to achieve Targeter

public class RouteTargeter implements Targeter {
    private Environment environment;
    public RouteTargeter(Environment environment){
       this.environment = environment;
    }   
    
	/** * Service names ending in this string will be replaced with implementations located in the environment */
	public static final String CLUSTER_ID_SUFFIX = "env";

	@Override
	public <T> T target(FeignClientFactoryBean factory, Builder feign, FeignContext context, HardCodedTarget
       
         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(a);this.realTarget = realTarget;
		}

		@Override
		public Class<T> type(a) {
			return realTarget.type();
		}

		@Override
		public String name(a) {
			return realTarget.name();
		}

		@Override
		public String url(a) {
			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;
		}

		/ * * *@returnThe actual unit number */
		private String locateCusterId(a) {
			// TODO your routing algorithm is here
			return environment.getProperty("feign.env");
		}

		@Override
		public Request apply(RequestTemplate input) {
			if (input.url().indexOf("http") != 0) {
				input.target(url());
			}
			returninput.request(); }}}Copy the code

3. Replace the default implementation with a custom Targeter implementation

    @Bean
	public RouteTargeter getRouteTargeter(Environment environment) {
		return new RouteTargeter(environment);
    }
Copy the code

This solution applies to Spring-Cloud-starter-OpenIgn 3.0 or above, and 3.0 or below may be added

	<repositories>
		<repository>
			<id>spring-milestones</id>
			<name>Spring Milestones</name>
			<url>https://repo.spring.io/milestone</url>
		</repository>
	</repositories>

Copy the code

The Targeter interface is part of the package scope before 3.0 and therefore cannot be directly inherited. The springCloud version of my friend is relatively low, so I didn’t upgrade the SpringCloud version rurally for the sake of system stability. Therefore this plan friend also did not adopt

The solution is still provided by the blogger gradeless programmer, whose implementation is linked below

Blog.csdn.net/weixin_4535…

Plan 3: Use FeignClientBuilder

This class does the following

/**
 * 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.
 *
 * @authorSven Doring * /
Copy the code

It does the same thing as @FeignClient, so it can be manually coded

Write a feignClient factory class

@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(); }}Copy the code

2. Write API implementation classes

@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(a){
        return "feign-other-provider-"+ env; }}Copy the code

My friend was going to use this scheme, but he didn’t adopt it, for reasons that will be explained later.

The scheme provided by bloggers lotern, below link for the realization of the scheme my.oschina.net/kaster/blog…

Solution 4: Before feignClient is injected into Spring, modify the FeignClientFactoryBean

Implement the core logic: Change the name before feignClient is injected into the Spring container

Those of you who have seen the spring-Cloud-starter-OpenFeign source code will know that OpenFefeign generates the specific client through getObject() in FeignClientFactoryBean. So let’s change the name before getObject is hosted for Spring

1. Define a special variable in the API to hold the space

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

Note: env is a placeholder for special variables

2. Handle the name of the FeignClientFactoryBean through the Spring afterloader

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; }}Copy the code

Note: this way can not directly use FeignClientFactoryBean) class, because FeignClientFactoryBean access modifier of this class is the default. So you have to reflect.

Second, any extension point provided before the bean is injected into Spring IOC can replace the name of the FeignClientFactoryBean, not necessarily with a BeanPostProcessor

3. Use import injection

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsServiceNameAppendEnvConfig.class)
public @interface EnableAppendEnv2FeignServiceName {


}
Copy the code

4, add @ EnableAppendEnv2FeignServiceName on start class

conclusion

Later, my friend adopted the fourth scheme, mainly this scheme has little change compared with the other three schemes.

A fourth friend a don’t understand of place, why want to use the import, directly in the spring. The automatic assembly factories configuration, so you need not in start class @ EnableAppendEnv2FeignServiceName or start classes on a pile of @ Enable watching nausea, ha ha.

I answered with a conspicuous @enable to let you know how I did it faster, to which he replied that you might as well just tell me how to do it. I was speechless.

The demo link

Github.com/lyb-geek/sp…