preface

Some time ago, I was chatting with a friend. He said that the boss of his department put forward a demand to him. The background of this demand is that their development environment and test environment share a set of Eureka, and the service provider’s serviceID and environment suffix are used to distinguish them. For example, the development environment of the user service, ServiceID, is user_dev, and the test environment, user_test. The ServiceID is automatically changed each time the service provider publishes, based on the environment variable.

When the consumer feign is called, it passes directly

@FeignClient(name = "user_dev")

Because they write FeignClient’s name directly in the code, they have to change the name manually every time they send a version to the test environment, such as changing user_dev to user_test. This change is acceptable in the case of relatively few services, but once there are many services, it is easy to change the omit. This results in a service provider that is supposed to invoke the test environment being called to invoke the provider of the development environment.

The requirement that their boss gave him was that the consumer invocation should be automatically called to the service provider of the corresponding environment, depending on the environment.

The following introduces the friends through baidu search out of a few schemes, and I help a friend to achieve another scheme

Scenario 1: Feign blocker + 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);
}

There are two caveats to the URI specified here

  • One is the prefix “//”. This is because the Feign Template does not allow URIs to begin with” http://”, so we use the “//” tag followed by the service name instead of the normal URI
  • The second is “$env”, which will be replaced with a specific environment later

2. Find a special variable flag in the RequestInterceptor


$env is replaced with the context

@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 your routing algorithm return here environment.getProperty("feign.env"); }}; }}

This scheme can be realized, but the friend did not adopt it, because the friend’s project is already online project, through the transformation of URL, the cost is relatively large. Gave up

The solution is provided by the blogger, a stepless programmer, and the link below is his implementation of the solution

https://blog.csdn.net/weixin_45357522/article/details/104020061

Plan 2: Rewrite RoutetArgeter

1. A special variable flag is defined in the API URL as follows

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

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

2. Implement Targeter based on HardCodedTarget

public class RouteTargeter implements Targeter { private Environment environment; public RouteTargeter(Environment environment){ this.environment = environment; Public static final String CLUSTER_ID_SUFFIX = "env"; 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 to the actual cell number */ private String LocateCusterId () {// TODO your routing algorithm returns here environment.getProperty("feign.env"); } @Override public Request apply(RequestTemplate input) { if (input.url().indexOf("http") ! = 0) { input.target(url()); } return input.request(); }}}

3. Use a custom Targeter implementation instead of the default implementation

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

This scheme is applicable to Spring-Cloud-Starter-OpenFeign version 3.0 or above, additional version 3.0 is required

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

The package with the Targeter interface before 3.0 belongs to the package scope, so it cannot be directly inherited. My friend’s version of SpringCloud is relatively low. Considering the stability of the system, I didn’t upgrade the version of SpringCloud rashly. Therefore, my friend did not adopt this plan

The solution is still provided by the blogger, the stepless programmer, and the link below is his implementation of the solution

https://blog.csdn.net/weixin_45357522/article/details/106745468

Scenario 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. * * @author Sven Doring * /

It works just as well as @FeignClient, so it can be manually coded

1. 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(); }}

2. Write the API implementation class

@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; }}

Originally the friend intended to use this kind of scheme, did not adopt finally, the reason behind will talk.

The scheme provided by bloggers lotern, below link for the realization of the program at https://my.oschina.net/kaster/blog/4694238

Scenario 4: Modify the FeignClientFactoryBean before FeignClient is injected into Spring

Implement the core logic: Before FeignClient is injected into the Spring container, change the name

Those of you who have read the Spring-cloud-starter-OpenFeign source code will know that OpenFeign generates the concrete client via getObject() in the FeignClientFactoryBean. So let’s replace the name before we host getObject to Spring

1. Define a special variable in the API for placeholder

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

Note: ENV is a special variable placeholder

2. Process the name of the FeignClientFactoryBean through the Spring postfix

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; }}

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 be replaced by the name of the FeignClientFactoryBean, not necessarily by the BeanPostProcessor

3. Use import injection

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


}

4, add @ EnableAppendEnv2FeignServiceName on start class

conclusion

Behind the friend used the fourth scheme, the main scheme relative to the other three scheme changes are relatively small.

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.

My answer was to open a conspicuous @enable so that you would know how to do it faster, and he said that you might as well just tell me how to do it. I was speechless.

The demo link

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route