Spring Cloud OpenFeign 源码随便看看

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)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值