问题

上一文介绍了 JSONObject 接受 MyBatis 的结果集的简单用法,但是在处理一对多的情况时,单纯的JSONObject就不好使了。

比如要查询一个角色下的多个用户,resultMap如下定义

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.alibaba.fastjson.JSONObject">
<id column="roleId" property="roleId"/>
<result column="roleName" property="roleName"/>
<collection property="users" ofType="com.alibaba.fastjson.JSONObject">
<id column="userId" property="userId"/>
<result column="nickname" property="nickname"/>
</collection>
</resultMap>

期望查出来的users 属性对应着一个数组,

然而实际查出来只是一个对象,只有一条数据。

解决方案

只需要建一个实体类继承 JSONObject ,里面有你要的集合类型的成员变量,就足够了。

比如我建的 One2Many 类:

1
2
3
public class One2Many extends JSONObject {
private List<JSONObject> users;
}

然后xml改为

1
2
3
4
5
6
7
8
<resultMap id="roleMap" type="com.heeexy.example.util.model.One2Many">
<id column="roleId" property="roleId"/>
<result column="roleName" property="roleName"/>
<collection property="users" ofType="com.alibaba.fastjson.JSONObject">
<id column="userId" property="userId"/>
<result column="nickname" property="nickname"/>
</collection>
</resultMap>

是不是非常简单?

更棒的是,这个 One2Many 类是可以复用的,里面再添加其它的成员变量就 OK 了。而且 Dao 层不需要改动,外面正常的还是用 JSONObject 就可以了。

原理

MyBatis 在处理嵌套结果的时候,会判断这个属性的类型,如果是集合,就会初始化一个集合来接收这个属性,否则就只是一个普通的 Object 了。

什么,不满意这个答案?那就拿出源码来吧!

首先我们直接看到最底层判断这个属性是不是集合的这段源码:

DefaultResultSetHandler.instantiateCollectionPropertyIfAppropriate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
private Object instantiateCollectionPropertyIfAppropriate(ResultMapping resultMapping, MetaObject metaObject) {
final String propertyName = resultMapping.getProperty();
//先去拿在metaObject已经存好的这个属性值,我们这里以users属性为例
Object propertyValue = metaObject.getValue(propertyName);
if (propertyValue == null) {
//在这里拿你的users的java类型,由于我们使用的是JSONObject,这里的类型都会返回Object,如果是One2Many,这里就会拿到List类型
Class<?> type = resultMapping.getJavaType();
if (type == null) {
type = metaObject.getSetterType(propertyName);
}
try {
//判断属性类型是不是集合,如果是结合才会初始化一个集合的值返回到下一步,否则都会返回null
if (objectFactory.isCollection(type)) {
propertyValue = objectFactory.create(type);
metaObject.setValue(propertyName, propertyValue);
return propertyValue;
}
} catch (Exception e) {
throw new ExecutorException("Error instantiating collection property for result '" + resultMapping.getProperty() + "'. Cause: " + e, e);
}
} else if (objectFactory.isCollection(propertyValue.getClass())) {
//propertyValue不为空的情况,即我们的JSONObject里已经塞入了users属性,即使是这样,MyBatis还是要求你塞入的users属性必须是集合,才返回到下一步,否则还是会返回null到下一步。
return propertyValue;
}
return null;
}

从上面这段代码我们就知道 MyBatis 确实有做这个判断,你定义的 users 属性到底是不是集合类型,

  1. 如果是并且没有初始化好的话,就帮你初始化一个集合到下一步,
  2. 如果已经初始化好了(通常这时候就是已经塞入了几个 user 对象了),就直接返回这个 users 的值到下一步
  3. 如果不是集合类型,就返回 null 到下一步。

那么下一步到底是干啥呢?正常情况下应该就是继续往 users 集合里添加元素吧。

DefaultResultSetHandler.linkObjects

1
2
3
4
5
6
7
8
9
10
11
private void linkObjects(MetaObject metaObject, ResultMapping resultMapping, Object rowValue) {
final Object collectionProperty = instantiateCollectionPropertyIfAppropriate(resultMapping, metaObject);
if (collectionProperty != null) {
//如果上一步返回来的不是null,那就向这个集合里添加元素
final MetaObject targetMetaObject = configuration.newMetaObject(collectionProperty);
targetMetaObject.add(rowValue);
} else {
//上一步返回了null,那就直接把这个属性赋上本值,就是这种情况导致了上面的第二张图片的情况,我们的users变成了一个对象,而不是想要的数组。
metaObject.setValue(resultMapping.getProperty(), rowValue);
}
}

评论和共享

问题背景

