Dynamically Modifying @FeignClient Path at Runtime Using BeanFactoryPostProcessor

This solution covers both Spring Boot 2.x and 3.x implementations.

Problem Description

A common requirement arises where each API interface carries an interfacePath defined in a custom annotation on the interface itself. For instance:

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

The goal is to automatically prepend @XXXMapping values to the @FeignClient path attribute, rather than manually configuring each Feign client.

How @FeignClient Scanning Works

Projects using Feign typically enable it via @EnableFeignClients on the main application class:

@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 {};
}

The @Import annotation registers FeignClientsRegistrar, which implements ImportBeanDefinitionRegistrar to inject bean definitions into the Spring container during initialization.

Analyzing the Registration Process

The registerBeanDefinitions method in FeignClientsRegistrar performs two key operations:

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

The registerFeignClients method employs a scanner to find all @FeignClient annotated interfaces:

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);
    }
}

The registerFeignClient method extracts attributes like value, path, and contextId from the annotation metadata, then configures a FeignClientFactoryBean to handle the actual client instantiation:

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);
}

The BeanDefinition contains an instanceSupplier that holds the parsed @FeignClient attributes as an AnnotationAttributes map. This InstanceSupplier is invoked later during bean instantiation to configure the actual Feign client.

Solution Approach

Since the InstanceSupplier retains the parsed attributes and these attributes determine request path during instantiation, we can intercept the BeanDefinition betwean registration and instantiation, then modify the attributes stored within the InstanceSupplier.

This interception occurs after ImportBeanDefinitionRegistrar processes but before bean instantiation—exactly where BeanFactoryPostProcessor executes.

Spring Boot 2.x Implemantation

Create a custom BeanFactoryPostProcessor to scan and modify Feign client bean definitions:

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

    private String scanPackage;
    private ResourceLoader resourceLoader;
    private Environment environment;

    @Override
    @SuppressWarnings("unchecked")
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        List<String> feignClients = discoverFeignClients();
        for (String clientName : feignClients) {
            GenericBeanDefinition definition = (GenericBeanDefinition) beanFactory.getBeanDefinition(clientName);
            Class<?> clientClass = definition.getBeanClass();
            
            Class<?> targetApi = findTargetApiInterface(clientClass);
            ApiPathMapping mapping = targetApi.getAnnotation(ApiPathMapping.class);
            String apiPath = mapping.value();
            
            Supplier<?> supplier = definition.getInstanceSupplier();
            injectPathFromAnnotation(supplier, apiPath);
        }
    }

    private Class<?> findTargetApiInterface(Class<?> clientClass) {
        return Arrays.stream(clientClass.getInterfaces())
            .filter(i -> i.getName().startsWith("com.example") && i.getName().endsWith("Api"))
            .findFirst()
            .orElseThrow(() -> new IllegalStateException("Target API interface not found"));
    }

    private void injectPathFromAnnotation(Supplier<?> supplier, String apiPath) {
        try {
            Field[] fields = supplier.getClass().getDeclaredFields();
            for (Field field : fields) {
                if (Map.class.isAssignableFrom(field.getType())) {
                    field.setAccessible(true);
                    @SuppressWarnings("unchecked")
                    Map<String, String> attrMap = (Map<String, String>) field.get(supplier);
                    String serviceName = attrMap.get("value");
                    attrMap.put("path", serviceName + apiPath);
                }
            }
        } catch (Exception ex) {
            log.error("Failed to inject Feign client path", ex);
        }
    }

    private List<String> discoverFeignClients() {
        ClassPathScanningCandidateComponentProvider provider = createScanner();
        provider.setResourceLoader(this.resourceLoader);
        provider.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        Set<BeanDefinition> candidates = provider.findCandidateComponents(scanPackage);
        return candidates.stream()
            .map(BeanDefinition::getBeanClassName)
            .collect(Collectors.toList());
    }

    private ClassPathScanningCandidateComponentProvider createScanner() {
        return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
            @Override
            protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
                return beanDefinition.getMetadata().isIndependent() 
                    && !beanDefinition.getMetadata().isAnnotation();
            }
        };
    }

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

    @Override
    public void setEnvironment(Environment environment) {
        this.environment = environment;
        this.scanPackage = environment.getProperty("feign.scan.package");
    }
}

Spring Boot 3.x Implementation

Upgrading to Spring Boot 3.x changes the bean definition structure. In 2.x, the beanClass stores the original class type, but in 3.x, it stores FeignClientFactoryBean. The actual Feign interface type moves to the factoryBeanObjectType attribute, and annotation properties shift to propertyValues.

Modify the postProcessBeanFactory method accordingly:

@Override
@SuppressWarnings("unchecked")
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    List<String> feignClients = discoverFeignClients();
    for (String clientName : feignClients) {
        GenericBeanDefinition definition = (GenericBeanDefinition) beanFactory.getBeanDefinition(clientName);
        String feignInterfaceType = String.valueOf(definition.getAttribute("factoryBeanObjectType"));
        
        try {
            Class<?> clientClass = Class.forName(feignInterfaceType);
            Class<?> targetApi = findTargetApiInterface(clientClass);
            ApiPathMapping mapping = targetApi.getAnnotation(ApiPathMapping.class);
            String apiPath = mapping.value();
            
            MutablePropertyValues propertyValues = definition.getPropertyValues();
            String serviceName = String.valueOf(propertyValues.get("name"));
            propertyValues.add("path", serviceName + apiPath);
        } catch (Exception ex) {
            log.error("Failed to configure Feign client: {}", clientName, ex);
        }
    }
}

The key differences in Spring Boot 3.x:

  • The factoryBeanObjectType attribute holds the original interface class name as a string
  • Use Class.forName() to load the interface class dynamically
  • Access propertyValues directly instead of reflecting into InstanceSupplier
  • The path property is added via MutablePropertyValues.add() method

Summary

This approach intercepts Feign client registration through BeanFactoryPostProcessor, allowing dynamic path modification based on custom annotations. The solution adapts to structural changes between Spring Boot 2.x and 3.x by handling different bean definition formats appropriately.

Tags: Spring Cloud feign BeanFactoryPostProcessor Spring Boot Dynamic Configuration

Posted on Wed, 13 May 2026 10:14:33 +0000 by AlanG