Simplify local Feign calls based on Nacos

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:

image

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:

image

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.

image

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:

image

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 with url, 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:

image

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.

image

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 simple Client.Default. If load balancing is used, you can use LoadBalancerFeignClient, as mentioned before, the delegate in LoadBalancerFeignClient actually uses Client.Default
  • Encoder and Decoder: Feign’s codec, use the corresponding SpringEncoder and ResponseEntityDecoder in the spring project. This In the process, we borrowed GsonHttpMessageConverter as the message converter to parse json
  • RequestInterceptor: 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 as RequestMapping, PostMapping, GetMapping, then the corresponding one is SpringMvcContract

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 the Client, Encoder, Decoder, Contract we created earlier >, used to build Feign.Builder
  • By injecting the configuration class, get the call url corresponding to the service in the configuration file through addressMapping
  • Use the target method to replace the url to be requested. If it exists in the configuration file, the url in the configuration file will be used first, otherwise @FeignClient will be used. The url configured in the annotation, if there is none, use the service name to access through LoadBalancerFeignClient

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:

image

Add a breakpoint in the process of replacing url. You can see that even if url is configured in the annotation, the service urlcoverage:

image

Using the interface to test, you can see that the above proxy object is used to access and the result is returned successfully:

image

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.

syntaxbug.com © 2021 All Rights Reserved.