FeignClient 支持占位符及其原理

问题与解决方案

在使用 FeignClient 的时候,测试环境和线上环境的域名是不同的,可以使用占位符来动态配置。如下

@FeignClient(name = "test-feign", url = "${feign.url}")
public interface TestFeignClient {
    ...
}

application.properties

feign.url=localhost:8080

原理

坑人的是 FeignClient 的注释中只说了 valuename 支持占位符,并没有说其他的是否支持。这是通过百度和调试源码发现 contextIdpathurl 也支持占位符。下面从源码进行分析:

通过在启动类上添加 @FeignClient 来启用 feign,可配置需要扫描的包:

@EnableFeignClients(basePackages = {"com.example.feign"})
@SpringBootApplication
public class FeignApplication {
...
}

EnableFeignClients 通过 @Import 引入了 FeignClientsRegistrar.class

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
}

FeignClientsRegistrar 实现了接口 ImportBeanDefinitionRegistrarImportBeanDefinitionRegistrar 的作用是在项目启动时向 Spring 注册一些 BeanDefinition

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {}

FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrarregisterBeanDefinitions 方法:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

主要的逻辑都在 registerFeignClients 中:

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {

    LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
    // 获取 EnableFeignClients 注解的属性,包含 basePackages
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
    if (clients == null || clients.length == 0) {
        ClassPathScanningCandidateComponentProvider scanner = getScanner();
        scanner.setResourceLoader(this.resourceLoader);
        scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
        // 获取要扫描的包,这里是 com.example.feign
        Set<String> basePackages = getBasePackages(metadata);
        // 扫描并创建 BeanDefinition,放入 candidateComponents(候选者)
        for (String basePackage : basePackages) {
            candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
        }
    }
    else {
        for (Class<?> clazz : clients) {
            candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
        }
    }

    for (BeanDefinition candidateComponent : candidateComponents) {
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            // FeignClient 必须是接口
            Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
			// 获取 @FeignClient 的属性,包括 name、contextId、url、path...
            Map<String, Object> attributes = annotationMetadata
                .getAnnotationAttributes(FeignClient.class.getCanonicalName());

            String name = getClientName(attributes);
            registerClientConfiguration(registry, name, attributes.get("configuration"));
			// 真正的解析、注册逻辑
            registerFeignClient(registry, annotationMetadata, attributes);
        }
    }
}

本文的关键就在 registerFeignClient 方法,此方法解析了 name、contextId、url、path 等属性并向 Spring 注册了 FeignClient

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;
    // 解析 contextId 
    String contextId = getContextId(beanFactory, attributes);
    // 解析 name 
    String name = getName(attributes);
    FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
    factoryBean.setBeanFactory(beanFactory);
    factoryBean.setName(name);
    factoryBean.setContextId(contextId);
    factoryBean.setType(clazz);
    factoryBean.setRefreshableClient(isClientRefreshEnabled());
    BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
        // 解析 contextId 
        factoryBean.setUrl(getUrl(beanFactory, attributes));
        // 解析 path 
        factoryBean.setPath(getPath(beanFactory, attributes));
        factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
        //... 省略 fallback 相关
        return factoryBean.getObject();
    });
    definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
    definition.setLazyInit(true);
    validate(attributes);

    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
    beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);

    // has a default, won't be null
    boolean primary = (Boolean) attributes.get("primary");

    beanDefinition.setPrimary(primary);

    String[] qualifiers = getQualifiers(attributes);
    if (ObjectUtils.isEmpty(qualifiers)) {
        qualifiers = new String[] { contextId + "FeignClient" };
    }

    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
    // 向 Spring 注册了 FeignClient
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

    registerOptionsBeanDefinition(registry, contextId);
}

上面的逻辑比较清晰,不需要额外说明,本文的重点在于占位符的解析,这里那 name 举例,由 getName 实现,先通过一系列的判断确定 name,然后通过 resolve 方法进行占位符的解析:

String getName(Map<String, Object> attributes) {
       return getName(null, attributes);
}

String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
    // serviceId
    String name = (String) attributes.get("serviceId");
    if (!StringUtils.hasText(name)) {
        name = (String) attributes.get("name");
    }
    if (!StringUtils.hasText(name)) {
        name = (String) attributes.get("value");
    }
    // 解析占位符
    name = resolve(beanFactory, name);
    return getName(name);
}

resolve 方法会被 getNamegetContextIdgetUrlgetPath 四个方法调用。

private String resolve(ConfigurableBeanFactory beanFactory, String value) {
    if (StringUtils.hasText(value)) {
        // 通过 environment 解析
        if (beanFactory == null) {
            return this.environment.resolvePlaceholders(value);
        }
        BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
        // 通过 beanFactory 解析
        String resolved = beanFactory.resolveEmbeddedValue(value);
        if (resolver == null) {
            return resolved;
        }
        // 通过 BeanExpressionResolver 解析
        return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null)));
    }
    return value;
}

所以说 value、name、contextId、url、path、serviceId 都是支持占位符的。