Dynamically modify the path of @FeignClient through BeanFactotyPostProcessor

Recently, a project has a requirement to dynamically modify the request path of @FeignClient after startup. Basically, what I found on the Internet is to use ${…} in @FeignClient and define Feign’s interface path through a configuration file. This does not satisfy us. needs

For some special reasons, each of our interfaces has an interfacePath, which is defined in the custom annotation on the interface.
That is to say, the interface defined by @FeignClient inherits from other modules, and the interfaces of other modules have a custom annotation describing the interfacePath of the interface, as follows:

@FeignClient(value = "x-module")
public interface XXXService extends XApi{<!-- -->
}
@XXXMapping("/member")
public interface XApi {<!-- -->

So we need to add the value of this @XXXMapping to the path attribute in each @FeignClient as a prefix for cross-service calls. If we want to manually process each @FeignClient prefix, it would be too unfriendly. We hope that this can be automatically handled by the program.

First, take a look at the scanning process of @FeignClient to see if there is a suitable time to deal with this problem.

For projects using Feign, the annotation @EnableFeignClients will usually be added to the startup class. Click on this annotation to take a look.

@Retention(RetentionPolicy.RUNTIME)
@Target({<!-- -->ElementType.TYPE})
@Documented
@Import({<!-- -->FeignClientsRegistrar.class})
public @interface EnableFeignClients {<!-- -->
    String[] value() default {<!-- -->};

    String[] basePackages() default {<!-- -->};

    Class<?>[] basePackageClasses() default {<!-- -->};

    Class<?>[] defaultConfiguration() default {<!-- -->};

    Class<?>[] clients() default {<!-- -->};
}

It imports a class FeignClientsRegistrar through the @Import annotation

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {<!-- -->

This class implements the ImportBeanDefinitionRegistrar interface. This interface is used to register some BeanDefinitions to the container during the initialization process of the Spring container. This belongs to the scope of the Spring source code and will not be described in detail here. Just look at its registerBeanDefinitions method implementation.

 public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {<!-- -->
        this.registerDefaultConfiguration(metadata, registry);
        this.registerFeignClients(metadata, registry);
    }

There are only two lines of code. The second line of the name is to register FeignClient. Then our @FeignClient can basically be sure that this line of code is being processed. Click on it.

This method is relatively long, so I will only post some key codes here.

 LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet();
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    ...
    ...
    ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
    scanner.setResourceLoader(this.resourceLoader);
    scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
    Set<String> basePackages = this.getBasePackages(metadata);
    Iterator var8 = basePackages.iterator();
    while(var8.hasNext()) {<!-- -->
         String basePackage = (String)var8.next();
         candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
    }
    
    Iterator var13 = candidateComponents.iterator();

    while(var13.hasNext()) {<!-- -->
        BeanDefinition candidateComponent = (BeanDefinition)var13.next();
        if (candidateComponent instanceof AnnotatedBeanDefinition) {<!-- -->
            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 = this.getClientName(attributes);
            this.registerClientConfiguration(registry, name, attributes.get("configuration"));
            this.registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
    ...
    ...

First define a scanner, filter out a set of BeanDefinitions through @FeignClient, which is the candidateComponents above, and then traverse, there is a line of code in it

Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());

Here we get various attributes defined in @FeignClient, such as value, path, contextId, etc.
Then call the registerFeignClient method to complete the registration and enter this method

 private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {<!-- -->
        String className = annotationMetadata.getClassName();
        Class clazz = ClassUtils.resolveClassName(className, (ClassLoader)null);
        ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory)registry : null;
        String contextId = this.getContextId(beanFactory, attributes);
        String name = this.getName(attributes);
        FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
        factoryBean.setBeanFactory(beanFactory);
        factoryBean.setName(name);
        factoryBean.setContextId(contextId);
        factoryBean.setType(clazz);
        factoryBean.setRefreshableClient(this.isClientRefreshEnabled());
        BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {<!-- -->
            factoryBean.setUrl(this.getUrl(beanFactory, attributes));
            factoryBean.setPath(this.getPath(beanFactory, attributes));
            factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
            Object fallback = attributes.get("fallback");
            if (fallback != null) {<!-- -->
                factoryBean.setFallback(fallback instanceof Class ? (Class)fallback : ClassUtils.resolveClassName(fallback.toString(), (ClassLoader)null));
            }

            Object fallbackFactory = attributes.get("fallbackFactory");
            if (fallbackFactory != null) {<!-- -->
                factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class)fallbackFactory : ClassUtils.resolveClassName(fallbackFactory.toString(), (ClassLoader)null));
            }

            return factoryBean.getObject();
        });
        definition.setAutowireMode(2);
        definition.setLazyInit(true);
        this.validate(attributes);
        AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
        beanDefinition.setAttribute("factoryBeanObjectType", className);
        beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
        boolean primary = (Boolean)attributes.get("primary");
        beanDefinition.setPrimary(primary);
        String[] qualifiers = this.getQualifiers(attributes);
        if (ObjectUtils.isEmpty(qualifiers)) {<!-- -->
            qualifiers = new String[]{<!-- -->contextId + "FeignClient"};
        }

        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
        this.registerOptionsBeanDefinition(registry, contextId);
    }

