[享學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)

但是,這樣使用其實是有一定弊端的,且有一定使用門檻:

  1. 使用Builder構建時,使用者還需知道Feign#newInstance這個API
  2. 使用者必須知道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的設計有很多自由的特色:高內聚性、工廠方法…當然這些都是源碼設計上的特點。而在使用層面,我認為它是一個上手非常容易,但精通略難的一種技術,難一般體現在如下幾點:

  1. 知識體系龐大,需要了解的甚多
  2. 設計不合理,閱讀難、擴展難

作為一個流行的開源框架,原因顯然不會是後者。

本文作為Feign核心內容的最後一篇文章就全部介紹完了,有了這些知識相信你完全有自信說自己已經精通Feign了。雖然還沒有了解它如何友好支援JSON,如何和Spring、Spring Cloud整合使用,但相站在精通核心內容的基礎上再看看它們,那些都是小兒科。但是,路漫漫其修遠兮,任重而道遠!!!

聲明

原創不易,碼字不易,多謝你的點贊、收藏、關注。把本文分享到你的朋友圈是被允許的,但拒絕抄襲