最近為項目組提供rest api 時遇到了關於接口參數的傳遞問題,主要是沒有充分考慮到第三方調用者的使用方式,應該盡量的去兼容公司之前提供出去的接口調用方式,這樣可以降低第三方調用者的學習成本,盡管之前的方式並不是那麼的推薦,好的做法是即兼容老的做法也支持推薦的做法。
對於基於http post接口,Content-type我會優先選擇application/json,但公司之前提供的接口恰恰采用了application/x-www-form-urlencoded,它是表單默認的提交類型,基於key/value形式提交到服務端的。spring mvc是如何接收下面兩種經典數據的? (至於form-data,它即可以傳鍵值對也可以上傳文件,這裡不涉及到文件所以只討論下面兩種):
下圖中的參數,是標准的json格式,對前端js非常友好。
下圖的可以看出參數形式與get請求時,URL後面的參數格式
為什麼不推薦采用application/x-www-form-urlencoded這種類型,它有如下問題:
需要去構建List<NameValuePair>,一般頁面傳遞的參數都是一個實體對象Model,需要額外的將這個Model轉換成List<NameValuePair>,如果這個對象復雜,那麼構建這個Key/Value就夠人煩的了。這裡給一個java通過apache httpclient調用的對比,看看哪一個簡單。
需要手工將model轉換成NameValuePair。
這裡只需要Model即可,不需要二次轉換,結構也非常清楚。
post man這類模似http請求的工具中,如果key對應的value是個對象,那麼你需要通過工具得到它的序列化之後的字符串然後填寫到字段中,想想都煩。如果你說我不需要通過這些模似工具測試,那就另當別論
如果需要提交的對象非常復雜,屬性非常多,如果將所有的屬性都構建到MultiValueMap中,那個Map的構建會非常復雜,試想如果對象有多級嵌套對象呢。所有為了避免這個問題,我們將需要提交的業務對象做為一個key來存儲,value就是對象序列化之後的字符串。再加了一些非業務參數,比如安全方面的token等參數,有效的降低了MultiValueMap構建的復雜度。但這種方式相對於json的傳遞方式來講層次更深。如下圖,我們的參數多了一層,jsonParam。
如果解決呢?
不能不兼容現有的模式,但又想支持json,焦點就是在參數的接收上,讓其能夠完美的兼容上述兩種參數傳遞,這裡可以從HttpMessageConverter著手,這個就是用來將請求的參數映射到spring mvc方法中的實體參數的。我們可以編寫一個自定義的類,內部借用FormHttpMessageConverter來接收MultiValueMap,即使方法參數上增加了@RequestBody的注解,也會走我們自定義的converter,就有機會去重新給參數賦值。
這個方法中需要解決一個問題,就是客戶端傳遞時每個參數都是當成字符串來處理的,這種導致我們通過FormHtppMessageConverter轉換成Map時,原本是對象的屬性被識別成字符串,而不是object,結果就是在反序列化時會出錯。好在,上面我們將需要提交的對象包裝了一次,產生一個公共的object參數jsonParam,只需要處理這一個特殊對象。做法就是從Map取出jsonParam,然後對其內容進行反序列化,更新Map值,再次進行反序列化就正常了。
上圖中的做法目前有如下問題
完整的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,我相信這種即能簡化編程又方便調試的優點應該能夠吸引它們。