Spring Cloud 是目前最火的微服务框架,Feign 作为基础组件之一,在 Spring Cloud 体系中发挥了重要的作用。
在本场 Chat 中,会讲到如下内容:
- Feign 远程调用
- @FeignClient 注解的详细讲解
适合人群: 对 Spring Cloud 感兴趣以及正在使用 Spring Cloud 的同学。
那晚,我和@FeignClient 注解的深度交流了一次,爽!
主要还是在技术群里看到有同学在问相关问题,比如: contextId 是干嘛的?name 相同的多个 Client 会报错?
然后觉得有必要写篇文章聊聊@FeignClient 的使用,百忙之中抽时间,写篇文章不容易啊,记得点赞。
Feign 基本介绍首先来个基本的普及,怕有些同学还没接触过 Spring Cloud。Feign 是 Netflix 开源的一个 REST 客户端,通过定义接口,使用注解的方式描述接口的信息,就可以发起接口调用。
GitHub 地址:https://github.com/OpenFeign/feign
下面是 GitHub 主页上给的一个最基本的使用示列,示列中采用 Feign 调用 GitHub 的接口。
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List 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 assignees; int milestone; List 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 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
官方提供的使用示列:
@FeignClient("stores")public interface StoreClient { @RequestMapping(method = RequestMethod.GET, value = "/stores") List 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 那么配置的值将作为服务名称,用于服务发现。反之只是一个名称。
serviceIdserviceId 已经废弃了,直接使用 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 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 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 是配置 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 定义
@Componentpublic 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 定义
@Componentpublic class UserRemoteClientFallbackFactory implements FallbackFactory { 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 注解就是干这件事情的。
qualifierqualifier 对应的是@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;
本文到此结束,感谢大家的阅读,对 Spring Cloud 感兴趣的可以去 Github(https://github.com/yinjihuan/spring-cloud)下载案例。
阅读全文: http://gitbook.cn/gitchat/activity/5e5a6f0cee72fc73e8bddd89
您还可以下载 CSDN 旗下精品原创内容社区 GitChat App ,阅读更多 GitChat 专享技术内容哦。