(七)服务接口调用-OpenFeign

1.Feign简介

Feign是一个声明式WebService客户端。使用Feign能让编写Web Service客户端更加简单。它的使用方法是定义一个服务接口然后在上面添加注解。Feign也支持可拔插式的编码器和解码器。Spring Cloud对Feign进行了封装,使其支持了Spring MVC标准注解和HttpMessageConverters。Feign可以与Eureka和Ribbon组合使用以支持负载均衡

Feign能干什么
Feign旨在使编写Java Http客户端变得更容易。
前面在使用Ribbon+RestTemplate时,利用RestTemplate对http请求的封装处理,形成了一套模版化的调用方法。但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以,Feign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义。在Feign的实现下,我们只需创建一个接口并使用注解的方式来配置它(以前是Dao接口上面标注Mapper注解,现在是一个微服务接口上面标注一个Feign注解即可),即可完成对服务提供方的接口绑定,简化了使用Spring cloud Ribbon时,自动封装服务调用客户端的开发量。

Feign集成了Ribbon
利用Ribbon维护了Payment的服务列表信息,并且通过轮询实现了客户端的负载均衡。而与Ribbon不同的是,通过feign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用。

Feign和OpenFeign区别

2.OpenFeign的使用步骤

  • 新建服务消费者
cloud-consumer-order-feign-80
  • pom
        <!--openfeign-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
        <!--eureka client-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <!-- 引入自己定义的api通用包,可以使用Payment支付Entity -->
        <dependency>
            <groupId>com.atguigu.springcloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!--web-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
  • yml
server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
  • 主启动类
@SpringBootApplication
@EnableFeignClients
public class OrderFeignApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderFeignApplication.class, args);
    }
}
  • 业务逻辑接口+@FeignClient配置调用provider服务
  • 新建PaymentFeignService接口并新增注解@FeignClient
@Component
@FeignClient(value = "cloud-payment-service")
public interface PaymentFeignService {

    @GetMapping(value = "payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
  • Controller
@RestController
public class OrderFeignController {

    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }
}
  • 先启动2个eureka集群7001/7002
  • 再启动2个微服务8001/8002
  • 启动cloud-consumer-order-feign-80
  • 调用http://localhost/consumer/payment/get/6 测试

OpenFeign超时控制

  • 服务提供方8001故意写暂停程序
    @GetMapping(value = "/payment/feign/timeout")
    public String paymentFeignTimeOut()
    {
        System.out.println("*****paymentFeignTimeOut from port: "+serverPort);
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
        return serverPort;
    }
  • 服务消费方80添加超时方法PaymentFeignService
@Component
@FeignClient(value = "cloud-payment-service")
public interface PaymentFeignService {

    @GetMapping(value = "payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);

    @GetMapping(value = "payment/feign/timeout")
    public CommonResult timeout();
}
  • 服务消费方80添加超时方法OrderFeignController
@RestController
public class OrderFeignController {

    @Resource
    private PaymentFeignService paymentFeignService;

    @GetMapping(value = "/consumer/payment/get/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
        return paymentFeignService.getPaymentById(id);
    }

    @GetMapping(value = "/consumer/payment/feign/timeout")
    public CommonResult<Payment> timeout() {
        return paymentFeignService.timeout();
    }
}
  • 测试http://localhost/consumer/payment/feign/timeout 出现错误页面

    OpenFeign默认等待1秒钟,超时后报错

  • OpenFeign默认支持Ribbon

  • yml文件需要添加开启OpenFeign客户端超时控制

server:
  port: 80

eureka:
  client:
    register-with-eureka: false
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/


#设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
  #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间
  ReadTimeout: 5000
  #指的是建立连接后从服务器读取到可用资源所用的时间
  ConnectTimeout: 5000

OpenFeign日志打印功能

