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,我相信这种即能简化编程又方便调试的优势应该可以吸引它们。

相关文章
相关标签/搜索