feign 是cloud 体系中除了网关、注册、配置中心之外的,最基础的大三件之一,它的使用场景就是各个微服务之间的相互调用,而openFeign 是对feign 一种封装后的产品,它比feign 更加迎合市场,所以目前大部分企业使用的也是openFeign,但是它的本质还是feign,所以后面看源码的时候,不要纠结两者的区别,它们本质是一个东西。
上述也说了feign 和openFeign 的本质都是前者,它们的使用场景也都是:微服务之间的相互调用。它们是将对服务的调用转换成了对本地接口的调用,同时内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。
feign 的使用方式是:使用feign 的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。
feign 本身不支持Spring MVC的注解,它有一套自己的注解,所以相应的使用它会增加一定的学习成本,导致它后面也慢慢的被市场所淘汰,当然也不是全面退出了市场,目前也有一些企业在使用这个,可以它的[官方api 文档](https://github.com/OpenFeig n/feign) 进行学习。
OpenFeign 是Spring Cloud 在Feign 的基础上支持了Spring MVC 的注解,如@RequesMapping 等,是一个轻量级的Http 封装工具对象,大大简化了Http 请求,使得我们对服务的调用转换成了对本地接口方法的调用。
一些概念性的知识,也就是八股文之类的就不介绍了,面试有需求的还是后面自己网上搜吧,我实在编不下去了,我们下面直接看代码。
首先要使用openFeign 还是先引入依赖,因为这里后面我们看源码的时候,源码版本是2.2.1 的,所以我引入的也是2.2.1 的starter。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
<version>2.2.1.RELEASEversion>
dependency>
然后需要有一个@FeignClient
注解修饰对象,这里编写了调用对于服务端暴露出来的api 服务。
注意啊:这个对象最好不要放在本身的微服务项目中,比如下面这个,就最好不要放在hailtaxi-driver 微服务中,而是提供一个新的微服务用于中间转发调用,因为这个对象所在的微服务是需要被客户端引入的。
@FeignClient(value = "hailtaxi-driver") // value = 项目名称
public interface ServiceFeignClientApi {
@RequestMapping(value = "/driver/info/{id}")
Driver info(@PathVariable(value = "id")String id);
}
上面也说了,客户端需要引入被@FeignClient
修饰对象所在的微服务,所以这里直接先修改客户端的pom 文件。
<dependency>
<groupId>com.itheimagroupId>
<artifactId>hailtaxi-apiartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
到了这一步,其实就已经可以编写客户端的调用接口了。
@RequestMapping(value = "/order")
public class OrderInfoController {
@Autowired
private ServiceFeignClientApi service;
@PostMapping
public OrderInfo add(){
service.info("1");
return new OrderInfo();
}
}
如果使用的是idea,上面service 对象会有一个标注提示,因为我们还没有声明这是一个feign 的服务,最后一步就是需要在客户端的启动类上添加一个注释修饰@EnableFeignClients
。basePackages 的值是上面service 接口对象所在的项目包。
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.itheima.driver.feign")
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class,args);
}
}
这样一个完整的openFeign 使用配置流程就结束了,可以说非常简单,所有的复杂内容全部都已经被框架包装了,我们使用非常简单,可以说跟gateway 的使用一样简单。
小结:openFeign 的使用主要就是两个注解,一个被调用微服务的@FeignClient
(注意:这里不是指服务端的微服务),另一个是客户端启动类上的@EnableFeignClients
,有这两个注解之后,服务之间的调用才会交给openFeign 来完成。
还有一点啊,这两个注解都是openFeign 提供的,但是后面看源码的时候会发现,真正的底层调用还是feign 的源码。
如果对于这种服务之间调用有过研究的话,比如dubbo 框架。其实看源码的时候就是分为两个部分:
我们这里看openFeign 的调用流程源码也是分两个部分,客户端启动的时候、客户端调用接口的时候。
上面描写使用的时候,就已经将版本限定在了2.2.1,所以这里就不用修改pom 文件了。
然后将openFeign 的2.2.1 版本源码下载 下来,我这里的是有点注释的,不需要的话,可以直接去官网下载。
我这里使用是idea,所以是找到项目的settings,然后通过modules 引入下载下来的项目即可,这里跟gateway 的引入是一样的。
还有一点需要注意,这里要使用2.2.1 版本的话,cloud 的版本也需要修改为Hoxton.SR1
版本的。
开始之前有点需要说明,这里涉及到spring boot 的内容,如果spring boot 自动配置的源码流程没有研究过的话,建议先去研究一下,回头在看这个,也可以看看我对于spring boot 的源码解析的文章。
spring boot 的内容这里就简单过,主要看openFeign 的逻辑。既然是自动配置,那么我们直接去看@EnableFeignClients
注解。
这里可以看到重点了,@Import
注解修饰,直接跟进FeignClientsRegistrar 对象的registerBeanDefinitions 方法。
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class) // 开启了 `FeignClient`扫描
public @interface EnableFeignClients
这里分两步,框架的一些配置注册,还有就是注册被@FeignClient
注解修饰的接口。注意这里并不是将信息注入IOC 的单例池中,而是构建出对应的BeanDefinition 对象,然后存入到beanFactory 中的BeanDefinitionMap 集合中。
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
// 完成 Feign 框架相关的配置注册
registerDefaultConfiguration(metadata, registry);
// 注册由 @FeignClient 修饰的接口 bean *****核心*****
registerFeignClients(metadata, registry);
}
分开看,首先是registerDefaultConfiguration
方法中,不用深究,这里就是构建了一个beanDefinition 存入了beanFactory 中,具体对象是FeignClientSpecification
。
重点看registerFeignClients
方法,这里代码太多,我们分段看重点。
首先第一步获取到@EnableFeignClients
注解的扫描路径,也就是EnableFeignClients 注解中的value 或basePackages 之类属性的值。
// 获取 @EnableFeignClients中配置的 @FeignClient 接口扫描路径
basePackages = getBasePackages(metadata);
获取到之后重点来了,这里没有的话,就算不是openFeign 的内容,就没有后面的内容了。
@FeignClient
注解的所有属性信息拿到;registerClientConfiguration
方法,根据上面拿到name 属性再次构建对应的一个FeignClientSpecification
对象的beanDefinition 并存入了beanFactory;registerFeignClient
方法,将接口上的所有方法,和@FeignClient
注解的信息都传入,然后分别去构建对应的代理对象的beanDefinition 并存入了beanFactory。// 拿到 @EnableFeignClients 中配置的 @FeignClient 接口扫描路径 后开始 扫描
for (String basePackage : basePackages) {
// 查找 basePackage 包路径下所有 由 @FeignClient 修饰的候选bean,返回其 BeanDefinition 的集合
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
// 针对每个标注了 @FeignClient 的候选 BeanDefinition (接口的BeanDefinition) 准备向容器中注册
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");// @FeignClient标注的必须是接口
// 获取 @FeignClient 注解的相关属性信息
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
// 获取@FeignClient(value = "hailtaxi-driver"),name属性,name属性和value属性是相同的含义,都是配置服务名
String name = getClientName(attributes);// name = hailtaxi-driver
registerClientConfiguration(registry, name, attributes.get("configuration"));
// 针对当前标注了 @FeignClient 注解的候选接口 BeanDefinition 向容器中注册bean信息
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
这里我们继续跟进registerFeignClient
方法,这里其实就做了一件事情,将传进来的方法构建成一个FeignClientFactoryBean 对象的beanDefinition 然后存入beanFactory 中,我们关注三行代码就行了。
开头第二行代码,构建一个FeignClientFactoryBean 类型的BeanDefinitionBuilder 对象,来完成后续构建BeanDefinition 的前提。
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
倒数第二行就是将构建完成的BeanDefinition 封装成一个BeanDeinitionHodler。
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,new String[] { alias });
最后就是通过BeanDefinitionReaderUtils 来存入到beanFactory。
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
小结:到这里@EnableFeignClients
注解的解析就结束了,后续就是开始构建bean 然后存入IOC 的单例池中了。
这里又涉及到了spring IOC 的一些内容,还是老话,没有研究过这个的,还是先去研究一下再来看这个,IOC 的东西我也详细写过文章,也可以直接去看我的。
这里我们就不一步一步跟了,直接去看doCerateBean 的内容,注意这里的调用时机:是在客户端中调用openFeign 接口的对象,注入IOC 的时候,我这里就是上面使用过程中涉及到的OrderInfoController
对象注入IOC 的时候触发的。
也不用看其他的,直接去看初始化过程中给OrderInfoController
对象中的接口进行注入的时候。也就是doCreateBean
方法中调用初始化属性赋值的时候,也就是populateBean
方法的调用。
上述内容中,我们可以知道所有被@FeignClient
注解修饰的接口构建成BeanDefinition 的时候,都是将FeignClientFactoryBean 作为了指向对应对象,因为它就是FactoryBean 对象,所以直接去拿对应的FeignClientFactoryBean
的getObject 方法。
跟进FeignClientFactoryBean 中的getObject 方法,直接就是return getTarget()
这里是调用getTarget 方法进行返回。这个里面也不用关心别的,直接去看loadBalance
方法。
这里可以看到抛开其他的内容,这里主要就是去用HystrixTargeter
对象来生成代理。
Client client = getOptional(context, Client.class);
if (client != null) {
// 将 feign 的 Client 对象设置进 Feign.Builder
builder.client(client);
// Targeter默认是 HystrixTargeter 在 FeignAutoConfiguration 中有配置
Targeter targeter = get(context, Targeter.class);
/**
* 实例创建(开启熔断后具有熔断降级效果)
* this= FeignClientFactoryBean
* builder= Feign$Builder
* context = FeignContext
* target = HardCodedTarget
*/
return targeter.target(this, builder, context, target);
}
直接看target 方法。这里说的就是默认的是Feign 的代理生成,也就是生成的FeignInvocationHandler 代理对象,但是如果在配置中开启了hystrix,那么就会生成一个HystrixInvocationHandler 代理对象。
**小结:**这样就是将FeignInvocationHandler 或者HystrixInvocationHandler 代理对象注入到了OrderInfoController 对象中的接口属性中,后续在调用接口的时候,直接走到这两个其中一个的invoke 方法中即可。
上面在启动完成之后,我们直接就可以看调用部分了,也就是FeignInvocationHandler 或者HystrixInvocationHandler 代理对象的invoke 方法,我们这里只分析HystrixInvocationHandler 代理对象的,因为它们两者其实没有什么太大的区别,主要是后者进行了负载和熔断等机制的操作。
跟进invoke 方法,代码首先是剔除走进来的一些通用调用,比如equals
、hashCode
、toString
之类的。
重点是new HystrixCommand
的代码调用。这里可以直接跟进invoke。
new HystrixCommand<Object>(setterMethodMap.get(method)) {
@Override
protected Object run() throws Exception {
try {
return HystrixInvocationHandler.this.dispatch.get(method).invoke(args);
} catch (Exception e) {
throw e;
} catch (Throwable t) {
throw (Error) t;
}
}
}
后续会走到SynchronousMethodHandler 对象的invoke 方法。然后就是executeAndDecode(template, options)
的调用。之后继续是client.execute(request, options);
调用。
最后会走到LoadBalancerFeignClient 对象的execute 方法。
到了这一步基本是已经走到最后了,再往下跟就是到了底层将请求封装调用发送的地方,后面就可以不用看了,主要就看这里。
URI 和URL 的封装就不用看了,大体跟gateway 的差不了太多,然后就是获取RibbonRequest 对象,用于后续的负载调用。
最后就是通过FeignLoadBalancer 对象的executeWithLoadBalancer 方法调用后续发送请求的逻辑。
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());// http://hailtaxi-driver/driver/status/1/2
String clientName = asUri.getHost();// hailtaxi-driver
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);//各种连接参数
// 开始进行负载均衡调用
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
小结:上述基本就是简单的介绍完了客户端调用的全流程,就是通过注入IOC 的代理对象,然后封装解析请求信息,然后获取负载对象完善URL,最后用底层逻辑通过http 请求调用到别的服务。
服务之间调用的组件在cloud 体系中是不可缺少的,而openFeign 的使用相对来可以说是非常简单,结合spring boot 体系也能做到快速开发,但是它也不是不可替代的,其它组件比如:dubbo、feign 之类的,甚至可以自己去开发一个类似的框架,怎么选择还是。