Preface
Chatting with a friend some time ago, he said that the boss of his department gave him a requirement. The background of this requirement is that their development environment and test environment share a set of eureka. The serviceId of the service provider plus the environment suffix is used as a distinction, such as user service. The development environment serviceId is user_dev, and the test environment is user_test. Each time the service provider publishes, it will automatically change the serviceId according to the environment variables.
When the consumer feign calls, pass directly
@FeignClient(name = "user_dev")
To make the call, because they directly wrote the name of feignClient directly in the code, they had to manually change the name every time they released the version to the test environment, such as changing user_dev to user_test. This kind of modification is less in service In the case of, it is also acceptable that once there are more services, it is easy to change the omissions, which will cause the service provider of the test environment to be called, and the result is to call the provider of the development environment.
Their boss asked him to call automatically according to the environment to call the service provider of the corresponding environment.
Here are some solutions that my friends searched out through Baidu, and another solution that I helped my friends implement later.
Option 1: Transform through feign interceptor + url
1. Make a special mark on the URI of the API
@FeignClient(name = "feign-provider")
public interface FooFeignClient {
@GetMapping(value = "//feign-provider-$env/foo/{username}")
String foo(@PathVariable("username") String username);
}
There are two points to note in the URI specified here
- One is the front "//", this is due to feign
The template does not allow the URI to start with " http://", so we use "//" to mark the service name immediately followed by the service name instead of the normal URI
- The second is "$env", this is the specific environment to be replaced later
2. Find the special variable mark in RequestInterceptor, put
Replace $env with the specific environment
@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");
}
};
}
}
This kind of scheme is achievable, but my friend did not adopt it, because my friend’s project is already an online project, and the cost is relatively high by modifying the URL. Just gave up
The program is provided by the blogger stepless programmer , the link below is his link to implement the program
https://blog.csdn.net/weixin_45357522/article/details/104020061
Option 2: Rewrite RouteTargeter
1. A special variable tag is defined in the URL of the API, the shape is as follows
@FeignClient(name = "feign-provider-env")
public interface FooFeignClient {
@GetMapping(value = "/foo/{username}")
String foo(@PathVariable("username") String username);
}
2. Realize 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";
@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. Use a custom Targeter implementation instead of the default implementation
@Bean
public RouteTargeter getRouteTargeter(Environment environment) {
return new RouteTargeter(environment);
}
This scheme is suitable for spring-cloud-starter-openfeign version 3.0 or higher, and version 3.0 or less will be added
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
</repository>
</repositories>
Targeter packages with this interface before 3.0 belong to the package scope, so they cannot be directly inherited. My friend's version of springcloud is relatively low, and later based on the consideration of system stability, I did not rush to upgrade the version of springcloud. Therefore, my friends did not adopt this plan
The program is still provided by the blogger stepless programmer , the link below is his link to implement the program
https://blog.csdn.net/weixin_45357522/article/details/106745468
Option 3: Use FeignClientBuilder
The role of this class is as follows
/**
* 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
*/
Its function is the same as @FeignClient, so it can be coded manually
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 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;
}
}
My friend originally planned to use this scheme, but finally didn't adopt it. The reason will be discussed later.
This program is provided by the blogger lotern , the link below is the link for him to realize the program
https://my.oschina.net/kaster/blog/4694238
Solution 4: Modify FeignClientFactoryBean before feignClient is injected into spring
implements the core logic : before feignClient is injected into the spring container, change the name
If you have seen the source code of spring-cloud-starter-openfeign, you should know that openfeign generates specific clients through getObject() in FeignClientFactoryBean. So we change the name before getObject is hosted by spring
1. Define a special variable in the API to occupy a place
@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 FeignClientFactoryBean through the spring post-processor
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: For cannot directly use FeignClientFactoryBean.class, because the permission modifier of FeignClientFactoryBean is default. So we have to use reflection.
Secondly, as long as it is the extension point provided before the bean is injected into the spring IOC, the name of FeignClientFactoryBean can be replaced, not necessarily the BeanPostProcessor
3. Use import injection
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsServiceNameAppendEnvConfig.class)
public @interface EnableAppendEnv2FeignServiceName {
}
4. Add @EnableAppendEnv2FeignServiceName to the startup class
to sum up
Later friends adopted the fourth scheme, the main one is relatively small compared to the other three schemes.
For the fourth solution, my friend has a puzzle. Why use import? Configure automatic assembly directly in spring.factories, so that you don’t need to use the startup class @EnableAppendEnv2FeignServiceName
Otherwise, a bunch of @Enable on the startup class looks disgusting, haha.
The answer I gave was to open a conspicuous @Enable to let you know how I did it faster. His answer is that it’s better to tell me how to do it directly. I was speechless.
demo link
https://github.com/lyb-geek/springboot-learning/tree/master/springboot-feign-servicename-route
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。