[享學Feign] 八、Feign是如何生成介面代理對象的?Feign實例的構建器Feign.Builder詳解
- 2020 年 2 月 21 日
- 筆記
如果你覺得自己很重要,就不要把你的API設計得很粗糙,一是要穩定,二是要非常簡單好用。
程式碼下載地址:https://github.com/f641385712/feign-learning
前言
一步步穩紮穩打,終於到了該研究平時接觸的API:feign.Feign本身。 feign.Feign它是最上層的API,是使用者直接使用的對象,它能完成對介面生成動態代理對象,從而很方面的讓你面向介面編程,而不用太過例會Feign內部的實現細節。
如果說前面七篇都是在打基礎,那麼它們均是在幫你理解這篇的內容而服務。那麼作為最重要的一步:生成介面代理對象有何內幕?以及平時我們用於構建Feign的構建器Feign.Builder有何文章可循,本篇文章將徹底為你揭秘,幫你完整掌握Feign的核心內容。
說明:本文是Feign核心內容的最後一篇,接下來的內容將聚焦在擴展、集成、訂製方面。
正文
作為Feign核心內容的最後一文,本文將聚焦在Feign實例本身,從源碼深處講解它的實現內幕。
feign.Feign
Feign的目的是簡化針對rest的Http Api的開發。在實現中,Feign是一個用於生成目標實例Feign#newInstance()的工廠,這個生成的實例便是介面的代理對象。
該類是個抽象類:
public abstract class Feign { // 唯一的public的抽象方法,用於為目標target創建一個代理對象實例 public abstract <T> T newInstance(Target<T> target); // -----------------靜態方法----------------- // 工具方法,生成configKey // MethodMetadata#configKey屬性的值就來自於此方法 public static String configKey(Class targetType, Method method) { ... } // Feign的實例統一,有且只能通過builder構建,文下有詳解,它是重點 public static Builder builder() { return new Builder(); } }
feign.Feign它有且僅有一個唯一實現類,但這個實現類並不面向使用者,使用者只需用Builder構建,面向介面/抽象類編程足矣。但作為本專欄的定位,還需弄清楚它的來龍去脈,迎難而上吧~
ParseHandlersByName
在接觸ReflectiveFeign之前,我認為很有必要了解下它。 該類有且僅提供一個方法:將feign.Target轉為Map<String, MethodHandler>,就是說為每一個configKey(其實就是一個Method),找到一個處理它的MethodHandler。
題外話:我個人感覺,相較於Spring家族,Feign的各種取名確實有點隨意,有點任性。。。
static final class ParseHandlersByName { // 提取器 private final Contract contract; private final Options options; private final Encoder encoder; private final Decoder decoder; private final ErrorDecoder errorDecoder; private final QueryMapEncoder queryMapEncoder; // 這個工廠只產生SynchronousMethodHandler實例 // 從這也很好理解:所有的Method最終的處理器都是SynchronousMethodHandler private final SynchronousMethodHandler.Factory factory; ... // 省略構造器 public Map<String, MethodHandler> apply(Target key) { // 通過Contract提取出該類所有方法的元數據資訊:MethodMetadata // 它會解析註解,不同的實現支援的註解是不一樣的 List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type()); // 一個方法一個方法的處理,生成器對應的MethodHandler處理器 // 上篇文章有講過,元數據都是交給RequestTemplate.Factory去構建成為一個請求模版的 Map<String, MethodHandler> result = new LinkedHashMap<>(); for (MethodMetadata md : metadata) { // 這裡其實我覺得使用介面RequestTemplate.Factory buildTemplate更加的合適 BuildTemplateByResolvingArgs buildTemplate; // 針對不同元數據參數,調用不同的RequestTemplate.Factory實現類完成處理 if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { // 若存在表單參數formParams,並且沒有body模版,那就執行表單形式的構建 buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else if (md.bodyIndex() != null) { // 若存在body,那就是body嘍 buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder); } else { // 否則就是普通形式:查詢參數構建方式 buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder); } // 通過factory.create創建出MethodHandler實例,快取結果 result.put(md.configKey(), factory.create(...)); return result; } }
步驟、過程介紹都標註在源碼處了,一句話總結ParseHandlersByName的作用:為指定介面類型的每個方法生成其對應的MethodHandler處理器(可能是默認方法直接執行處理、也可能是發送http請求去處理)。
該步驟中,涉及到元數據提取、編碼、模版數據填充等動作,均交給不同的組件去完成,組件化的設計有助於模組化、可插拔等特性。
ReflectiveFeign
Reflective中文釋義:反射的。 它是feign.Feign的唯一實現,但是它並不提供public的構造器,因此外部並不能直接構建它。
public class ReflectiveFeign extends Feign { // 文上有解釋次類的作用:提供方法,給介面每個方法生成一個處理器 private final ParseHandlersByName targetToHandlersByName; // 它是調度中心 private final InvocationHandlerFactory factory; private final QueryMapEncoder queryMapEncoder; ... // 省略構造器 }
準備好幾大「工具」後,下面開始幹活了:實現父類newInstance()抽象方法,給Target<T>生成一個實例(實為代理對象)返回。
ReflectiveFeign: @Override public <T> T newInstance(Target<T> target) { // 拿到該介面所有方法對應的處理器的Map Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); // 真要處理調用的Method對應的處理器Map Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<>(); // 簡單的說:對介面默認方法作為處理方法提供支援,不用發http請求嘍,一般可忽略 List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<>(); // 查閱該介面所有的Method。 // .getMethods()會獲取介面的所有的public方法,包括default方法哦(因為defualt方法也是public的) for (Method method : target.type().getMethods()) { ... //如果是Object的方法,直接continue。method.getDeclaringClass() == Object.class ... // 如果是Defualt默認方法,那該方法就用DefaultMethodHandler去處理 ... // 否則,就nameToHandler.get(Feign.configKey(target.type(), method))用它去處理 } // 為該目標介面類型創建一個InvocationHandler // 它持有methodToHandler這個Map,負責全局調度所有的Method方法 // 並且為此介面創建一個代理對象 InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); // 根據前文知道:要想調用DefaultMethodHandler#invoke之前,必須先bind // 因此這裡千萬別忘了來一次bind綁定動作 for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; }
就這樣,通過ReflectiveFeign的newInstance方法給該介面創建出來了一個代理實例,它使用的處理器是FeignInvocationHandler。
說明:關於
FeignInvocationHandler如何完成全局調度,本專欄第三篇文章就已講解。
另外想強調一點:ReflectiveFeign為介面生成動態代理實例的步驟比較重要,我把相關說明都備註在源碼處,希望同學自己可以總結一份文字步驟出來,學以致用。
feign.Feign.Builder
了解了ReflectiveFeign的作用,它作為feign.Feign的唯一實現,但其實我們在使用過程中幾乎不會使用它和接觸它,因為構建實例均通過工廠來進行,這便是feign.Feign.Builder。
public static class Builder { // 請求模版的攔截器,默認的空的,木有哦,你可以自定義,在builder的時候加進來 private final List<RequestInterceptor> requestInterceptors = new ArrayList<>(); // 這是Feign自己的日誌級別,默認不輸出日誌 // 因此若你要列印請求日誌,這裡級別需要調高 private Logger.Level logLevel = Logger.Level.NONE; // 默認使用的日誌記錄器,也是不作為 private Logger logger = new NoOpLogger(); // 默認使用的提取器,就是支援@RequestLine源生註解的這種 private Contract contract = new Contract.Default(); // 默認使用JDK的`HttpURLConnection`發送請求,並且是非SSL加密的 private Client client = new Client.Default(null, null); // 請務必注意:默認情況下Feign是開啟了重試的 // 100ms重試一次,一共重試5次。最長持續1s鍾 // 在生產環境下,重試請務必慎用 private Retryer retryer = new Retryer.Default(); // 默認10s鏈接超時,60s讀取超時 private Options options = new Options(); // 默認編碼器:只支援String類型的編碼 // 請注意:編碼器的生效時機哦~~~(沒有標註@Param註解會交給編碼器處理) private Encoder encoder = new Encoder.Default(); // 默認只能解碼String類型和位元組數組類型 private Decoder decoder = new Decoder.Default(); // 支援把@QueryMap標註在Map or Bean前面 // Bean需要有public的get方法才算一個屬性 private QueryMapEncoder queryMapEncoder = new QueryMapEncoder.Default(); // 把異常、錯誤碼包裝為FeignException異常向上拋出 private ErrorDecoder errorDecoder = new ErrorDecoder.Default(); // FeignInvocationHandler是它唯一的實現 private InvocationHandlerFactory invocationHandlerFactory = new InvocationHandlerFactory.Default(); // 默認不會解碼404 private boolean decode404; private boolean closeAfterDecode = true; // 異常傳播策略:NONE代表不包裝 不處理,直接拋 private ExceptionPropagationPolicy propagationPolicy = NONE; }
這個Builder構建器的內容非常豐富,是對前面講解幾乎所有組件的一個總結。
我們設計一個程式希望它對外是可擴展的,因此通過Builder暴露出這些鉤子是完全合理的,當然只有你真正理解了這些組件的作用,你才能在訂製上做到遊刃有餘。
說明:源碼上的標註我都有簡要的說明,欲知詳細,請參考本專欄的第三篇和第四篇文章。
Builder的方法效果同set方法,沒什麼太多可說的,但這裡我摘出幾個,相對重視一下:
Feign.Builder: // 這兩個方法調用其一,方法二是一的加強版 public Builder decoder(Decoder decoder) { this.decoder = decoder; return this; } public Builder mapAndDecode(ResponseMapper mapper, Decoder decoder) { this.decoder = new ResponseMappingDecoder(mapper, decoder); return this; } // 攔截器一個個添加,可以多次調用添加多個 public Builder requestInterceptor(RequestInterceptor requestInterceptor) { this.requestInterceptors.add(requestInterceptor); return this; } // 請注意:如果一次性添加多個,那麼此方法相當於set方法哦,並不是add public Builder requestInterceptors(Iterable<RequestInterceptor> requestInterceptors) { this.requestInterceptors.clear(); for (RequestInterceptor requestInterceptor : requestInterceptors) { this.requestInterceptors.add(requestInterceptor); } return this; } // 構建:返回一個ReflectiveFeign實例 public Feign build() { ... return new ReflectiveFeign( ... ); }
創建介面代理實例(程式碼示例)
有了Builder構建器,這樣得到介面的代理實例,就可這麼做啦:
@Test public void fun1() { Feign feign = Feign.builder().build(); DemoClient client = feign.newInstance(new Target.HardCodedTarget<>(DemoClient.class, "http://localhost:8080")); System.out.println(client); }
執行程式,控制台輸出:
HardCodedTarget(type=DemoClient, url=http://localhost:8080)
但是,這樣使用其實是有一定弊端的,且有一定使用門檻:
- 使用Builder構建時,使用者還需知道
Feign#newInstance這個API - 使用者必須知道
Target的實現類HardCodedTarget才能完成構建
那麼,作為一個合格的Builder,能否能屏蔽一些底層API,做到一步到位呢? 答案是肯定的,builder還額外提供了如下兩個方法:
Feign.Builder // 很顯然:方法一對調用者更加友好,因為Class和URL這些是使用者沒有門檻的 // 方法一依賴於方法二,它內部幫你構建Target的實例 public <T> T target(Class<T> apiType, String url) { return target(new HardCodedTarget<T>(apiType, url)); } public <T> T target(Target<T> target) { return build().newInstance(target); }
那麼對於上例,其實完全可以非常簡單的一步到位,簡單都如下程式碼所示:
@Test public void fun2(){ DemoClient client = Feign.builder().target(DemoClient.class, "http://localhost:8080"); System.out.println(client); }
運行程式,結果同上。 因此這麼做才是在生產上的推薦做法,同時也不得不誇讚一下:Feign.Builder是個合格的構建者。
總結
關於feign.Feign對象本身的介紹到這就結束了。 Feign的設計有很多自由的特色:高內聚性、工廠方法…當然這些都是源碼設計上的特點。而在使用層面,我認為它是一個上手非常容易,但精通略難的一種技術,難一般體現在如下幾點:
- 知識體系龐大,需要了解的甚多
- 設計不合理,閱讀難、擴展難
作為一個流行的開源框架,原因顯然不會是後者。
本文作為Feign核心內容的最後一篇文章就全部介紹完了,有了這些知識相信你完全有自信說自己已經精通Feign了。雖然還沒有了解它如何友好支援JSON,如何和Spring、Spring Cloud整合使用,但相站在精通核心內容的基礎上再看看它們,那些都是小兒科。但是,路漫漫其修遠兮,任重而道遠!!!
聲明
原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲。


