Feign远程调用组件
0、现有调用方式存在问题
//http://localhost:8090/autodeliver/checkState/1545133
@GetMapping("/checkState/{userId}")
public Integer findResumeOpenState(@PathVariable Long userId){
String url="http://lagou-service-resume/resume/openstate/" + userId;
Integer forObject = restTemplate.getForObject(url ,Integer.class);
return forObject;
}
存在不便之处
- 1)拼接url
- 2)restTmplate.getForObJect
这两处代码都⽐较模板化,能不能不让我我们来写这种模板化的东⻄ 另外来说,拼接url⾮常的low,拼接字符串,拼接参数,很low还容易出错
1、Feign简介
Feign是Netflix开发的⼀个轻量级RESTful的HTTP服务客户端(⽤它来发起请求,远程调⽤的),是以Java接⼝注解的⽅式调⽤Http请求,⽽不⽤像Java中通过封装HTTP请求报⽂的⽅式直接调⽤,Feign被⼴泛应⽤在Spring Cloud 的解决⽅案中。类似于Dubbo,服务消费者拿到服务提供者的接⼝,然后像调⽤本地接⼝⽅法⼀样去调⽤,实际发出的是远程的请求。
-
Feign可帮助我们更加便捷,优雅的调⽤HTTP API:不需要我们去拼接url然后呢调⽤restTemplate的api,在SpringCloud中,使⽤Feign⾮常简单,创建⼀个接⼝(在消费者--服务调⽤⽅这⼀端),并在接⼝上添加⼀些注解,代码就完成了
-
SpringCloud对Feign进⾏了增强,使Feign⽀持了SpringMVC注解(OpenFeign)
本质:封装了Http调⽤流程,更符合⾯向接⼝化的编程习惯,类似于Dubbo的服务调⽤
Dubbo的调⽤⽅式其实就是很好的⾯向接⼝编程
2、Feign配置应用
我们创建一个简历投递微服务,lagou-service-autodeliver-8096
(1)pom.xml 配置文件,这里我们重点要加入spring-cloud-starter-openfeign
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
</dependencies>
(2)新建启动类 AutodeliverApplicaton8096 ,标记上 注解@EnableFeignClients,启用Feign客户端,因为我们不使用RestTemplete了,所以这里没有注入
@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients
public class AutodeliverApplicaton8096 {
public static void main(String[] args) {
SpringApplication.run(AutodeliverApplicaton8096.class, args);
}
}
注意:此时去掉Hystrix熔断的⽀持注解@EnableCircuitBreaker,因为Feign会⾃动引⼊,不需要显式指定
(3)下面进入核心的一步,类似Dubbo一样,新建一个Service ResumeServiceFeignClient,这里的接口内容,其实就是自动投递微服务的RestFul接口定义,这里可以原封不动的复制过来
//原来的调用方式
//String url="http://lagou-service-resume/resume/openstate/" + userId;
//表明当前类是一个Feign客户端,value指定该客户端 要请求的服务名称(登记到注册中心上的服务提供者的名称)
@FeignClient(value = "lagou-service-resume")
@RequestMapping("/resume")
public interface ResumeServiceFeignClient {
//调⽤的请求路径
//@RequestMapping(value = "/openstate/{userId}",method= RequestMethod.GET)
@GetMapping("/openstate/{userId}")
//这里的value=userID ==> 必须得写上
public Integer findResumeOpenState(@PathVariable(value = "userId") Long userId);
}
注意点:
-
@FeignClient注解的name属性⽤于指定要调⽤的服务提供者名称,和服务提供者yml⽂件中spring.application.name保持⼀致
-
接⼝中的接⼝⽅法,就好⽐是远程服务提供者Controller中的Hander⽅法(只不过如同本地调⽤了),那么在进⾏参数绑定的时,可以使⽤@PathVariable、@RequestParam、@RequestHeader等,这也是OpenFeign对SpringMVC注解的⽀持,但是需要注意value必须设置,否则会抛出异常
(4)改造AutoDeliverController,直接注入接口,可以完成调用
@RestController
@RequestMapping("/autodeliver")
public class AutoDeliverController {
@Autowired
private ResumeServiceFeignClient client;
@GetMapping("/checkState/{userId}")
public Integer findResumeOpenState(@PathVariable Long userId){
return client.findResumeOpenState(userId);
}
}
3、Feign对负载均衡的支持
超时现象
首先来看一个现象
8080端口的服务,设置了一段休眠时间,10秒
8081端口的服务,正常快速返回
当我们使用Feign客户端进行调用的时候,发现都是返回的8081,这是为什么呢?
其实请求已经到达了8080端口,只是因为Feign默认的请求处理超时时⻓1s,这一秒过后,发现还没有返回,就把请求漂移到了其他的ip中,其内部,就是Ribbon负载均衡机制在起作用
负载均衡配置
- 根据如下配置,当访问到故障请求的时候,它会再尝试访问⼀次当前实例(次数由MaxAutoRetries配置),
- 如果不⾏,就换⼀个实例进⾏访问,如果还不⾏,再换⼀次实例访问(更换次数由MaxAutoRetriesNextServer配置),
- 如果依然不⾏,返回失败信息。
#针对的被调⽤⽅微服务名称,不加就是全局⽣效
lagou-service-resume:
ribbon:
#请求连接超时时间
ConnectTimeout: 2000
#请求处理超时时间
ReadTimeout: 15000
#对所有操作都进⾏重试
OkToRetryOnAllOperations: true
MaxAutoRetries: 0 #对当前选中实例重试次数,不包括第⼀次调⽤
MaxAutoRetriesNextServer: 0 #切换实例的重试次数
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule #负载策略调整
##NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule #负载策略调整
4、Feign对熔断器的支持
(1)在Feign客户端⼯程配置⽂件(application.yml)中开启Feign对熔断器的⽀持
# 开启Feign的熔断功能
feign:
hystrix:
enabled: true
(2)之前在Hystrix中配置一个失败回调函数,当服务降级时就会自动调用,那么,在Feign中将会如何调用呢?降级回退逻辑需要定义⼀个类,实现FeignClient接⼝,实现接⼝中的⽅法
@Component
public class ResumeFallback implements ResumeServiceFeignClient {
@Override
public Integer findResumeOpenState(Long userId) {
return -11;
}
}
(3) 在@FeignClient注解中,配置回调方法的类名 fallback = ResumeFallback.class
@FeignClient(value = "lagou-service-resume",fallback = ResumeFallback.class,path = "/resume")
//@RequestMapping("/resume")
public interface ResumeServiceFeignClient {
//调⽤的请求路径
//@RequestMapping(value = "/openstate/{userId}",method= RequestMethod.GET)
@GetMapping("/openstate/{userId}")
//这里的value=userID ==> 必须得写上
public Integer findResumeOpenState(@PathVariable(value = "userId") Long userId);
}
(4) 此时我们再次请求调用,发现马上就进行服务降级了
这里,我们就有疑问了,刚刚我们不是在Ribbon那里设置了超时时间为15000秒了么?怎么没生效?
回答是:当我们启用了hystrix后,hystrix这里也有一个超时时间,而且在默认的情况下,设置得很短,我们可以通过配置进行修改。
# 开启Feign的熔断功能
feign:
hystrix:
enabled: true
hystrix:
command:
default:
execution:
isolation:
thread:
##########################################Hystrix的超时时⻓设置
timeoutInMilliseconds: 15000
注意点
-
开启Hystrix之后,Feign中的⽅法都会被进⾏⼀个管理了,⼀旦出现问题就进⼊对应的回退逻辑处理
-
针对超时这⼀点,当前有两个超时时间设置(Feign/hystrix),熔断的时候是根据这两个时间的最⼩值来进⾏的,即处理时⻓超过最短的那个超时时间了就熔断进⼊回退降级逻辑
5、Feign对请求压缩和响应压缩的支持
Feign ⽀持对请求和响应进⾏GZIP压缩,以减少通信过程中的性能损耗。通过下⾯的参数 即可开启请求与响应的压缩功能:
feign:
compression:
request:
enabled: true # 开启请求压缩
mime-types: text/html,application/xml,application/json # 设置压缩的数据类型,此处也是默认值
min-request-size: 2048 # 设置触发压缩的⼤⼩下限,此处也是默认值
response:
enabled: true # 开启响应压缩
6、Feign的日志级别配置
(1)新建一个日志配置类 FeignConfig
@Configuration
public class FeignConfig {
@Bean
Logger.Level feignLevel() {
return Logger.Level.FULL;
}
}
(2)修改application.yml文件,增加日志功能,注意,这里需要配置Feign客户端服务接口的全限定类名 com.lagou.edu.service.ResumeServiceFeignClient ,说明我们针对这个Feign客户端进行监控日志
logging:
level:
# Feign⽇志只会对⽇志级别为debug的做出响应
com.lagou.edu.service.ResumeServiceFeignClient: debug
(3)此时再去调用接口时,可以发现日志详情了
7、Feign核心源码剖析
@EnableFeignClients
#######################################################################################
## org.springframework.cloud.openfeign.EnableFeignClients
#######################################################################################
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
String[] value() default {};
String[] basePackages() default {};
}
可以看到 FeignClientsRegistrar 实现了 ImportBeanDefinitionRegistrar接口只要实现了这个接口,那么spring在启动的时候,就会调用这个方法,进行注册bean,所以我们重点看看看实现的 registerBeanDefinitions方法
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, EnvironmentAware {
}
public interface ImportBeanDefinitionRegistrar {
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
下面来看下 registerBeanDefinitions方法
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, EnvironmentAware {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
## ===> 把全局默认配置注入到容器
registerDefaultConfiguration(metadata, registry);
## ===> 把标记了@FeignClient的类创建对象注入到容器
-->>>> 针对添加了@FeignClent注解的接口的操作
registerFeignClients(metadata, registry);
}
}
registerDefaultConfiguration 注册默认的配置
#######################################################################################
## org.springframework.cloud.openfeign.FeignClientsRegistrar#registerDefaultConfiguration
#######################################################################################
private void registerDefaultConfiguration(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
##===> 从元数据中,根据类型名称获取Map集合
Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
##===> 包含了key为defaultConfiguration的
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
String name;
if (metadata.hasEnclosingClass()) {
name = "default." + metadata.getEnclosingClassName();
}
else {
name = "default." + metadata.getClassName();
}
## ===> 进行了注册类型
registerClientConfiguration(registry, name,defaultAttrs.get("defaultConfiguration"));
}
}
registerFeignClients 注册Feign客户端的方法
#######################################################################################
## org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClients
#######################################################################################
public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
## ===> 定义扫描器,主要是想扫描 @FeignClient
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");
if (clients == null || clients.length == 0) {
scanner.addIncludeFilter(annotationTypeFilter);
## ===> 如果我们在注解@FeignClient配置了basePackage,则使用这个路径,否则使用启动类下面的所有包的路径作为基本路径包
basePackages = getBasePackages(metadata);
}
## ===> 遍历所有的包路径
for (String basePackage : basePackages) {
## ===> 根据包路径,获取BeanBefinition集合
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
## ===> 遍历BeanBefinition集合
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类型获取特性Map
Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());
String name = getClientName(attributes);
## ===> 获取@FeignClient上的configuration,进行注册
registerClientConfiguration(registry, name,attributes.get("configuration"));
## ===> 接下来进入正式环节了
registerFeignClient(registry, annotationMetadata, attributes);
}
}
}
}
继续深入了解registerFeignClient方法
#######################################################################################
## org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient
#######################################################################################
private void registerFeignClient(BeanDefinitionRegistry registry,
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
String className = annotationMetadata.getClassName();
##===> 这里可以看到的是有一个 FeignClientFactoryBean对象,是一个FactoryBean,那么到这里,其实使用的时候,从容器中获取到的对象,就是使用FactoryBean.getObject返回对象,该对象就是对应接口类的对象
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
validate(attributes);
definition.addPropertyValue("url", getUrl(attributes));
definition.addPropertyValue("path", getPath(attributes));
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
## 最后调用的是registerBeanDefinition方法,完成对象的注册,同时会触发FactoryBean.getObject 方法
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}
我们进入到 FeignClientFactoryBean内部,找到 getObject方法
#######################################################################################
## org.springframework.cloud.openfeign.FeignClientFactoryBean#getObject
#######################################################################################
@Override
public Object getObject() throws Exception {
return getTarget();
}
进入到getTarget方法后,可以发现
进入到loadBalance方法中
进入target方法
进入build方法
这里展示的是build之后的newInstance()方法
深入了解factory.create方法,我们可以看到出自这里,最终返回的是ReflectiveFeign.FeignInvocationHandler
进入FeignInvocationHandler方法
进入invoke方法,动态代理在执行时,都会触发这个方法
进入executeAndDecode方法
进入client的execute方法
进入executeWithLoadBalancer方法
进入submit方法
进入到selectServer方法
进入getServerFromLoadBalancer方法,
到这里,就是走到了Ribbon负载均衡的领域了,可以参照SpringCloud系列 (五)Ribbon负载均衡 进行分析负载均衡的源码