1 抛砖引玉
先来看一段十分基础的业务代码
Map<String, Object> map = service.getDataByName("悟空GoKu");
Long userId = (Long)map.get("userId");
String phone = (String)map.get("phone");
每次我写这种map获取返回数据总是感觉十分别扭
- map就像个无底洞,你不看服务提供方代码的话就不知道里面到底
放了什么key
。 - 拿到数据之后都要自己
强转
一下,有点麻烦。 - 这玩意有潜在的
类型转换异常发生
。
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();
}
上面说了我不喜欢这样获取数据,但也不喜欢定义一个类来接收,因为这样会造成类数量激增
,可能一个请求接口就得对应创建一个请求类。
有人说不定义成类,有些额外的功能就无法使用:
- swagger这种文档注解无法完美兼容。
不想用swagger,代码侵入性太强,样式、目录展示也一般,接口文档推荐开源yapi
swagger有时确实方便,增加参数时,代码跟文档同时更新,不必额外维护文档,但我还是不喜欢将文档跟代码耦合在一起。
- 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。
- 这种接口我们不可能去写一份文档来维护。
数据反序列化问题(划重点)
。
如果某个方法内部使用了缓存,且通过json反序列化后才返回,容易引发调用方发生异常
@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();
}
@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 总结
最后来几句总结阐述下本文的观点,仅代表个人看法
- 前端请求接口,面向json编程,用map来传输数据,部分接口的返回数据可定义成VO类。
- 服务端之间的方法调用,多个参数的话需要定义DTO类来传输数据。
- 前后端联调,有一份清晰可见的接口文档极为重要,写好代码的同时不要忘记也要写好文档。
特别想diss那些字段不写注释、代码加了字段又不同步到文档的后端开发者hhh