专栏首页猿天地那天晚上和@FeignClient注解的深度交流

那天晚上和@FeignClient注解的深度交流

废话篇

那晚,我和@FeignClient 注解的深度交流了一次,爽!

主要还是在技术群里看到有同学在问相关问题,比如: contextId 是干嘛的?name 相同的多个 Client 会报错?

然后觉得有必要写篇文章聊聊@FeignClient 的使用,百忙之中抽时间,写篇文章不容易啊,记得点赞。

正式篇

Feign 基本介绍

首先来个基本的普及,怕有些同学还没接触过 Spring Cloud。Feign 是 Netflix 开源的一个 REST 客户端,通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。

GitHub 地址:

https://github.com/OpenFeign/feign[1]

下面是 GitHub 主页上给的一个最基本的使用示列,示列中采用 Feign 调用 GitHub 的接口。

interface GitHub {
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);

  @RequestLine("POST /repos/{owner}/{repo}/issues")
  void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);

}

public static class Contributor {
  String login;
  int contributions;
}

public static class Issue {
  String title;
  String body;
  List<String> assignees;
  int milestone;
  List<String> labels;
}

public class MyApp {
  public static void main(String... args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");

    // Fetch and print a list of the contributors to this library.
    List<Contributor> contributors = github.contributors("OpenFeign", "feign");
    for (Contributor contributor : contributors) {
      System.out.println(contributor.login + " (" + contributor.contributions + ")");
    }
  }
}

Spring Cloud OpenFeign 介绍

Spring Cloud OpenFeign 是 Spring Cloud 团队将原生的 Feign 结合到 Spring Cloud 中的产物。从上面原生 Feign 的使用示列来看,用的注解都是 Feign 中自带的,但我们在开发中基本上都是基于 Spring MVC 的注解,不是很方便调用。所以 Spring Cloud OpenFeign 扩展了对 Spring MVC 注解的支持,同时还整合了 Ribbon 和 Eureka 来提供均衡负载的 HTTP 客户端实现。

GitHub 地址:

https://github.com/spring-cloud/spring-cloud-openfeign[2]

官方提供的使用示列:

@FeignClient("stores")
public interface StoreClient {
    @RequestMapping(method = RequestMethod.GET, value = "/stores")
    List<Store> getStores();

    @RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
    Store update(@PathVariable("storeId") Long storeId, Store store);
}

FeignClient 注解的使用介绍

value, name

value 和 name 的作用一样,如果没有配置 url 那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。

serviceId

serviceId 已经废弃了,直接使用 name 即可。

contextId

比如我们有个 user 服务,但 user 服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,比如:

Client 1

@FeignClient(name = "optimization-user")
public interface UserRemoteClient {
  @GetMapping("/user/get")
  public User getUser(@RequestParam("id") int id);
}

Client 2

@FeignClient(name = "optimization-user")
public interface UserRemoteClient2 {
  @GetMapping("/user2/get")
  public User getUser(@RequestParam("id") int id);
}

这种情况下启动就会报错了,因为 Bean 的名称冲突了,具体错误如下:

Description:
The bean 'optimization-user.FeignClientSpecification', defined in null, could not be registered. A bean with that name has already been defined in null and overriding is disabled.
Action:
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true

解决方案可以增加下面的配置,作用是允许出现 beanName 一样的 BeanDefinition。

spring.main.allow-bean-definition-overriding=true

另一种解决方案就是为每个 Client 手动指定不同的 contextId,这样就不会冲突了。

上面给出了 Bean 名称冲突后的解决方案,下面来分析下 contextId 在 Feign Client 的作用,在注册 Feign Client Configuration 的时候需要一个名称,名称是通过 getClientName 方法获取的:

String name = getClientName(attributes);
registerClientConfiguration(registry, name,
attributes.get("configuration"));
private String getClientName(Map<String, Object> client) {
    if (client == null) {
      return null;
    }
    String value = (String) client.get("contextId");
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("value");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("name");
    }
    if (!StringUtils.hasText(value)) {
      value = (String) client.get("serviceId");
    }
    if (StringUtils.hasText(value)) {
      return value;
    }
    throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
        + FeignClient.class.getSimpleName());
}

