feign的一個註解居然隱藏這麼多知識!

引言

最近由於業務的需要,需要接入下阿里雲的一個介面,打開文檔看了看這個介面看下來還是比簡單的目測個把小時就可以搞定,但是接入的過程還是比較坎坷的。首先我看了看他給的示例,首先把阿里雲文檔推薦的demo下載下來,把它的例子跑起來,替換下幾個必要的參數比如秘鑰啥的。這些秘鑰一般公司都會有專職的人員與阿里雲去對接,你只要負責管他要就行了。不過也不排除也有得公司需要自己去對接阿里雲。說到這裡就想吐槽下,對接阿里雲的時候技術支援群居然是釘釘,所以需要他們的支援就必須要下載個釘釘,
電腦上莫名的有需要多裝一個軟體。扯遠了我們還是回到正題,把它demo下載下來,然後把對應的秘鑰等參數替換下,然後運行下demo看看是否能夠正常返回結果,做這一步主要是為了保證產品給過來的秘鑰等參數是否正確。如果能夠掉通介面,那就說明參數沒啥問題的接著我們就可以著手來寫業務程式碼了。接入阿里雲二要素認證//market.aliyun.com/products/57000002/cmapi029454.html?spm=5176.10695662.1194487.1.60066c190NsSkZ#sku=yuncode2345400003
把官網的demo下載下來跑起來看看,官網給出的例子還是比較簡單粗暴的,就是封裝了一個Apachehttplcient工具類一大坨的程式碼,個人還是習慣性的使用feign來進行調用,因為feign的程式碼乾淨整潔,雖然底層也是通過HttpClient來實現,但是實現對我來說是無感的,畢竟業務程式碼看起來乾淨整潔。它的demo如下:

public static void main(String[] args) {
	    String host = "//safrvcert.market.alicloudapi.com";
	    String path = "/safrv_2meta_id_name/";
	    String method = "GET";
	    String appcode = "你自己的AppCode";
	    Map<String, String> headers = new HashMap<String, String>();
	    //最後在header中的格式(中間是英文空格)為Authorization:APPCODE 83359fd73fe94948385f570e3c139105
	    headers.put("Authorization", "APPCODE " + appcode);
	    Map<String, String> querys = new HashMap<String, String>();
	    querys.put("__userId", "__userId");
	    querys.put("customerID", "customerID");
	    querys.put("identifyNum", "identifyNum");
            querys.put("identifyNumMd5", "identifyNumMd5");
	    querys.put("userName", "userName");
	    querys.put("verifyKey", "verifyKey");


	    try {
	    	/**
	    	* 重要提示如下:
	    	* HttpUtils請從
	    	* //github.com/aliyun/api-gateway-demo-sign-java/blob/master/src/main/java/com/aliyun/api/gateway/demo/util/HttpUtils.java
	    	* 下載
	    	*
	    	* 相應的依賴請參照
	    	* //github.com/aliyun/api-gateway-demo-sign-java/blob/master/pom.xml
	    	*/
	    	HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);
	    	//錯誤資訊見X-Ca-Error-Message欄位
                System.out.println(response.toString());
	    	//獲取response的body
	    	System.out.println(EntityUtils.toString(response.getEntity()));
	    } catch (Exception e) {
	    	e.printStackTrace();
	    }
	}
HttpResponse response = HttpUtils.doGet(host, path, method, headers, querys);

根據它提供的程式碼我們可以看出來他是用一個httpUtils 類來實現http請求的,我們可以把這個httpClient類 替換成我們的FeignClient
替換後的程式碼如下:

