我为何既支持又反对接口用Map来传输数据?

1 抛砖引玉

先来看一段十分基础的业务代码前端

Map<String, Object> map = service.getDataByName("悟空GoKu");
  Long userId = (Long)map.get("userId");
  String phone = (String)map.get("phone");
复制代码

每次我写这种map获取返回数据老是感受十分别扭web

  1. map就像个无底洞,你不看服务提供方代码的话就不知道里面到底 放了什么key
  2. 拿到数据以后都要本身 强转一下,有点麻烦。
  3. 这玩意有潜在的 类型转换异常发生

2 请求参数Request

首先要确定的是用map来传输参数(前端http请求后端接口)真的是方便,不须要额外去定义一个类,想往里面塞什么数据就塞什么,就以下面的例子,不须要为每一个接口都定义一个RequestVo类,统一map接收数据库

@PostMapping(value = "/map")
public ApiResponse testMap(@RequestBody Map<String, Object> map) {
    //获取map中的数据
    Long userId = (Long)map.get("userId");
    String phone = (String)map.get("phone");
    //业务代码...
    return ApiResponse.ok();
}
复制代码

上面说了我不喜欢这样获取数据,但也不喜欢定义一个类来接收,由于这样会形成类数量激增,可能一个请求接口就得对应建立一个请求类。编程

有人说不定义成类,有些额外的功能就没法使用:json

  1. swagger这种文档注解没法完美兼容。

不想用swagger,代码侵入性太强,样式、目录展现也通常,接口文档推荐开源yapi后端

swagger有时确实方便,增长参数时,代码跟文档同时更新,没必要额外维护文档,但我仍是不喜欢将文档跟代码耦合在一块儿。api

  1. validator验证注解没法使用。

验证逻辑我仍是喜欢写在controller,逻辑更清晰。缓存

原来的注解不必定适合某些复杂验证,那岂不是要自定义注解,又回到了类激增的问题app

说回了map,map.get(key)这样获取数据确实别扭,但咱们能够封装请求啊, 例如上一篇文章<<当技术leader说要把接口设计成RESTful,我拒绝了>>提到的ApiRequest。编辑器

public class ApiRequest implements Serializable {
  //....省略部分代码    
  private Map<String, Object> data;  //经过拦截器处理后请求参数已存放在这里
  public Long getDataParamAsLong(String name, Long defaultValue) {
      Long i = defaultValue;
      try{
          i = StringUtils.isNotEmpty(getDataParamAsString(name)) ? 
              Long.valueOf(getDataParamAsString(name)) : defaultValue;
      }catch (Exception e){
          e.printStackTrace();
      }
      return i;
  }
}
复制代码
@PostMapping(value = "/test")
public ApiResponse test(ApiRequest apiRequest) {
    Long userId = apiRequest.getDataParamAsLong("userId", 0L);
    //省略部分代码....
    ApiResponse response = ApiResponse.ok()
    return response;
}
复制代码

这里已是面向json编程了,而不是以往的面向对象。

3 响应参数Response

对于返回数据,我通常会在controller层拿到service的数据后再根据业务需求来处理数据(结构修改,数据整合),最终用map整合再response,给前端一个合适的结构, 而不是数据库查到什么就整个类对象返回

当前有些返回数据我也会定义一个类ReponseVo封装,最后return给前端

你这先后矛盾啊,以前还说不要面向对象。

这里主要考虑的有些场景下,多个接口返回的数据彻底同样,能够共用。 对于部分app开发者来讲,他们会依赖后台的接口来定义本身的model,类似的数据会要求后台返回的字段命名和结构同样,以便他们能共用model。

这.......其实他们能够本身定义属于他们的model,而不是彻底依赖后台字段命名。

4 service层数据传输

前面说的是前端/移动端http请求咱们的网关接口,这里是说咱们服务端之间的远程方法调用/本地方法调用,也能够理解成service层方法调用。若是是多个参数的话,须要封装一个DTO,这里最好不用map。

  1. 这种接口咱们不可能去写一份文档来维护。
  2. 数据反序列化问题(划重点)

若是某个方法内部使用了缓存,且经过json反序列化后才返回,容易引起调用方发生异常

//set 
@PostMapping(value = "/setData")
public ApiResponse setData(ApiRequest request) {
    //省略部分代码...
    Map<String, Object> map = new HashMap<>();
    map.put("id", 123L);
    map.put("name", "悟空GoKu");
    stringRedisTemplate.set("KEY_GOKU", JsonUtil.toJsonString(map));
    return ApiResponse.ok();
}
//get 
@PostMapping(value = "/getData")
public ApiResponse getData(ApiRequest request) {
    Map<String, Object> map = stringRedisTemplate.get("KEY_GOKU", Map.class);
    Long id = (Long)map.get("id");  //会发生异常ClassCastException
    return ApiResponse.ok();
}
复制代码

json 反序列化 map 时若是原来的整数值小于 int 最大值,反序列化后本来为 Long 类型的字段,会变为 Integer 类型

json 序列化的优点在于可读性更强。但没有携带类型信息,只有提供了准确的类型信息才能准确地进行反序列化,这点也特别容易引起线上问题。

5 总结

最后来几句总结阐述下本文的观点,仅表明我的见解

  1. 前端/移动端请求接口,面向json编程,用map来传输数据,部分接口的返回数据可定义成VO类
  2. 服务端之间的方法调用,多个参数的话须要定义DTO类来传输数据。
  3. 先后端联调,有一份清晰可见的接口文档极为重要,写好代码的同时不要忘记也要写好文档。

特别想diss那些字段不写注释、代码加了字段又不一样步到文档的后端开发者hhh

相关文章
相关标签/搜索