  • 是什么?
    Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节。说白了就是对Feign接口的调用情况进行监控和输出
  • 日志级别
    • NONE:默认的,不显示任何日志;
    • BASIC:仅记录请求方法、URL、响应状态码及执行时间;
    • HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
    • FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据
  • 配置日志Bean
@Configuration
public class FeignConfig {

    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }

}
  • yml文件开启日志feign客户端
logging:
  level:
    # feign日志以什么级别监控哪个接口
    com.mine.springcloud.order.service.PaymentFeignService: debug
  • 后台日志查看

3.服务调用Feign高级

3.1Feign的配置

从Spring Cloud Edgware开始,Feign支持使用属性自定义Feign。对于一个指定名称的FeignClient(例如该Feign Client的名称为 feignName ),Feign支持如下配置项:

feign:
 client:
  config:
   feignName: ##定义FeginClient的名称
    connectTimeout: 5000  # 相当于Request.Options
    readTimeout: 5000   # 相当于Request.Options
     # 配置Feign的日志级别,相当于代码配置方式中的Logger
    loggerLevel: full
     # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
    errorDecoder: com.example.SimpleErrorDecoder
     # 配置重试,相当于代码配置方式中的Retryer
    retryer: com.example.SimpleRetryer
     # 配置拦截器,相当于代码配置方式中的RequestInterceptor
    requestInterceptors:
     - com.example.FooRequestInterceptor
     - com.example.BarRequestInterceptor
    decode404: false
  • feignName :FeginClient的名称
  • connectTimeout : 建立链接的超时时长
  • readTimeout : 读取超时时长
  • loggerLevel: Fegin 的日志级别
  • errorDecoder :Feign的错误解码器
  • retryer : 配置重试
  • requestInterceptors : 添加请求拦截器
  • decode404 : 配置熔断不处理404异常

3.2请求压缩

Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

feign:
 compression:
  request:
   enabled: true # 开启请求压缩
  response:
   enabled: true # 开启响应压缩

同时,我们也可以对请求的数据类型,以及触发压缩的大小下限进行设置:

feign:
 compression:
  request:
   enabled: true # 开启请求压缩
   mime-types: text/html,application/xml,application/json # 设置压缩的数据类型
   min-request-size: 2048 # 设置触发压缩的大小下限

注:上面的数据类型、压缩大小下限均为默认值。

3.3源码分析

通过上面的使用过程,@EnableFeignClients和@FeignClient两个注解就实现了Feign的功能,那我们从@EnableFeignClients注解开始分析Feign的源码

( 1)EnableFeignClients注解

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({FeignClientsRegistrar.class})
public @interface EnableFeignClients {
//略
}

通过 @EnableFeignClients 引入了FeignClientsRegistrar客户端注册类

(2)FeignClientsRegistrar注册类

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,ResourceLoaderAware, EnvironmentAware {
    public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
      this.registerDefaultConfiguration(metadata, registry);
      this.registerFeignClients(metadata, registry);
    }
}

通过其类结构可知,由于实现了 ImportBeanDefinitionRegistrar接口,那么在registerBeanDefinitions()中就会解析和注册BeanDefinition,主要注册的对象类型有两种:

  • 注册缺省配置的配置信息
  • 注册那些添加了 @FeignClient的类或接口 : 这也是我们讨论的重点
public void registerFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry) {
    ClassPathScanningCandidateComponentProvider scanner = this.getScanner();
    scanner.setResourceLoader(this.resourceLoader);
    Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
    AnnotationTypeFilter annotationTypeFilter = newAnnotationTypeFilter(FeignClient.class);
    Class<?>[] clients = attrs == null ? null : (Class[])((Class[])attrs.get("clients"));
    Object basePackages;
    if (clients != null && clients.length != 0) {
      final Set<String> clientClasses = new HashSet();
      basePackages = new HashSet();
      Class[] var9 = clients;
      int var10 = clients.length;
      for(int var11 = 0; var11 < var10; ++var11) {
        Class<?> clazz = var9[var11];
       ((Set)basePackages).add(ClassUtils.getPackageName(clazz));
        clientClasses.add(clazz.getCanonicalName());
     }
     AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
        protected boolean match(ClassMetadata metadata) {
          String cleaned = metadata.getClassName().replaceAll("\$", ".");
          return clientClasses.contains(cleaned);
       }
     };
      scanner.addIncludeFilter(new FeignClientsRegistrar.AllTypeFilter(Arrays.asList(filter,annotationTypeFilter)));
   } else {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = this.getBasePackages(metadata);
   }
    Iterator var17 = ((Set)basePackages).iterator();
    while(var17.hasNext()) {
      String basePackage = (String)var17.next();
      Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
      Iterator var21 = candidateComponents.iterator();
      while(var21.hasNext()) {
        BeanDefinition candidateComponent =(BeanDefinition)var21.next();
        if (candidateComponent instanceof AnnotatedBeanDefinition) {
          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 = this.getClientName(attributes);
          this.registerClientConfiguration(registry, name,attributes.get("configuration"));
          this.registerFeignClient(registry, annotationMetadata,attributes);
        }
     }
   }
}

