Feign簡介

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的抽象層次比其它幾個工具、框架都更高。

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

首先,一般來說抽象層次越高,其中包含的功能也就越多。

此外,抽象層次越高,使用起來就越簡便。例如,上面這個例子中,把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=

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章