Simplify local Feign calls, veterans teach you how to play

In normal work, OpenFeignit 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 192other 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 FeignClientcall 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.168network 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.168virtual network segment and the virtual network segment in docker 172.17belong 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 FeignClientadd urlparameters 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 urland modify the code again. If you forget, it will cause online problems

  • If there are FeignClienta lot of tests, each needs to be configured url, 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 ImportBeanDefinitionRegistrarof 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.registerBeanDefinitionsBeanDefinitionBeanDefinition

In this way, Feign scans @FeignClientthe annotated interface, and then generates proxy objects step by step. The specific process can be seen in the following picture:

Subsequent requests FeignInvocationHandlerare 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 FeignClientprocess 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);
    }
}

registerBeanDefinitionsConfigurationClassPostProcessorThe specific call time of the method is when the method is executed later postProcessBeanDefinitionRegistry, and registerBeanDefinitionthe method will be BeanDefinitionput into a map, and the bean will be instantiated according to it later.

On the configuration class by @Importintroducing it:

@Configuration
@Import(MyBeanDefinitionRegistrar.class)
public class MyConfiguration {
}

Inject this Usertest:

@Service
@RequiredArgsConstructor
public class UserService {
    private final User user;

    public void getUser(){
        System.out.println(user.toString());
    }
}

The result is printed, indicating that we BeanDefinitionhave 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 FeignClientconfigured 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, RequestInterceptorand other content.

  • Client: The initiator of the actual http request, if it does not involve load balancing, it can be used simply Client.Default, and it can be used when load balancing is used LoadBalancerFeignClient. As mentioned earlier, LoadBalancerFeignClientthe one in the middle delegateis actually usedClient.Default

  • EncoderAnd Decoder: Feign's codec, use the corresponding SpringEncoderand in the spring project ResponseEntityDecoder, in this process we use it GsonHttpMessageConverteras 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 as RequestMapping, PostMapping, and so on, then the corresponding use isGetMappingSpringMvcContract

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 addressMappingname 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.enablethis property in the configuration file trueis :

@ConditionalOnProperty(value = "feign.local.enable", havingValue = "true")

Finally, it is our top priority LocalFeignClientRegistrar. We still implement it according to the official idea ImportBeanDefinitionRegistrarof ​​building through the interface BeanDefinitionand then registering.

Moreover, FeignClientsRegistrarmany basic functions have been implemented in the source code, such as scanning and scanning packages, getting FeignClient, name, contextIdetc. url, so there are very few places that need to be changed, and you can safely copy and exceed its code.

First create LocalFeignClientRegistrarand 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 BeanDefinitionthe work before creation. This part mainly completes the package scanning and testing @FeignClientwhether 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 BeanDefinitionand register. Feign’s source code is used to FeignClientFactoryBeancreate a proxy object. We don’t need it here, and we can directly replace it with Feign.buildercreation.

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 , , , beanFactorywe created earlier , to buildClientEncoderDecoderContractFeign.Builder

  • By injecting the configuration class, by addressMappinggetting the call corresponding to the service in the configuration fileurl

  • targetReplace the request by the method , urlif it exists in the configuration file, use the configuration file first url, otherwise use @FeignClientthe configuration in the annotation url, if there is none, use the service name to LoadBalancerFeignClientaccess

resources/META-INFCreate a file in the directory and spring.factoriesregister 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 feignpackages:

<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 FeignClientthe 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 urlprocess, 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.enablechange falseor 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, urlit 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.

Guess you like

Origin blog.csdn.net/JACK_SUJAVA/article/details/131326888