@FeignClient介绍
@FeignClient
是 Spring Cloud 中用于声明一个 Feign 客户端的注解。由于SpringCloud采用分布式微服务架构,难免在各个子模块下存在模块方法互相调用的情况。比如订单服务要调用库存服务的方法,@FeignClient()注解就是为了解决这个问题的。
Feign 是一个声明式的 Web Service 客户端,它的目的是让编写 HTTP 客户端变得更简单。通过 Feign,只需要创建一个接口,并使用注解来描述请求,就可以直接执行 HTTP 请求了。
@FeignClient()注解的源码要求它必须在Interface接口上使用( FeignClient注解被@Target(ElementType.TYPE)修饰,表示FeignClient注解的作用目标在接口上)
**SpringBoot服务的启动类必须要有
@EnableFeignClients
注解才能使@FeginClient注解生效。**
@FeignClient工作原理及整体流程
Feign服务调用的工作原理可以总结为以下几个步骤
- 首先通过@EnableFeignCleints注解开启FeignCleint。
- 根据Feign的规则实现接口,添加@FeignCleint注解。程序启动后,会扫描所有有@FeignCleint的类,并将这些信息注入到ioc容器中。
- 注入时从FeignClientFactoryBean.class获取FeignClient。
- 当接口的方法被调用时,通过jdk的代理,来生成具体的RequesTemplate,RequesTemplate生成http的Request。
- Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp。
- Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。
整体流程
@FeignClient常用属性
name、value
指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
这两个属性的作用是一样的,如果没有配置url,那么配置的值将作为服务的名称,用于服务的发现,反之只是一个名称。
@FeignClient(name ="order-server")publicinterfaceOrderRemoteClient{@GetMapping("/order/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
注意:
- 这里写的是你要调用的那个服务的名称(
spring.application.name
属性配置),而不是你自己的那个服务的名称。 - 如果同一个工程中出现两个接口使用一样的服务名称会报错。原因是Client名字注册到容器中重复了。除非指定不同的
contextId
参数。 > Description:> The bean ‘order-server.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
两种解决方案:
- 增加配置 spring.main.allow-bean-definition-overriding=true
- 为每个FeignClient手动指定不同的contextId
contextId
比如我们有个user服务,但user服务中有很多个接口,我们不想将所有的调用接口都定义在一个类中,那就可以给不同的client指定contextId,不然就会报
Consider renaming one of the beans or enabling overriding by setting spring.main.allow-bean-definition-overriding=true
异常。
注意:contextId不能带_等符号。
@FeignClient(name ="order-server")publicinterfaceOrderRemoteClient{@GetMapping("/api/order/detail", contextId ="OrderRemoteClient")publicOrderdetail(@RequestParam("orderId")String orderId);}
上面给出了Bean名称冲突后的解决方案,下面来分析下contextId在Feign Client的作用,在注册Feign Client Configuration的时候需要一个名称,名称是通过getClientName方法获取的:
String name =getClientName(attributes);registerClientConfiguration(registry, name,
attributes.get("configuration"));
privateStringgetClientName(Map<String,Object> client){if(client ==null){returnnull;}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;}thrownewIllegalStateException("Either 'name' or 'value' must be provided in @"+FeignClient.class.getSimpleName());}
可以看到如果配置了contextId就会用contextId,如果没有配置就会去value然后是name最后是serviceId。默认都没有配置,当出现一个服务有多个Feign Client的时候就会报错了。
其次的作用是在注册FeignClient中,contextId会作为Client 别名的一部分,如果配置了qualifier优先用qualifier作为别名。
privatevoidregisterFeignClient(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优先用qualifierString qualifier =getQualifier(attributes);if(StringUtils.hasText(qualifier)){
alias = qualifier;}BeanDefinitionHolder holder =newBeanDefinitionHolder(beanDefinition, className,newString[]{ alias });BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);}
url
url用于配置指定服务的地址,相当于直接请求这个服务。像调试等场景可以使用。
@FeignClient(name ="order-server", url ="http://localhost:8085")publicinterfaceOrderRemoteClient{@GetMapping("/api/order/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
path
path定义当前FeignClient访问接口时的统一前缀。
比如接口地址是/order/detail, 如果你定义了前缀是order, 那么具体方法上的路径就只需要写/detail即可。
@FeignClient(name ="order-server", url ="http://localhost:8085", path ="/api/order")publicinterfaceOrderRemoteClient{@GetMapping("/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
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 ="order-server", path ="/api/order", qualifier="orderRemoteClient")publicinterfaceOrderRemoteClient{@GetMapping("/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
Feign Client注入
@Autowired@Qualifier("orderRemoteClient")privateOrderRemoteClient orderRemoteClient;
configuration
configuration是配置Feign配置类,在配置类中可以自定义Feign的Encoder、Decoder、LogLevel、Contract等。
configuration定义
publicclassFeignConfiguration{@BeanpublicLogger.LevelgetLoggerLevel(){returnLogger.Level.FULL;}@BeanpublicBasicAuthRequestInterceptorbasicAuthRequestInterceptor(){returnnewBasicAuthRequestInterceptor("user","password");}@BeanpublicCustomRequestInterceptorcustomRequestInterceptor(){returnnewCustomRequestInterceptor();}// Contract,feignDecoder,feignEncoder.....}
使用示列
@FeignClient(value ="order-server", configuration =FeignConfiguration.class)publicinterfaceOrderRemoteClient{@GetMapping("/api/order/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
fallback
定义容错的处理类,也就是回退逻辑,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口,无法知道熔断的异常信息。
fallback定义
@ComponentpublicclassOrderRemoteClientFallbackimplementsOrderRemoteClient{@OverridepublicOrderdetail(String orderId){returnnewOrder("order-998","默认fallback");}}
使用示列
@FeignClient(value ="order-server", fallback =OrderRemoteClientFallback.class)publicinterfaceOrderRemoteClient{@GetMapping("/api/order/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
fallbackFactory
也是容错的处理,可以知道熔断的异常信息。工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
fallbackFactory定义
@ComponentpublicclassOrderRemoteClientFallbackFactoryimplementsFallbackFactory<OrderRemoteClient>{privateLogger logger =LoggerFactory.getLogger(OrderRemoteClientFallbackFactory.class);@OverridepublicOrderRemoteClientcreate(Throwable cause){returnnewOrderRemoteClient(){@OverridepublicOrderdetail(String id){
logger.error("OrderRemoteClient.detail 异常", cause);returnnewOrder("order-998","默认");}};}}
使用示列
@FeignClient(value ="order-server", fallbackFactory =OrderRemoteClientFallbackFactory.class)publicinterfaceOrderRemoteClient{@GetMapping("/order/detail")publicOrderdetail(@RequestParam("orderId")String orderId);}
@FeignClient添加Header信息
在@RequestMapping中添加
@FeignClient(
url ="${orderServer_domain:http://order:8082}",
value ="order-server",
contextId ="OrderServerClient",
path ="/api/order")publicinterfaceOrderRemoteClient{@RequestMapping(value="/detail", method =RequestMethod.POST,
headers ={"Content-Type=application/json;charset=UTF-8"})Orderdetail(@RequestParam("orderId")String orderId);}
使用@RequestHeader注解添加
@FeignClient(
url ="${orderServer_domain:http://order:8082}",
value ="order-server",
contextId ="OrderServerClient",
path ="/api/order")publicinterfaceOrderRemoteClient{@RequestMapping(value="/detail", method =RequestMethod.POST)List<String>detail(@RequestHeaderMap<String,String> headerMap,@RequestParam("orderId")String orderId);}
使用@Headers注解添加
@FeignClient(
url ="${orderServer_domain:http://order:8082}",
value ="order-server",
contextId ="OrderServerClient",
path ="/api/order")publicinterfaceOrderRemoteClient{@RequestMapping(value="/detail", method =RequestMethod.POST)@Headers({"Content-Type: application/json;charset=UTF-8"})List<String>detail(@RequestHeaderMap<String,String> headerMap,@RequestParam("orderId")String orderId);}
实现RequestInterceptor接口
@ConfigurationpublicclassFeignRequestInterceptorimplementsRequestInterceptor{@Overridepublicvoidapply(RequestTemplate temp){
temp.header(HttpHeaders.AUTHORIZATION,"XXXXX");}}
FeignClient 源码
//// Source code recreated from a .class file by IntelliJ IDEA// (powered by FernFlower decompiler)//packageorg.springframework.cloud.openfeign;importjava.lang.annotation.Documented;importjava.lang.annotation.ElementType;importjava.lang.annotation.Inherited;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;importorg.springframework.core.annotation.AliasFor;@Target({ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Documented@Inheritedpublic@interfaceFeignClient{@AliasFor("name")Stringvalue()default"";StringcontextId()default"";@AliasFor("value")Stringname()default"";String[]qualifiers()default{};Stringurl()default"";booleandismiss404()defaultfalse;Class<?>[]configuration()default{};Class<?>fallback()defaultvoid.class;Class<?>fallbackFactory()defaultvoid.class;Stringpath()default"";booleanprimary()defaulttrue;}
版权归原作者 狂奔的小棕棕 所有, 如有侵权,请联系我们删除。