项目后端与前端全部使用 JSON 进行数据交互,比如查询用户列表,通常后端从数据库查出的数据放入实体类再转为 JSON 返回给前端,但是前端的小伙伴表示明明只需要 nickname,avatar,userId 三个字段,为什么传过来的数据多出 phone,address ,profile 之类十几个字段,里面都 null,0 等值?

其实是因为 User 实体类定义了太多字段,转为 JSON 的时候都被包括进去了。稍微多几个还能忍,但通常 User/Order 这些实体类字段特别多,对调试开发实在非常不友好。

因此本次任务就是:确保每个接口返回字段都与接口文档上一模一样,去除冗余字段。

解决过程就省略了,直接抛出我最后的解决方案吧:就是 MyBatis 的返回值几乎完全抛弃实体类,全部使用com.alibaba.fastjson.JSONObject

使用 JSONObject

先最简化直观的看一下 JSONObject 的使用吧。

Controller:

1
2
3
4
@GetMapping("/list")
public JSONObject listUser(@RequestBody JSONObject requestJson){
return userService.listUser(requestJson);
}

Service:

1
JSONObject listUser(JSONObject jsonObject);

ServiceImpl:

1
2
3
4
5
6
7
8
9
@Override
public JSONObject listUser(JSONObject jsonObject) {
//fillPageParam是自定义的封装分页参数
CommonUtil.fillPageParam(jsonObject);
int count = userDao.countUser(jsonObject);
List<JSONObject> list = userDao.listUser(jsonObject);
//自定义successPage封装分页结果
return CommonUtil.successPage(jsonObject, list, count);
}

Dao:

1
2
3
4
public interface UserDao {
int countUser(JSONObject jsonObject);
List<JSONObject> listUser(JSONObject jsonObject);
}

UserMapper.xml , 这里的 resultType 直接就是 JSONObject ,如果是一对多的情况,就要多加一步,请移步看这篇文章

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<select id="countUser" resultType="Integer">
SELECT count(0)
FROM sys_user u
WHERE age=#{age}
</select>
<select id="users" resultType="com.alibaba.fastjson.JSONObject">
SELECT
id userId,
nickname nickname,
avatar avatar
FROM sys_user
WHERE age=#{age}
LIMIT #{offSet}, #{pageRow}
</select>

为什么可以使用JSONObject

因为 JSONObject 实现了 Map<String,Object> , MyBatis 那边完全是把它当成 Map<String,Object> 处理的,相信不少人都直接用过 map 来接收MyBatis 返回结果。

fastjson 是这样的,Gson 并不是,所以 Gson 的 JsonObject 是不可以的。

有兴趣了解更深入的原理的话,请参考我此系列的其它文章。

优劣对比

便捷性

JSONObject 便捷到可以说是无脑,接收参数、 sql 传参、封装 sql 结果、返回到前端,全程使用。

JavaBean 则接口层接收参数转为实体类,返回给前端再转为 json,新增一个业务通常还需要多建一个实体类。

工具方法拓展

JSONObject 可以方便地封装出通用的工具方法,比如封装分页信息,比如封装处理结果的成功和失败信息,特别是校验参数字段非空,不同的接口通常需要校验的字段不同,如下面这个简单的方法就可以校验各字段是否都非空。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static void hasAllRequired(final JSONObject jsonObject, String requiredColumns) {
if (!StringTools.isNullOrEmpty(requiredColumns)) {
String[] columns = requiredColumns.split(",");
String missCol = "";
for (String column : columns) {
Object val = jsonObject.get(column.trim());
if (StringTools.isNullOrEmpty(val)) {
missCol += column + " ";
}
}
if (!StringTools.isNullOrEmpty(missCol)) {
jsonObject.clear();
jsonObject.put("returnCode", ErrorEnum.E_90003.getErrorCode());
jsonObject.put("returnMsg", "缺少必填参数:" + missCol.trim());
throw new CommonJsonException(jsonObject);
}
}
}

JavaBean 在处理分页上通常要引各自写的 PageBean 工具类 , 使用过程通常不会比上面ServiceImpl里的分页简单。

JavaBean 的参数校验都不用说,更头痛更复杂了,不知道有没有人有用起来很爽的方法。

返回字段

JSONObject 想返回几个字段就几个字段,而 JavaBean 如果只想返回某几个字段的话,就需要单独建个实体类。

字段有改动的话,JSONObject 灵活性就更强了,直接在 mapper.xml 里改下就完事。

业务层

不可否认的是,JSONObject 在业务层进行处理时,失去了 JavaBean 的编译提示,也失去了 IDE 的快捷补全,更有可能出现取错值的情况。

比如 jsonObject.getString(“pasword”) ,输入错了单词都可能没注意,最终取出来 null 。

可读性

