feign form支持

feign是一個非常好用的http客戶端工具,簡單入門請見上篇文章,不多做介紹
但是在使用feign的時候也碰到了一點小坑,今天就來講講怎麼解決這個坑

feign bean提交

看官方文檔,feign post提交的時候可以使用bean傳輸,不需要每個參數註解@Param,然而feign會把這個bean的內容寫入到http的 body中去。contentType爲applicationJson
spring mvc接收需要在接口對應的bean上註解@RequestBody,從body中讀取這個bean的內容。
如下

@Headers("Content-Type: application/json")
@RequestLine("POST /test1")
public BaseResponse test1(Demo demo);
@ResponseBody
@RequestMapping(value = "/test1", method = RequestMethod.POST)
public BaseResponse test1(@RequestBody Demo demo) {
    return BaseResponse.SUCCESS_RESPONSE;
}

使用一直很舒暢,因爲自己的項目都是restful風格的接口
直到調用公司其他的項目組接口發現完蛋了,對方不是用body接收復雜對象

臨時方案

爲了解決這種問題只能使用@Param了,但是參數多的時候會寫很長的方法參數

@Headers("Content-Type: application/x-www-form-urlencoded")
@RequestLine("POST /test2")
public BaseResponse test2(@Param("id") int id,@Param("name")String name);
@ResponseBody
@RequestMapping(value = "/test2", method = RequestMethod.POST)
public BaseResponse test2(Demo demo) {
   return BaseResponse.SUCCESS_RESPONSE;
}

另一種方式就是使用@QueryMap了

@Headers("Content-Type: application/x-www-form-urlencoded")
@RequestLine("POST /test2")
public BaseResponse test3(@QueryMap Map<String,Object> param);

這樣暫時是對付過去了,但是總歸不是很優雅。

擴展feign

仔細研究feign的源碼後,發現feign是根據目標接口生成代理對象,生成代理對象的過程中會根據接口方法生成一個MethodMetadata對象,其中封裝了方法簽名configKey,form表單參數列表formParams,參數index對應的參數名indexToName等。屬性如下:

  private String configKey;
  private transient Type returnType;
  private Integer urlIndex;
  private Integer bodyIndex;
  private Integer headerMapIndex;
  private Integer queryMapIndex;
  private boolean queryMapEncoded;
  private transient Type bodyType;
  private RequestTemplate template = new RequestTemplate();
  private List<String> formParams = new ArrayList<String>();
  private Map<Integer, Collection<String>> indexToName =
      new LinkedHashMap<Integer, Collection<String>>();
  private Map<Integer, Class<? extends Expander>> indexToExpanderClass =
      new LinkedHashMap<Integer, Class<? extends Expander>>();
  private Map<Integer, Boolean> indexToEncoded = new LinkedHashMap<Integer, Boolean>();
  private transient Map<Integer, Expander> indexToExpander;

而生成這個對象的類是Contract,可以在Feign構造器中設置。
可以自己擴展Contract,將複雜對象的參數名設置進indexToName就行了,這裏雖然是int->集合的類型。但是在調用我們遠程接口時,feign會將我們的參數轉化爲param->value的map形式。而feign在轉換的過程中,如果indexToName index對應的name有多個的話,會迭代這個collection,然後講傳入的參數設置進去,並不會解析其中的屬性,如下:

   @Override
    public RequestTemplate create(Object[] argv) {
      RequestTemplate mutable = new RequestTemplate(metadata.template());
      ...
      Map<String, Object> varBuilder = new LinkedHashMap<String, Object>();
      for (Entry<Integer, Collection<String>> entry : metadata.indexToName().entrySet()) {
        int i = entry.getKey();
        Object value = argv[entry.getKey()];
        if (value != null) { // Null values are skipped.
          ...
          for (String name : entry.getValue()) {
            varBuilder.put(name, value);
          }
        }
      }
      ...
      return template;
    }

以class Demo{ int id,String name} 爲例,如果indexToName爲 0->[“id”,”name”],最後解析出來就是
{“id”:“{id:1,name:chen}”,“name”“{id:1,name:chen}”};根本不是我們想要的結果
最後靈機一動,直接將參數名設爲一個固定字符串就行,反正轉換調用的參數時可以獲得屬性名
方案如下:
1.將帶有@FormBean標註的參數的參數名定義爲@FORM@+index,indexToName中爲index->”@FORM@index”
“@FORM@”爲自定義的一個特殊字符,怕衝突可以使用class的hashcode
2.調用時,在encoder中將參數名爲”@FORM@”開頭的參數刪除,將傳入的參數轉換爲map,添加到參數鍵值對中

public class FormContract extends Contract.Default {

    public static  String FORM_PARAM_NAME="@FORM@";

    private String formParamName;

    public FormContract() {
        this(FORM_PARAM_NAME);
    }

    public FormContract(String formParamName) {
        this.formParamName = formParamName;
    }

    @Override
    protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
        boolean isHttpAnnotation = super.processAnnotationsOnParameter(data, annotations, paramIndex);
        for (Annotation annotation : annotations) {
            Class<? extends Annotation> annotationType = annotation.annotationType();
            if (annotationType == FormBean.class) {
                FormBean paramAnnotation = (FormBean) annotation;
                //註解了FormBean 的參數名定義爲@FORM@+index
                String name=formParamName+paramIndex;
                nameParam(data, name, paramIndex);
                Class<? extends Param.Expander> expander = paramAnnotation.expander();
                if (expander != Param.ToStringExpander.class) {
                    data.indexToExpanderClass().put(paramIndex, expander);
                }
                data.indexToEncoded().put(paramIndex, paramAnnotation.encoded());
                isHttpAnnotation = true;
                String varName = '{' + name + '}';
                if (!data.template().url().contains(varName) &&
                        !searchMapValuesContainsSubstring(data.template().queries(), varName) &&
                        !searchMapValuesContainsSubstring(data.template().headers(), varName)) {
                    data.formParams().add(name);
                }
            }            
        }
        return isHttpAnnotation;
    }

    private static <K, V> boolean searchMapValuesContainsSubstring(Map<K, Collection<String>> map,String search) {
    ......
    }
}
@Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
        ......
        @SuppressWarnings("unchecked")
        Map<String, Object> data = (Map<String, Object>) object;
        formObjectExpand(data);
        processors.get(formType).process(data, template);
    }
    private void formObjectExpand(Map<String, Object> data) {
        List<String> readyRemove=new ArrayList<>();
        Map<String,Object> insert=new HashMap<>();
        for(String key:data.keySet()){
            if(key.startsWith(FORM_PARAM_NAME)){
                Object value = data.get(key);
                readyRemove.add(key);
                try {
                    insert.putAll(objectConvertor.toMap(value));
                } catch (ConvertException e) {
                    LoggerUtil.logException(e);
                }
            }
        }
        //ConcurrentModificationException
        data.putAll(insert);
        for (String key : readyRemove) {
            data.remove(key);
        }
    }

具體例子請見我的github代碼 FormContractFormEncoder
生成feign客戶端的方式請見FeinFactory

調用代碼變爲

    @Headers("Content-Type: application/x-www-form-urlencoded")
    @RequestLine("POST /test2")
    public BaseResponse test4(@FormBean Demo demo);

第一次擴展沒中文文檔的開源框架,英語不好,水平有限,理解有問題的地方請不吝指出。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章