[享學Feign] 三、原生Feign的核心API詳解(一):UriTemplate、HardCodedTarget...

  • 2020 年 2 月 21 日
  • 筆記

美國為什麼敢打伊拉克?因為懷疑他有大規模殺傷性武器;美國為什麼不敢打俄羅斯?因為他真的有大規模殺傷性武器。戰場上,請用實力說話

程式碼下載地址:https://github.com/f641385712/feign-learning

前言

前兩篇文章站在使用的角度介紹了源生Feign,相信讀過的話都知道如何使用了。那麼接下來就要動動真格,扒開內褲看看裡面到底是什麼東西。

本文將著重了解Feign的核心API,有點像啃API、啃源碼的意思,所以可能相對枯燥、難懂,所以需要堅持。因為學習起來雖然枯燥,但意義卻是巨大的,正所謂「上天不會虧待努力的人,也不會同情假勤奮的人」。


正文

核心API的講解,將按照自上而下,有內向外的「方向」一一描述。


核心API

註解的使用起來門檻低且簡單,這是元編程的最大優勢。但作為一位專業認識,要真正掌握它還得深入,這就是Feign的核心API部分,它是一切高級使用、訂製的基石。


Template

它用於表示:按照 RFC 6570 標準書寫的表達式模版,有了此模版後續就可以用變數進行替換形成最終值。

說明:模版字元串遵循的標準是RFC 6570,各位可做簡單的學習了解,不用太深。(Spring MVC遵循的是Ant風格的模版,理解起來貌似更容易些…)

