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

文章目录

  • Feign 配置的注册流程
  • 在 Starter 中手动注册 Feign
    • 通过 FeignClientBuilder 构建 FeignClient
      • 1.扫描约定目录下的符合某种特征的类
      • 2.注册这些扫描到的类的 BeanDefinition
      • 3.配置 FeignClient 并注册到 Spring Bean 容器中
    • 验证并测试
  • 总结
    • 抛出新的问题 (WTF!?)
      • 如何在 Starter 中注入全局的 FeignClient 配置?

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

Feign 配置的注册流程

既然说到注册 FeignClient, 那一定就会回到 “工厂 Bean” FeignClientFactoryBean. 实现了 FactoryBeanFeignClientFactoryBeangetObject() 方法中调用了自身内部实现的 getTarget(), 该方法就是配置并获取 FeignClient 的核心方法:
OpenFeign#2 - 在 Starter 中手动注册 FeignClient_第1张图片
其中, feign(FeignContext) 负责配置 Feign 并应用 @EnableFeignClients@FeignClient 注解属性以及配置类中的配置, 调用流程如下:
OpenFeign#2 - 在 Starter 中手动注册 FeignClient_第2张图片
可以看到, 默认的配置会在以每个 “contextId” 标识的 FeignContext 中都注册.


接着回到 configureUsingConfiguration(FeignContext, Feign.Builder) 方法中, 其逻辑就是从 NamedContextFactory 中获取配置的属性 Bean, 设值到 Feign.Builder 中:
OpenFeign#2 - 在 Starter 中手动注册 FeignClient_第3张图片
需要注意的是, 由于 configureUsingXXX 在调用时机上晚于 Feign.Builder 的获取, 所以配置类中注入的配置 Bean 会覆盖提前在 Feign.Builder 中指定的:
OpenFeign#2 - 在 Starter 中手动注册 FeignClient_第4张图片

在 Starter 中手动注册 Feign

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

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


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

通过 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).
     * 
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 都用于追加至其后: *
{@code
     * if (!StringUtils.hasText(url)) {
     *     ...
     *     url = name
     *     ...
     *     url += cleanPath();
     * }
     * String url = this.url + cleanPath();
     * }
* * @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, 再在其之上进行配置.

你可能感兴趣的:(#,Spring,Cloud,openfeign)