Although this method is long but very clear, it first defines a FeignClientFactoryBean, then generates a BeanDefinitionBuilder, and passes in an InstanceSupplier through lambda, which holds the FactoryBean. In the InstanceSupplier, @FeignClient is determined by setting the url and path attributes of the FactoryBean. requested path

We use debug to observe what the final generated BeanDefinition looks like. Enter the registerBeanDefinition method and first obtain the beanName. This name is the full path name of our own interface.
The second line of code actually registers the BeanDefinition with the container. Make a breakpoint on this line and set the breakpoint conditions to facilitate locating our @FeignClient class.

You can see that the generated BeanDefinition has an instanceSupplier attribute

The internal AnnotationAttributes is the configuration parsed from the @FeignClient annotation, including value, path, etc., which is a Map structure

Seeing this, you already have a general idea. The InstanceSupplier here holds various attributes parsed from @FeignClinent, and will process these attributes into FeignClient’s request path when instantiated in the future.

Then we only need to modify the properties held by InstanceSupplier after this step and before instantiation to dynamically modify the request path of @FeignClient.

The processing of ImportBeanDefinitionRegistrar occurs in the processing process of BeanFactoryPostProcessor, then we can customize a BeanFactoryPostProcessor to obtain the Feign-processed BeanDefinition, take its InstanceSupplier, and reflect to modify its properties.

Customize a BeanFactoryPostProcessor

public class FeignClientProcessor implements BeanFactoryPostProcessor, ResourceLoaderAware, EnvironmentAware {<!-- -->
    private String feignClientPackage;
    private ResourceLoader resourceLoader;
    private environment environment;
    ...
    ...

The ResourceLoaderAware and EnvironmentAware interfaces are implemented to scan for classes annotated with @FeignClient. Because the name of the BeanDefinition registered by Feign is the full path name of our interface, it can be scanned and retrieved according to the class name in the container. As you can see above, it is scanned by Feign. The process can be directly copied and used.

Among them, in the callback of EnvironmentAware, a Feign scan path is set, because it is still in the early stage of Spring container refresh, and the configuration cannot be obtained through the @Value annotation.

 @Override
    public void setEnvironment(Environment environment) {<!-- -->
        this.environment = environment;
        this.feignClientPackage = environment.getProperty("feign.client.package");
    }

The scanned code is basically the source code of Feign, slightly modified. The scanned path is customized, rather than scanning from the root path, because in our own project, the Feign interface is at the specified location, and then the scanned BeanDefinition is converted into Class name, so you get a list of all class names annotated by @FeignClient

 private List<String> scanFeignClient() {<!-- -->
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(feignClientPackage);
        return candidateComponents.stream().map(BeanDefinition::getBeanClassName).collect(Collectors.toList());
    }

    private ClassPathScanningCandidateComponentProvider getScanner() {<!-- -->
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {<!-- -->
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {<!-- -->
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent() & amp; & amp; !beanDefinition.getMetadata().isAnnotation()) {<!-- -->
                    isCandidate = true;
                }

                return isCandidate;
            }
        };
    }

