前言:
feign是springCloud的一个重要组件,目前的微服务项目基本都是以feign作为服务间调用组件,它是一个声明式的rest客户端。
本文中的示例均基于一个简单的微服务demo,创建了两个springboot项目,并在nacos上进行了注册,结构如下:
一、feign使用
1.使用
1. 引入依赖
openfeign是需要引入的主依赖,我们使用feign时候一般和注册中心一起使用做自动负载均衡,所以我们也引入loadbalancer
<!-- openfeign 如果要做自动负载均衡,则还需要需要 loadbalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- springCloud 提供的一个负载均衡器 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId> spring-cloud-starter-loadbalancer</artifactId>
</dependency>
2.在springboot项目的启动类上加上注解 @EnableFeignClients
@SpringBootApplication
@EnableFeignClients
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
3.编写feign接口,指向要调用的远端服务
step1.编写一个Intetface,并加上@FeignClient注解
step2.接口中定义方法,方法使用MVC的注解,get/put/post/delete,路径注意和被调用服务的接口的访问路径一直
**注意** 如果要做负载均衡,一般会在@FeignClient的value中方被调用服务的注册名,比如我的示例中,buness-server服务在nacos中的注册名称叫 rui-buness,所以@FeignClien的value值就要写成@FeignClient(value = "rui-buness")
4.通过feign调用其他服务
直接像调用普通的service中的方法一样使用feign接口调用远端服务即可。如下图
2.结合nacos实现负载均衡
工作中最常用的负载均衡方式就是权重负载均衡,feign结合loadbalance及nacos来实现。
a.首先,打开loadbalance的nacos权重负载均衡开关,配置如下:
spring:
cloud:
loadbalancer:
nacos:
enabled: true
b.然后,在nacos的控制端界面,打开服务列表,修改节点权重。操作方法如下:
c.最后,在项目中通过feign正常调用即可。效果如下图
二.feign的日志配置
1.日志使用:
在配置文件中加入如下配置
# 修改remote目录下的日志级别
logging:
level:
com.rui.remote: debug
# feign日志
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
2.配置讲解:
feign的日志输入级别为debug,所以要看到fengn的日志,首先需要将feign调用的日志level调整到debug
feign自身的日志有 四个级别,分别为
1)None:不显示日志,默认是这个级别
2)Basic:只显示请求的mothed,url以及响应的状态码还有执行事件
3)Headers:打印请求和相应的请求头的基本信息
4)Full:打印请求和响应的header和body还有元数据
三.feign的拦截器的使用
1.为何要使用feign的拦截器
在工作中,我们的服务都会使用secutity做鉴权,security会校验请求的token,如果没有token的话,请求会被拦截无法执行。
所以,为了解决这个问题,一般使用两种方案。
方案1.发起feign调用的服务增加一个feign的拦截器,在feign调用请求发出之前,在请求的header中塞入当前的token,这样请求到达另一个服务时候,就会正常校验token了。
方案2.发起feign调用的服务增加一个feign的拦截器,在feign调用请求发出之前,在请求的header中塞入一个识别标识,在被调用服务中,也增加一个拦截,且该拦截的优先级需要高于security的拦截,然后在该拦截中验证,请求是否带有我们自定义的标识,如果有则放行。该方案一般还会配合自定义注解,比如定义一个注解@inner,将这个注解放在controller上,然后我们的放行过滤器只对访问有inner注解的资源放行,没有该注解的,即使请求头有我们自定义的标识也不放行。
2.如何使用feign的拦截器
我们刚聊了使用feign拦截器的场景,这里就以上文中提到的方案1作为例子,展示一下如何通过feign的拦截器在请求头中塞入token。
step1。自定义拦截器,实现RequestInterceptor接口
public class RuiFeignRequestInterceptor implements RequestInterceptor
{
/**
* apply方法是RequestInterceptor接口需要实现的方法
* 其参数requestTemplate代表feign调用的请求
* @param requestTemplate
*/
@Override
public void apply(RequestTemplate requestTemplate) {
// 从当前请求中拿到请求头
HttpServletRequest httpServletRequest = ((ServletRequestAttributes)RequestContextHolder.getRequestAttributes()).getRequest();
if (!StringUtils.isEmpty(httpServletRequest))
{
Map<String, String> headers = getHeaders(httpServletRequest);
// 在请求头中拿到 authorization
String authentication = headers.get(SecurityConstants.AUTHORIZATION_HEADER);
if (!StringUtils.isEmpty(authentication))
{
// 将authorization塞入feign调用请求头
requestTemplate.header(SecurityConstants.AUTHORIZATION_HEADER, authentication);
}
}
}
/**
* 工具方法,用于获取请求头
*/
public static Map<String, String> getHeaders(HttpServletRequest request)
{
Map<String, String> map = new LinkedCaseInsensitiveMap<>();
Enumeration<String> enumeration = request.getHeaderNames();
if (enumeration != null)
{
while (enumeration.hasMoreElements())
{
String key = enumeration.nextElement();
String value = request.getHeader(key);
map.put(key, value);
}
}
return map;
}
}
step2.将刚才定义的FeignRequestInterceptor 交给spring容器
/**
* Feign 配置注册
**/
@Configuration
public class MyFeignConfiguration
{
@Bean
public RequestInterceptor requestInterceptor()
{
return new RuiFeignRequestInterceptor();
}
}
step3.在启动类的@EnableFeignClients注解上指定默认配置文件
@SpringBootApplication
// 这里指定feign的配置文件
@EnableFeignClients(defaultConfiguration = MyFeignConfiguration.class)
public class AdminServerApplication {
public static void main(String[] args) {
SpringApplication.run(AdminServerApplication.class, args);
}
}
step4.测试
postMan中,我们发送请求,带上一个token
调用方查看日志,可以看到,请求头中已经带上了token
四.feign的优化
1.okhttp介绍
okhttp是一套高性能的处理Http网络的框架,对于安卓开发来说,RetroFit + OkHttp几乎是一套标配。很多公司在feign的使用中,也会选择okhttp作为作为连接池。
2.使用okhttp
1>引入依赖
<!-- okhttp 是一个处理网络请求的框架,用于替换feign默认的框架 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
2>配置文件中增加配置
# okhtp
feign:
okhttp:
enabled: true
通过以上两步,就可以将feign的默认http框架替换成okhttp
五.feign的原理
个人觉得,关于每一个组件的具体原理,并不需要大家花太大的代价去了解,尤其feign这种本来就不是很复杂的组件,使用时候基本就能猜测出原理,面试中其实面试官更多想要的也是面试者对技术的理解,而不是必须详细的知道技术的细节,毕竟技术的实现迭代很快,但是技术的思维是一直受用的。
1.feign原理概述
对于feign的原理,其实在不清楚的情况下可以做一个大致的猜测。
使用feign时候,我们其实做了两件事,一件事是在config中对feignclient进行配置,一件是自己定义了interface接口声明远程调用。所以可以推测,feign的实现肯定也会做两件事,分别是,根据我们的config来创建对应的feignClient;找到我们的interface,根据interface创建对应的动态代理对象;在调用feignService时候,实际是调用对应代理对象的invoke,然后代理对象中一定会去解析url,做负载均衡。
不看源码仅通过猜测我们就可以得到以上推断,而实际上,feign的实现也是这么做的,是不是每一个步骤更复杂。不想浪费时间看源码的小伙伴现在就可以关浏览器了,想了解一下源码具体实现的小伙伴继续往下看。
2.feign源码
1.首先,@EnableFeignClients注解中写了@Import(FeignClientsRegistrar.class),所以我们知道自动装配会装配FeignClientsRegistrar。
2.FeignClientsRegistrar类实现了ImportBeanDefinitionRegistrar,所以服务启动时候会进入registerBeanDefinitions方法
3.该方法做了两件事,registerDefaultConfiguration,registerFeignClients。具体点进这俩方法。
registerDefaultConfiguration做的事情是拿到FeginClientConfig上EnableClients的所有注解属性,从中获取defalultConfiguration属性,并注册到BeanDefinitionMap中。
registerFeignClients会有拿取client属性,如果clients中有我们定义的FeginClient,我们会将其添加到我们的candidateComponents(BeanDeifition集合中),如果没有,则会创建一个扫描器,去扫描我们basePackage指定的包路径下添加了@FeginClient注解的类,并添加到我们的BeanDefinition中。
4.细看一下registerFeignClients
看到这个factoryBean,它是由入参的的BeanDefinitionregistry类型的registry转换成我们的ConfigurableBeanFactory类,然后获取contextId和name,type和isEnable,BeanDefinitionBuilder.genericBeanDefinition会去注册我们的BeanDenition,在这里会获取我们的url属性和path属性,这个就是我们实际调用的http地址了。同时去设置我们的fallback和fallbackFactory属性。完成属性填充后,通过Spring提供的registerBeanDefinition方法向BeanDefinitionRegistry注册了刚实例化的这个BeanDefinitionHolder。这里完成的是将FeignClient注解的类的信息交给工厂bean代理类,并将代理类的定义注册到Spring的容器中。至此,已经把要创建的接口代理对象的信息放入registry里面,之后spring在启动调用refresh方法的时候会负责bean的实例化。在实例化过程中,调用FeignClientFactoryBean的getObject方法
5.spring调用上图中的回调函数,进入getObject,最终走到ReflectiveFeign.newInstance()方法,这一段的代码和调用栈如图
6.至此,代理对象实例被创建。
7.当代码调用feignService时候,其实是调用代理对象
第一个红色框体中会做路径替换等操作
第二个红框executeAndDecode方法,这里把请求交给了之前创建的LoadBalancerFeignClient,执行了它的execute方法
以上就是feign源码的大体流程。其中一些handler的细节,encoder,decoder编码的细节没有详细说,大家可以根据上面的截图自己debug看一下。