In normal work, OpenFeign
it is very common to be used as a calling component between microservices. The call method of interface and annotation highlights a simplicity, so that we can realize the interface call between services without paying attention to internal details.
But after working for a long time, I found that Feign is also troublesome to use. Let’s look at a problem first, and then see how we solve it in our work, so as to simplify the use of Feign.
first look at the problem
During the development of a project, we usually distinguish between a development environment, a test environment, and a production environment. If some projects have higher requirements, there may be a pre-production environment.
The development environment, as an environment for joint debugging with front-end development, is generally used more casually. When we are developing locally, we sometimes register locally started microservices to the registration center nacos to facilitate debugging.
In this way, a microservice in the registry may have multiple service instances, like this:
The sharp-eyed friends must have noticed that the IP addresses of the two instances are a little different.
The online environment now generally uses containerized deployment, which is usually mirrored by pipeline tools and then thrown into docker to run, so let's take a look at the ip of the service in the docker container:
As you can see, this is one of the service addresses registered on nacos, and the 192
other ip at the beginning of the list is the LAN address of the service we started locally. Take a look at the picture below to understand the whole process at a glance.
in conclusion:
-
Both services register their information on nacos through the host's ip and port
-
Use the internal ip address of docker when registering the service in the online environment
-
Use the local LAN address when registering the local service
Then the problem arises at this time. When I start another serviceB locally to FeignClient
call the interface in serviceA, because of Feign's own load balancing, it is possible to balance the request load to two different serviceA instances.
If the call request is load-balanced to the local serviceA, then there is no problem. Both services are in the same 192.168
network segment and can be accessed normally. But if the load balancing requests to serviceA running in docker, then the problem arises, because the network is unreachable, so the request will fail:
To put it bluntly, the local 192.168
virtual network segment and the virtual network segment in docker 172.17
belong to two different network segments of the pure second layer, and cannot communicate with each other, so they cannot be called directly.
Then, if you want to stably send the request to the local service during debugging, there is a way to specify to FeignClient
add url
parameters in and specify the address of the call:
@FeignClient(value = "serviceA",url = "http://127.0.0.1:8088/")
public interface ClientA {
@GetMapping("/test/get")
String get();
}
But this will also cause some problems:
-
When the code goes online, you need to delete the annotations
url
and modify the code again. If you forget, it will cause online problems -
If there are
FeignClient
a lot of tests, each needs to be configuredurl
, and it is very troublesome to modify
So, what can be done to improve it? In order to solve this problem, we still have to start with Feign's principle.
Feign principle
Feign's implementation and working principle, I have written a simple source code analysis before, you can simply spend a few minutes to lay the groundwork, Feign core source code analysis . Once you understand the principle, it will be easier to understand later.
To put it simply, it is the annotation added in the project @EnableFeignClients
. There is a very important line of code in the implementation:
@Import(FeignClientsRegistrar.class)
This class implements the interface. In the method ImportBeanDefinitionRegistrar
of this interface , it can be manually created and registered, and then spring will generate beans according to the instantiation and put them into the container.registerBeanDefinitions
BeanDefinition
BeanDefinition
In this way, Feign scans @FeignClient
the annotated interface, and then generates proxy objects step by step. The specific process can be seen in the following picture:
Subsequent requests FeignInvocationHandler
are intercepted by proxy objects, and processors are distributed according to corresponding methods to complete subsequent http request operations.
ImportBeanDefinitionRegistrar
As mentioned above ImportBeanDefinitionRegistrar
, it is very important in the whole FeignClient
process of creating a proxy, so let's first write a simple example to see its usage. First define an entity class:
@Data
@AllArgsConstructor
public class User {
Long id;
String name;
}
Pass BeanDefinitionBuilder
, pass in specific values to the constructor of this entity class, and finally generate one BeanDefinition
:
public class MyBeanDefinitionRegistrar
implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata,
BeanDefinitionRegistry registry) {
BeanDefinitionBuilder builder
= BeanDefinitionBuilder.genericBeanDefinition(User.class);
builder.addConstructorArgValue(1L);
builder.addConstructorArgValue("Hydra");
AbstractBeanDefinition beanDefinition = builder.getBeanDefinition();
registry.registerBeanDefinition(User.class.getSimpleName(),beanDefinition);
}
}
registerBeanDefinitions
ConfigurationClassPostProcessor
The specific call time of the method is when the method is executed later postProcessBeanDefinitionRegistry
, and registerBeanDefinition
the method will be BeanDefinition
put into a map, and the bean will be instantiated according to it later.
On the configuration class by @Import
introducing it:
@Configuration
@Import(MyBeanDefinitionRegistrar.class)
public class MyConfiguration {
}
Inject this User
test:
@Service
@RequiredArgsConstructor
public class UserService {
private final User user;
public void getUser(){
System.out.println(user.toString());
}
}
The result is printed, indicating that we BeanDefinition
have successfully manually created a bean in a custom way and put it into the spring container:
User(id=1, name=Hydra)
Well, the preparatory work is over here, and the formal renovation work will start below.
transform
Let’s summarize here first. What we struggle with is that the local environment needs to be FeignClient
configured url
, but the online environment does not, and we don’t want to modify the code back and forth.
In addition to generating dynamic proxies and interception methods like in the source code, the official documentation also provides us with a method of manually creating FeignClient.
https://docs.spring.io/spring-cloud-openfeign/docs/2.2.9.RELEASE/reference/html/#creating-feign-clients-manually
To put it simply, we can manually create a Feign client through Feign's Builder API as follows.
Take a brief look, this process also needs to configure Client
, Encoder
, Decoder
, Contract
, RequestInterceptor
and other content.
-
Client
: The initiator of the actual http request, if it does not involve load balancing, it can be used simplyClient.Default
, and it can be used when load balancing is usedLoadBalancerFeignClient
. As mentioned earlier,LoadBalancerFeignClient
the one in the middledelegate
is actually usedClient.Default
-
Encoder
AndDecoder
: Feign's codec, use the correspondingSpringEncoder
and in the spring projectResponseEntityDecoder
, in this process we use itGsonHttpMessageConverter
as a message converter to parse json -
RequestInterceptor
: Feign's interceptor, which has many general business purposes, such as adding and modifying header information, etc., it may not be used if it is not used here -
Contract
: Literally means a contract. Its function is to analyze and verify the interface we pass in to see if the use of annotations conforms to the specification, and then extract the metadata about http into a result and return it. If we use annotations such asRequestMapping
,PostMapping
, and so on, then the corresponding use isGetMapping
SpringMvcContract
In fact, this is the only one that is needed here Contract
, and the others are optional configuration items. We write a configuration class and inject all these needed things into it:
@Slf4j
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({LocalFeignProperties.class})
@Import({LocalFeignClientRegistrar.class})
@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")
public class FeignAutoConfiguration {
static {
log.info("feign local route started");
}
@Bean
@Primary
public Contract contract(){
return new SpringMvcContract();
}
@Bean(name = "defaultClient")
public Client defaultClient(){
return new Client.Default(null,null);
}
@Bean(name = "ribbonClient")
public Client ribbonClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory){
return new LoadBalancerFeignClient(defaultClient(), cachingFactory,
clientFactory);
}
@Bean
public Decoder decoder(){
HttpMessageConverter httpMessageConverter=new GsonHttpMessageConverter();
ObjectFactory<HttpMessageConverters> messageConverters= () -> new HttpMessageConverters(httpMessageConverter);
SpringDecoder springDecoder = new SpringDecoder(messageConverters);
return new ResponseEntityDecoder(springDecoder);
}
@Bean
public Encoder encoder(){
HttpMessageConverter httpMessageConverter=new GsonHttpMessageConverter();
ObjectFactory<HttpMessageConverters> messageConverters= () -> new HttpMessageConverters(httpMessageConverter);
return new SpringEncoder(messageConverters);
}
}
On this configuration class, there are three lines of annotations, which we will explain a little bit.
The first is the imported configuration class LocalFeignProperties
, which has three attributes, namely whether to enable the local routing switch, scan the package name of the FeignClient interface, and the local routing mapping relationship we want to do, which stores the service addressMapping
name and the corresponding url address:
@Data
@Component
@ConfigurationProperties(prefix = "feign.local")
public class LocalFeignProperties {
// 是否开启本地路由
private String enable;
//扫描FeignClient的包名
private String basePackage;
//路由地址映射
private Map<String,String> addressMapping;
}
The following line of annotation indicates that the current configuration file will take effect only when feign.local.enable
this property in the configuration file true
is :
@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")
Finally, it is our top priority LocalFeignClientRegistrar
. We still implement it according to the official idea ImportBeanDefinitionRegistrar
of building through the interface BeanDefinition
and then registering.
Moreover, FeignClientsRegistrar
many basic functions have been implemented in the source code, such as scanning and scanning packages, getting FeignClient
, name
, contextId
etc. url
, so there are very few places that need to be changed, and you can safely copy and exceed its code.
First create LocalFeignClientRegistrar
and inject the required ResourceLoader
, BeanFactory
, Environment
.
@Slf4j
public class LocalFeignClientRegistrar implements
ImportBeanDefinitionRegistrar, ResourceLoaderAware,
EnvironmentAware, BeanFactoryAware{
private ResourceLoader resourceLoader;
private BeanFactory beanFactory;
private Environment environment;
@Override
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader=resourceLoader;
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
@Override
public void setEnvironment(Environment environment) {
this.environment=environment;
}
//先省略具体功能代码...
}
Then look at BeanDefinition
the work before creation. This part mainly completes the package scanning and testing @FeignClient
whether the annotation is added to the interface. The following code is basically copying the source code, except for changing the path of the scanning package, using the package name we configured in the configuration file.
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
ClassPathScanningCandidateComponentProvider scanner = ComponentScanner.getScanner(environment);
scanner.setResourceLoader(resourceLoader);
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
scanner.addIncludeFilter(annotationTypeFilter);
String basePackage =environment.getProperty("feign.local.basePackage");
log.info("begin to scan {}",basePackage);
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
log.info(candidateComponent.getBeanClassName());
// verify annotated class is an interface
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
Assert.isTrue(annotationMetadata.isInterface(),
"@FeignClient can only be specified on an interface");
Map<String, Object> attributes = annotationMetadata
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = FeignCommonUtil.getClientName(attributes);
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
Next, create BeanDefinition
and register. Feign’s source code is used to FeignClientFactoryBean
create a proxy object. We don’t need it here, and we can directly replace it with Feign.builder
creation.
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = FeignCommonUtil.getContextId(beanFactory, attributes,environment);
String name = FeignCommonUtil.getName(attributes,environment);
BeanDefinitionBuilder definition = BeanDefinitionBuilder
.genericBeanDefinition(clazz, () -> {
Contract contract = beanFactory.getBean(Contract.class);
Client defaultClient = (Client) beanFactory.getBean("defaultClient");
Client ribbonClient = (Client) beanFactory.getBean("ribbonClient");
Encoder encoder = beanFactory.getBean(Encoder.class);
Decoder decoder = beanFactory.getBean(Decoder.class);
LocalFeignProperties properties = beanFactory.getBean(LocalFeignProperties.class);
Map<String, String> addressMapping = properties.getAddressMapping();
Feign.Builder builder = Feign.builder()
.encoder(encoder)
.decoder(decoder)
.contract(contract);
String serviceUrl = addressMapping.get(name);
String originUrl = FeignCommonUtil.getUrl(beanFactory, attributes, environment);
Object target;
if (StringUtils.hasText(serviceUrl)){
target = builder.client(defaultClient)
.target(clazz, serviceUrl);
}else if (StringUtils.hasText(originUrl)){
target = builder.client(defaultClient)
.target(clazz,originUrl);
}else {
target = builder.client(ribbonClient)
.target(clazz,"http://"+name);
}
return target;
});
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
definition.setLazyInit(true);
FeignCommonUtil.validate(attributes);
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
// has a default, won't be null
boolean primary = (Boolean) attributes.get("primary");
beanDefinition.setPrimary(primary);
String[] qualifiers = FeignCommonUtil.getQualifiers(attributes);
if (ObjectUtils.isEmpty(qualifiers)) {
qualifiers = new String[] { contextId + "FeignClient" };
}
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
During this process, several things were mainly done:
-
By getting the , , ,
beanFactory
we created earlier , to buildClient
Encoder
Decoder
Contract
Feign.Builder
-
By injecting the configuration class, by
addressMapping
getting the call corresponding to the service in the configuration fileurl
-
target
Replace the request by the method ,url
if it exists in the configuration file, use the configuration file firsturl
, otherwise use@FeignClient
the configuration in the annotationurl
, if there is none, use the service name toLoadBalancerFeignClient
access
resources/META-INF
Create a file in the directory and spring.factories
register our automatic configuration class through spi:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.feign.local.config.FeignAutoConfiguration
Finally, local packaging is enough:
mvn clean install
test
Introduce the package we have packaged above. Since the package is already included spring-cloud-starter-openfeign
, there is no need to quote additional feign
packages:
<dependency>
<groupId>com.cn.hydra</groupId>
<artifactId>feign-local-enhancer</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
Add configuration information to the configuration file to enable components:
feign:
local:
enable: true
basePackage: com.service
addressMapping:
hydra-service: http://127.0.0.1:8088
trunks-service: http://127.0.0.1:8099
Create an interface, we can write any address in FeignClient
the annotation , which can be used to test whether it will be overwritten by the service address in the configuration file later:url
@FeignClient(value = "hydra-service",
contextId = "hydra-serviceA",
url = "http://127.0.0.1:8099/")
public interface ClientA {
@GetMapping("/test/get")
String get();
@GetMapping("/test/user")
User getUser();
}
Start the service, and you can see the operation of executing the scan package during the process:
Add a breakpoint during the replacement url
process, and you can see that even if it is configured in the annotation url
, it will be overwritten by the service in the configuration file first url
:
Using the interface to test, you can see that the above proxy object is used to access and successfully return the result:
If the project needs to release the official environment, you only need to feign.local.enable
change false
or delete the configuration, and add the original Feign to the project @EnableFeignClients
.
Summarize
This article provides an idea to simplify Feign calls in the local development process. Compared with the previous troublesome modifications FeignClient
, url
it can save a lot of invalid labor, and through this process, it can also help you understand the ones we usually use. How components are combined with spring, familiar with spring extension points.