Then process it for each class
First get the interface path of the custom annotation through the interface implemented by the class, then get the BeanDefinition processed by Feign from the container through the class name, take its InstanceSupplier, find the map that stores the @FeignClient attribute by reflection, and add the splicing request prefix as the path to the map. middle

 List<String> feignClientList = scanFeignClient();
    feignClientList.forEach(item -> {<!-- -->
        GenericBeanDefinition beanDefinition = (GenericBeanDefinition)configurableListableBeanFactory.getBeanDefinition(item);
        Class<?> clazz = beanDefinition.getBeanClass();
        Class<?> apiInterface = Arrays.stream(clazz.getInterfaces()).filter(i -> i.getName().startsWith("com.xxx") & amp; & amp; i.getName(). endsWith("Api")).findAny().orElseThrow(() -> new RuntimeException("The base path is not defined"));
        XXXMapping annotation = apiInterface.getAnnotation(XXXMapping.class);
        String interfacePath = annotation.value();
        Supplier<?> instanceSupplier = beanDefinition.getInstanceSupplier();
        try {<!-- -->
            Field[] declaredFields = instanceSupplier.getClass().getDeclaredFields();
            for (Field field : declaredFields) {<!-- -->
                if (field.getType().isAssignableFrom(Map.class)) {<!-- -->
                    field.setAccessible(true);
                    Map<String, String> map = (Map)field.get(instanceSupplier);
                    String basePath = map.get("value");
                    map.put("path", basePath + interfacePath);
                }
            }
        } catch (Exception e) {<!-- -->
            log.error("Failed to initialize FeignClient:", e);
        }
    });

Complete code

@Component
@Log4j2
public class FeignClientProcessor implements BeanFactoryPostProcessor, ResourceLoaderAware, EnvironmentAware {

    private String feignClientPackage;

    private ResourceLoader resourceLoader;

    private environment environment;

    @Override
    @SuppressWarnings("unchecked")
    public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) throws BeansException {
        List feignClientList = scanFeignClient();
        feignClientList.forEach(item -> {
            GenericBeanDefinition beanDefinition = (GenericBeanDefinition)configurableListableBeanFactory.getBeanDefinition(item);
            Class clazz = beanDefinition.getBeanClass();
            Class apiInterface = Arrays.stream(clazz.getInterfaces()).filter(i -> i.getName().startsWith("com.aic") & amp; & amp; i.getName(). endsWith("Api")).findAny().orElseThrow(() -> new RuntimeException("The base path is not defined"));
            XXXMapping annotation = apiInterface.getAnnotation(XXXMapping.class);
            String interfacePath = annotation.value();
            Supplier instanceSupplier = beanDefinition.getInstanceSupplier();
            try {
                Field[] declaredFields = instanceSupplier.getClass().getDeclaredFields();
                for (Field field : declaredFields) {
                    Class type = field.getType();
                    if (type.isAssignableFrom(Map.class)) {
                        field.setAccessible(true);
                        Map map = (Map)field.get(instanceSupplier);
                        String basePath = map.get("value");
                        map.put("path", basePath + interfacePath);
                    }
                }
            } catch (Exception e) {
                log.error("Failed to initialize FeignClient:", e);
            }
        });
    }

    private List<String> scanFeignClient() {<!-- -->
        ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(feignClientPackage);
        return candidateComponents.stream().map(BeanDefinition::getBeanClassName).collect(Collectors.toList());
    }

    private ClassPathScanningCandidateComponentProvider getScanner() {<!-- -->
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {<!-- -->
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {<!-- -->
                boolean isCandidate = false;
                if (beanDefinition.getMetadata().isIndependent() & amp; & amp; !beanDefinition.getMetadata().isAnnotation()) {<!-- -->
                    isCandidate = true;
                }

                return isCandidate;
            }
        };
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

    @Override
    public void setEnvironment(Environment environment) {<!-- -->
        this.environment = environment;
        this.feignClientPackage = environment.getProperty("feign.client.package");
    }
}