In daily work, OpenFeign
is very commonly used as a calling component between microservices. The calling method of interfaces and annotations highlights simplicity, allowing us to implement interfaces between services without paying attention to internal details. transfer.
However, after using it for a long time at work, I found that Feign also has some troubles to use. Let’s first look at a problem, and then see how we solve it at work, in order to simplify the use of Feign.
Look at the problem first
During the development of a project, we usually distinguish between development environment, test environment and production environment. If some projects have higher requirements, there may be a pre-production environment.
As an environment for joint debugging with front-end development, the development environment is generally used casually. When we are doing local development, we sometimes register locally started microservices to the registration center nacos to facilitate debugging.
In this way, a microservice in the registration center may have multiple service instances, like the following:
Sharp-eyed friends must have noticed that the IP addresses of these two instances are slightly different.
Online environments now generally use containerized deployment, which is usually mirrored by pipeline tools and then thrown into docker for running, so let’s take a look at the IP address of the service in the docker container:
As you can see, this is one of the service addresses registered on nacos, and the other IP starting with 192
in the list is the LAN address of the service we started locally. Take a look at the picture below to understand the entire process at a glance.
in conclusion:
- Both services register their information on nacos through the host’s IP and port.
- Use the docker internal IP address when registering the service in the online environment
- Use the local LAN address when registering a local service
Then the problem arises at this time. When I start a serviceB locally and call the interface in serviceA through FeignClient
, because of Feign’s own load balancing, it is possible to load balance the request to two different serviceA instance.
If this call request is load balanced to 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 request goes to serviceA running in docker, then the problem arises. Because the network is not connected, the request will fail:
To put it bluntly, the local 192.168
and the virtual network segment in docker 172.17
belong to two different pure layer 2 network segments and cannot access each other, so they cannot be called directly.
Then, if you want to send the request to the local service stably during debugging, one way is to add the url
parameter to FeignClient
and specify the calling address:
@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
url
in the annotation and modify the code again. If you forget it, it will cause online problems. - If there are many
FeignClient
to be tested, each one needs to be configured withurl
, which is very troublesome to modify.
So, what are some ways to improve? In order to solve this problem, we still have to start with Feign’s principle.
Feign Principle
I have written a simple source code analysis before about the implementation and working principle of Feign. You can simply spend a few minutes to lay the groundwork and analyze the core source code of Feign. Once you understand the principle, it will be easier to understand later.
To put it simply, it is the @EnableFeignClients
annotation added to the project. There is a very important line of code in the implementation:
@Import(FeignClientsRegistrar.class)
This class implements the ImportBeanDefinitionRegistrar
interface. In the registerBeanDefinitions
method of this interface, you can manually create a BeanDefinition
and register it. After that, spring will use BeanDefinition
instantiates the generated bean and puts it into the container.
In this way, Feign scans the interface with the @FeignClient
annotation added, and then generates the proxy object step by step. The specific process can be seen in the picture below:
When subsequent requests are made, the FeignInvocationHandler
of the proxy object is used to intercept, and the processor is distributed according to the corresponding method to complete subsequent http request operations.
ImportBeanDefinitionRegistrar
The ImportBeanDefinitionRegistrar
mentioned above is very important in the entire process of creating a proxy for FeignClient
, 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; }
Through BeanDefinitionBuilder
, pass in specific values to the construction method of this entity class, and finally generate a 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); } }
The specific calling time of the registerBeanDefinitions
method is when the ConfigurationClassPostProcessor
executes the postProcessBeanDefinitionRegistry
method, and the registerBeanDefinition
method will BeanDefinition
is put into a map, and beans are instantiated based on it later.
Introduce it through @Import
on the configuration class:
@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 successfully manually created a bean by customizing BeanDefinition
and placed it in the spring container:
User(id=1, name=Hydra)
Okay, this is the end of the preparation work, and now the formal transformation work begins.
Renovation
Let’s summarize here first. The point we struggle with is that the local environment needs to configure url
in FeignClient
, but the online environment does not need it, and we don’t want to modify the code back and forth.
In addition to generating dynamic proxies and interception methods as in the source code, the official documentation also provides us with a method to manually create 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.
A brief look at this process also requires configuration of Client
, Encoder
, Decoder
, Contract
, and RequestInterceptor
and other content.
Client
: The initiator of the actual http request. If load balancing is not involved, you can use a simpleClient.Default
. If load balancing is used, you can useLoadBalancerFeignClient
, as mentioned before, thedelegate
inLoadBalancerFeignClient
actually usesClient.Default
Encoder
andDecoder
: Feign’s codec, use the correspondingSpringEncoder
andResponseEntityDecoder
in the spring project. This In the process, we borrowedGsonHttpMessageConverter
as the message converter to parse jsonRequestInterceptor
: Feign’s interceptor has many general business uses, such as adding and modifying header information, etc. If it is not used here, it does not need to be used.Contract
: Literally means contract. Its function is to parse and verify the interface we passed in to see whether the use of annotations complies with the specifications, and then extract the metadata about http into the result and return it. If we use annotations such asRequestMapping
,PostMapping
,GetMapping
, then the corresponding one isSpringMvcContract
In fact, the only one required here is Contract
, and the others are optional configuration items. We write a configuration class and inject all the necessary 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 step by step.
The first is the introduced configuration class LocalFeignProperties
, which has three properties, namely whether to enable local routing, the package name for scanning the FeignClient interface, and the local routing mapping relationship we want to do, addressMapping
stores the service name and corresponding url address:
@Data @Component @ConfigurationProperties(prefix = "feign.local") public class LocalFeignProperties {<!-- --> // Whether to enable local routing private String enable; //Scan the package name of FeignClient private String basePackage; //Routing address mapping private Map<String,String> addressMapping; }
The following line of annotations indicates that the current configuration file will only take effect when the feign.local.enable
attribute in the configuration file is true
:
@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")
Finally, our top priority is LocalFeignClientRegistrar
. We still follow the official idea of building BeanDefinition
through the ImportBeanDefinitionRegistrar
interface and then registering it.
Moreover, many basic functions have been implemented in the source code of FeignClientsRegistrar
, such as scanning the package and obtaining the name
and contextId of
, FeignClient
url
, etc., so there are very few places that need to be changed, and you can safely copy its code.
First create LocalFeignClientRegistrar
and inject the required ResourceLoader
, BeanFactory
, and 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; } //Omit the specific function code first... }
Then take a look at the work before creating BeanDefinition
. This part mainly completes the scanning of the package and the test of detecting whether the @FeignClient
annotation is added to the interface. The following code basically copies the source code, except for changing the path of the scanned package and 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 it. Feign’s source code uses FeignClientFactoryBean
to create the proxy object. We don’t need it here. We directly replace it with Feign.builder
Create.
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); }
In this process, we mainly did the following things:
- Through
beanFactory
we got theClient
,Encoder
,Decoder
,Contract
we created earlier >, used to buildFeign.Builder
- By injecting the configuration class, get the call
url
corresponding to the service in the configuration file throughaddressMapping
- Use the
target
method to replace theurl
to be requested. If it exists in the configuration file, theurl
in the configuration file will be used first, otherwise@FeignClient will be used.
Theurl
configured in the annotation, if there is none, use the service name to access throughLoadBalancerFeignClient
Create the spring.factories
file in the resources/META-INF
directory and register our automatic configuration class through spi:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.feign.local.config.FeignAutoConfiguration
Finally, just package it locally:
mvn clean install
Test
Import the package we created above. Since the package already contains spring-cloud-starter-openfeign
, there is no need to import the feign
package additionally:
<dependency> <groupId>com.cn.hydra</groupId> <artifactId>feign-local-enhancer</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
Add configuration information to the configuration file and enable the component:
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 a FeignClient
interface. We can write any address in the annotated url
, which can be used to test whether it will be overwritten by the service address in the configuration file:
@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. During the process, you can see the operation of scanning the package:
Add a breakpoint in the process of replacing url
. You can see that even if url
is configured in the annotation, the service url in the configuration file will be given priority. code>coverage:
Using the interface to test, you can see that the above proxy object is used to access and the result is returned successfully:
If the project needs to release a formal environment, you only need to change the configuration feign.local.enable
to false
or delete it, and add Feign's original @EnableFeignClients to the project
That’s it.