可以看到如果配置了 contextId 就会用 contextId,如果没有配置就会去 value 然后是 name 最后是 serviceId。默认都没有配置,当出现一个服务有多个 Feign Client 的时候就会报错了。

其次的作用是在注册 FeignClient 中,contextId 会作为 Client 别名的一部分,如果配置了 qualifier 优先用 qualifier 作为别名。

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
        .genericBeanDefinition(FeignClientFactoryBean.class);
    validate(attributes);
    definition.addPropertyValue("url", getUrl(attributes));
    definition.addPropertyValue("path", getPath(attributes));
    String name = getName(attributes);
    definition.addPropertyValue("name", name);
    String contextId = getContextId(attributes);
    definition.addPropertyValue("contextId", contextId);
    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);

    // 拼接别名
    String alias = contextId + "FeignClient";
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();


    boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
                                // null


    beanDefinition.setPrimary(primary);

    // 配置了qualifier优先用qualifier
    String qualifier = getQualifier(attributes);
    if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
    }


    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
        new String[] { alias });
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
  }

url

url 用于配置指定服务的地址,相当于直接请求这个服务,不经过 Ribbon 的服务选择。像调试等场景可以使用。

使用示列

@FeignClient(name = "optimization-user", url = "http://localhost:8085")
public interface UserRemoteClient {
  @GetMapping("/user/get")
  public User getUser(@RequestParam("id") int id);
}

decode404

当调用请求发生 404 错误时,decode404 的值为 true,那么会执行 decoder 解码,否则抛出异常。

解码也就是会返回固定的数据格式给你:

{"timestamp":"2020-01-05T09:18:13.154+0000","status":404,"error":"Not Found","message":"No message available","path":"/user/get11"}
抛异常的话就是异常信息了,如果配置了 fallback 那么就会执行回退逻辑:

configuration

configuration 是配置 Feign 配置类,在配置类中可以自定义 Feign 的 Encoder、Decoder、LogLevel、Contract 等。

configuration 定义

public class FeignConfiguration {
  @Bean
  public Logger.Level getLoggerLevel() {
    return Logger.Level.FULL;
  }
  @Bean
  public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
    return new BasicAuthRequestInterceptor("user", "password");
  }

  @Bean
  public CustomRequestInterceptor customRequestInterceptor() {
    return new CustomRequestInterceptor();
  }
  // Contract,feignDecoder,feignEncoder.....
}
使用示列
@FeignClient(value = "optimization-user", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
  @GetMapping("/user/get")
  public User getUser(@RequestParam("id")int id);
}
fallback

定义容错的处理类,也就是回退逻辑,fallback 的类必须实现 Feign Client 的接口,无法知道熔断的异常信息。

fallback 定义

@Component
public class UserRemoteClientFallback implements UserRemoteClient {
  @Override
  public User getUser(int id) {
    return new User(0, "默认fallback");
  }

}
使用示列
@FeignClient(value = "optimization-user", fallback = UserRemoteClientFallback.class)
public interface UserRemoteClient {

  @GetMapping("/user/get")
  public User getUser(@RequestParam("id")int id);

}

fallbackFactory

也是容错的处理,可以知道熔断的异常信息。

fallbackFactory 定义

@Component
public class UserRemoteClientFallbackFactory implements FallbackFactory<UserRemoteClient> {
  private Logger logger = LoggerFactory.getLogger(UserRemoteClientFallbackFactory.class);

  @Override
  public UserRemoteClient create(Throwable cause) {
    return new UserRemoteClient() {
      @Override
      public User getUser(int id) {
        logger.error("UserRemoteClient.getUser异常", cause);
        return new User(0, "默认");
      }
    };
  }
}

使用示列

@FeignClient(value = "optimization-user", fallbackFactory = UserRemoteClientFallbackFactory.class)
public interface UserRemoteClient {

  @GetMapping("/user/get")
  public User getUser(@RequestParam("id")int id);

}

path

path 定义当前 FeignClient 访问接口时的统一前缀,比如接口地址是/user/get, 如果你定义了前缀是 user, 那么具体方法上的路径就只需要写/get 即可。

使用示列

@FeignClient(name = "optimization-user", path="user")
public interface UserRemoteClient {

  @GetMapping("/get")
  public User getUser(@RequestParam("id") int id);
  
}

primary

