OpenFeign
openFeign是springcloud中,服务间进行调用的常用方式。了解它,可以更好的处理服务间调用问题。
@EnableFeignClients
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
项目模块装配Feign相关。
重点关注。@Import。导入要给FeignClientsRegistrar
类。
class FeignClientsRegistrar
implements 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) {
@Override
protected 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 unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
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
client = ((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 beans
context.setParent(this.parent);
// jdk11 issue
// https://github.com/spring-cloud/spring-cloud-netflix/issues/3101
context.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 unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
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
client = ((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 # 是否以配置文件中配置为主。默认为true
config: #对应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),发出请求。