[享學Feign] 五、原生Feign的編碼器Encoder、QueryMapEncoder

  • 2020 年 2 月 21 日
  • 筆記

士不可不弘毅,任重而道遠。

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

前言

編碼和解碼的概念最初是出現在硬體範疇,最後引申到軟體領域,它和加密、解密很多時候是一組同義詞。下面這段解釋摘抄自百科:

  • 編碼器:將訊號(如比特流)或數據進行編製、轉換為可用以通訊、傳輸和存儲的訊號形式的設備。
    • 功能:轉換訊號形式
  • 解碼器:是一種能將數字視音頻數據流解碼還原成模擬視音頻訊號的硬體/軟體設備。

很多框架都存在著編碼、解碼動作,Feign也不例外。本專欄將連續花兩篇文章,就專註於講解Feign的編碼器Encoder、解碼器Decoder,以及它們是如何工作的,它對後續理解如何整合Spring MVC是有非常多的幫助。


正文

編解碼一般是一對逆操作,而對於Http的編碼解碼並不是這樣的,因為他倆面向的對象不一樣:

  • 編碼器作用於請求Request階段
  • 解碼器作用域響應Response階段

編碼器Encoder

將對象編碼到HTTP請求體中。功能類似於javax.websocket.Encoder。當方法參數沒有標註@Param註解時,編碼器會起作用。

所以說,如果你不給參數標註@Param註解,就可以通過Encoder編碼器把POJO編碼進Body體里(如果你需要JSON格式,可以藉助JSON庫)

