Feign源碼解析系列-核心初始化

来源:https://www.cnblogs.com/killbug/archive/2019/03/19/10562164.html
-Advertisement-
Play Games

開始 初始化Feign客戶端當然是整個過程中的核心部分,畢竟初始化完畢就等著調用了,初始化時候準備的什麼,流程就走什麼。 內容 從上一篇中,我們已經知道,對於掃描到的每一個有@FeignClient,都會組裝一個FactoryBean即FeignClientFactoryBean註冊到spring容 ...


開始

初始化Feign客戶端當然是整個過程中的核心部分,畢竟初始化完畢就等著調用了,初始化時候準備的什麼,流程就走什麼。

內容

從上一篇中,我們已經知道,對於掃描到的每一個有@FeignClient,都會組裝一個FactoryBean即FeignClientFactoryBean註冊到spring容器中,如此在spring 容器初始化的時候,創建FeignClient的Bean時都會調用FeignClientFactoryBean的getObject方法。
FeignClientFactoryBean是Spring的FactoryBean,在Spring的世界里可以通過xml定義bean,也可以通過@Bean註解的方法組裝bean,但如果我們要的bean產生過程比較複雜,使用配置或單純的new不好解決,這時候使用FactoryBean就比較合適了,在Spring中想要找某個類型的bean時,如果是FactoryBean定義的,就會調用它的getObject獲取這個bean。
FeignClientFactoryBean的getObject方法:

public Object getObject() throws Exception {
   FeignContext context = applicationContext.getBean(FeignContext.class);
   // 構建Feign.Builder
   Feign.Builder builder = feign(context);
   if (!StringUtils.hasText(this.url)) {
      String url;
      if (!this.name.startsWith("http")) {
         url = "http://" + this.name;
      }
      else {
         url = this.name;
      }
      url += cleanPath();
      return loadBalance(builder, context, new HardCodedTarget<>(this.type,
            this.name, 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 lod 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 targeter.target(this, builder, context, new HardCodedTarget<>(
         this.type, this.name, url));
}

構建feign.builder時會向FeignContext獲取配置的Encoder,Decoder等各種信息。FeignContext在上篇中已經提到會為每個Feign客戶端分配了一個容器,它們的父容器就是spring容器,凡是在子容器中找不到的對象,再從父容器中找。
我們可以在Feign.Builder中看全部的可配置的屬性,會發現有些信息在feignclient註解上有可以直接通過註解屬性欄位進行設置,比如ecode404,而有些屬性是只能通過註解屬性configuration配置configuration類來註入配置信息,比如:Retryer。另外除了通過在註解屬性上進行配置信息外,也可以通過FeignClientProperties來配置這些信息。
在configureFeign方法中看到可以通通過defaultToProperties屬性來控制兩者的優先順序,預設為true,比如defaultToProperties設置為false時,則會先向Feign.Builder放配置文件配置的信息,然後再放註解上配置的,後放的當然可以覆蓋先放的,所以註解配置的優先順序就算高的(除了RequestInterceptor,這個是沒有什麼優先順序的,是add上去的)。

protected Feign.Builder feign(FeignContext context) {
   FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
   Logger logger = loggerFactory.create(this.type);
   // @formatter:off
   Feign.Builder builder = get(context, Feign.Builder.class)
         // required values
         .logger(logger)
         .encoder(get(context, Encoder.class))
         .decoder(get(context, Decoder.class))
         .contract(get(context, Contract.class));
   // @formatter:on
   configureFeign(context, builder);
   return builder;
}
protected void configureFeign(FeignContext context, Feign.Builder builder) {
   FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
   if (properties != null) {
      if (properties.isDefaultToProperties()) {
         configureUsingConfiguration(context, builder);
         configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
         configureUsingProperties(properties.getConfig().get(this.name), builder);
      } else {
         configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
         configureUsingProperties(properties.getConfig().get(this.name), builder);
         configureUsingConfiguration(context, builder);
      }
   } else {
      configureUsingConfiguration(context, builder);
   }
}
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
   Logger.Level level = getOptional(context, Logger.Level.class);
   if (level != null) {
      builder.logLevel(level);
   }
   Retryer retryer = getOptional(context, Retryer.class);
   if (retryer != null) {
      builder.retryer(retryer);
   }
   ErrorDecoder errorDecoder = getOptional(context, ErrorDecoder.class);
   if (errorDecoder != null) {
      builder.errorDecoder(errorDecoder);
   }
   Request.Options options = getOptional(context, Request.Options.class);
   if (options != null) {
      builder.options(options);
   }
   Map<String, RequestInterceptor> requestInterceptors = context.getInstances(
         this.name, RequestInterceptor.class);
   if (requestInterceptors != null) {
      builder.requestInterceptors(requestInterceptors.values());
   }
   if (decode404) {
      builder.decode404();
   }
}

無論是通過配置文件還是註解屬性,能夠控制的都是一個feignclient整體的配置。而我們在寫feign介面的方法是,還需要定義這個介面方法的http描述信息,比如請求路徑,請求方式,參數定義等等。也就是說,對於一個單獨的請求來說,完整配置的粒度要到feign介面里的方法級別。
在getObject方法的最後會調用Targeter.target方法來組裝對象,Targeter是可以被擴展的,先不展開了,在預設的實現中會調用前面組裝好的Feign.Builder的target方法:

class DefaultTargeter implements Targeter {
   @Override
   public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
                  Target.HardCodedTarget<T> target) {
      return feign.target(target);
   }
}

Feign.Builder的target方法會觸發建造者的構建操作:

public <T> T target(Target<T> target) {
  return build().newInstance(target);
}  
public Feign build() {
    SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
        new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
                                             logLevel, decode404);
    ParseHandlersByName handlersByName =
        new ParseHandlersByName(contract, options, encoder, decoder,
                                errorDecoder, synchronousMethodHandlerFactory);
    return new ReflectiveFeign(handlersByName, invocationHandlerFactory);
  }