@FeignClient(name = "verifyIdCardAndNameFeignClient", url = "//safrvcert.market.alicloudapi.com")
public interface VerifyIdCardAndNameFeignClient {
    @RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    Response verifyIdCardAndNameMap(@RequestParam Map<String,String> app, @RequestHeader("Authorization") String authorization);

相對比較下來下面這個HttpClientUtils程式碼是不是比較簡潔
在這裡插入圖片描述
按照這個demo功能確實是實現了,說實話個人還是不是很喜歡用map來作為參數,map作為入參的話,參數全靠猜可讀性以及可維護性有點差,個人還是習慣性的封裝一個javaBean作為實體。阿里文檔其實也有提到一嘴,雖然他只說到數據查詢這一層。
在這裡插入圖片描述
下面我們就修改下請求參數把它改成一個javaBean,改變後的程式碼

@RequestMapping(value = "/safrv_2meta_id_name/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
Response verifyIdCardAndNameDTO(@RequestBody AliyunVerifyIdCardAndNameReq app, @RequestHeader("Authorization") String authorization);

在這裡插入圖片描述
請求並沒有成功,根據報錯返回的資訊看下來應該是沒有接受到參數。我們是GET請求的方式然後參數傳遞的是實體導致沒有接收到。feignClient不支援get方式傳遞實體類嗎?後來經過查詢資料發現了一個註解@SpringQueryMap 我們把上述程式碼@RequestBody替換成@SpringQueryMap完美解決這個問題

@SpringQueryMap

spring cloud 2.1.x 以上的版本,提供了一個新的註解@SpringQueryMap,為何這個註解可以幫我們實現。源碼之下無秘密,我們可以翻翻
feign的源碼相對來說應該是比較簡單的,我們可以簡單的來看下源碼。看源碼是不是也不知道從哪裡看起,從頭看到尾肯定也不現實,
不從頭開始看,又不知道源碼在哪裡,有個很簡單的方法我們直接拿著這個註解全局搜一下,看看有哪些地方使用到了,在每個地方都打上一個斷點試試
在這裡插入圖片描述
我們全局搜下發現使用的地方主要在QueryMapParameterProcessor這個類裡面。所以我們可以在這個類裡面打上一個斷點試試。


/**
 * {@link SpringQueryMap} parameter processor.
 *
 * @author Aram Peres
 * @see AnnotatedParameterProcessor
 */
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {

	private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;

	@Override
	public Class<? extends Annotation> getAnnotationType() {
		return ANNOTATION;
	}

	@Override
	public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
		int paramIndex = context.getParameterIndex();
		MethodMetadata metadata = context.getMethodMetadata();
		if (metadata.queryMapIndex() == null) {
			metadata.queryMapIndex(paramIndex);
			metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
		}
		return true;
	}
}

我們發現打這個類的話在容器啟動的時候會進行載入,並且會執行processArgument方法,這個我們先不管這個方法,接下來我們來看看
Feign真正發起調用的地方找到SynchronousMethodHandler#invoke方法

public RequestTemplate create(Object[] argv) {
	... 省略部分程式碼
	// metadata.queryMapIndex() 就是QueryMapParameterProcessor #processArgument方法賦值的
      if (metadata.queryMapIndex() != null) {
        // add query map parameters after initial resolve so that they take
        // precedence over any predefined values
        // 通過下標獲取到需要特殊處理的對象,這裡有個問題只會處理方法參數的第一個@SpringQueryMap註解,
        // 原因就是QueryMapParameterProcessor #processArgument這個方法只會把第一個下標賦值進去,然後這裡也只會取第一個下標,所以只會處理第一個@SpringQueryMap註解
        Object value = argv[metadata.queryMapIndex()];
        //將對象轉換為map  這裡需要注意下默認使用解析參數的是FieldQueryMapEncoder類所以它並不會去解析父類的參數,如果需要解析父類的參數我們需要在feign的Config裡面指定QueryMapEncoder為FieldQueryMapEncoder
        Map<String, Object> queryMap = toQueryMap(value);
        //拼接解析完成的對象為URL參數
        template = addQueryMapQueryParameters(queryMap, template);
      }
... 省略部分程式碼
}

上述程式碼邏輯還是挺好理解的

  • 首先去判斷是否需要處理下querymap
  • 通過下標獲取到需要特殊處理的對象
  • 將對象轉換為map(這裡有個坑默認不會去解析父類的欄位)
  • 拼接追加mapurl

總結

  • 上面通過@SpringQueryMap註解實現了get傳參,但是如果需要傳遞多個@SpringQueryMap註解我們可以怎麼來實現呢?
  • 或者我們可以自己動手來實現一個我們自己的SpringQueryMap,我們該如何實現?
  • @SpringQueryMap註解默認是不會去解析父類的參數,如果需要解析父類的參數需要修改Feignconfig# QueryMapEncoderFieldQueryMapEncoder
  • 如果我們自己去實現了一個AnnotatedParameterProcessor所有默認的PathVariableParameterProcessor
    RequestParamParameterProcessor、RequestHeaderParameterProcessor、QueryMapParameterProcessor都會失效,為啥會失效我們去看看SpringMvcContract這個類。所以自定義AnnotatedParameterProcessor需要慎重。

結束

  • 由於自己才疏學淺,難免會有紕漏,假如你發現了錯誤的地方,還望留言給我指出來,我會對其加以修正。
  • 如果你覺得文章還不錯,你的轉發、分享、讚賞、點贊、留言就是對我最大的鼓勵。
  • 感謝您的閱讀,十分歡迎並感謝您的關注。
Tags: