Feign简介

Feign

Feign     WHAT    WHY    HOW        maven依赖        自动装配        编写接口        调用接口        注意事项    原理html

WHAT

Feign的GitHub描述以下:java

Feign is a Java to Http client binder inspired by Retrofit, JAXRS-2.0, and WebSocket. Feign's first goal was reducing the complexity of binding Denominator uniformly to Http APIs regardless of ReSTfulness.git

简单的说,Feign是一套Http客户端"绑定器"。我的理解,这个"绑定"有点像ORM。ORM是把数据库字段和代码中的实体"绑定"起来;Feign提供的基本功能就是方便、简单地把Http的Request/Response和代码中的实体"绑定"起来。github

举个例子,在咱们系统调用时,咱们是这样写的:web

 @FeignClient(url = "${feign.url.user}", name = "UserInfoService",
         configuration = FeignConfiguration.UserInfoFeignConfiguration.class)
 public interface UserInfoService {
 
     /**
      * 查询用户数据
      *
      * @param userInfo 用户信息
      * @return 用户信息
      */
     @PostMapping(path = "/user/getUserInfoRequest")
     BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo);
 }
 
 // 使用时
 BaseResult<UserInfoBean> response = UserInfoService.queryUserInfo(Body4UserInfo.of(userBean.getId()));

上面这段代码里,咱们只须要建立一个Body4UserInfo,而后像调用本地方法那样,就能够拿到返回对象BaseResult<UserInfoBean>了。spring

WHY

与其它的Http调用方式,例如URLConnection、HttpClient、RestTemplate相比,Feign有哪些优点呢?数据库

最核心的一点在于,Feign的抽象层次比其它几个工具、框架都更高。json

640?wx_fmt=jpeg&tp=webp&wxfrom=5&wx_lazy=1&wx_co=1

首先,通常来讲抽象层次越高,其中包含的功能也就越多。
api

此外,抽象层次越高,使用起来就越简便。例如,上面这个例子中,把Body4UserInfo转换为HttpRequest、把HttpResponse转换为BaseResult<UserInfoBean>的操做,就不须要咱们操心了。并发

固然,单纯从这一个例子中,看不出Feign提供了多大的帮助。可是能够想一下:若是咱们调用的接口,有些参数要用RequestBody传、有些要用RequestParam传;有些要求加特殊的header;有些要求Content-Type是application/json、有些要求是application/x-www-form-urlencoded、还有些要求application/octet-stream呢?若是这些接口的返回值有些是applicaion/json、有些是text/html,有些是application/pdf呢?不一样的请求和响应对应不一样的处理逻辑。咱们若是本身写,可能每次都要从新写一套代码。而使用Feign,则只须要在对应的接口上加几个配置就能够。写代码和加配置,显而后者更方便。

此外,抽象层次越高,代码可替代性就越好。若是尝试过Apache的HttpClient3.x升级到4.x,就知道这种接口不兼容的升级改造是多么痛苦。若是要从Apache的HttpClient转到OkHttp上,因为使用了不一样的API,更要费一番周折。而使用Feign,咱们只须要修改几行配置就能够了。即便要从Feign转向其它组件,我只须要给UserInfoService提供一个新的实现类便可,调用方代码甚至一行都不用改。若是咱们升级一个框架、重构一个组件,须要改的代码成百上千行,那谁也不敢乱动代码。代码的可替代性越好,咱们就越能放心、顺利的对系统作重构和优化。

并且,抽象层次越高,代码的可扩展性就越高。若是咱们使用的仍是URLConnection,那么连Http链接池都很难实现。若是咱们使用的是HttpClient或者RESTTemplate,那么作异步请求、合并请求都须要咱们本身写不少代码。可是,使用Feign时,咱们能够轻松地扩展Feign的功能:异步请求、并发控制、合并请求、负载均衡、熔断降级、链路追踪、流式处理、Reactive……,还能够经过实现feign.Client接口或自定义Configuration来扩展其它自定义的功能。

