SpringCloud @FeignClient详解

一、@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服务调用的工作原理可以总结为以下几个步骤:

  1. 首先通过@EnableFeignCleints注解开启FeignCleint。

  2. 根据Feign的规则实现接口,添加@FeignCleint注解。程序启动后,会扫描所有被@FeignCleint标记的接口,并将这些信息注入到ioc容器中。

  3. 注入时从FeignClientFactoryBean.class获取FeignClient。

  4. 当接口的方法被调用时,通过jdk的代理,来生成具体的RequesTemplate,RequesTemplate生成http的Request。

  5. Request交给Client去处理,其中Client可以是HttpUrlConnection、HttpClient也可以是Okhttp。

  6. Client被封装到LoadBalanceClient类,这个类结合类Ribbon做到了负载均衡。

三、@FeignClient注解属性表

属性 解释
value 指定要调用的服务的名称,对应服务注册中心中的服务名。例如,@FeignClient(value = “service-provider”)表示要调用名为"service-provider"的服务。
contextId Feign客户端的上下文ID,用于区分不同的Feign客户端。默认情况下,Feign客户端的上下文ID与接口类名相同。可以通过contextId参数来指定自定义的上下文ID。
name value的别名,用于指定要调用的服务的名称,与value参数作用相同。
url 指定要调用的服务的URL地址,可以直接指定服务的URL而不通过服务注册中心进行服务发现。
configuration 指定Feign客户端的配置类,用于配置Feign客户端的相关属性,如超时时间、重试策略等。
decode404 指定是否将404作为正常响应处理,默认为false。
fallback 指定Feign客户端的降级处理类,用于处理远程调用失败时的降级逻辑。
fallbackFactory 指定Feign客户端的降级处理工厂类,用于创建降级处理类的实例。
path 指定Feign客户端的基础路径,用于拼接请求URL。
primary 指定Feign客户端是否为主要的,当有多个Feign客户端时,可以通过primary参数指定主要的Feign客户端。
qualifier 指定Feign客户端的限定符,用于区分相同类型的Feign客户端。

四、@FeignClient常用属性

name、value

指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现 这两个属性的作用是一样的,如果没有配置url,那么配置的值将作为服务的名称,用于服务的发现,反之只是一个名称。

@FeignClient(name = "order-server")
public interface OrderRemoteClient {

 @GetMapping("/order/detail")
 public Order detail(@RequestParam("orderId") String orderId);
}

注意

  • 这里写的是你要调用的那个服务的名称(spring.application.name属性配置),而不是项目文件中的模块名称。

contextId

如果同一个工程中出现两个接口使用一样的服务名称会报错。原因是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, 具体原因可以查看Spring的源码, 位于org.springframework.cloud.openfeign.FeignClientsRegistrar类中

注意:contextId= "名称" 中的名称,不能用“_”会报错,可以用“-”

url

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

@FeignClient(name = "order-server", url = "http://localhost:8085")
public interface OrderRemoteClient {

 @GetMapping("/api/order/detail")
 public Order detail(@RequestParam("orderId") String orderId);
}

path

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

@FeignClient(name = "order-server", url = "http://localhost:8085", path = "/api/order")
public interface OrderRemoteClient {

 @GetMapping("/detail")
 public Order detail(@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")
public interface OrderRemoteClient {

 @GetMapping("/detail")
 public Order detail(@RequestParam("orderId") String orderId);
}

Feign Client注入

@Autowired
@Qualifier("orderRemoteClient")
private OrderRemoteClient orderRemoteClient;

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 = "order-server", configuration = FeignConfiguration.class)
public interface OrderRemoteClient {

    @GetMapping("/api/order/detail")
    public Order detail(@RequestParam("orderId") String orderId);

}

fallback

定义容错的处理类,也就是回退逻辑,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口,无法知道熔断的异常信息。

fallback定义

@Component
public class OrderRemoteClientFallback implements OrderRemoteClient {
    @Override
    public Order detail(String orderId) {
        return new Order("order-998", "默认fallback");
    }

}

使用示列

@FeignClient(value = "order-server", fallback = OrderRemoteClientFallback.class)
public interface OrderRemoteClient {

    @GetMapping("/api/order/detail")
    public Order detail(@RequestParam("orderId") String orderId);

}

fallbackFactory

也是容错的处理,可以知道熔断的异常信息。工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。

fallbackFactory定义

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

    @Override
    public OrderRemoteClient create(Throwable cause) {
        return new OrderRemoteClient() {
            @Override
            public Order detail(String id) {
                logger.error("OrderRemoteClient.detail 异常", cause);
                return new Order("order-998", "默认");
            }
        };
    }
}

使用示列

@FeignClient(value = "order-server", fallbackFactory = OrderRemoteClientFallbackFactory.class)
public interface OrderRemoteClient {

    @GetMapping("/order/detail")
    public Order detail(@RequestParam("orderId") String orderId);

}

五、@FeignClient添加Header信息

在@RequestMapping中添加

@FeignClient(
        url = "${orderServer_domain:http://order:8082}",
        value = "order-server",
        contextId = "OrderServerClient",
        path = "/api/order"
        )
public interface OrderRemoteClient {
    @RequestMapping(value="/detail", method = RequestMethod.POST,
        headers = {"Content-Type=application/json;charset=UTF-8"})
    Order detail(@RequestParam("orderId") String orderId);
}

使用@RequestHeader注解添加

@FeignClient(
 url = "${orderServer_domain:http://order:8082}",
 value = "order-server",
 contextId = "OrderServerClient",
 path = "/api/order"
 )
public interface OrderRemoteClient {
 @RequestMapping(value="/detail", method = RequestMethod.POST)
 List<String> detail(@RequestHeader Map<String, String> headerMap, @RequestParam("orderId") String orderId);
}

使用@Headers注解添加

@FeignClient(
 url = "${orderServer_domain:http://order:8082}",
 value = "order-server",
 contextId = "OrderServerClient",
 path = "/api/order"
 )
public interface OrderRemoteClient {
 @RequestMapping(value="/detail", method = RequestMethod.POST)
 @Headers({"Content-Type: application/json;charset=UTF-8"})
 List<String> detail(@RequestHeader Map<String, String> headerMap, @RequestParam("orderId") String orderId);
}

实现RequestInterceptor接口添加

@Configuration
public class FeignRequestInterceptor implements RequestInterceptor {

 @Override
 public void apply(RequestTemplate temp) {
 temp.header(HttpHeaders.AUTHORIZATION, "XXXXX");
 }

}

六、FeignClient 源码

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.cloud.openfeign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface FeignClient {
 @AliasFor("name")
 String value() default "";

 String contextId() default "";

 @AliasFor("value")
 String name() default "";

 String[] qualifiers() default {};

 String url() default "";

 boolean dismiss404() default false;

 Class<?>[] configuration() default {};

 Class<?> fallback() default void.class;

 Class<?> fallbackFactory() default void.class;

 String path() default "";

 boolean primary() default true;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,542评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,822评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,912评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,449评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,500评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,370评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,193评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,074评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,505评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,722评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,841评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,569评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,168评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,783评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,918评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,962评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,781评论 2 354

推荐阅读更多精彩内容