spring-cloud-openfeign github
之前的文章简单看过了 Feign 的源码,了解了 Feign 的工作原理,这次看看 Spring Cloud OpenFeign 对 Feign 做了哪些封装和扩展。
从一个简单的用例开始
/**
* 来自官网的一个例子
* @see https://docs.spring.io/spring-cloud-openfeign/docs/3.0.3/reference/html/
*/
@SpringBootApplication
@EnableFeignClients
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
/**
* 客户端模板
*/
@FeignClient("stores")
public interface StoreClient {
@RequestMapping(method = RequestMethod.GET, value = "/stores")
List<Store> getStores();
@RequestMapping(method = RequestMethod.GET, value = "/stores")
Page<Store> getStores(Pageable pageable);
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
}
这个例子中可以看到使用 @EnableFeignClients 来实现下面的 StoreClient 接口被扫描到和注册相关的bean到 spring 中。可以看到已经和之前说的 Feign 的使用方式有点差别了。
第一是注解,第二在 spring 中使用。主要看看 Spring Cloud OpenFeign 是如何集成 Feign,这部分是如何设计的。看完感觉和 mybatis-springboot 差不多的套路。
Feign 的模板接口是何时被加载并注册到 Spring 中的?
看看 @EnableFeignClients 的代码
/**
* 这里是一个套路。
* 自定义注解让 spring 去扫描的时候常用的手段,但必须要制定一个扫描的包(basePackages)
* mybatis 也用了同样的手段去扫描 mapper 接口,并注册到 spring 中。
*/
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
/**
* Alias for the {@link #basePackages()} attribute. Allows for more concise annotation
* declarations e.g.: {@code @ComponentScan("org.my.pkg")} instead of
* {@code @ComponentScan(basePackages="org.my.pkg")}.
* @return the array of 'basePackages'.
*/
String[] value() default {};
/**
* Base packages to scan for annotated components.
* <p>
* {@link #value()} is an alias for (and mutually exclusive with) this attribute.
* <p>
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based
* package names.
* @return the array of 'basePackages'.
*/
String[] basePackages() default {};
/**
* Type-safe alternative to {@link #basePackages()} for specifying the packages to
* scan for annotated components. The package of each class specified will be scanned.
* <p>
* Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute.
* @return the array of 'basePackageClasses'.
*/
Class<?>[] basePackageClasses() default {};
/**
* A custom <code>@Configuration</code> for all feign clients. Can contain override
* <code>@Bean</code> definition for the pieces that make up the client, for instance
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
*
* @see FeignClientsConfiguration for the defaults
* @return list of default configurations
*/
Class<?>[] defaultConfiguration() default {};
/**
* List of classes annotated with @FeignClient. If not empty, disables classpath
* scanning.
* @return list of FeignClient classes
*/
Class<?>[] clients() default {};
}
通过这个注解去扫描指定包下含有 @FeignClient 注解的 Feign 模板接口,当然是 Application 顶级包下的子包,不然需要指定 basePackages。
关键代码应该是 FeignClientsRegistrar.class 这个类。
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
......
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 配置 org.springframework.cloud.openfeign.FeignContext 相关的配置
registerDefaultConfiguration(metadata, registry);
// 解析 @FeignClient 并生成 feign 的模板接口实例并注册到 spring 容器中
registerFeignClients(metadata, registry);
}
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
// 向 FeignContext 中注册 defaultConfiguration
registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
}
}
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 获取 @EnableFeignClients 中配置的 feign 接口模板所在的包
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
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));
Set<String> basePackages = getBasePackages(metadata);
for (String basePackage : basePackages) {
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
}
}
else {
for (Class<?> clazz : clients) {
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
}
}
// 解析 @FeignClient 中的值
for (BeanDefinition candidateComponent : candidateComponents) {
if (candidateComponent instanceof AnnotatedBeanDefinition) {
// verify annotated class is an interface
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 = getClientName(attributes);
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 实例化(动态代理) feign 模板接口的对象,并注册到 spring 中
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
/**
* 实例化(动态代理) feign 模板接口的对象,并注册到 spring 中
* 通常的想法是 feign 使用 jdk 的动态代理获取模板接口的实例,
* 再使用 BeanDefinitionRegistry 一顿注册就好了。
* 但这里采用的方式是 org.springframework.beans.factory.FactoryBean
* mybatis 好像也是采用了相同的套路,所以有类似的需求的时候也要想到这些套路
* @param attributes @FeignClient 中的一堆值
*/
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> attributes) {
// 这里只传递 className 不就好了? 后面 annotationMetadata 都没看到被使用了。
String className = annotationMetadata.getClassName();
Class clazz = ClassUtils.resolveClassName(className, null);
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
? (ConfigurableBeanFactory) registry : null;
String contextId = getContextId(beanFactory, attributes);
String name = getName(attributes);
// 创建 FeignClientFactoryBean 并各种赋值
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
factoryBean.setBeanFactory(beanFactory);
factoryBean.setName(name);
factoryBean.setContextId(contextId);
factoryBean.setType(clazz);
factoryBean.setRefreshableClient(isClientRefreshEnabled());
// 向 spring 中注册bean
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
factoryBean.setUrl(getUrl(beanFactory, attributes));
factoryBean.setPath(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(), null));
}
Object fallbackFactory = attributes.get("fallbackFactory");
if (fallbackFactory != null) {
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
}
// 应该是这里创建 feign 的模板接口实例
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);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
registerOptionsBeanDefinition(registry, contextId);
}
/**
* 注册 feign 的相关配置
* 核心是使用了 org.springframework.cloud.context.named.NamedContextFactory。
* 内部维护了 Map<String, AnnotationConfigApplicationContext> contexts。
* 这个 map 的 key 为参数中的 name,value 是一个 ApplicationContext。
* 这样不同的 feign 客户端有着不同的配置,通过 feign 的 clientName 来区分。
* 即使通过 NamedContextFactory#getInstance() 获取的是相同class的 bean,得到的也是不同的bean。
* 当没有指定的 clientName 的 AnnotationConfigApplicationContext 时候,会从 defaultConfiguration 中获取 bean
* 这个套路在loadBalancer中也能看到。
*
* @param name @FeignClient 中的 clientName 或者 @EnableFeignClients 中的 defaultConfiguration
* @param configuration AnnotationConfigApplicationContext 注册的 class类
*/
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
builder.addConstructorArgValue(name);
builder.addConstructorArgValue(configuration);
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),
builder.getBeanDefinition());
}
......
}
通过对 @FeignClient 注解的解析获取构建 feign 的相关配置,并使用了 factorybean 来构建复杂的bean。
作为 spring cloud 的 rpc 通讯的模块,spring cloud openfeign 为了做到使每个 feign client 可以有不同的配置使用了 NamedContextFactory,这里的实现类为 org.springframework.cloud.openfeign.FeignContext,这样构成 feign 所需要的组件就可以通过 NamedContextFactory 进行隔离,不同的 clientName 获取的可以是不同的组件。当开发者不指定任何组件的时候 NamedContextFactory 可以提供一个默认的组件配置,org.springframework.cloud.openfeign.FeignClientsConfiguration。
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
......
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return springEncoder(formWriterProvider, encoderProperties, customizers);
}
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider,
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
PageableSpringEncoder encoder = new PageableSpringEncoder(
springEncoder(formWriterProvider, encoderProperties, customizers));
if (springDataWebProperties != null) {
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
encoder.setSizeParameter(springDataWebProperties.getPageable().getSizeParameter());
encoder.setSortParameter(springDataWebProperties.getSort().getSortParameter());
}
return encoder;
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
}
@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
}
@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
return Retryer.NEVER_RETRY;
}
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(logger);
}
......
}
这里提供了默认的构成 feign 的组件,其中解析 spring mvc 注解的 SpringMvcContract。
Feign Client 的构建过程
通过 org.springframework.cloud.openfeign.FeignClientFactoryBean#getObject 获取了注册到 spring 中的实例对象。
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
.......
/**
* 这里使用不同的 contextId (也就是clientName)
* 获取不同的 applicationContext,然后获取不同的配置
* 再根据这些配置创建 feign
* 这样就实现了不同的 feign 会有不同的配置的效果。
*/
protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(contextId, type);
if (instance == null) {
throw new IllegalStateException("No bean found of type " + type + " for " + contextId);
}
return instance;
}
protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(contextId, type);
}
protected <T> T getInheritedAwareOptional(FeignContext context, Class<T> type) {
if (inheritParentContext) {
return getOptional(context, type);
}
else {
return context.getInstanceWithoutAncestors(contextId, type);
}
}
@Override
public Object getObject() {
return getTarget();
}
/**
* 创建 Feign 模板接口实例
* 这里并不是 spring 容器启动的时候就执行的,
* 在之前已经设置了 definition.setLazyInit(true);
*
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
// 这里的 FeignContext 就是 NamedContextFactory 的实现
// NamedContextFactory 在创建每一个 applicationContext 的时候设置了相关的环境配置
// 在 org.springframework.core.env.MapPropertySource#MapPropertySource 中设置了
// key = feign, value = {feign.client.name = clientName} 的环境变量
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
: applicationContext.getBean(FeignContext.class);
// 开始 feign.ReflectiveFeign 使用 jdk 的动态代理创建模板接口的实例
Feign.Builder builder = feign(context);
// 如果 @FeignClient#url 没有指定值,那就认为是需要负载均衡
// 根据不同的需求创建不同的 client 实例
if (!StringUtils.hasText(url)) {
if (LOG.isInfoEnabled()) {
LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
}
if (!name.startsWith("http")) {
url = "http://" + name;
}
else {
url = name;
}
url += cleanPath();
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
}
// 如果 @FeignClient#url 指定了值,那就认为是不需要负载均衡,直接走原来的动态代理方式创建实例
if (StringUtils.hasText(url) && !url.startsWith("http")) {
url = "http://" + url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
// 到了这里的判断是不需要负载均衡的,所以不能使用 FeignBlockingLoadBalancerClient
// FeignBlockingLoadBalancerClient 有负载均衡的功能,也是对 client 实现的一个装饰
// 所以只需要 FeignBlockingLoadBalancerClient 中的这个代理就好了
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
// 同上,RetryableFeignBlockingLoadBalancerClient增加了重试的功能
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
// 这里对原生的 feign 进行了拓展,
// 因为原生的 feign 并不会使用 fallback 去处理方法调用中的各种异常
// Targeter 为 spring cloud 自定义的,默认的实现直接调用了 feign.Feign.Builder#target()
// 另外的实现就是 org.springframework.cloud.openfeign.FeignCircuitBreakerTargeter
// 也就是熔断器中才会使用这个 fallback
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}
......
}
feign clent 的构建过程和之前的差不多,就像上面说的那样 feign 的组件可以根据不同的下游服务选择不同的组件配置,但是获取的方式都是相同的 NamedContextFactory#getInstance()。而且对 rpc 调用进行了负载均衡(client 的实现类),容错策略也有了 fallback,集成到了 CircuitBreaker 当中。
简单的总结
通过自定义的 @FeignClient 注解定义 feign client 的相关信息,@EnableFeignClients 启动 feign client 的构建和注册到 spring 中,使用了 NamedContextFactory 这种机制隔离了配置。在类似的场景中提供了一种通用的实现模板(例如 mybatis-springboot)。