分享

Feign简介

 景昕的花园 2023-10-10 发布于北京

Feign

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

WHAT

Feign的GitHub描述如下:

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.

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

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

 @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>了。

WHY

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

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

首先,一般来说抽象层次越高,其中包含的功能也就越多。

此外,抽象层次越高,使用起来就越简便。例如,上面这个例子中,把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;
  }
 
 }
 // 后续略

    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多