SpringCloud系列 (七)Feign远程调用组件

486 阅读9分钟

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负载均衡 进行分析负载均衡的源码

源码示例