openFeign这个框架要解决的问题是:通常在调用远程接口的时候,如果是http请求,需要我们通过restTemplate去拼接调用参数和连接,然后发起调用,openFeign帮我们把拼接参数的这个过程包装了起来,通过代理对象的模式,帮我们构建http请求,而我们在使用的时候,就向写mybatis中的mapper接口一样,即可
openFeign的源码,看起来是比较简单的,如果有看过spring的源码,看起来就会快很多
1.使用
在使用的时候
引入jar包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
然后在启动类上,加上@EnableFeignClients注解
最后,在我们定义的接口上,加上@FeignClient注解
@FeignClient注解所要修饰的接口,是我们在自己的应用中定义的,需要和调用的远程接口定义保持一模一样
2.源码
我们从启动类上的注解开始来看,通常在这个注解中,会有一些逻辑的处理
我们可以看到,FeignClientsRegistrar是ImportBeanDefinitionRegistrar的实现类,ImportBeanDefinitionRegistrar是spring提供的扩展点之一,我们可以在其实现方法中,自己注入对象到beanDefinitionMap集合中
2.1 registerDefaultConfiguration
可以看到,在实现方法中,有两个方法,registerDefaultConfiguration这个方法的逻辑看起来比较简单,就是根据@EnableFeignClients注解,build了一个beanDefinition对象,然后注入到了beanDefinitionMap集合中
2.2 registerFeignClients
我们需要关注的是第二个方法:registerFeignClients(),
public void registerFeignClients(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
// 1.初始化一个扫描器,是为了去指定的包下,扫描指定的bean
ClassPathScanningCandidateComponentProvider scanner = getScanner();
scanner.setResourceLoader(this.resourceLoader);
Set<String> basePackages;
Map<String, Object> attrs = metadata
.getAnnotationAttributes(EnableFeignClients.class.getName());
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
FeignClient.class);
final Class<?>[] clients = attrs == null ? null
: (Class<?>[]) attrs.get("clients");
//如果@Enable注解中没有指定client,会走这里的逻辑
if (clients == null || clients.length == 0) {
// 指定scanner扫描时,需要处理的类
scanner.addIncludeFilter(annotationTypeFilter);
// 获取要扫描的包
basePackages = getBasePackages(metadata);
}
else {
final Set<String> clientClasses = new HashSet<>();
basePackages = new HashSet<>();
for (Class<?> clazz : clients) {
basePackages.add(ClassUtils.getPackageName(clazz));
clientClasses.add(clazz.getCanonicalName());
}
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
@Override
protected boolean match(ClassMetadata metadata) {
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
return clientClasses.contains(cleaned);
}
};
scanner.addIncludeFilter(
new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
}
// 依次遍历要扫描的包,然后扫描指定的bean,在这里,就是加了@FeignClients注解的对象
for (String basePackage : basePackages) {
// 这里就是扫描bean的方法,和spring的扫描bean的方法极其一样
Set<BeanDefinition> candidateComponents = scanner
.findCandidateComponents(basePackage);
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"));
// 中间是解析@FeignClients注解的属性信息,然后会调用这里的register方法进行注册
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
可以看到,上面的方法其实就是去解析在指定包下,加了@FeignClients注解的class文件,解析了之后,会调用registerFeignClient方法进行bean的注册
在注册的方法中,有一个方法特别重要,
BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
这里会看到,会把beanDefinition的beanClass对象设置为入参的beanClass,也就是FeignClientFactoryBean.class
这里的FeignClientFactoryBean对象极其重要,所有的逻辑都在这个bean中
如果有了解过mybatis和spring整合源码的,就会知道,是通过了一个factoryBean来实现的;这里也一样,也是通过factoryBean来实现的
截止到此,应用启动时,openFeign所要做的事情,已经结束了,简单来说,在启动的时候,会把加了@FeginClients注解的class文件,转换成beanDefinition对象,并把beanDefinition的beanClass设置为FeignClientFactoryBean,然后把beanDefinition对象放入到beanDefinitionMap集合中
做完这些之后,在spring源码启动的时候,会从beanDefinitionMap中,依次取出beanDefinition进行初始化,在初始化的时候,发现当前bean是factoryBean的子类,就会尝试调用实现类的getObject()方法获取要注入的对象,所以,真正的进行动态代理,或者说,openFeign真正起到核心左右的点,就在factoryBean的getObject()方法
3.FeignClientFactoryBean(重要)
这个getObject()方法是在进行属性注入的时候,调用的;所以核心代码在getTarget()方法中
在getTarget()方法中,大致有两个分支逻辑,分别是:指定了微服务名和使用url两个分支
针对两个分支,中间的处理逻辑不太一样,因为如果使用的是微服务名的话,需要在集成负载均衡的框架,来做ip和端口的选择;这是使用微服务时,接下来的代码中多出来的代码;如果使用的是url,直接指定了ip和端口,那就不需要由负载均衡这个过程
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
// 这里的if 是一个分支,这个分支,如果我们在@FeignClients注解中,没有指定url,那就使用name(这个name,需要配置为微服务的名字)
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
// 最核心的在这里,会根据微服务名称进行负载均衡,然后发起调用
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//走到这里的是另外一个分支,指定了url,url里面已经知道了具体的ip和端口
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.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();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
// 最核心的是在这里的target()
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
3.1 使用url,直接指定ip和端口
我们来看下如果指定了ip和端口的时候,相关的处理逻辑:targeter.target()(使用微服务,通过负载均衡的方式,最后也会调用这个方法)
targeter有两个实现类,hystrixTargeter比defaultTargeter多了一个降级的处理,是由一个配置来决定调用哪个的,我们先忽略这两个区别点,我们直接看默认的defaultTargeter实现类:
feign.Feign.Builder#target(feign.Target)
我们可以看到,在build()方法中,最后是返回了ReflectiveFeign对象,所以我们要看下这个对象的newInstance()方法干了什么
feign.ReflectiveFeign#newInstance
这里我们可以看到,最后在通过Proxy.newProxyInstance()的时候,指定的handler是通过factory.create()创建的,因为最后代理对象在被调用的时候,调用的就是handler的invoke方法,所以我们要看下这个handler初始化的逻辑
根据上面截图可以看到,最后就是初始化了一个对象:
feign.ReflectiveFeign.FeignInvocationHandler#FeignInvocationHandler
这里暂时,不再往下继续延伸,后面再介绍,我们只需要知道,这里返回了一个FeignInvovationHandler对象作为代理对象生成时的handler对象
3.2 如果使用微服务名,处理逻辑
我们接着来看,如果使用的是微服务名称,这里的逻辑有什么区别:根据上面target()方法来看,如果使用的是微服务名,会进入到loadBalance()这个分支,进行处理;可以看到,这里也是调用的targeter.target()方法,所以,我们可以看到,接下里的逻辑和3.1部分一模一样的
根据上面的论述,可以看到,最后是通过newInstance()初始化了一个对象,我们先知道,这里会返回一个对象,这个对象其实是一个代理对象,因为看newInstance()方法,就知道,最后是Proxy.newProxyInstance()方法来初始化的
所以,截止到此,我们知道,不管是使用微服务名称进行动态负载均衡,还是直接指定url,最后底层返回的都是一个代理对象
3.3 SynchronousMethodHandler
接下来要单独说一个类,这个类是极其重要的类,在前面3.1 newInstance()方法中,调用factory.create()方法的时候,入参了一个methodToHandler对象,这个对象极其重要;我们先看 在for循环中,如果走到else的逻辑中,会通过targetToHandlersByName.apply(target)方法返回的map集合中获取到methodHandler
这里可以看到,在create的时候,入参的methodHandler,实际上,入参到了feignInvocationHandler对象中的dispatch属性上,这里属性特别特别重要
我们接着来看nameToHandler是怎么来的
通过上面的逻辑,我们看到,在newInstance()方法中,如果满足Util.isDefault(method),使用的是defaultMethodHandler,如果不满足,使用的是SynchronousMethodHandler
这里我们需要关注的是SynchronousMethodHandler
一定一定要记住一个点,feignInvocationHandler对象的dispatch属性,就是一个map集合,这个map集合中的value是SynchronousMethodHandler
上面这句话一定一定要理解,多看几遍3.3 这部分,一定要理解了这一句话之后,再看第4部分
3.3 这部分先有个印象,在后面会用到
4.在方法被调用时
接下来的这部分,是目标方法在被调用时,代理对象是如何处理的
我们知道, 前面返回的代理对象,最后使用的invocationHandler是FeignInvocationHandler,所以,在目标方法被调用的时候,是会调用到FeignInvocationHandler.invoke()方法
这里的dispatch对象,前面3.3 说了好几遍,不做过多的解释了,这里get()获取到的是SynchronousMethodHandler,所以,我们直接来看看SynchronousMethodHandler的方法
最重要的,是这里的client对象,如果是使用了url地址,就是直接指定了ip和端口,那这里的client对象是default
如果使用的是微服务名,那这里的client对象是LoadBalancerClient对象,这个点我暂时还没有看到赋值的地方,后面再细看
4.1 Default.execute()
4.2 LoadBalancerFeignClient.execute()
这里调用的逻辑,不再细贴代码了,底层就是通过ribbon负载均衡去选取一个ip和端口,然后发起调用,这个其实就和ribbon的代码差不多
总结
所以,openFeign其实底层,就是通过代理对象的方式,帮我们去发起远程调用,在代理对象中,会分别区分使用微服务名进行负载均衡的逻辑和程序员直接指定ip、端口的方式,本质上没太大的区别,只是一个集成了ribbon,一个直接发起调用