可以想象,我們只是定義了介面,通過介面的方法我們需要達成一個請求應用的操作,肯定是需要產生一個類來實現這些介面的,這裡使用動態代理非常合適,那麼事情就變得簡單了,通過jdk自帶的動態代理方式為介面產生一個代理實現類。這個實現思路可以借鑒到其他的場景,比如比較熟悉的mybatis定義的mapper介面,也是不需要實現的,實現的方式和這裡是一模一樣。
這個實現從ReflectiveFeign的newInstance(target)方法開始:

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;
}

從實現的代碼中可以看到熟悉的Proxy.newProxyInstance方法產生代理類。而這裡需要對每個定義的介面方法進行特定的處理實現,所以這裡會出現一個MethodHandler的概念,就是對應方法級別的InvocationHandler。
for迴圈是在過濾不必要的方法,有意思的一個地方:Util.isDefault(method)這個方法展開看一下:

/**
 * Identifies a method as a default instance method.
 */
public static boolean isDefault(Method method) {
  // Default methods are public non-abstract, non-synthetic, and non-static instance methods
  // declared in an interface.
  // method.isDefault() is not sufficient for our usage as it does not check
  // for synthetic methods.  As a result, it picks up overridden methods as well as actual default methods.
  final int SYNTHETIC = 0x00001000;
  return ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC | SYNTHETIC)) ==
          Modifier.PUBLIC) && method.getDeclaringClass().isInterface();
}

註釋說,沒有使用Method.isDefault()是因為嫌棄它不夠全面的識別,說應該過濾掉合成(synthetic)方法,synthetic methods是編譯時自動加入的方法。

另外,Map<String, MethodHandler>的key是用Feign.configKey(target.type(), method)生成的,我覺得是可以通用:

public static String configKey(Class targetType, Method method) {
  StringBuilder builder = new StringBuilder();
  builder.append(targetType.getSimpleName());
  builder.append('#').append(method.getName()).append('(');
  for (Type param : method.getGenericParameterTypes()) {
    param = Types.resolve(targetType, targetType, param);
    builder.append(Types.getRawType(param).getSimpleName()).append(',');
  }
  if (method.getParameterTypes().length > 0) {
    builder.deleteCharAt(builder.length() - 1);
  }
  return builder.append(')').toString();
}