public class Template {    	// 唯一構造器,訪問許可權是default  	Template( ... ) { ... }    	// 填充參數 表達式可以是個feign.template.Expression對象  	// 一個{}就是一個Expression對象,根據name(key)進行匹配  	public String expand(Map<String, ?> variables) { ... }  	protected String resolveExpression(Expression expression, Map<String, ?> variables) { ... }    	// Uri Encode  URI編碼  	// encodeSlash true的話連/也會給轉義,默認不轉義它  	private String encode(String value) { ... }    	// 拿到當前template下所有的變數的**名稱**  	public List<String> getVariables() { ... }  }

該類並不是抽象類,並且它還有4個子類,分別對應著可以使用模版語法的幾個註解:


UriTemplate

顧名思義,它是用於處理URI的模版,用於處理@RequestLine的模版

public class UriTemplate extends Template {    	public static UriTemplate create(String template, Charset charset) {  	  return new UriTemplate(template, true, charset);  	}  	... // 省略其它構造器  }

本類源碼非常簡單,並沒有在父類基礎做擴展,僅提供了構建實例的靜態方法(因為父類構造器是default許可權的)。

使用示例:

@Test  public void fun1() {      UriTemplate template = UriTemplate.create("http://example.com/{foo}", StandardCharsets.UTF_8);      Map<String, Object> params = new HashMap<>();      params.put("foo", "bar");      String result = template.expand(params);      System.out.println(result);        System.out.println("=======================================");        // 對斜杠不要轉義      template = UriTemplate.create("http://example.com/{empty}{foo}index.html{frag}",false, StandardCharsets.UTF_8);        params.clear();      // params.put("empty",null);      params.put("foo","houses/");      params.put("frag","?g=sec1.2");      result = template.expand(params);      System.out.println(result);  }

運行程式,輸出:

http://example.com/bar  =======================================  http://example.com/houses/index.html%3Fg=sec1.2

小細節:/沒被轉義,但?被轉義為了%3,可見默認情況下它會對URI上的特殊符號均進行轉義;另外,{empty}因為沒有給值(效果同賦值為null),所以根本就沒出現在URI里。

說明:這是模版處理的一個很大的特點 --> 若對應key不存在或值為null,那麼此部分表達式將被忽略 需要注意的是:若值的空串,那是表示有值,空串而已嘛,還是有意義的

我找了一個關於RFC6570語法格式的在線測試工具:http://www.utilities-online.info/uritemplate,你可以試試,熟悉熟悉feign的URL模版語法。


QueryTemplate

查詢參數(Query String parameter)的模版。用於處理@QueryMap的模版

public final class QueryTemplate extends Template {    	// 這裡name沒有用字元串而是使用了模版類型,是因為name也可以是個模版{}  	// 大部分情況下它可以是字元串即可  	private final Template name;  	// 因為一個key可以對應多值,所以用List肯定沒錯嘍  	private List<String> values;      	// 當一key多值時候用什麼分隔符,支援:, t | 等等    	// 默認它是CollectionFormat.EXPLODED,也就是會以foo=bar&foo=baz這種形式拼接起來    	private final CollectionFormat collectionFormat;    	// 請注意:默認這裡使用的是CollectionFormat.EXPLODED哦    	public static QueryTemplate create(String name, Iterable<String> values, Charset charset) {      	return create(name, values, charset, CollectionFormat.EXPLODED);    	}  	... // 省略append等其它方法    	public List<String> getValues() {  	  return values;  	}  	public String getName() {  	  return name.toString();  	}      	// 可以看到它expand實際上是對name進行expand而已啦~~~~~  	@Override  	public String expand(Map<String, ?> variables) {  	  String name = this.name.expand(variables);  	  return this.queryString(name, super.expand(variables));  	}    	...  }

該模版用於構造一個查詢參數,一次性構建一個,若有多個key需要構建多次。

使用示例:

@Test  public void fun2() {  	// 可以看到key也是可以使用模版的。當然你也可以直接使用字元串即可,也可以混合使用      QueryTemplate template = QueryTemplate.create("hobby-{arg}", Arrays.asList("basket", "foot"), StandardCharsets.UTF_8);      Map<String, Object> params = new HashMap<>();      // params.put("arg", "1");        String result = template.expand(params);      System.out.println(result);        template = QueryTemplate.create("grade", Arrays.asList("1", "2"), StandardCharsets.UTF_8, CollectionFormat.CSV);      System.out.println(template);  }

輸出:

hobby-%7Barg%7D=basket&hobby-%7Barg%7D=foot  grade=1,2

注意:這裡arg必傳,不能給null,否則會輸出:hobby-%7Barg%7D=basket&hobby-%7Barg%7D=foot原樣輸出了,不會忽略內部表達式的。


HeaderTemplate

構造請求頭的模版,幾乎同上,用於處理@QueryMap的模版,本處省略。


BodyTemplate

用於表示標註有@Body註解的模版。

public final class BodyTemplate extends Template {      // 雖不強制必須是Json,但對它進行了特殊支援    private static final String JSON_TOKEN_START = "{";    private static final String JSON_TOKEN_END = "}";    private static final String JSON_TOKEN_START_ENCODED = "%7B";    private static final String JSON_TOKEN_END_ENCODED = "%7D";    private boolean json = false;      public static BodyTemplate create(String template) {       return new BodyTemplate(template, Util.UTF_8);    }    private BodyTemplate(String value, Charset charset) {       super(value, ExpansionOptions.ALLOW_UNRESOLVED, EncodingOptions.NOT_REQUIRED, false, charset);       // 判斷是否是Json。如果你的模版字元串是以%7B打頭%7D結尾的,就標記是JSON,後面填充時會特殊處理       // 說明:這個自己手動構造構造不出來的,只有通過編碼器處理過才有可能這裡是true       if (value.startsWith(JSON_TOKEN_START_ENCODED) && value.endsWith(JSON_TOKEN_END_ENCODED)) {        this.json = true;      }    }      // 若是JSON,會進行特殊處理    @Override    public String expand(Map<String, ?> variables) {      String expanded = super.expand(variables);      if (this.json) {        /* decode only the first and last character */        StringBuilder sb = new StringBuilder();        sb.append(JSON_TOKEN_START);        sb.append(expanded,            expanded.indexOf(JSON_TOKEN_START_ENCODED) + JSON_TOKEN_START_ENCODED.length(),            expanded.lastIndexOf(JSON_TOKEN_END_ENCODED));        sb.append(JSON_TOKEN_END);        return sb.toString();      }      return expanded;    }  }

該模版唯一的特點便是:對JSON格式進行了兼容處理,沒啥太多好說的。

使用示例:

@Test  public void fun3(){      BodyTemplate template = BodyTemplate.create("data:{body}");        Map<String, Object> params = new HashMap<>();      params.put("body", "{"name": "YourBatman","age": 18}");        String result = template.expand(params);      System.out.println(result);  }

輸出:data:{"name": "YourBatman","age": 18}


Target

它作用類似於JAXRS裡面的javax.ws.rs.client.WebTarget這個類,用於把請求模版RequestTemplate 轉換為實際請求實例 feign.Request,此Request後續交給Client發送Http請求。

// 它是個泛型介面  public interface Target<T> {      // 此target作用的介面的類型    Class<T> type();    // 此target配置的一個key,一般情況下沒啥用    // 但會在和feign-hystrix整合時,會作為它的groupKey來使用,這也是它的唯一被使用的地方    String name();    // 發送請求的Base URL。如:https://example/api/v1    String url();      // 最重要的方法:用於把請求模版組裝、加上Base Url、轉換為一個真正的Request    // 此input模版里包含有很多的:QueryTemplate/HeaderTemplate/UriTemplate等等等等    Request apply(RequestTemplate input);  }

該介面有兩個實現類:

說明:Feign的源碼設計上存在一大特點,偏向於把介面默認實現類以及相關類以靜態內部類的形式內聚在一起,所以它的程式碼內聚性是非常之高的。


EmptyTarget

顧名思義,它就是「啥都不做」,它的特點是木有Base URL。

public static final class EmptyTarget<T> implements Target<T> {    	// 說明:它並不需要URL屬性  	private final Class<T> type;      private final String name;        public static <T> EmptyTarget<T> create(Class<T> type) {        return new EmptyTarget<T>(type, "empty:" + type.getSimpleName());      }      ...      // 它並不需要URL,因為不需要做啥      @Override      public String url() {        throw new UnsupportedOperationException("Empty targets don't have URLs");      }    	// 如果請求模版的URL里已經包含了http,也就是說是絕對路徑了,那就拋錯  	// input.request() -> Request.create(this.method, this.url(), this.headers(), this.requestBody())  	// 此處的this.url()就是個相對路徑嘍。比如/api/v1/demo這種      @Override      public Request apply(RequestTemplate input) {        if (input.url().indexOf("http") != 0) {          throw new UnsupportedOperationException("Request with non-absolute URL not supported with empty target");        }        return input.request();      }  	...  }

它最大的特點就是不能有Base Url,所以就好似什麼都沒做一樣


HardCodedTarget

硬編碼目標類。它的三大參數都不能為null

public static class HardCodedTarget<T> implements Target<T> {        private final Class<T> type;      private final String name;      private final String url; // 相較於Empty,它必須有url    	...  	// 不同於其它的,它的實例是通過構造器創建的      public HardCodedTarget(Class<T> type, String name, String url) {        this.type = checkNotNull(type, "type");        this.name = checkNotNull(emptyToNull(name), "name");        this.url = checkNotNull(emptyToNull(url), "url");      }      ... // 省略3個get方法    	// 如果請求模版的URL不含有http,也就是說是個相對路徑,這裡就把Base Url加進去  	// 如果請求模版已經是絕對路徑了,那就不管啦  	@Override      public Request apply(RequestTemplate input) {        if (input.url().indexOf("http") != 0) {          input.target(url());        }        return input.request();      }  }

該實現類是常用的、預設的,比如feign.Feign類就是用它來代理目標介面類的,需要掌握。


Client

顧名思義,它就是用來發送feign.Request這個Http請求的,請注意:實現此介面的子類實現請確保是執行緒安全的(因為可能是多執行緒發送)

public interface Client {  	// Options是一些選項參數,比如:  	// connectTimeoutMillis鏈接超時時間,默認是:10s  	// readTimeoutMillis鏈接超時時間,默認是:60s  	Response execute(Request request, Options options) throws IOException;  }

該介面簡單:有且僅有這一個方法表示執行http請求。有如下兩個實現類:


Default

作為Client介面的默認實現,它是基於JDK的HttpURLConnection來發送Http請求的。feign.Feign默認情況下就是使用它來完成http請求的發送。

class Default implements Client {    	// 這些類都是java.net包下的      private final SSLSocketFactory sslContextFactory;      private final HostnameVerifier hostnameVerifier;    	// 發送Request 請求 -> 最終通過HttpURLConnection 完成的最終發送      @Override      public Response execute(Request request, Options options) throws IOException {       // 通過feign.Request構建出一個HttpURLConnection        HttpURLConnection connection = convertAndSend(request, options);        // 得到Response,交給後面去解析        return convertResponse(connection, request);      }  	...  }

這是Feign預設的發送請求的方式:底層使用JDK源生的HttpURLConnection。對於JDK源生的這個Client,請務必注意它自動把GET給你轉為POST的場景,避免踩坑哦。


Proxied

支援到JDK的java.net.Proxy(比如翻牆),使用極少,略。

說明:此處指的是java.net.Proxy,而不是java.lang.reflect.Proxy


Retryer

重試器。它能對每次執行Http請求Client#execute()的狀態克隆保持,從而根據配置決定是否應該重試。

public interface Retryer extends Cloneable {      // 如果允許重試,則return返回(可能在睡眠後)。    // 否則會把這個異常繼續傳播    void continueOrPropagate(RetryableException e);      @Override    Retryer clone();     	// 永不重試實例   	// 生產環境下:建議使用永不重試,否則請做好冪等    Retryer NEVER_RETRY = new Retryer() {      @Override      public void continueOrPropagate(RetryableException e) {        throw e;      }      @Override      public Retryer clone() {        return this;      }    };  }

內置的有且僅有一個實現:feign.Retryer.Default。


Default

Feign默認使用的是Default這個實例,而不是NEVER_RETRY永不重試哦。

class Default implements Retryer {    	// 重試參數      private final int maxAttempts;      private final long period;      private final long maxPeriod;    	// 內部統計指標計數      int attempt; //計數:重試了幾次      long sleptForMillis; // 一共休息了多久    	// 空構造,默認重試參數  	// period:默認100ms重試一次  	// maxPeriod:最大重試1秒鐘  	// maxAttempts:最多重試5次  	//      public Default() {        this(100, SECONDS.toMillis(1), 5);      }      ...    	@Override      public void continueOrPropagate(RetryableException e) {       // 若超過了最大重試次數,異常繼續向上拋出        if (attempt++ >= maxAttempts) {          throw e;        }        // ... // 一套重試邏輯  	}    	// 注意:此處克隆就是new一個新的實例供以使用(配置完全一樣嘍)      @Override      public Retryer clone() {        return new Default(period, maxPeriod, maxAttempts);      }  }

因此注意注意注意:默認情況下,Feign是有重試機制的,並且是100ms重試一次,默認重試5次。

說明:很多小夥伴跌到在這裡,生產環境下我建議你務必關閉Feign的重試機制,避免不必要的麻煩


總結

Feign的核心API內容不少,第一部分就講解到這了。

Template負責對模版語法的解析、Target代理著介面並且把RequestTemplate轉為Request、Client負責把Request通過Http請求發送出去、Retryer負責失敗重試邏輯。還有請求具體發送流程,它是通過MethodHandler去處理的,這在下篇文章會詳細介紹。

聲明

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

Tags: