OpenFeign#2 - 在 Starter 中手动注册 FeignClient

接上一篇(OpenFeign#1 - FeignClient 是如何注册的?), 本篇将详细说明如何在非全局上下文中 (例如在 Starter 中) 注册 FeignClient.

Feign 配置的注册流程

既然说到注册 FeignClient, 那一定就会回到 “工厂 Bean” FeignClientFactoryBean. 实现了 FactoryBean<Object>FeignClientFactoryBeangetObject() 方法中调用了自身内部实现的 getTarget(), 该方法就是配置并获取 FeignClient 的核心方法:
在这里插入图片描述
其中, feign(FeignContext) 负责配置 Feign 并应用 @EnableFeignClients@FeignClient 注解属性以及配置类中的配置, 调用流程如下:
在这里插入图片描述
可以看到, 默认的配置会在以每个 “contextId” 标识的 FeignContext 中都注册.


接着回到 configureUsingConfiguration(FeignContext, Feign.Builder) 方法中, 其逻辑就是从 NamedContextFactory 中获取配置的属性 Bean, 设值到 Feign.Builder 中:
在这里插入图片描述
需要注意的是, 由于 configureUsingXXX 在调用时机上晚于 Feign.Builder 的获取, 所以配置类中注入的配置 Bean 会覆盖提前在 Feign.Builder 中指定的:
在这里插入图片描述

在 Starter 中手动注册 Feign

这一节我们讨论下如何在非应用上下文中 (不能被自动扫描到的上下文中) 手动注册 FeignClient, 期望达到的效果就是对宿主服务代码无侵入且能 “享受到” 正常构建 FeignClient 过程中的各种配置逻辑.

通过上面一章节我们知道, 可以使用 Feign.Builder 来构建一个 FeignClient 的实例. 但是有一个问题, 通过这种方式注入的实际上是只应用了当前上下文的配置属性 Bean, 并且从源码也可以看出, Feign.Builder 的使用只是 FeignClientFactoryBean#getTarget 逻辑中的其中一部分 (并没有调用 configureUsingXXX 方法为 Builder 配置).
那么, 有没有一种方式可以从 FeignClientFactoryBean 来创建 Feign 的实例, 可以应用到全局默认的配置 (在宿主服务的 @EnableFeignClientsdefaultConfiguration 中指定的配置类)?


这样的话就必然需要调用 FeignClientFactoryBean 的 getTarget 方法, 而非 Feign.Builder 的 target 方法. 因此我们来看看 FeignClientFactoryBean 的 getTarget 方法都被哪里调用过:
在这里插入图片描述
可以看到有这么一个类存在: FeignClientBuilder, 稍微阅读一下它的注释:
在这里插入图片描述
可见, 这就是我们需要的类, 这个类没有被其他任何类引用, 种种迹象说明 Feign 提供给使用者的一个工具类, 如其描述: “这个构建起构建的 FeignClient 与使用 @FeignClient 构建出来的一致”. 并且, 这个构建器也提供了 FeignClientBuilder.Builder#customize 方法来自定义 Feign.Builder:
在这里插入图片描述

通过 FeignClientBuilder 构建 FeignClient

接下来, 我们就来探讨下如何通过 FeignClientBuilder 构建 FeignClient.
最简单的硬编码的方式就是你直接调用 FeignClientBuilder 构建一个 FeignClient 并将其注册到 Spring Bean 容器中. 这个方式就不演示了.

下面我们来实现稍微一点点的配置和使用解耦, 并结合自动扫描的方式来在 Starter 中注册 FeignClient.
随便 “起” 一个 Starter, 写一个自动配置类并把其全限定名填到 META-INF 的 spring.factories 中:

@ConditionalOnClass(FeignContext.class)
@Configuration(proxyBeanMethods = false)
public class FeignClientConfigurerScannerAutoConfiguration implements InitializingBean {

  // P.S.
  // ! 我们还可以利用注入的 feignContext 在 Starter 中为所有的 FeignClient 添加默认的配置
  //   通过调用方法: org.springframework.cloud.context.named.NamedContextFactory#setConfigurations
  private final FeignContext feignContext;
  
  public FeignClientConfigurerScannerAutoConfiguration(FeignContext feignContext) {
    this.feignContext = feignContext;
  }

  @Override
  public void afterPropertiesSet() {
    this.registerFeignClients();
  }
}

在上面的 registerFeignClients() 方法中我们主要做这几件事情:

  1. 扫描约定目录下的符合某种特征的类
  2. 注册这些扫描到的类的 BeanDefinition
  3. 配置 FeignClient 并注册到 Spring Bean 容器中

1.扫描约定目录下的符合某种特征的类

要实现类路径扫描, 我们需要用到这个类: ClassPathScanningCandidateComponentProvider, 如其名这是一个通过指定类路径扫描符合条件的候选组件的扫描器. 我们只需要提供要扫描的资源类型以及扫描条件(过滤器)和类路径, 就可以扫描出候选类了, 代码片段如下:

final ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(false);
componentProvider.setResourcePattern("**/*.class");
// FeignClientConfigurer.class 是我们抽象出来的屏蔽了 FeignClientBuilder 配置细节的类
componentProvider.addIncludeFilter(new SubClassTypeFilter(FeignClientConfigurer.class));
final Set<BeanDefinition> candidateComponents = candidateComponentProvider.findCandidateComponents("foo.bar.xxx");

// ---

/**
 * 这是一个类型过滤器, 可以按照我们的定制过滤出需要的类型.
 * 这里我们的逻辑是实现了某个父类的子类.
 */
class SubClassTypeFilter implements TypeFilter {
    private final Class<?> abstractClass;
    public SubClassTypeFilter(Class<?> abstractClass) {
        this.abstractClass = abstractClass;
    }
    @Override
    public boolean match(@NonNull MetadataReader metadataReader, @NonNull MetadataReaderFactory metadataReaderFactory) {
        final ClassMetadata classMetadata = metadataReader.getClassMetadata();
        return StringUtils.equals(classMetadata.getSuperClassName(), abstractClass.getName());
    }
}

如上所述, 我们的逻辑是扫描指定包下, 实现了我们自定义的 FeignClientConfigurer 的子类. 这个类主要是以抽象模版的方式提供了可于 FeignClientBuilder 配置的抽象方法并且全部空实现, 供调用者选择是否实现. 详细代码如下:

public abstract class FeignClientConfigurer<T> {
    /**
     * Description: 服务标识 (Service ID).
     * <br>Details: 如果没有提供 {@link FeignClientConfigurer#url()}, 则会根据该属性从负载均衡策略中选取一个实例.
     *
     * @see FeignClient#name()
     */
    private final String name;
    
    /**
     * Description: FeignClient 类型, 必须是接口.
     */
    private final Class<T> target;
    
    /**
     * @param name   (Required) {@link FeignClientConfigurer#name}.
     * @param target (Required) {@link FeignClientConfigurer#target}.
     */
    protected FeignClientConfigurer(String name, Class<T> target) {
        this.name = name;
        this.target = AssertKit.isInterface(target);
    }
    
    // -------------------------------------------------------------------------------------------------------------------------------------
    /**
     * Details: 如果没有提供该属性, 则会用 {@link FeignClientConfigurer#name} 覆盖该属性.
     *
     * @see FeignClient#url()
     */
    protected String url() {
        return null;
    }
    
    /**
     * Details: 无论是提供了 {@link FeignClientConfigurer#name} 还是 {@link FeignClientConfigurer#url()}, path 都用于追加至其后:
     * <pre>{@code
     * if (!StringUtils.hasText(url)) {
     *     ...
     *     url = name
     *     ...
     *     url += cleanPath();
     * }
     * String url = this.url + cleanPath();
     * }</pre>
     *
     * @see FeignClient#path()
     * @see FeignClientFactoryBean#cleanPath()
     */
    @SuppressWarnings("JavadocReference")
    protected String path() {
        return null;
    }
    
    /**
     * @see FeignClient#contextId()
     */
    protected String contextId() {
        return null;
    }
    
    /**
     * @see FeignClient#decode404()
     * @see Feign.Builder#decode404()
     */
    protected boolean decode404() {
        return false;
    }
    
    /**
     * Description: 提供 {@link Feign.Builder} 的 Customizer.
     *
     * @return {@link FeignBuilderCustomizer}
     * @author LiKe
     * @date 2023-03-29 10:02:05
     */
    protected FeignBuilderCustomizer customizer() {
        return null;
    }
    
    /**
     * @see FeignClient#fallback()
     */
    protected Class<? extends T> fallback() {
        return null;
    }
    
    /**
     * @see FeignClient#fallbackFactory()
     */
    protected Class<? extends FallbackFactory<? extends T>> fallbackFactory() {
        return null;
    }
    
    protected boolean inheritParentContext() {
        return true;
    }
    
    /**
     * Description: 获取应用上下文的 Shortcut.
     *
     * @return {@link ApplicationContext}
     */
    protected ApplicationContext getApplicationContext() {
        return Runtime.Spring.getApplicationContext();
    }
    
    /**
     * Description: 完成 {@link FeignClient} 的配置, 生成代理对象.
     *
     * @return T {@link FeignClient} 的接口, 用于生成代理对象.
     */
    public final T configure() {
        final FeignClientBuilder.Builder<T> builder = new FeignClientBuilder(getApplicationContext()).forType(this.target, this.name);
        // @formatter:off
        if (nonNull(url())) builder.url(url());
        if (nonNull(contextId())) builder.contextId(contextId());
        if (nonNull(customizer())) builder.customize(customizer());
        if (nonNull(fallback())) builder.fallback(fallback());
        if (nonNull(fallbackFactory())) builder.fallbackFactory(fallbackFactory());
        if (nonNull(path())) builder.path(path());
        // @formatter:on
        return builder.decode404(this.decode404()).inheritParentContext(this.inheritParentContext()).build();
    }
}

使用方法是, 在指定的类路径下, 实现一个接口作为 FeignClient, 无需注解. 再实现一个该 FeignClient 的配置类(继承 FeignClientConfigurer). 然后就能被自动扫描到. 我们先简单实现:

// DemoFeignClient
public interface DemoFeignClient {
    @GetMapping("/hello")
    String hello(@RequestParam("name") String name);
}

// DemoFeignClientConfigurer
public class DemoFeignClientConfigurer extends FeignClientConfigurer<DemoFeignClient> {
    protected DemoFeignClientConfigurer() {
        super("demo", DemoFeignClient.class);
    }
}

2.注册这些扫描到的类的 BeanDefinition

注册第一步扫描到的 BeanDefinition:

final Set<BeanDefinition> candidateComponents = candidateComponentProvider.findCandidateComponents("foo.bar.xxx");
// ~ Scan and register FeignClientConfigurer
candidateComponents.forEach(beanDefinition /* ScannedGenericBeanDefinition */ -> {
    final String beanName = "embedded.feignClientConfigurer." + ClassUtils.getShortName(AssertKit.nonNull(beanDefinition.getBeanClassName()));
    registry.registerBeanDefinition(beanName, beanDefinition);
});

3.配置 FeignClient 并注册到 Spring Bean 容器中

// ~ Build embedded FeignClient
final Map<String, FeignClientConfigurer> configurersgetApplicationContext().getBeansOfType(FeignClientConfigurer.class);
configurers.keySet().forEach(configurerBeanName -> {
    final FeignClientConfigurer configurer = configurers.get(configurerBeanName);
    final String beanName = configurer.getName();
    final Object target = configurer.configure(); // 配置 FeignClient
    getBeanFactory().registerSingleton(beanName, target);
});

验证并测试

“起” 一个 SpringBoot 工程 (spring.application.name: demo), 引入我们的 Starter. 写两个接口用于测试:

@GetMapping("/hello")
public String hello(@RequestParam("name") String name) {
    log.info("Hello from '{}'", name);
    return "Fin";
}
/**
 * Description: 从内嵌的 FeignClient 完成调用 '/hello' 接口.
 */
@GetMapping("/embedded.feign/hello")
public String embeddedFeignHello() {
    return applicationContext.getBean(DemoFeignClient.class).hello("embedded.feign");
}

分别从两个接口请求, 可以看到第二个接口 “从 Nacos 转了一圈回到了 /hello 接口”.

总结

通过 FeignClientBuilder 的方式构建 FeignClient, 我们不仅可以达到和原生使用 @FeignClient 一样的构建 FeignClient 的效果, 更是编程式可定制的. 结合框架设计, 一定能够实现更加灵活的 FeignClient 定制.
并且本文只简要提了一下的 FeignContext, 我们可以通过其为 FeignClient 提供全局的框架级的自动配置和请求拦截器 (比如在请求头中附加当前认证令牌等).

抛出新的问题 (WTF!?)

如何在 Starter 中注入全局的 FeignClient 配置?

经测试, 在 Starter 中通过 FeignContext 注入配置的时机晚于主类并且晚于宿主服务的自动注入 FeignClient, 除非要求宿主服务的 FeignClient 全部懒加载, 不然还来不及应用 Starter 中的配置, FeignClient 就被 getObject() 了.
除非有一种方式能够强制宿主服务满足某一条件的 Bean 延迟初始化?

思路1:@EnableFeignClients 上为 defaultConfigurations 指定配置类? 这样做代码有侵入性

思路2: 在全局注入 Feign.Builder, 这样添加 Feign 请求拦截器的时候先注入 Feign.Builder 就行了.
无论在哪个 Starter, 需要配置 Feign.Builder 的时候先注入全局的预配置的 Feign.Builder, 再在其之上进行配置.

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值