OpenFeign
openFeign是springcloud中,服务间进行调用的常用方式。了解它,可以更好的处理服务间调用问题。
@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
项目模块装配Feign相关。
重点关注。@Import。导入要给FeignClientsRegistrar
类。
class FeignClientsRegistrarimplements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
FeignClientsRegistrar类中实现接口ImportBeanDefinitionRegistrar
,通过实现方法registerBeanDefinitions。完成@FeignClient注解相关类的注入到ioc容器。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {registerDefaultConfiguration(metadata, registry);registerFeignClients(metadata, registry);
}
@FeignClient相关类的扫描,注入
只记录常用的方式,其他可以看源码。这里只关注通过组件扫描注入。组件扫描用到类
ClassPathScanningCandidateComponentProvider
,可以重写类的isCandidateComponent方法。完成相关组件的扫描,并封装为beanDefinition。
protected ClassPathScanningCandidateComponentProvider getScanner() {return new ClassPathScanningCandidateComponentProvider(false, this.environment) {@Overrideprotected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {boolean isCandidate = false;if (beanDefinition.getMetadata().isIndependent()) {if (!beanDefinition.getMetadata().isAnnotation()) {isCandidate = true;}}return isCandidate;}};}
在openFeign中,只是扫描过滤了,非注解等相关类。通过设置IncludeFilter
过滤,过滤类型是注解过滤FeignClient
。
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));}
扫描范围为,通过方法getBasePackages获取跟包。逻辑主要是,标注注解EnableFeignClients
的属性value,basePackages,basePackageClasses配置的内容。如果未配置,默认为当前类所在的包。我的理解是标注注解EnableFeignClients的类的包。
protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {Map<String, Object> attributes = importingClassMetadata.getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());Set<String> basePackages = new HashSet<>();for (String pkg : (String[]) attributes.get("value")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg);}}for (String pkg : (String[]) attributes.get("basePackages")) {if (StringUtils.hasText(pkg)) {basePackages.add(pkg);}}for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {basePackages.add(ClassUtils.getPackageName(clazz));}if (basePackages.isEmpty()) {basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));}return basePackages;}
扫描并返回对应类的beanDefinition后,遍历。获取注解FeignClient上的属性信息,通过BeanDefinitionBuilder构建类型是FeignClientFactoryBean
的beanDefinition。并注册到BeanDefinitionRegistry中。FeignClientFactoryBean
是一个工厂bean,可以通过getObject获取到@FeignClient注解代理的Bean。
FeignClientFactoryBean
上面介绍的,注解@FeignClient标注的类,都被封装为FeignClientFactoryBean类型的beadDefinition注册到IOC。接下来看看,通过FeignClientFactoryBean如何获取到注解@FeignClient的代理对象,完成方法的调用。
// 通过getObject获取client对象。
@Override
public Object getObject() {return getTarget();
}
<T> T getTarget() {FeignContext context = beanFactory != null? beanFactory.getBean(FeignContext.class): applicationContext.getBean(FeignContext.class);Feign.Builder builder = feign(context);// @FeignClient注解中url属性不存在的业务逻辑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 LoadBalancerFeignClient) {// not load balancing because we have a url,// but ribbon is on the classpath, so unwrapclient = ((LoadBalancerFeignClient) client).getDelegate();}if (client instanceof FeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((FeignBlockingLoadBalancerClient) client).getDelegate();}if (client instanceof RetryableFeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();}builder.client(client);}Targeter targeter = get(context, Targeter.class);return (T) targeter.target(this, builder, context,new HardCodedTarget<>(type, name, url));}
contexts,每一个@FeignClient标注的接口都有自己的应用上下文的理解(下面会用到)
通过工厂类获取目标对象的时候,首先获取FeignContext
对象。FeignContext是用于创建和管理Feign Client所依赖的各种类的工厂类
。
每个Feign Client会关联一个AnnotationConfigApplicationContext
实例,用于存取Feign Client所依赖的各种类的实例
。
(1)configurations中有根据name配置到的配置类,注册到AnnotationConfigApplicationContext。name = contextId-->value-->name-->serviceId(优先级从前到后,哪个属性不为空) + "FeignClientSpecification"
;
(2)default默认的配置类,注册到AnnotationConfigApplicationContext。默认的配置类是@EnableFeignClients注解中属性defaultConfiguration中配置的配置类
。
(3)注册PropertyPlaceholderAutoConfiguration
(4)设置parent为当前服务应用上下文
(5)刷新应用上下文
protected AnnotationConfigApplicationContext createContext(String name) {AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();if (this.configurations.containsKey(name)) {for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {context.register(configuration);}}for (Map.Entry<String, C> entry : this.configurations.entrySet()) {if (entry.getKey().startsWith("default.")) {for (Class<?> configuration : entry.getValue().getConfiguration()) {context.register(configuration);}}}context.register(PropertyPlaceholderAutoConfiguration.class,this.defaultConfigType);context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName,Collections.<String, Object>singletonMap(this.propertyName, name)));if (this.parent != null) {// Uses Environment from parent as well as beanscontext.setParent(this.parent);// jdk11 issue// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101context.setClassLoader(this.parent.getClassLoader());}context.setDisplayName(generateDisplayName(name));context.refresh();return context;}
注册AnnotationConfigApplicationContext,首先是注册服务接口的配置类
,其次是注册全局的配置类
,最后是注册默认的配置类defaultConfigType
。defaultConfigType是FeignContext类实例化默认传入的FeignClientsConfiguration.class。
每个FeignClient注解标注的接口,对应的AnnotationConfigApplicationContext属性,allowEagerClassLoading = true。也就是说后加载的允许覆盖前面加载的类。
configurations
configurations
中保存了每个Feign Client所依赖的配置类,在创建AnnotationConfigApplicationContext的过程中,这些配置类会被注入到Bean工厂中。
Feign.Builder
用于构建feign对象。
- 通过ContextId获取到对应的AnnotationConfigApplicationContext实例。获取Feign.Builder对象。
- Feign.Builder的build方法创建feign对象
ReflectiveFeign
。 - ReflectiveFeign对象的newInstance方法返回的就是一个代理对象,代理对象的InvocationHandler实现类就是ReflectiveFeign对象的内部类FeignInvocationHandler。
@FeignClient中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 LoadBalancerFeignClient) {// not load balancing because we have a url,// but ribbon is on the classpath, so unwrapclient = ((LoadBalancerFeignClient) client).getDelegate();}if (client instanceof FeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((FeignBlockingLoadBalancerClient) client).getDelegate();}if (client instanceof RetryableFeignBlockingLoadBalancerClient) {// not load balancing because we have a url,// but Spring Cloud LoadBalancer is on the classpath, so unwrapclient = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();}builder.client(client);}Targeter targeter = get(context, Targeter.class);return (T) targeter.target(this, builder, context,new HardCodedTarget<>(type, name, url));
拼接请求路径。
通过url,path拼接路径。lg:http://… + /path;
获取client
通过方法getOptional获取client。
Client client = getOptional(context, Client.class);
- 通过FeignContext的getInstance方法,获取到contextId对应的应用上下文applicationContext。
- 在这个应用上下文中获取到类型为Client.class的bean对象。并返回对象。
- 根据client所实现的接口类型,转换为对应的类型。
- 相同的方式,通过contextId获取类型为Targeter.class的bean对象。
- 调用Targeter的target方法,默认是调用Feign.build的build()方法创建
ReflectiveFeign
对象,调用对象的newInstance
方法,创建代理对象。返回代理@FeignClient标注接口的代理对象。
方法执行,调用代理对象ReflectiveFeign中的内部类FeignInvocationHandler的invoke方法。根据Method找到对应的MethodHandler。默认实现是SynchronousMethodHandler。
- 方法之前前,通过RequestInterceptor对请求进行拦截处理。
- 调用client.execute方法,请求调用。
- 结果封装
@FeignClient中url属性值:不存在
@FeignClient中url为空,则url默认 = http:// + name + path
;
通过loadBalance方法构建请求代理对象。loadBalance。
loadBalance
- 通过FeignContext,获取到当前contextId对应的applicationContext。
- 通过applicationContext获取类型为client.class的实现类。
- 通过Feign.build设置client为通过applicationContext获取到的client。
- 通过applicationContext获取类型为Targeter.class的实现类。
- 调用Targeter.target方法获取到代理对象。
配置文件
feign: client: defaultToProperties: false # 是否以配置文件中配置为主。默认为trueconfig: #对应FeignClientProperties类的config成员变量default: # 日志级别logger-level: BASIC# 超时时间connect-timeout: 10000
将config下的配置封装为config = Map<String,FeignClientConfiguration>。FeignClientConfiguration封装上面的配置。lg:日志级别,超时时长。Map中的key为@FeignClient注解属性contextId。默认为default,也可以为没有给Feign接口配置对应的参数。
配置文件中的参数优先级别。feign接口级别的配置 --> 默认配置
请求参数体封装
入口在ReflectiveFeign的newInstance方法。
ParseHandlersByName
ParseHandlersByName是Feign.build的build()方法的时候构建的。
openFeign中的contract默认是SpringmvcContract,支持mvc注解的解析。
ParseHandlersByName.apply方法。调用contract的parseAndValidateMetadata方法。解析获取到Feign接口的methodMetaData集合。然后遍历方法的源数据,构造Map对象,key:feign的接口名 + "#" + 方法名 + "(" + parameterTypeName集合 + ")"
。
value:SynchronousMethodHandler对象。对象里面包含MethodMetadata对象。
我们知道,Feign接口代理的执行,最终会调用SynchronousMethodHandler.invoke方法。SynchronousMethodHandler对象中保存了client对象和方法的源信息。就可以进行业务的调用。
SpringMvcFeignContract
SpringMvcFeignContract继承SpringMvcContract。遍历Feign接口中的Method,调用Contract的parseAndValidateMetadata方法,解析封装为MethodMetadata。遍历MethodMetadata,对template进行处理。最下面,将 buildTemplate传给SynchronousMethodHandler对象。
方法执行时,调用SynchronousMethodHandler的invoke方法,invoke方法中调用buildTemplate的create方法,根据入参构建RequestTemplate对象。
调用executeAndDecodej进行请求调用和编码处理。
(1)获取RequestInteceptor,对template进行请求前拦截。
(2)通过调用client.execute(request, options),发出请求。