开发者

SpringCloud之@FeignClient()注解的使用详解

目录
  • @FeignClient介绍
  • @FeignClient工作原理及整体流程
  • @FeignClient常用属性
    • name、value
    • contextId
    • url
    • path
    • primary
    • qualifier
    • configuration
    • fallback
    • fallbackFactory
  • @FeignClient添加Header信息
    • 在@RequestMapping中添加
    • 使用@RequestHeader注解添加
    • 使用@Headers注解添加
    • 实现RequestInterceptor接口
  • 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做到了负载均衡。

      整体流程:

      SpringCloud之@FeignClient()注解的使用详解

      @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属性配置),而不是你自己的那个服务的名称。
      • 如果同一个工程中出现两个接口使用一样的服python务名称会报错。原因是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")
      public interface OrderRemoteClient {
      	
      	@GetMapping("/api/order/detail", contextId = "OrderRemoteClient")
      	public Order detail(@RequestParam("orderId") String orderId);
      }

      上面给出了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用于配置指定服务的地址,相当于直接请求这个服务。像调试等场景可以使用。

      @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("/detaiwww.devze.coml")
      	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://or编程der: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.annotatjavascription.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;
      }
      

      总结

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