接上一篇(OpenFeign#1 - FeignClient 是如何注册的?), 本篇将详细说明如何在非全局上下文中 (例如在 Starter 中) 注册 FeignClient
.
既然说到注册 FeignClient
, 那一定就会回到 “工厂 Bean” FeignClientFactoryBean
. 实现了 FactoryBean
的 FeignClientFactoryBean
在 getObject()
方法中调用了自身内部实现的 getTarget()
, 该方法就是配置并获取 FeignClient
的核心方法:
其中, feign(FeignContext)
负责配置 Feign 并应用 @EnableFeignClients
和 @FeignClient
注解属性以及配置类中的配置, 调用流程如下:
可以看到, 默认的配置会在以每个 “contextId” 标识的 FeignContext
中都注册.
接着回到 configureUsingConfiguration(FeignContext, Feign.Builder)
方法中, 其逻辑就是从 NamedContextFactory
中获取配置的属性 Bean, 设值到 Feign.Builder
中:
需要注意的是, 由于 configureUsingXXX 在调用时机上晚于 Feign.Builder
的获取, 所以配置类中注入的配置 Bean 会覆盖提前在 Feign.Builder
中指定的:
这一节我们讨论下如何在非应用上下文中 (不能被自动扫描到的上下文中) 手动注册
FeignClient
, 期望达到的效果就是对宿主服务代码无侵入且能 “享受到” 正常构建FeignClient
过程中的各种配置逻辑.
通过上面一章节我们知道, 可以使用 Feign.Builder
来构建一个 FeignClient
的实例. 但是有一个问题, 通过这种方式注入的实际上是只应用了当前上下文的配置属性 Bean, 并且从源码也可以看出, Feign.Builder
的使用只是 FeignClientFactoryBean#getTarget
逻辑中的其中一部分 (并没有调用 configureUsingXXX 方法为 Builder 配置).
那么, 有没有一种方式可以从 FeignClientFactoryBean
来创建 Feign 的实例, 可以应用到全局默认的配置 (在宿主服务的 @EnableFeignClients
中 defaultConfiguration
中指定的配置类)?
这样的话就必然需要调用 FeignClientFactoryBean
的 getTarget 方法, 而非 Feign.Builder
的 target 方法. 因此我们来看看 FeignClientFactoryBean
的 getTarget 方法都被哪里调用过:
可以看到有这么一个类存在: FeignClientBuilder
, 稍微阅读一下它的注释:
可见, 这就是我们需要的类, 这个类没有被其他任何类引用, 种种迹象说明 Feign 提供给使用者的一个工具类, 如其描述: “这个构建起构建的 FeignClient
与使用 @FeignClient
构建出来的一致”. 并且, 这个构建器也提供了 FeignClientBuilder.Builder#customize
方法来自定义 Feign.Builder
:
接下来, 我们就来探讨下如何通过 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()
方法中我们主要做这几件事情:
BeanDefinition
FeignClient
并注册到 Spring Bean 容器中要实现类路径扫描, 我们需要用到这个类: 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);
}
}
注册第一步扫描到的 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);
});
// ~ 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
提供全局的框架级的自动配置和请求拦截器 (比如在请求头中附加当前认证令牌等).
经测试, 在 Starter 中通过 FeignContext
注入配置的时机晚于主类并且晚于宿主服务的自动注入 FeignClient
, 除非要求宿主服务的 FeignClient
全部懒加载, 不然还来不及应用 Starter 中的配置, FeignClient
就被 getObject()
了.
除非有一种方式能够强制宿主服务满足某一条件的 Bean 延迟初始化?
思路1: 在 @EnableFeignClients
上为 defaultConfigurations
指定配置类? 这样做代码有侵入性
思路2: 在全局注入 Feign.Builder
, 这样添加 Feign 请求拦截器的时候先注入 Feign.Builder
就行了.
无论在哪个 Starter, 需要配置 Feign.Builder
的时候先注入全局的预配置的 Feign.Builder,
再在其之上进行配置.