public interface Encoder {    	// 變數輸入到Map<String, ?>,表示要編碼的對象是一個表單  	Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD;    	// 唯一介面方法:object 需要被編碼的對象(有可能是POJO,有可能是字元串)  	// bodyType:body類型  	// template:請求模版    	void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException;  }

需要注意的是:如果方法參數並沒有被模版使用,那麼他會被收集放到一個Map里,然後交給Encoder處理。形如這個例子:

@RequestLine("POST /")  Session login(@Param("username") String username, @Param("password") String password);

這裡的username和password均沒有被@RequestLine使用到,所以它會被收集起放進一個Map里(LinkedHashMap)交給編碼器處理。


Default

Feign內置的唯一編碼器實現,也就是預設實現嘍。

class Default implements Encoder {    	@Override  	public void encode(Object object, Type bodyType, RequestTemplate template) {    	 // 1、若bodyType是String類型,那就把object直接toString()後放進去即可  這是特殊的處理...  	 // 2、若是位元組數組類型,那就強轉放進去嘍  	 // 3、否則就報錯  	  if (bodyType == String.class) {  	    template.body(object.toString());  	  } else if (bodyType == byte[].class) {  	    template.body((byte[]) object, null);  	  } else if (object != null) {  	    throw new EncodeException(  	        format("%s is not a type supported by this encoder.", object.getClass()));  	  }  	}  }

這是一個非常"簡陋"的編碼器實現,它僅能處理String類型、byte[]類型,那麼我們最最最最常用的類型:Object是個POJO,但bodyType是個String(JSONStr嘛)它就會報錯,當然拉這是後面集成、訂製的主要內容。

本文還是輔以案例,必須定位編碼器Encoder到底是什麼,搞清楚它何時生效~


使用示例

下面我造出了7個使用Demo案例,相信能覆蓋你實際使用中99%的場景~

public interface EncoderClient {        // 1、都標註有@Param註解,並且並且並且都被模版使用了      @RequestLine("POST /{name}/{age}")      String encoderDemo1(@Param("name") String name, @Param("age") Integer age);        // 2、都標註有@Param註解,但模版只使用一個      @RequestLine("POST /{name}")      String encoderDemo2(@Param("name") String name, @Param("age") Integer age);        // 3、都標註有@Param註解,但模版都沒有使用      @RequestLine("POST /")      String encoderDemo3(@Param("name") String name, @Param("age") Integer age);        // 4、不標註@Pram註解,是String類型      @RequestLine("POST /")      String encoderDemo4(String name);        // 5、不標註@Pram註解,是Object類型,但實際傳String類型      @RequestLine("POST /")      String encoderDemo5(Object name);        // 6、不標註@Pram註解,是POJO      @RequestLine("POST /")      String encoderDemo6(Person person);        // 6、標註@Pram註解,是POJO      @RequestLine("POST /")      String encoderDemo7(@Param("person") Person person);    }

測試程式程式碼:

@Test  public void fun1(){      EncoderClient client = FeignClientFactory.create(EncoderClient.class);        client.encoderDemo1("YourBatman1", 18);      System.err.println(" ------------------------- ");        try { client.encoderDemo2("YourBatman2", 18); }catch (Exception e) {e.printStackTrace();}      System.err.println(" ------------------------- ");        try { client.encoderDemo3("YourBatman3", 18); }catch (Exception e) {e.printStackTrace();}      System.err.println(" ------------------------- ");        try { client.encoderDemo4("YourBatman4"); }catch (Exception e) {e.printStackTrace();}      System.err.println(" ------------------------- ");        try { client.encoderDemo5("YourBatman5"); }catch (Exception e) {e.printStackTrace();}      System.err.println(" ------------------------- ");        try { client.encoderDemo6(new Person()); }catch (Exception e) {e.printStackTrace();}      System.err.println(" ------------------------- ");        try { client.encoderDemo7(new Person()); }catch (Exception e) {e.printStackTrace();}  }

Demo1

[EncoderClient#encoderDemo1] ---> POST http://localhost:8080/YourBatman1/18 HTTP/1.1  ...

因為標註都標註有@Param且都被模版使用了,所以不會經過編碼器Encoder處理。

Demo2

feign.codec.EncodeException: class java.util.LinkedHashMap is not a type supported by this encoder.  	at feign.codec.Encoder$Default.encode(Encoder.java:94)  	at feign.ReflectiveFeign$BuildFormEncodedTemplateFromArgs.resolve(ReflectiveFeign.java:345)  	...

還沒進入到請求發送階段就拋錯,在構建RequestTemplate拋錯的。這是因為:還剩有一個標註有@Param的屬性沒有用到,所以會被搜集到Map里,最終交給編碼器處理:

而默認的編碼器Default它處理不了Map,所以就拋錯。

Demo3

feign.codec.EncodeException: class java.util.LinkedHashMap is not a type supported by this encoder.  	at feign.codec.Encoder$Default.encode(Encoder.java:94)  	...

同樣報錯,且原因完全同上。唯一區別是上面的Map裝一個值,這裡兩個值都沒被用到,所以裝了兩個值。

Demo4

[EncoderClient#encoderDemo4] ---> POST http://localhost:8080/ HTTP/1.1  [EncoderClient#encoderDemo4] Content-Length: 11  [EncoderClient#encoderDemo4]  [EncoderClient#encoderDemo4] YourBatman4  [EncoderClient#encoderDemo4] ---> END HTTP (11-byte body)  ...  // 這個錯不要緊,是服務端拋出的,你忽略即可  // 但是這是Feign對Response響應報錯的經典格式,留個映象不要覺得陌生即可  feign.FeignException$BadRequest: status 400 reading EncoderClient#encoderDemo4(String)  	at feign.FeignException.clientErrorStatus(FeignException.java:159)

非常正常的work,沒有標註註解的參數被順利放進了請求Body裡面(因為預設的編碼器支援String類型)。

為何沒有使用@Body註解,也沒使用@Param註解它就能「自動」進入body體里,這就是編碼器的功勞

Demo5

feign.codec.EncodeException: class java.lang.String is not a type supported by this encoder.  	at feign.codec.Encoder$Default.encode(Encoder.java:94)  	...

從報錯資訊中你或許覺得詫異:不是支援String類型麽?為嘛報錯是不支援呢?

原因就出現在這:這裡的bodyType是方法參數的靜態類型所以是Object類型,但又不為null,所以就拋錯嘍。

Demo6

feign.codec.EncodeException: class com.youtbatman.java.beans.Person is not a type supported by this encoder.  	at feign.codec.Encoder$Default.encode(Encoder.java:94)  	...

這個報錯非常容易理解,預設編碼器它編碼不了Person所以拋錯…

Demo7

feign.codec.EncodeException: class java.util.LinkedHashMap is not a type supported by this encoder.  	at feign.codec.Encoder$Default.encode(Encoder.java:94)  	...

注意這個拋錯和Demo6的區別:這裡Person標註有@Param但沒用,會被裝進Map里,所以它報的是不支援Map的錯。


自定義編碼器

我們知道預設的編碼器並不能解決最為常用的JSON字元串格式的通訊形式,那麼下面我將自定義一個編碼器,讓它成為可能。

說明:依賴於Jackson庫實現,若對Jackson不太了解的,請務必參見我的全網最好專欄:[享學Jackson] 一站式搞定Jackson

public class MyEncoder implements Encoder {        private static final JsonMapper MAPPER = new JsonMapper();        @Override      public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {          if (object != null) {              try {                  template.body(Request.Body.bodyTemplate(MAPPER.writeValueAsString(object), StandardCharsets.UTF_8));              } catch (JsonProcessingException e) {                  e.printStackTrace();              }          }      }  }

把自定義的編碼器放進Feign裡面:Feign.builder().encoder(new MyEncoder())

再次運行上面的Demo6和Demo7,結果均一切正常,日誌如下:

[EncoderClient#encoderDemo6] ---> POST http://localhost:8080/ HTTP/1.1  [EncoderClient#encoderDemo6] Content-Length: 30  [EncoderClient#encoderDemo6]  [EncoderClient#encoderDemo6] {"name":"YourBatman","age":18}  [EncoderClient#encoderDemo6] ---> END HTTP (30-byte body)  ...

POJO到JSON串在Body體了,一切正常。 實際上Feign官方是提供了feign-jackson模組供以便捷使用的,留給後續章節討論。


QueryMapEncoder

QueryMapEncoder負責將對象編碼為Map查詢參數名到值的映射。

public interface QueryMapEncoder {  	Map<String, Object> encode(Object object);      // 這個Defualt實際就是	FieldQueryMapEncoder 本身    // 說明:已被標註為@deprecated,希望用`BeanQueryMapEncoder`代替    class Default extends FieldQueryMapEncoder {    }  }

需要知道的是,雖然看似有3個子類,按其實可以認為只有一個。現在推薦使用BeanQueryMapEncoderFieldQueryMapEncoder效果一樣,但已被標記為過期~)


BeanQueryMapEncoder

查詢映射將使用POJO 可訪問 的getter屬性方法最後作為查詢參數拼接上去,拼接的順序並不保證,如果某個屬性為null,將不會拼接。

說明:這裡所謂的可訪問的Get方法是基於JDK的內省:Introspector.getBeanInfo(type).getPropertyDescriptors()拿到所有方法,篩選出讀方法既表示有效的屬性,所以Get方法必須必須必須是public的才算有效

使用示例:

Feign.Builder默認使用的FieldQueryMapEncoder(效果同BeanQueryMapEncoder),當然你也可以手動指定一把Feign.builder()..queryMapEncoder(new BeanQueryMapEncoder())

@RequestLine("GET /")  String queryMapEncoderDemo1(@QueryMap Person person);      @Getter  @Setter  public class Person {      private String name = "YourBatman";      private Integer age = 18;  }      @Test  public void fun2(){      EncoderClient client = FeignClientFactory.create(EncoderClient.class);      client.queryMapEncoderDemo1(new Person());  }

執行後控制台列印:

// 這就是該編碼器的作用:把POJO的屬性拼接過來了  [EncoderClient#queryMapEncoderDemo1] ---> GET http://localhost:8080/?name=YourBatman&age=18 HTTP/1.1  ...

請特別注意:@QueryMap註解不能省略,因為按照前面知識若沒有此註解,那麼Person就會被Defualt這個Encoder編碼,從而拋錯:feign.codec.EncodeException: class com.youtbatman.java.beans.Person is not a type supported by this encoder.

說明:所以說@QueryMap只能標註在Map類型前面,這是準確的~


總結

關於Feign的編碼器Encoder部分到這就講完了,本專欄第一篇文章早早已介紹了Feign的工作原理圖,從圖中知道Encoder是負責對請求Request(實際為RequestTemplate)進行編碼,外後面發送Http請求做準備,所以有時候你把它理解為適配器也不為過~

聲明

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