放眼Java世界的各大框架、组件,不管是URLConnection、HttpClient、RESTTemplate和Feign,Servlet、Struts1.0/2.0和SpringMVC,仍是JDBCConnection、myBatis/Hibernate和Spring-Data JPA,Redis、Jedis和Redisson,越新、越好用的框架,其抽象层级一般都更高。这对咱们一样也是一个启示:咱们须要去学习、了解和掌握技术的底层原理;可是在设计和使用时,咱们应该从底层跳出来、站在更高的抽象层级上去设计和开发。尤为是对业务开发来讲,频繁的需求变动是难以免的,咱们只有作出可以“以不变应万变”、“以系统的少许变动应对需求的大量变动”,才能从无谓的加班、copy代码、查工单等重复劳动中解脱出来。怎样“以不变应万变”呢?提升系统设计的抽象层次就是一个不错的办法。

HOW

Feign有好几种用法:既能够在代码中直接使用FeignBuilder来构建客户端、也可使用Feign自带的注解、还可使用SpringMVC的注解。这里只介绍下使用SpringMVC注解的方式。 

maven依赖

咱们系统引入的依赖是这样的:

         <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-openfeign</artifactId>
             <version>2.1.1.RELEASE</version>
             <exclusions>
                 <exclusion>
                     <artifactId>spring-web</artifactId>
                     <groupId>org.springframework</groupId>
                 </exclusion>
             </exclusions>
         </dependency>
 
         <dependency>
             <groupId>io.github.openfeign</groupId>
             <artifactId>feign-okHttp</artifactId>
             <version>10.1.0</version>
         </dependency>

直接引入spring-cloud-starter-openfeign,是由于这个包内有feign的自动装配相关代码,不须要咱们再本身手写。

另外,这里之因此是openfeign、而不是原生的feign,是由于原生的Feign只支持原生的注解,openfeign是SpringCloud项目加入了对SpringMVC注解的支持以后的版本。

引入feign-okHttp则是为了在底层使用okHttp客户端。默认状况下,feign会直接使用URLConnection;若是系统中引入了Apache的HttpClient包,则OpenFeign会自动把HttpClient装配进来。若是要使用OkHttpClient,首先须要引入对应的依赖,而后修改一点配置。 

自动装配

若是使用了SpringBoot,那么直接用@EnableFeignClient就能够自动装配了。若是没有使用SpringBoot,则须要本身导入一下其中的AutoConfiguration类:

 /**
 * 非SpringBoot的系统须要增长这个类,并保证Spring Context启动时加载到这个类
 */
 @Configuration
 @ImportAutoConfiguration({FeignAutoConfiguration.class})
 @EnableFeignClients(basePackages = "com.test.test.feign")
 public class FeignConfiguration {
 
 }

上面这个类能够没有具体的实现,可是必须有几个注解。

  • @Configuration

    使用这个注解是为了让Spring Conetxt启动时装载这个类。在xml文件里配<context:component-scan base-package="com.test.user">,或者使用@Component能够起到相同的做用。

  • @ImportAutoConfiguration({FeignAutoConfiguration.class})

    使用这个注解是为了导入FeignAutoConfiguration中自动装配的bean。这些bean是feign发挥做用所必须的一些基础类,例如feignContext、feignFeature、feignClient等等。

  • @EnableFeignClients(basePackages = "com.test.user.feign")

    使用这个注解是为了扫描具体的feign接口上的@FeignClient注解。这个注解的用法到后面再说。 

为了使用okHttp、而不是Apache的HttpClient,咱们还须要在系统中增长两行配置:

 # 使用properties文件配置
 feign.okHttp.enabled=true
 feign.Httpclient.enabled=false

这两行配置也能够用yml格式配置,只要能被SpringContext解析到配置就行。配置好之后,FeignAutoConfiguration就会按照OkHttpFeignConfiguration的代码来把okHttp3.OkHttpClient装配到FeignClient里去了。

 @Configuration
 @ConditionalOnClass(Feign.class)
 @EnableConfigurationProperties({ FeignClientProperties.class,
   FeignHttpClientProperties.class })
 public class FeignAutoConfiguration {
     // 其它略
 
  @Configuration
  @ConditionalOnClass(ApacheHttpClient.class)
  @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
  @ConditionalOnMissingBean(CloseableHttpClient.class)
  @ConditionalOnProperty(value = "feign.Httpclient.enabled", matchIfMissing = true)
  protected static class HttpClientFeignConfiguration {
      // 其它略
 }
 
  @Configuration
  @ConditionalOnClass(OkHttpClient.class)
  @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
  @ConditionalOnMissingBean(okHttp3.OkHttpClient.class)
  @ConditionalOnProperty("feign.okHttp.enabled")
  protected static class OkHttpFeignConfiguration {
      // 其它略
 }

除了这两个配置以外,FeignClientProperties和FeignHttpClientProperties里面还有不少其它配置,你们能够关注下。

编写接口

依赖和配置都弄好以后,就能够写一个Fiegn的客户端接口了:

 @FeignClient(url = "${feign.url.user}", name = "UserInfoService",
         configuration = FeignConfiguration.UserFeignConfiguration.class)
 public interface UserInfoService {
 
     @PostMapping(path = "/user/getUserInfoRequest")
     BaseResult<UserInfoBean> queryUserInfo(Body4UserInfo userInfo);

首先,咱们只须要写一个接口,并在接口上加上@FeignClient注解、接口方法上加上@RequestMapping(或者@PostMapping、@GetMappping等对应注解)。Feign会根据@EnableFeignClients(basePackages = "com.test.user.feign")的配置,扫描到@FeignClient注解,并为注解类生成动态代理。所以,咱们不须要写具体的实现类。

而后,配置好@FeignClient和@PostMapping中的各个字段。@PostMapping注解字段比较简单,和咱们写@Controller时的配置方式基本同样。@FeignClient注解字段有下面这几个:

 @Target(ElementType.TYPE)
 @Retention(RetentionPolicy.RUNTIME)
 @Documented
 public @interface FeignClient {
 
  @AliasFor("name")
  String value() default "";
 
  @Deprecated
  String serviceId() default "";
 
  String contextId() default "";
 
  @AliasFor("value")
  String name() default "";
 
  String qualifier() default "";
 
  String url() default "";
 
  boolean decode404() default false;
 
  Class<?>[] configuration() default {};
 
  Class<?> fallback() default void.class;
 
  Class<?> fallbackFactory() default void.class;
 
  String path() default "";
 
  boolean primary() default true;
 
 }

每一个字段的配置含义你们能够参考GitHub上的文档,或者看这个类的javadoc。经常使用的大概就是name、url、configuration这几个。

name字段有两种含义。若是是配合SpringCloud一块儿使用,而且没有配置url字段的状况下,那么name字段就是服务提供方在Eureka上注册的服务名。Feign会根据name字段到Eureka上找到服务提供方的url。若是没有与SpringCloud一块儿使用,name字段会用作url、contextId等字段的备选:若是没有配置后者,那么就拿name字段值当作后者来使用。

url字段用来指定服务方的地址。这个地址能够不带协议前缀(Http://,feign默认是Http,若是要用Https须要增长配置),例如咱们配置了“ka.test.idc/”,实际调用时则是“Http://ka.test.idc/”。

configuration字段用来为当前接口指定自定义配置。有些接口调用须要在feign通用配置以外增长一些自定义配置,例如调用百度api须要走代理、调用接口须要传一些额外字段等。这些自定义配置就能够经过configuration字段来指定。不过configuration字段只能指定三类自定义配置:Encoder、Decoder和Contract。Encoder和Decoder分别负责处理对象到HttpRequest和HttpResponse到对象的转换;Contract则定义了如何解析这个接口和方法上的注解(SpringCloud就是经过Contract接口的一个子类SpringMvcContract来解析方法上的SpringMVC注解的)。 

调用接口

定义好了上面的接口后,咱们使用起来就很简单了:

 @Service("UserInfoBusiness")
 public class UserInfoBusinessImpl implements UserInfoBusiness {
     @Resource
     private UserInfoService UserInfoService;
     @Override
     public UserInfoBean getUserInfo(String id) {
         //feign链接
         BaseResult<UserInfoVo> response = UserInfoService.queryUserInfoRequest(UserInfoService.Body4UserInfo.of(id));
         // 其它略
     }

能够看到这里的代码,和咱们使用其它的bean的方式是同样的。

注意事项

使用Feign客户端须要注意几个事情。

Feign的RequestMapping不能与本系统中SpringMVC的配置冲突

Feign接口上定义RequestMapping地址与本系统中Controller定义的地址不能有冲突。例如: 

 @Controller
 public class Con{
     @PostMapping("/test")
     public void test(){}
 }
 
 @FeignClient(name="testClient")
 public interface Fei{
     @PostMapping("/test")
     public void test();
 }
 

上面这种状况下,Feign解析会报错。

自定义configuration不能被装载到SpringContext中

经过@FeignClient注解中configuration字段指定的自定义配置类,不能被SpringIoC扫描、装载进来,不然可能会有问题。

通常的文档都是这么写的,可是咱们系统在调用时的自定义配置是会被SpringIOC扫描装载的,并无遇到什么问题。

与SpringMVC配合使用时,须要单独声明HttpMessageConverters

须要指定一个这样的bean,不然在装配Feign时会出现循环依赖的问题:

 
     @Bean
     public HttpMessageConverters HttpMessageConverters() {
         return new HttpMessageConverters();
     }

使用@RequestParam注解时,必须指定name字段

在SpringMVC中,@RequestParam注解若是不指定name字段,那么会以变量名做为queryString的参数名;可是在FeignClient中使用@RequestParam时,则必须指定name字段,不然会没法解析参数。

 @Controller
 public class Con{
     /**这里的@RequestParam不用指定name,调用时会根据变量名自动解析为 test=? */
     @PostMapping("/test")
     public void test(@RequestParam String test){}
 }
 
 
 @FeignClient(name="test")
 public interface Fei{
     /**这里的@RequestParam必须指定name,不然调用时会报错 */
     @GetMapping("/test")
     public String test(@RequestParam(name="test") String test);
     
 }

原理

提及来其实很简单,和其它使用注解的框架同样,Feign是经过动态代理来动态实现@FeignClient的接口的。

详细一点来讲,Feign经过FeignClientBuilder来动态构建被代理对象。在构建动态代理时,经过FeignClientFactoryBean和Feign.Builder来把@FeignClient接口、Feign相关的Configuration组装在一块儿。 

 public class FeignClientBuilder{
      public static final class Builder<T> {
 
   private FeignClientFactoryBean feignClientFactoryBean;
   /**
    * @param <T> the target type of the Feign client to be created
    * @return the created Feign client
    */
   public <T> T build() {
    return this.feignClientFactoryBean.getTarget();
   }
 
 }
  // 其它略
 }
 
 class FeignClientFactoryBean
   implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
 
  <T> T getTarget() {
   FeignContext context = this.applicationContext.getBean(FeignContext.class);
   Feign.Builder builder = feign(context);
 
   if (!StringUtils.hasText(this.url)) {
    if (!this.name.startsWith("Http")) {
     this.url = "Http://" + this.name;
    }
    else {
     this.url = this.name;
    }
    this.url += cleanPath();
    return (T) loadBalance(builder, context,
      new HardCodedTarget<>(this.type, this.name, this.url));
   }
   if (StringUtils.hasText(this.url) && !this.url.startsWith("Http")) {
    this.url = "Http://" + this.url;
   }
   String url = this.url + cleanPath();
   Client client = getOptional(context, Client.class);
   if (client != null) {
    if (client instanceof LoadBalancerFeignClient) {
     // not load balancing because we have a url,
     // but ribbon is on the classpath, so unwrap
     client = ((LoadBalancerFeignClient) client).getDelegate();
    }
    builder.client(client);
   }
   Targeter targeter = get(context, Targeter.class);
   // 在这个里面生成一个代理
   return (T) targeter.target(this, builder, context,
     new HardCodedTarget<>(this.type, this.name, url));
  }      
  // 其它略
 }
 // 中间跳转略
 public class ReflectiveFeign extends Feign {
   public <T> T newInstance(Target<T> target) {
     Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
     Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
     List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
 
     for (Method method : target.type().getMethods()) {
       if (method.getDeclaringClass() == Object.class) {
         continue;
       } else if (Util.isDefault(method)) {
         DefaultMethodHandler handler = new DefaultMethodHandler(method);
         defaultMethodHandlers.add(handler);
         methodToHandler.put(method, handler);
       } else {
         methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
       }
     }
     InvocationHandler handler = factory.create(target, methodToHandler);
     // 在这里生成动态代理。
     T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
         new Class<?>[] {target.type()}, handler);
 
     for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
       defaultMethodHandler.bindTo(proxy);
     }
     return proxy;
   }
 
 }
 // 后续略


qrcode?scene=10000004&size=102&__biz=MzUzNzk0NjI1NQ==&mid=2247484242&idx=1&sn=408f9fb2e4b7d7573623d704c080e2ff&send_time=

相关文章
相关标签/搜索