targetToHandlersByName.apply(target);會解析介面方法上的註解,從而解析出方法粒度的特定的配置信息,然後生產一個SynchronousMethodHandler
然後需要維護一個<method,MethodHandler>的map,放入InvocationHandler的實現FeignInvocationHandler中。
在FeignInvocationHandler中的的invoke方法實現:

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  if ("equals".equals(method.getName())) {
    try {
      Object
          otherHandler =
          args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
      return equals(otherHandler);
    } catch (IllegalArgumentException e) {
      return false;
    }
  } else if ("hashCode".equals(method.getName())) {
    return hashCode();
  } else if ("toString".equals(method.getName())) {
    return toString();
  }
  return dispatch.get(method).invoke(args);
}

當代理類接到執行請求時, 通過一個map分發給對應的MethodHandler執行,如此就實現了針對每個方法的個性化代理實現。
所以,結構就是一個InvocationHandler對應多個MethodHandler:

MethodHandler的實現這裡是使用SynchronousMethodHandler,它實現的invoke方法如下:

public Object invoke(Object[] argv) throws Throwable {
  RequestTemplate template = buildTemplateFromArgs.create(argv);
  Retryer retryer = this.retryer.clone();
  while (true) {
    try {
      return executeAndDecode(template);
    } catch (RetryableException e) {
      retryer.continueOrPropagate(e);
      if (logLevel != Logger.Level.NONE) {
        logger.logRetry(metadata.configKey(), logLevel);
      }
      continue;
    }
  }
}

到這裡就會創建http請求模版,這部分後續再深入。

結束

可以看到產生的FeignClient的代理對象,代理了介面方法,實際會生成一個http請求模版,進行請求操作。
回到前面觸發的地方是spring調用FeignClientFactoryBean的getObject方法,所以產生的這個FeignClient的代理對象會在spring容器中,我們直接可以從spring容器中拿來使用。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 跨域問題是由於瀏覽器為了防止CSRF攻擊(Cross-site request forgery跨站請求偽造),避免惡意攻擊而帶來的風險而採取的同源策略限制。當一個頁面中使用XMLHTTPRequest(XHR請求)對象發送HTTP請求時,必須保證當前頁面和請求的對象是同源的,即協議、功能變數名稱和埠號要完 ...
  • 使用python進行數據分析時,經常會用Pandas類庫處理數據,將數據轉換成我們需要的格式。Pandas中的有兩個數據結構和處理數據相關,分別是Series和DataFrame。 Series Series是一種類似於一維數組的對象,它有兩個屬性,value和index索引。可以像數組那樣通過索引 ...
  • 1 //幫我們創建容器 2 @RunWith(SpringJUnit4ClassRunner.class) 3 //指定創建容器時使用的配置文件 4 @ContextConfiguration("classpath:applicationContext.xml") 5 public class Te... ...
  • 背景人物介紹 “小明“,98後,9年義務教育比較“優秀”,沒考上大學,或者說沒勇氣參加高考,走的“單招”(你可能沒聽說過,就是高職學校的自主招生),一番努力下,考上了一所普通高職大專學生,高職學校一般為訂單培養,校企合作。大學3年努力一把,即將面臨畢業,6月份之前,需要找到工作! 就業範圍 北方人, ...
  • XML- XML(EXtensibleMarkupLanguage) - 官方文檔http://www.w3school.com.cn/xml/index.asp- 概念:父節點,子節點,先輩節點,兄弟節點,後代節點XPath- XPath(XML Path Language), 是一門在XML文檔 ...
  • 給定一個字元串 s 和一些長度相同的單詞 words。找出 s 中恰好可以由 words 中所有單詞串聯形成的子串的起始位置。註意子串要與 words 中的單詞完全匹配,中間不能有其他字元,但不需要考慮 words 中單詞串聯的順序。 示例 1:輸入: s = "barfoothefoobarman ...
  • 遇到用戶要根據下層物料反查最上層BOM物料是什麼。 試了一下,通過函數 CS_WHERE_USED_MAT 來查詢,但是只能往上查詢一層,類似事務碼CS15的效果。如果要找最上層物料,需要自己寫迭代進行查詢。 或者可以參考SAP程式 RCS15001,可以實現多級查詢。 ...
  • 又到了金三銀四找工作的時間,相信很多開發者都在找工作或者準備著找工作了。一般應對面試,我們無可厚非的去刷下麵試題。對於PHPer來說,除了要熟悉自己所做的項目,還有懂的基本的演算法。下麵來分享下PHP面試中常會問到的演算法:冒泡排序和快速排序 冒泡排序:一一對比排序 基本思想: 重覆地走訪過要排序的元素 ...