该方法主要是扫描类路径,对所有的 FeignClient生成对应的 BeanDefinitio 。同时又调用了registerClientConfiguration 注册配置的方法,这里是第二处调用。这里主要是将扫描的目录下,每个项目的配置类加载的容器当中。调用 registerFeignClient 注册对象。

(3) 注册FeignClient对象

private void registerFeignClient(BeanDefinitionRegistry registry,AnnotationMetadata annotationMetadata,Map<String, Object> attributes) {
  // 1.获取类名称,也就是本例中的FeignService接口
  String className = annotationMetadata.getClassName();
  // 2.BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
  // AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder
  // 然后注册到Spring中
  // 注意:beanDefinition类为FeignClientFactoryBean,故在Spring获取类的时候实际返回的是
  // FeignClientFactoryBean类
  BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
  validate(attributes);
 
  // 3.添加FeignClientFactoryBean的属性,
  // 这些属性也都是我们在@FeignClient中定义的属性
  definition.addPropertyValue("url", getUrl(attributes));
  definition.addPropertyValue("path", getPath(attributes));
  String name = getName(attributes);
  definition.addPropertyValue("name", name);
  definition.addPropertyValue("type", className);
  definition.addPropertyValue("decode404", attributes.get("decode404"));
  definition.addPropertyValue("fallback", attributes.get("fallback"));
  definition.addPropertyValue("fallbackFactory",attributes.get("fallbackFactory"));
  definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
  // 4.设置别名 name就是我们在@FeignClient中定义的name属性
  String alias = name + "FeignClient";
  AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
  boolean primary = (Boolean)attributes.get("primary"); // has a default,won't be null
  beanDefinition.setPrimary(primary);
  String qualifier = getQualifier(attributes);
  if (StringUtils.hasText(qualifier)) {
    alias = qualifier;
  }
  // 5.定义BeanDefinitionHolder,
  // 在本例中 名称为FeignService,类为FeignClientFactoryBean
  BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition,className,new String[] { alias});
  BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

通过分析可知:我们最终是向 Spring中注册了一个bean,bean的名称就是类或接口的名称(也就是本例中的FeignService),bean的实现类是FeignClientFactoryBean,其属性设置就是我们在@FeignClient中定义的属性。那么下面我们在Controller中对FeignService的的引入,实际就是引入了
FeignClientFactoryBean 类。

(4) FeignClientFactoryBean类
对@EnableFeignClients注解的源码进行了分析,了解到其主要作用就是把带有@FeignClient注解的类或接口用FeignClientFactoryBean类注册到Spring中。

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,ApplicationContextAware {
  public Object getObject() throws Exception {
    return this.getTarget();
 }
  //略
}

通过 FeignClientFactoryBean 类结构可以发现其实现了FactoryBean类,那么当从
ApplicationContext中获取该bean的时候,实际调用的是其getObject()方法。返回调用getTarget()方法

<T> T getTarget() {
    FeignContext context = (FeignContext)this.applicationContext.getBean(FeignContext.class);
    Builder builder = this.feign(context);
    if (!StringUtils.hasText(this.url)) {
      if (!this.name.startsWith("http")) {
        this.url = "http://" + this.name;
      } else {
        this.url = this.name;
      }
      this.url = this.url + this.cleanPath();
      return this.loadBalance(builder, context, new HardCodedTarget(this.type, this.name, this.url));
    } else {
      if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
        this.url = "http://" + this.url;
      }
      String url = this.url + this.cleanPath();
      Client client = (Client)this.getOptional(context, Client.class);
      if (client != null) {
        if (client instanceof LoadBalancerFeignClient) {
          client = ((LoadBalancerFeignClient)client).getDelegate();
      }
      builder.client(client);
     }
      Targeter targeter = (Targeter)this.get(context, Targeter.class);
      return targeter.target(this, builder, context, new HardCodedTarget(this.type, this.name, url));
   }
 }
  • FeignClientFactoryBean 实现了FactoryBean的getObject、getObjectType、isSingleton方法;实现了InitializingBean的afterPropertiesSet方法;实现了ApplicationContextAware的setApplicationContext方法
  • getObject 调用的是getTarget方法,它从applicationContext取出FeignContext,然后构造Feign.Builder并设置了logger、encoder、decoder、contract,之后通过configureFeign根据FeignClientProperties来进一步配置Feign.Builder的retryer、errorDecoder、request.Options、requestInterceptors、queryMapEncoder、decode404
  • 初步配置完 Feign.Builder之后再判断是否需要loadBalance,如果需要则通过loadBalance方法来设置,不需要则在Client是LoadBalancerFeignClient的时候进行unwrap

(5) 发送请求
由上可知,FeignClientFactoryBean.getObject()具体返回的是一个代理类,具体为FeignInvocationHandler

static class FeignInvocationHandler implements InvocationHandler {
    private final Target target;
    private final Map<Method, MethodHandler> dispatch;
    FeignInvocationHandler(Target target, Map<Method, MethodHandler>dispatch) {
      this.target = (Target)Util.checkNotNull(target, "target", newObject[0]);
      this.dispatch = (Map)Util.checkNotNull(dispatch, "dispatch for %s",new Object[]{target});
    }
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if (!"equals".equals(method.getName())) {
        if ("hashCode".equals(method.getName())) {
          return this.hashCode();
        } else {
          return "toString".equals(method.getName()) ? this.toString() : ((MethodHandler)this.dispatch.get(method)).invoke(args);
        }
      } else {
        try {
          Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return this.equals(otherHandler);
        } catch (IllegalArgumentException var5) {
          return false;
        }
      }
    }
    public boolean equals(Object obj) {
      if (obj instanceof ReflectiveFeign.FeignInvocationHandler) {
        ReflectiveFeign.FeignInvocationHandler other = (ReflectiveFeign.FeignInvocationHandler)obj;
        return this.target.equals(other.target);
      } else {
        return false;
      }
   }
    public int hashCode() {
      return this.target.hashCode();
    }
    public String toString() {
      return this.target.toString();
    }
}
  • FeignInvocationHandler 实现了InvocationHandler,是动态代理的代理类。
  • 当执行非 Object方法时进入到this.dispatch.get(method)).invoke(args)
  • dispatch 是一个map集合,根据方法名称获取MethodHandler。具体实现类为SynchronousMethodHandler
final class SynchronousMethodHandler implements MethodHandler { 
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = this.buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while(true) {
      try {
        return this.executeAndDecode(template);
      } catch (RetryableException var8) {
        //略
      }
    }
  }
  Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = this.targetRequest(template);
    if (this.logLevel != Level.NONE) {
      this.logger.logRequest(this.metadata.configKey(), this.logLevel,request);
    }
    long start = System.nanoTime();
    Response response;
    try {
      response = this.client.execute(request, this.options);
    } catch (IOException var15) {
     //略
    }
  }
}
  • SynchronousMethodHandler 内部创建了一个RequestTemplate对象,是Feign中的请求模板对象。内部封装了一次请求的所有元数据。
  • retryer 中定义了用户的重试策略。
  • 调用 executeAndDecode方法通过client完成请求处理,client的实现类是LoadBalancerFeignClient
原文地址:https://www.cnblogs.com/everyingo/p/14768009.html