Spring Cloud OpenFeign

1. Feign简介

Feign是Netflix开发的声明式,模板化的HTTP客户端,其灵感来自Retrofit,JAXRS-2.0以及WebSocket

  • Feign可更加便捷,优雅的调用HTTP API
  • 在SpringCloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了
  • Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等
  • SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便

    2. 基于Feign的服务调用示例

    2.1. 示例工程准备

    复用之前eureka单机版的示例项目02-springcloud-eureka,命名为06-springcloud-feign

    2.2. 引入Feign依赖

    在服务消费者 shop-service-order 工程添加Feign依赖 ``` org.springframework.cloud spring-cloud-starter-openfeign
  1. ### 2.3. 开启Feign的支持
  2. 在服务消费者的启动类上,通过`@EnableFeignClients`注解开启Spring Cloud Feign的支持功能

@SpringBootApplication(scanBasePackages = “com.moon.order”) @EntityScan(“com.moon.entity”) // 指定扫描实体类的包路径 @EnableFeignClients // 开启Feign的支持 public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } }

  1. ### 2.4. 创建Feign服务调用的接口
  2. #### 2.4.1. 基础使用步骤
  3. 在服务消费者 `shop-service-order` 创建一个Feign接口,此接口是在Feign中调用微服务的核心接口。

/*

  • @FeignClient 注解,用于标识当前接口为Feign调用微服务的核心接口
  • value/name属性:指定需要调用的服务提供者的名称 */ @FeignClient(“shop-service-product”) // 或者:@FeignClient(name = “shop-service-product”) public interface ProductFeignClient {

    /*

    • 创建需要调用的微服务接口方法,SpringCloud 对 Feign 进行了增强兼容了 SpringMVC 的注解
    • 在使用的两个注意点:
      1. FeignClient 接口有参数时,必须在参数加@PathVariable(“XXX”)和@RequestParam(“XXX”)注解,并且必须要指定对应的参数值(原来SpringMVC是可以省略)
      1. feignClient 返回值为复杂对象时,其对象类型必须有无参构造函数
      1. 方法的名称不需要与被调用的服务接口名称一致 */ @GetMapping(“/product/{id}”) Product findById(@PathVariable(“id”) Long id);

}

  1. #### 2.4.2. 基础使用步骤总结
  2. 1. 启动类添加`@EnableFeignClients`注解,表示开启对Feign的支持,Spring会扫描标记了`@FeignClient`注解的接口,并生成此接口的代理对象
  3. 1. `@FeignClient`注解通过`name/value`属性指定需要调用的微服务的名称,用于创建Ribbon的负载均衡器。所以Ribbon从注册中心中获取服务列表,并通过负载均衡算法调用相应名称的服务。如:`@FeignClient("service-xxx")`即指定了服务提供者的名称`service-xxx`Feign会从注册中心获取服务列表,并通过负载均衡算法进行服务调用名为`service-xxx`的服务
  4. 1. 在接口方法中使用`@GetMapping("/xxxx")`SpringMVC的注解,指定调用的urlFeign将根据url进行远程调用
  5. #### 2.4.3. Feign组件使用注意事项
  6. - 定义接口方法对于形参绑定时,`@PathVariable``@RequestParam``@RequestHeader`等可以指定参数属性,在Feign中绑定参数必须通过`value`属性来指明具体的参数名,不然会抛出异常
  7. - `FeignClient` 返回值为复杂对象时,其对象类型必须有无参构造函数
  8. ### 2.5. 配置消费者调用服务接口
  9. 修改消费者`shop-service-order``OrderController`控制类,注入`ProductFeignClient`接口实例,并在相应的方法中使用`ProductFeignClient`实例方法完成微服务调用即可

@RestController @RequestMapping(“order”) public class OrderController { / 日志对象 / private static final Logger LOGGER = LoggerFactory.getLogger(OrderController.class);

  1. // 注入FeignClient服务调用接口
  2. @Autowired
  3. private ProductFeignClient productFeignClient;
  4. /**
  5. * 根据商品id创建订单
  6. *
  7. * @param id 商品的id
  8. * @return
  9. */
  10. @PostMapping("/{id}")
  11. public String createOrder(@PathVariable Long id) {
  12. // 使用Feign组件实现服务远程调用,直接调用FeignClient的接口定义的相应方法即可
  13. Product product = productFeignClient.findById(id);
  14. LOGGER.info("当前下单的商品是: ${}", product);
  15. return "创建订单成功";
  16. }

}

  1. 启动相应的服务,进行测试
  2. ## 3. Feign 和 Ribbon 的联系
  3. - Ribbon 是一个基于 HTTP TCP 客户端的负载均衡的工具。它可以在客户端配置`RibbonServerList`(服务端列表),使用 `HttpClient` `RestTemplate` 模拟http请求,步骤比较繁琐
  4. - Feign 是在 Ribbon 的基础上进行了一次改进,是一个使用起来更加方便的 HTTP 客户端。采用接口的方式,只需要创建一个接口,然后在上面添加注解即可,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。然后就像是调用自身工程的方法调用,而感觉不到是调用远程方法,使得编写客户端变得非常容易
  5. ## 4. Feign 的负载均衡
  6. Feign中本身已经集成了Ribbon依赖和自动配置,因此不需要额外引入依赖,也不需要再注册 `RestTemplate` 对象。<br />配置负载均衡的方式与使用Ribbon的配置方式一致,即也可以通过修改项目配置文件中 `ribbon.xx` 来进行全局配置。也可以通过`服务名.ribbon.xx` 来对指定服务配置<br />启动两个`shop-service-product`服务,重新测试可以发现使用Ribbon的轮询策略进行负载均衡<br />![](https://gitee.com/moonzero/images/raw/master/code-note/20201015140621794_15061.png)
  7. ## 5. Feign 相关配置
  8. ### 5.1. Feign 可配置项说明
  9. Spring Cloud Edgware 版本开始,Feign支持使用属性自定义Feign。对于一个指定名称的Feign Client(例如该Feign Client的名称为 feignName ),Feign支持如下配置项:

Feign 属性配置

feign: client: config: shop-service-product: # 需要调用的服务名称 connectTimeout: 5000 # 相当于Request.Options readTimeout: 5000 # 相当于Request.Options loggerLevel: full # 配置Feign的日志级别,相当于代码配置方式中的Logger errorDecoder: com.example.SimpleErrorDecoder # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder retryer: com.example.SimpleRetryer # 配置重试,相当于代码配置方式中的Retryer requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor

  1. - com.example.FooRequestInterceptor
  2. - com.example.BarRequestInterceptor
  3. decode404: false
  1. 部分属性配置说明:
  2. - `feignName`FeignClient的名称,即上面例子的`shop-service-product`
  3. - `connectTimeout`:建立链接的超时时长
  4. - `readTimeout`:读取超时时长
  5. - `loggerLevel`Feign的日志级别
  6. - `errorDecoder`Feign的错误解码器
  7. - `retryer`:配置重试
  8. - `requestInterceptors`:添加请求拦截器
  9. - `decode404`:配置熔断不处理404异常
  10. ### 5.2. 请求压缩配置
  11. Spring Cloud Feign 支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗。通过下面的参数即可开启请求与响应的压缩功能:

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

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

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

  1. > 注:上面的数据类型、压缩大小下限均为默认值。
  2. ### 5.3. 日志级别
  3. 如果在开发或者运行阶段希望看到Feign请求过程的日志记录,默认情况下Feign的日志是没有开启的。要想用属性配置方式来达到日志效果,只需在 `application.yml` 中添加如下内容即可:

配置feign日志的输出

feign: client: config: shop-service-product: # 需要调用的服务名称 loggerLevel: full # 配置Feign的日志级别,相当于代码配置方式中的Logger

日志配置

logging: level:

  1. # 配置只输出ProductFeignClient接口的日志
  2. com.moon.order.feign.ProductFeignClient: debug
  1. 配置参数说明:
  2. - `logging.level.xx: debug`:配置Feign只会对日志级别为debug的做出响应
  3. - `feign.client.config.服务名称.loggerLevel` 配置Feign的日志级别,其中Feign有以下四种日志级别:
  4. - `NONE`【性能最佳,适用于生产】:不记录任何日志(默认值)
  5. - `BASIC`【适用于生产环境追踪问题】:仅记录请求方法、URL、响应状态代码以及执行时间
  6. - `HEADERS`:记录BASIC级别的基础上,记录请求和响应的header
  7. - `FULL`【比较适用于开发及测试环境定位问题】:记录请求和响应的headerbody和元数据。
  8. ![](https://gitee.com/moonzero/images/raw/master/code-note/20201015210138012_24744.png)
  9. ## 6. Feign 源码分析
  10. 通过使用过程可知,`@EnableFeignClients``@FeignClient`两个注解就实现了Feign的功能,所以从`@EnableFeignClients`注解开始分析Feign的源码
  11. ### 6.1. [EnableFeignClients ](/EnableFeignClients ) 注解

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

  1. 通过 `@EnableFeignClients` 引入了`FeignClientsRegistrar`客户端注册类
  2. ### 6.2. FeignClientsRegistrar 客户端注册类

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { // ….省略代码 @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 注册默认配置 registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); } // ….省略代码 }

  1. 根据源码可知,`FeignClientsRegistrar`类实现了`ImportBeanDefinitionRegistrar`接口,在实现的`registerBeanDefinitions()`里就会解析和注册BeanDefinition,主要注册的对象类型有两种:
  2. 1. 注册缺省配置的配置信息

private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true);

  1. if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
  2. String name;
  3. if (metadata.hasEnclosingClass()) {
  4. name = "default." + metadata.getEnclosingClassName();
  5. }
  6. else {
  7. name = "default." + metadata.getClassName();
  8. }
  9. registerClientConfiguration(registry, name,
  10. defaultAttrs.get("defaultConfiguration"));
  11. }

}

  1. 2. 注册添加了标识`@FeignClient`注解的类或接口

public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader);

  1. Set<String> basePackages;
  2. Map<String, Object> attrs = metadata
  3. .getAnnotationAttributes(EnableFeignClients.class.getName());
  4. AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
  5. FeignClient.class);
  6. final Class<?>[] clients = attrs == null ? null
  7. : (Class<?>[]) attrs.get("clients");
  8. if (clients == null || clients.length == 0) {
  9. scanner.addIncludeFilter(annotationTypeFilter);
  10. basePackages = getBasePackages(metadata);
  11. }
  12. else {
  13. final Set<String> clientClasses = new HashSet<>();
  14. basePackages = new HashSet<>();
  15. for (Class<?> clazz : clients) {
  16. basePackages.add(ClassUtils.getPackageName(clazz));
  17. clientClasses.add(clazz.getCanonicalName());
  18. }
  19. AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
  20. @Override
  21. protected boolean match(ClassMetadata metadata) {
  22. String cleaned = metadata.getClassName().replaceAll("\\$", ".");
  23. return clientClasses.contains(cleaned);
  24. }
  25. };
  26. scanner.addIncludeFilter(
  27. new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
  28. }
  29. for (String basePackage : basePackages) {
  30. Set<BeanDefinition> candidateComponents = scanner
  31. .findCandidateComponents(basePackage);
  32. for (BeanDefinition candidateComponent : candidateComponents) {
  33. if (candidateComponent instanceof AnnotatedBeanDefinition) {
  34. // verify annotated class is an interface
  35. AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
  36. AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
  37. Assert.isTrue(annotationMetadata.isInterface(),
  38. "@FeignClient can only be specified on an interface");
  39. Map<String, Object> attributes = annotationMetadata
  40. .getAnnotationAttributes(
  41. FeignClient.class.getCanonicalName());
  42. String name = getClientName(attributes);
  43. registerClientConfiguration(registry, name,
  44. attributes.get("configuration"));
  45. registerFeignClient(registry, annotationMetadata, attributes);
  46. }
  47. }
  48. }

}

  1. `registerFeignClients()`方法主要是扫描类路径,对所有的FeignClient生成对应的`BeanDefinition`。同时又调用了 `registerClientConfiguration` 注册配置的方法。这里是第二次调用,主要是将扫描的目录下,每个项目的配置类加载的容器当中。调用 `registerFeignClient` 注册对象
  2. ### 6.3. FeignClient 对象的注册
  3. 在上一步中,获取`@FeignClient`注解的数据封装到一个map集合后,调用`registerFeignClient(registry, annotationMetadata, attributes);`方法,往spring容器中注册`BeanDefinition`对象

private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map attributes) { // 1. 获取类名称,也就是本例中的FeignService接口 String className = annotationMetadata.getClassName(); /*

  1. * 2. BeanDefinitionBuilder的主要作用就是构建一个AbstractBeanDefinition
  2. * AbstractBeanDefinition类最终被构建成一个BeanDefinitionHolder然后注册到Spring容器中
  3. * 注意:beanDefinition类为FeignClientFactoryBean,所以在Spring获取类的时候实际返回的是FeignClientFactoryBean
  4. */
  5. BeanDefinitionBuilder definition = BeanDefinitionBuilder
  6. .genericBeanDefinition(FeignClientFactoryBean.class);
  7. validate(attributes);
  8. // 3. 添加FeignClientFactoryBean的属性,这些属性都是在@FeignClient中定义的属性
  9. definition.addPropertyValue("url", getUrl(attributes));
  10. definition.addPropertyValue("path", getPath(attributes));
  11. String name = getName(attributes);
  12. definition.addPropertyValue("name", name);
  13. String contextId = getContextId(attributes);
  14. definition.addPropertyValue("contextId", contextId);
  15. definition.addPropertyValue("type", className);
  16. definition.addPropertyValue("decode404", attributes.get("decode404"));
  17. definition.addPropertyValue("fallback", attributes.get("fallback"));
  18. definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
  19. definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
  20. // 4. 设置别名 name就是我们在@FeignClient中定义的name属性
  21. String alias = contextId + "FeignClient";
  22. AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
  23. boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
  24. beanDefinition.setPrimary(primary);
  25. String qualifier = getQualifier(attributes);
  26. if (StringUtils.hasText(qualifier)) {
  27. alias = qualifier;
  28. }
  29. // 5. 定义BeanDefinitionHolder,在本例中名称为FeignService,类为FeignClientFactoryBean
  30. BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
  31. new String[] { alias });
  32. BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);

}

  1. 通过源分析可知:最终是向Spring中注册了一个beanbean的名称就是类或接口的名称(也就是本例中的FeignService),bean的实现类是`FeignClientFactoryBean`,其属性设置就是在`@FeignClient`中定义的属性。那么下面在Controller中对`FeignService`的的引入,实际就是引入了`FeignClientFactoryBean`
  2. ### 6.4. FeignClientFactoryBean 类
  3. `@EnableFeignClients`注解的源码进行了分析,了解到其主要作用就是把带有`@FeignClient`注解的类或接口用`FeignClientFactoryBean`类注册到Spring容器中。

class FeignClientFactoryBean implements FactoryBean, InitializingBean, ApplicationContextAware { // ….省略代码 @Override public Object getObject() throws Exception { return getTarget(); } // ….省略代码 }

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

T getTarget() { FeignContext context = applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context);

  1. if (!StringUtils.hasText(this.url)) {
  2. if (!this.name.startsWith("http")) {
  3. url = "http://" + this.name;
  4. }
  5. else {
  6. url = this.name;
  7. }
  8. url += cleanPath();
  9. return (T) loadBalance(builder, context, new HardCodedTarget<>(this.type,
  10. this.name, url));
  11. }
  12. if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
  13. this.url = "http://" + this.url;
  14. }
  15. String url = this.url + cleanPath();
  16. Client client = getOptional(context, Client.class);
  17. if (client != null) {
  18. if (client instanceof LoadBalancerFeignClient) {
  19. // not load balancing because we have a url,
  20. // but ribbon is on the classpath, so unwrap
  21. client = ((LoadBalancerFeignClient)client).getDelegate();
  22. }
  23. builder.client(client);
  24. }
  25. Targeter targeter = get(context, Targeter.class);
  26. return (T) targeter.target(this, builder, context, new HardCodedTarget<>(
  27. this.type, this.name, url));

}

  1. 代码流程说明:
  2. - `FeignClientFactoryBean`类实现三个接口,分别是:
  3. - `FactoryBean`接口的`getObject``getObjectType``isSingleton`方法;
  4. - 实现了`InitializingBean`接口的`afterPropertiesSet`方法;
  5. - 实现了`ApplicationContextAware`接口的`setApplicationContext`方法
  6. - `getObject()`方法中调用的是`getTarget()`方法,它从applicationContext取出FeignContext,然后构造`Feign.Builder`并设置了loggerencoderdecodercontract,之后通过configureFeign根据`FeignClientProperties`来进一步配置`Feign.Builder`retryererrorDecoderrequest.OptionsrequestInterceptorsqueryMapEncoderdecode404
  7. - 初步配置完`Feign.Builder`之后再判断是否需要loadBalance,如果需要则通过loadBalance方法来设置,不需要则在Client`LoadBalancerFeignClient`的时候进行unwrap
  8. ### 6.5. 发送请求的实现
  9. 从上面的源码分析可知,`FeignClientFactoryBean.getObject()`具体返回的是一个代理类,具体为`FeignInvocationHandler`

public class ReflectiveFeign extends Feign { // ….省略代码 static class FeignInvocationHandler implements InvocationHandler {

  1. private final Target target;
  2. private final Map<Method, MethodHandler> dispatch;
  3. FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
  4. this.target = checkNotNull(target, "target");
  5. this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
  6. }
  7. @Override
  8. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  9. if ("equals".equals(method.getName())) {
  10. try {
  11. Object otherHandler =
  12. args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
  13. return equals(otherHandler);
  14. } catch (IllegalArgumentException e) {
  15. return false;
  16. }
  17. } else if ("hashCode".equals(method.getName())) {
  18. return hashCode();
  19. } else if ("toString".equals(method.getName())) {
  20. return toString();
  21. }
  22. return dispatch.get(method).invoke(args);
  23. }
  24. @Override
  25. public boolean equals(Object obj) {
  26. if (obj instanceof FeignInvocationHandler) {
  27. FeignInvocationHandler other = (FeignInvocationHandler) obj;
  28. return target.equals(other.target);
  29. }
  30. return false;
  31. }
  32. @Override
  33. public int hashCode() {
  34. return target.hashCode();
  35. }
  36. @Override
  37. public String toString() {
  38. return target.toString();
  39. }

} // ….省略代码 }

  1. `FeignInvocationHandler`类说明:
  2. - `FeignInvocationHandler`实现了`InvocationHandler`接口,是动态代理的代理类。
  3. - 当执行非`Object`方法时进入到`this.dispatch.get(method)).invoke(args)`
  4. - dispatch是一个map集合,根据方法名称获取`MethodHandler`。具体实现类为`SynchronousMethodHandler`

final class SynchronousMethodHandler implements MethodHandler { // ….省略代码 @Override public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template); } catch (RetryableException e) { // ….省略代码 } } }

Object executeAndDecode(RequestTemplate template) throws Throwable { Request request = targetRequest(template);

  1. if (logLevel != Logger.Level.NONE) {
  2. logger.logRequest(metadata.configKey(), logLevel, request);
  3. }
  4. Response response;
  5. long start = System.nanoTime();
  6. try {
  7. response = client.execute(request, options);
  8. } catch (IOException e) {
  9. if (logLevel != Logger.Level.NONE) {
  10. logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start));
  11. }
  12. throw errorExecuting(request, e);
  13. }
  14. // ....省略代码

} // ….省略代码 }

```

  • SynchronousMethodHandler内部创建了一个RequestTemplate对象,是Feign中的请求模板对象。内部封装了一次请求的所有元数据。
  • retryer中定义了用户的重试策略。
  • 调用executeAndDecode方法通过client完成请求处理,client的实现类是LoadBalancerFeignClient