一周排行
    -Advertisement-
    Play Games
  • C#TMS系統代碼-基礎頁面BaseCity學習 本人純新手,剛進公司跟領導報道,我說我是java全棧,他問我會不會C#,我說大學學過,他說這個TMS系統就給你來管了。外包已經把代碼給我了,這幾天先把增刪改查的代碼背一下,說不定後面就要趕鴨子上架了 Service頁面 //using => impo ...
  • 委托與事件 委托 委托的定義 委托是C#中的一種類型,用於存儲對方法的引用。它允許將方法作為參數傳遞給其他方法,實現回調、事件處理和動態調用等功能。通俗來講,就是委托包含方法的記憶體地址,方法匹配與委托相同的簽名,因此通過使用正確的參數類型來調用方法。 委托的特性 引用方法:委托允許存儲對方法的引用, ...
  • 前言 這幾天閑來沒事看看ABP vNext的文檔和源碼,關於關於依賴註入(屬性註入)這塊兒產生了興趣。 我們都知道。Volo.ABP 依賴註入容器使用了第三方組件Autofac實現的。有三種註入方式,構造函數註入和方法註入和屬性註入。 ABP的屬性註入原則參考如下: 這時候我就開始疑惑了,因為我知道 ...
  • C#TMS系統代碼-業務頁面ShippingNotice學習 學一個業務頁面,ok,領導開完會就被裁掉了,很突然啊,他收拾東西的時候我還以為他要旅游提前請假了,還在尋思為什麼回家連自己買的幾箱飲料都要叫跑腿帶走,怕被偷嗎?還好我在他開會之前拿了兩瓶芬達 感覺感覺前面的BaseCity差不太多,這邊的 ...
  • 概述:在C#中,通過`Expression`類、`AndAlso`和`OrElse`方法可組合兩個`Expression<Func<T, bool>>`,實現多條件動態查詢。通過創建表達式樹,可輕鬆構建複雜的查詢條件。 在C#中,可以使用AndAlso和OrElse方法組合兩個Expression< ...
  • 閑來無聊在我的Biwen.QuickApi中實現一下極簡的事件匯流排,其實代碼還是蠻簡單的,對於初學者可能有些幫助 就貼出來,有什麼不足的地方也歡迎板磚交流~ 首先定義一個事件約定的空介面 public interface IEvent{} 然後定義事件訂閱者介面 public interface I ...
  • 1. 案例 成某三甲醫預約系統, 該項目在2024年初進行上線測試,在正常運行了兩天後,業務系統報錯:The connection pool has been exhausted, either raise MaxPoolSize (currently 800) or Timeout (curren ...
  • 背景 我們有些工具在 Web 版中已經有了很好的實踐,而在 WPF 中重新開發也是一種費時費力的操作,那麼直接集成則是最省事省力的方法了。 思路解釋 為什麼要使用 WPF?莫問為什麼,老 C# 開發的堅持,另外因為 Windows 上已經裝了 Webview2/edge 整體打包比 electron ...
  • EDP是一套集組織架構,許可權框架【功能許可權,操作許可權,數據訪問許可權,WebApi許可權】,自動化日誌,動態Interface,WebApi管理等基礎功能於一體的,基於.net的企業應用開發框架。通過友好的編碼方式實現數據行、列許可權的管控。 ...
  • .Net8.0 Blazor Hybird 桌面端 (WPF/Winform) 實測可以完整運行在 win7sp1/win10/win11. 如果用其他工具打包,還可以運行在mac/linux下, 傳送門BlazorHybrid 發佈為無依賴包方式 安裝 WebView2Runtime 1.57 M ...