rest api參數與content-type

最近爲項目組提供rest api 時遇到了關於接口參數的傳遞問題,主要是沒有充分考慮到第三方調用者的使用方式,應該儘可能的去兼容公司以前提供出去的接口調用方式,這樣能夠下降第三方調用者的學習成本,儘管以前的方式並非那麼的推薦,好的作法是即兼容老的作法也支持推薦的作法。

對於基於http post接口,Content-type我會優先選擇application/json,但公司以前提供的接口偏偏採用了application/x-www-form-urlencoded,它是表單默認的提交類型,基於key/value形式提交到服務端的。spring mvc是如何接收下面兩種經典數據的? (至於form-data,它便可以傳鍵值對也能夠上傳文件,這裏不涉及到文件因此只討論下面兩種):前端

  • Content-type=application/json:須要在參數上增長@RequestBody這個註解,說明參數是從http的requestbody中獲取。

    

        下圖中的參數,是標準的json格式,對前端js很是友好。java

       

  • Content-type=application/x-www-form-urlencoded,參數上不能增長@RequestBody的註解

         下圖的能夠看出參數形式與get請求時,URL後面的參數格式spring

       


爲何不推薦採用application/x-www-form-urlencoded這種類型,它有以下問題:apache

  • 測試困難,就別想經過postman這類工具測試:提交到服務端其實是一個MultiValueMap,若是value中的對象也是一個對象,那麼在構建這個參數時就很是困難,看下它的過程
    •  採用key1=value1&key2=value2這種形式將全部參數拼接起來,從一長串字符中想了解每一個參數的含義沒有個好眼力怕是不行。
    •  value要進行編碼,編碼以後的對調試者不友好。
    •  value是複雜對象的狀況更加糟糕,通常只能經過程序來序列化獲得參數,想手寫基本不可能。
  • 客戶端調用複雜

        須要去構建List<NameValuePair>,通常頁面傳遞的參數都是一個實體對象Model,須要額外的將這個Model轉換成List<NameValuePair>,若是這個對象複雜,那麼構建這個Key/Value就夠人煩的了。這裏給一個java經過apache httpclient調用的對比,看看哪個簡單。編程

    • application/x-www-form-urlencoded

                須要手工將model轉換成NameValuePair。json

          

    • application/json

                 這裏只須要Model便可,不須要二次轉換,結構也很是清楚。api

            

  • key/value的語言表達形式沒有json強,下面兩種你更加喜歡哪個呢?
    • 字符串

             

              post man這類模似http請求的工具中,若是key對應的value是個對象,那麼你須要經過工具獲得它的序列化以後的字符串而後填寫到字段中,想一想都煩。若是你說我不須要經過這些模似工具測試,那就另當別論安全

            

    • json

              

  • 數據結構更加複雜

   若是須要提交的對象很是複雜,屬性很是多,若是將全部的屬性都構建到MultiValueMap中,那個Map的構建會很是複雜,試想若是對象有多級嵌套對象呢。全部爲了不這個問題,咱們將須要提交的業務對象作爲一個key來存儲,value就是對象序列化以後的字符串。再加了一些非業務參數,好比安全方面的token等參數,有效的下降了MultiValueMap構建的複雜度。但這種方式相對於json的傳遞方式來說層次更深。以下圖,咱們的參數多了一層,jsonParam。數據結構

    



若是解決呢?
不能不兼容現有的模式,但又想支持json,焦點就是在參數的接收上,讓其可以完美的兼容上述兩種參數傳遞,這裏能夠從HttpMessageConverter着手,這個就是用來將請求的參數映射到spring mvc方法中的實體參數的。咱們能夠編寫一個自定義的類,內部借用FormHttpMessageConverter來接收MultiValueMap,即便方法參數上增長了@RequestBody的註解,也會走咱們自定義的converter,就有機會去從新給參數賦值。mvc

這個方法中須要解決一個問題,就是客戶端傳遞時每一個參數都是當成字符串來處理的,這種致使咱們經過FormHtppMessageConverter轉換成Map時,本來是對象的屬性被識別成字符串,而不是object,結果就是在反序列化時會出錯。好在,上面咱們將須要提交的對象包裝了一次,產生一個公共的object參數jsonParam,只須要處理這一個特殊對象。作法就是從Map取出jsonParam,而後對其內容進行反序列化,更新Map值,再次進行反序列化就正常了。
  
     上圖中的作法目前有以下問題

  • 序列化的字段是約定好的,也是基於咱們的post model基本上來處理的,是針對性的converter
  • 代碼最後面調用的jackon的convertValue,對須要反序列化的對象類型有要求,好像不支持泛型類型,好比這種類型的就不行: CommonParamInfoDto<SearchParamInfo<ProductSearchInfo>>

     完整的conveter代碼以下,其實主要代碼就是上圖貼圖中的那麼對特定字段的序列化處理,其它的方法都是默認便可。

public class ObjectHttpMessageConverter implements HttpMessageConverter<Object> {

    private final FormHttpMessageConverter formHttpMessageConverter = new FormHttpMessageConverter();
    private final ObjectMapper objectMapper = new ObjectMapper();

    private static final LinkedMultiValueMap<String, ?> LINKED_MULTI_VALUE_MAP = new LinkedMultiValueMap<>();
    private static final Class<? extends MultiValueMap<String, ?>> LINKED_MULTI_VALUE_MAP_CLASS
            = (Class<? extends MultiValueMap<String, ?>>) LINKED_MULTI_VALUE_MAP.getClass();

    @Override
    public boolean canRead(Class clazz, MediaType mediaType) {
        return objectMapper.canSerialize(clazz) && formHttpMessageConverter.canRead(MultiValueMap.class, mediaType);
    }

    @Override
    public boolean canWrite(Class clazz, MediaType mediaType) {
        return false;
    }

    @Override
    public List<MediaType> getSupportedMediaTypes() {
        return formHttpMessageConverter.getSupportedMediaTypes();
    }


    @Override
    public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        Map input = formHttpMessageConverter.read(LINKED_MULTI_VALUE_MAP_CLASS, inputMessage).toSingleValueMap();
        String jsonParamKey="jsonParam";
        if(input.containsKey(jsonParamKey)) {
            String jsonParam = input.get(jsonParamKey).toString();
            SearchParamInfo<Object> searchParamInfo = new SearchParamInfo<Object>();
            Object jsonParamObj = JsonHelper.json2Object(jsonParam, searchParamInfo.getClass());
            input.put("jsonParam", jsonParamObj);
        }
        Object objResult= objectMapper.convertValue(input, clazz);
        return objResult;
    }

    @Override
    public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws UnsupportedOperationException {
        throw new UnsupportedOperationException("");
    }
}

 

     配置,寫好了conveter以後,須要在配置文件中配置上才能生效。

    

 

     最後,咱們的方法就能夠這樣寫,便可以支持 key/value對,也支持json

    

個人目的在於api的參數即能支持application/x-www-form-urlencoded也能支持application/json,上面是我目前能想到的辦法,若是你們有其它更好的辦法多多指點。

 

     我又能夠愉快的使用post man測試了。並且能夠推薦第三方調用者優先使用json,我相信這種即能簡化編程又方便調試的優勢應該可以吸引它們。

相關文章
相關標籤/搜索