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

5,406 阅读4分钟

1 抛砖引玉

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

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

每次我写这种map获取返回数据总是感觉十分别扭

  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();
}

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

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

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

不想用swagger,代码侵入性太强,样式、目录展示也一般,接口文档推荐开源yapi

swagger有时确实方便,增加参数时,代码跟文档同时更新,不必额外维护文档,但我还是不喜欢将文档跟代码耦合在一起。

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

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

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

说回了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