通常大家可能认为 JavaBean 有更好的可读性,我觉得只要变量名取得好,JSONObject 也是没有什么问题的。毕竟类名是固定死的,变量名和参数名才能更好地反映此处业务的处理。

尤其是如今前后端分离的项目,前后端开发人员一般对照着文档确认字段含义。

总结

JSONObject 开发非常方便,具有更强的灵活性,适用于中小型项目,简单业务的开发,也适用于文档至上的前后端分离开发与微服务项目。

更具体的用法,可以参考 Github 项目

评论和共享

​ MyBatis 想要打印日志,时不时想要来句 log.debug() 、log.error() ,需要个打印机 ,可是自己又不想去实现(而且跟着整个项目用同样的打印系统才是王道啊),需要去用别人家的打印机,要用别人家的产品啊,那问题可就来了。

问题 1

​ 市面上各家的打印机 slf4j、java.util.logging、log4j 甚至 System.out 都是各种不同的用法,这使用起来就太麻烦了。

思路

​ 不管市面上打印机有多少型号,我家 MyBatis 包里的类只用自家的统一接口,我家的类只管 log.debug()、log.error()…

​ 定义好了接口,就需要实现类 impl 来实现这些 debug()、error() 方法了 , 咱假装是自己来实现,其实去调用真正打印机 slf4、log4j 的方法,这样就把别人家的打印机和咱自家的接口关联起来啦。

​ 这就是适配器模式

​ 我家的每个实现类其实就是一个适配器,每个适配器去适配一种打印机。比如 slf4jLoggerImpl 就完成了对 slf4j 打印机的适配,slf4jLoggerImpl .debug() 调用了slf4j.Logger.debug()。

​ 这样市面上每多一种打印机,比如想用 log4j2 了,我就只需要加一种适配器 log4j2LoggerImpl 去适配它就可以了。

代码

​ 定义接口

1
2
3
4
5
public interface Log {
boolean isDebugEnabled();
void debug(String s);
...
}

​ 在实现类里完成适配,比如适配 slf4j 的 Slf4jLoggerImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import org.apache.ibatis.logging.Log;
import org.slf4j.Logger;

class Slf4jLoggerImpl implements Log {
private final Logger log;

//注意这个地方,入参为 org.slf4j.Logger
//说明咱这个适配器要用起来,是需要传入一个真正的 slf4j 家的打印机进来的
public Slf4jLoggerImpl(Logger logger) {
log = logger;
}

//看似外面调用咱 MyBatis 的 Log 的 debug()
//其实是调用 org.slf4j.Logger 的 debug()
@Override
public void debug(String s) {
log.debug(s);
}
}

问题 2

​ 适配器咱是做好了,slf4j、log4j、stdout…做了那么多适配器,可是怎么用啊?我真正要打印机的时候,怎么才能知道我该调用哪款适配器啊?难道每次取打印机的时候,都去查一遍咱的系统配了那款打印机(jar) 包么?而且以后多加了1种打印机,我还去每个地方都改一遍,都多加一道判断么?

思路

​ 这其实就是获取实例的时候的问题,咱获取实例太累了,不如来个统一的工厂吧,我每次想打印的时候,都去找工厂要一台打印机,你工厂按照我 Log 的接口给我一个实例就是了。我也不管你给的具体实现到底是 slf4j 家的,还是 log4j 的,甚至可能是 NoLoggingImpl 每次调接口都不处理的这种空壳打印机。

​ 这样一来,有了统一的工厂,判断系统用哪种适配器的任务就可以在工厂完成了。

​ 而且以后就算新加了打印机,也只要改改工厂的代码,在工厂里多加一重判断就可以了。

​ 这就是工厂模式

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public final class LogFactory {
private static Constructor<? extends Log> logConstructor;
static {
tryImplementation(new Runnable() {
@Override
public void run() {
useSlf4jLogging();
}
});
tryImplementation(new Runnable() {
@Override
public void run() {
useCommonsLogging();
}
});
}

private LogFactory() {
// disable construction
}
public static Log getLog(String logger) {
try {
return logConstructor.newInstance(logger);
} catch (Throwable t) {
}
}
}

​ LogFactory 提供了对外的 getLog(String logger) 方法,给需要 logger 的地方提供一个 Log 的实例。

​ 内部的实现依靠 logConstructor 这个构造器通过反射来实例化一个 Log 的 Impl ,也就是之前的适配器,比如 Slf4jLoggerImpl 。

​ 判断采用哪种logConstructor 的任务则在类初始化的时候就执行了,依次尝试了我们的每一款适配器,碰上报错就说明没 jar 包,下一款,全都没有的话,就用 NoLoggingImpl 了。

评论和共享

  • 第 1 页 共 1 页
作者的图片

heeexy

世上是不是就没有你不认识的字了?


JAVA


南京