primary 对应的是@Primary 注解,默认为 true,官方这样设置也是有原因的。当我们的 Feign 实现了 fallback 后,也就意味着 Feign Client 有多个相同的 Bean 在 Spring 容器中,当我们在使用@Autowired 进行注入的时候,不知道注入哪个,所以我们需要设置一个优先级高的,@Primary 注解就是干这件事情的。

qualifier

qualifier 对应的是@Qualifier 注解,使用场景跟上面的 primary 关系很淡,一般场景直接@Autowired 直接注入就可以了。

如果我们的 Feign Client 有 fallback 实现,默认@FeignClient 注解的 primary=true, 意味着我们使用@Autowired 注入是没有问题的,会优先注入你的 Feign Client。

如果你鬼斧神差的把 primary 设置成 false 了,直接用@Autowired 注入的地方就会报错,不知道要注入哪个对象。

解决方案很明显,你可以将 primary 设置成 true 即可,如果由于某些特殊原因,你必须得去掉 primary=true 的设置,这种情况下我们怎么进行注入,我们可以配置一个 qualifier,然后使用@Qualifier 注解进行注入,示列如下:

Feign Client 定义

@FeignClient(name = "optimization-user", path="user", qualifier="userRemoteClient")
public interface UserRemoteClient {

  @GetMapping("/get")
  public User getUser(@RequestParam("id") int id);
  
}

Feign Client 注入

@Autowired
@Qualifier("userRemoteClient")
private UserRemoteClient userRemoteClient;

参考资料

[1]

feign: https://github.com/OpenFeign/feign

[2]

spring-cloud-openfeign: https://github.com/spring-cloud/spring-cloud-openfeign

本文分享自微信公众号 - 猿天地(cxytiandi),作者:尹吉欢

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-01-07

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 内网穿透工具-ittun

    相信大家在工作和学习都避免不了快速外网调试。比如微信支付、支付宝支付,消息推送,短信发送,邮件发送等等。都需外网能访问你本地服务器进行调试。 软件下载地址:h...

    猿天地
  • 高性能NIO框架Netty-对象传输

    上篇文章高性能NIO框架Netty入门篇我们对Netty做了一个简单的介绍,并且写了一个入门的Demo,客户端往服务端发送一个字符串的消息,服务端回复一个字符串...

    猿天地
  • 还在手动对参数进行签名校验?太落后了吧!

    有做过开放平台的同学肯定知道,对外的 API 都要做签名校验,防止重放等来保证安全性。既然是统一的校验,那就没必要让每个开发接口的同学都去手动的进行校验,这个时...

    猿天地
  • 那天晚上和@FeignClient注解的深度交流

    主要还是在技术群里看到有同学在问相关问题,比如: contextId 是干嘛的?name 相同的多个 Client 会报错?

    黄泽杰
  • Python Elasticsearch API操作ES集群

    关键是DSL语法的编写涉及查询与聚合可以通过kibana的visualize或者devtool先测试出正确语法,然后结合python对列表、字典、除法、字符串等...

    三杯水Plus
  • Python爬虫的一次提问,引发的“乱码”问题

    近日,有位小伙伴向我请教,在爬取某网站时,网页的源代码出现了中文乱码问题。之前关于爬虫乱码有很多粉丝的各式各样的问题,今天恋习Python与大家一起总结下关于网...

    一墨编程学习
  • 【Java】22 网络连接

    java.net.InetAddress此类表示互联网协议 (IP) 地址。IP 地址是 IP 使用的 32 位或 128 位无符号数字,它是一种低级协议,U...

    Demo_Null
  • Akka 指南 之「分布式数据」

    为了使用分布式数据(Distributed Data),你需要将以下依赖添加到你的项目中:

    CG国斌
  • Mac os上显示与隐藏文件

    在Finder中使用快捷Command + Shift + .快速切换显示与隐藏文件

    _kyle
  • 文本挖掘分析《欢乐颂》到底谁和谁堪称好闺蜜、谁和谁又最为般配?

    听说最近大家都在看《欢乐颂》,这部热剧里,女性可谓是绝对的主角,22楼5个女房客的互动好像把男性角色们的风头都抢光了;但是热门剧中又总是不能缺了言情戏的点缀。...

    机器学习AI算法工程

扫码关注云+社区

领取腾讯云代金券