提要

本文是 小小商城-SSM 版的 细节详解系列 之一,项目 github:https://github.com/xenv/S-mall-ssm 本文代码大部分在 github 中 可以找到。

中小型项目使用 Mybatis 如何减少 mapper 的工作量?市面上已经有一款产品 ,叫”通用 mapper”,但是我使用之后有点失望。一个是它侵入型极强,要改掉 mybatis 的 factory,数据库的名字和列名都要改成指定格式,错一个也不行。另外,还不支持关联查询,可谓是有点鸡肋了。于是,就有了我这么一个通用 mapper 的实现,可以让 Mybatis 像 Hibernate 一样使用。只要配置好自定义注解,用 mybatis-generator 生成好代码,就可以自动处理 各种关联查询(一对多、多对一自动插入),还提供接口修改遍历填充关联查询的深度,支持手写的扩展 mappper 接入到填充系统。

那么,这套系统到底是如何工作的呢?我做了一个示意图来说明 Category 表 下面的情况,其他表同理。 (mybatis-generator 隔离手写代码和机器代码请参考我写的文章:SSM 开发 | 合理配置 mybatis-generator,隔离机器生成代码和额外增加代码

当服务层调用的时候,通用 mapper 会先查询 服务器层指定的 CategoryMapper,从数据库拿到数据。然后通用 mapper 调用 MapperCore 对 拿到的 Category 进行一对多和多对一对象的填充。最后填充完的 category 对象将会返回给服务层。

一对多和多对一如何配置呢?这需要我们在 extension 类中 用自定义注解配置,然后机器生成的 Category 类 将继承 我们的 extension,因此 我们 最终拿到的 category 就是 有关联对象变量的 对象了。

那么,我们如何知道 一对多 多对一 的对象应该由哪个 mapper 来填充呢,那么我们就需要在 category 类上指定好 对应的 mapper (使用泛型继承)即可,怎么让它自动指定呢?对了,就是用 mybatis-generator 自定义插件。

在看具体实现之前,我假定您已经对 mybatis-generator 、自定义注解、反射有一定的了解。

具体实现

1.创建 实体 extension 配置一对多、多对一信息

创建 extension 参见::SSM 开发 | 合理配置 mybatis-generator,隔离机器生成代码和额外增加代码

配置一对多、多对一信息使用自定义 注解,注解的定义在这里 ORMAnnotation 语法和 hibernate 差不多,临时变量不需要再使用注解,enum 用了 (var=””),取消了 OneToOne,因为本质上和 ManyToOne 是一样的,OneToOne 还容易弄混。

配置好注解之后,效果是这样的: extension

2.定义 mybatis-generator 插件,让自动生成的 mapper 和 实体类 带上 泛型 信息,以供通用 mapper 读取

自定义 mybatis-generator 插件 参见我的文章:SSM 开发 | 开发自定义插件,使 mybatis-generator 支持软删除

插件代码见:MapperExtendsPlugin.java POJOExtendsPlugin.java

之后用 mybatis-generator 生成出来的代码,就会是这样的

1
2
3
undefined
public interface CategoryMapper extends BaseMapper<Category, CategoryExample> {
}
1
2
public class Category extends CategoryExtension implements POJOMapper<CategoryMapper> {
}

这样,我们在填充时,读取这个类的时候,就可以通过反射,拿到 <> 中的内容,比如现在知道了一个 product 对象,里面要我们填充一个 Categorty 类,那么我们先来查 Category 类,知道对应的 Mapper 是 CategoryMapper,对应的 Example 是 CategoryEmaple,那么,我们就可以愉快的调用 CategoryMapper 拿到一个 category ,然后插回 product 即可。

3.开发通用 mapper :静态代理+递归填充

我们先通过拿到 mybatis 自带的 sqlSessionTemplate ,需要在 applicationContext.xml 中 注册一下

1
2
3
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
<constructor-arg index="0" ref="sqlSession"/>
</bean>

然后我们开发一个 MapperFactory 工厂类,用来在 Service 层获取我们的通用 mapper,并且从 SqlSessionTemplate 拿到 CateogoryMapper,塞进通用 mapper 里面,最后返回通用 mapper MapperFactory.java

1
2
3
4
5
6
7
8
9
10
11
@Component
public class MapperFactory {
@Resource
private SqlSessionTemplate sqlSessionTemplate;
public Mapper getMapper(Class mapperInterface) throws Exception {
Mapper mapper = new Mapper();
mapper.setSqlSessionTemplate(sqlSessionTemplate);
mapper.setMybatisMapper(mapperInterface);
return mapper;
}
}

通用 mapper 里面其实比较简单,就是直接使用反射做通用具体 mapper 的静态代理,并且调用 mapperCore 中 的递归填充器 Mapper.java

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Mapper extends Mapper4ORM {

private int defaultTraversalDepth = 2;

public Object selectByPrimaryKey(Integer id) throws Exception {
return selectByPrimaryKey(id, defaultTraversalDepth);
}

public Object selectByPrimaryKey(Integer id, Integer depth) throws Exception {
Object object = mapper.getClass().getMethod("selectByPrimaryKey", Integer.class).invoke(mapper, id);
fillOnReading(object, depth);
return object;
}

public int insert(Object object) throws Exception {
fillOnWriting(object);
return (int) mapper.getClass().getMethod("insert", object.getClass()).invoke(mapper, object);
}

public int insertSelective(Object object) throws Exception {
fillOnWriting(object);
return (int) mapper.getClass().getMethod("insertSelective", Object.class).invoke(mapper, object);
}

public int updateByPrimaryKeySelective(Object object) throws Exception {
fillOnWriting(object);
return (int) mapper.getClass().
getMethod("updateByPrimaryKeySelective", object.getClass()).invoke(mapper, object);
}

public int updateByPrimaryKey(Object object) throws Exception {
fillOnWriting(object);
return (int) mapper.getClass().getMethod("updateByPrimaryKey", object.getClass()).invoke(mapper, object);
}

public List selectByExample(Object example) throws Exception {
return selectByExample(example, defaultTraversalDepth);
}

public List selectByExample(Object example, int depth) throws Exception {
List result = (List) mapper.getClass().getMethod("selectByExample", example.getClass()).invoke(mapper, example);
for (int i = 0; i < result.size(); i++) {
Object item = result.get(i);
fillOnReading(item, depth);
result.set(i, item);
}
return result;
}
}

最后,我们开发我们核心类,就是开发递归填充器,对一对多、多对一进行读取、处理、回填 Mapper4ORM.java

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
/**
* 通用 Mapper | 核心,处理一对多,多对一的插入
*/

@SuppressWarnings("unchecked")
public class Mapper4ORM {
Object mapper;

private Class mapperInterface;

private SqlSessionTemplate sqlSessionTemplate;

void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
this.sqlSessionTemplate = sqlSessionTemplate;
}

void setMybatisMapper(Class mapperInterface) throws Exception {
this.mapperInterface = mapperInterface;
mapper = getMapper(mapperInterface);
}

public Object getMapper(Class mapperInterface) throws Exception {
return sqlSessionTemplate.getMapper(mapperInterface);
}

public BaseExample getExample(Class mapperInterface) throws Exception {
ParameterizedType t = (ParameterizedType) mapperInterface.getGenericInterfaces()[0];
Class exampleClass = (Class) t.getActualTypeArguments()[1];
return (BaseExample) exampleClass.newInstance();
}

public Class getMapperInterfaceByPOJO(Class POJOClass) throws Exception {
ParameterizedType t = (ParameterizedType) POJOClass.getGenericInterfaces()[0];
return (Class) t.getActualTypeArguments()[0];
}

/**
* 获取一个类里的,有指定annotation的,所有 Filed
*
* @param objectClass 一个类
* @param annotationClass 指定的 annotation
* @return 所有的 Filed
*/
List<Field> getFieldsEquals(Class objectClass, Class annotationClass) {
if (objectClass == null) {
return null;
}
List<Field> fields = new ArrayList<>();
for (Class temp = objectClass; temp != Object.class; temp = temp.getSuperclass()) {
fields.addAll(Arrays.asList(temp.getDeclaredFields()));
}
List<Field> result = new ArrayList<>();
for (Field field : fields) {
if (field.getAnnotation(annotationClass) != null)
result.add(field);
}
return result;
}

/**
* 读取时,处理所有 多对一 的填充
*
* @param object 被填充的对象
* @param depth 当前深度
* @throws Exception 反射异常
*/
public void fillManyToOneOnReading(Object object, int depth) throws Exception {
if (object == null) {
return;
}
Class clazz = object.getClass();
// 获取所有 ManyToOne注解的Filed
List<Field> result = getFieldsEquals(clazz, ManyToOne.class);
for (Field field : result) {
//获取外键的表名
String joinColumn = field.getAnnotation(JoinColumn.class).name();

//获取要填充对象的mapper
Class targetMapperClass = getMapperInterfaceByPOJO(field.getType());
Object targetMapper = getMapper(targetMapperClass);
//获取外键值
Integer joinColumnValue = (Integer) clazz.
getMethod("get" + StringUtils.capitalize(joinColumn)).invoke(object);
if (joinColumnValue == null) {
continue;
}
//配置查询器example
BaseExample example = getExample(targetMapperClass);
Object criteria = example.createCriteria();
// 配置criteria
criteria.getClass().getMethod("andIdEqualTo", Integer.class).invoke(criteria, joinColumnValue);
//查询,获取结果列表
List targetResults = (List) targetMapper.getClass().getMethod("selectByExample", example.getClass()).
invoke(targetMapper, example);

//判断是否为空 ,不为空插入 filed
if (targetResults.size() > 0) {
Object targetResult = targetResults.get(0);
fillOnReading(targetResult, depth - 1);
clazz.getMethod("set" + StringUtils.capitalize(field.getName()), targetResult.getClass())
.invoke(object, targetResult);
}
}
}

/**
* 读取时,处理所有 一对多 的填充
*
* @param object 被填充的对象
* @param depth 当前深度
* @throws Exception 反射异常
*/
public void fillOneToManyOnReading(Object object, int depth) throws Exception {
if (object == null) {
return;
}
Class clazz = object.getClass();
// 获取所有 ManyToOne注解的Filed
List<Field> result = getFieldsEquals(clazz, OneToMany.class);
for (Field field : result) {
//获取外键的表名
String joinColumn = field.getAnnotation(JoinColumn.class).name();
//得到其Generic的类型
Type genericType = field.getGenericType();
ParameterizedType pt = (ParameterizedType) genericType;
//得到List泛型里的目标类型对象
Class targetClass = (Class) pt.getActualTypeArguments()[0];
//获取要填充对象的mapper
Class targetMapperClass = getMapperInterfaceByPOJO(targetClass);
Object targetMapper = getMapper(targetMapperClass);
//获取外键值
Integer joinColumnValue = (Integer) clazz.
getMethod("getId").invoke(object);
//配置查询器example
BaseExample example = getExample(targetMapperClass);
Object criteria = example.createCriteria();
// 配置criteria
criteria.getClass().getMethod("and" + StringUtils.capitalize(joinColumn) + "EqualTo", Integer.class).invoke(criteria, joinColumnValue);
//查询,获取结果列表
List targetResults = (List) targetMapper.getClass().getMethod("selectByExample", example.getClass()).
invoke(targetMapper, example);
for (int i = 0; i < targetResults.size(); i++) {
Object item = targetResults.get(i);
fillOnReading(item, depth - 1);
targetResults.set(i, item);
}
//插入 filed
clazz.getMethod("set" + StringUtils.capitalize(field.getName()), List.class)
.invoke(object, targetResults);

}
}

/**
* 读取时,处理所有 Enum 的填充
*
* @param object 被填充的对象
* @throws Exception 反射异常
*/
public void fillEnumOnReading(Object object) throws Exception {
if (object == null) {
return;
}
Class clazz = object.getClass();
// 获取所有 ManyToOne注解的Filed
List<Field> result = getFieldsEquals(clazz, Enumerated.class);
for (Field field : result) {
//获取Enum对应的,String类型的变量名
String varName = field.getAnnotation(Enumerated.class).var();

//获取值
String enumString = (String) clazz.
getMethod("get" + StringUtils.capitalize(varName)).invoke(object);

// 转成Enum,插回 filed
Enum resultObj = Enum.valueOf((Class<Enum>) field.getType(), enumString);
clazz.getMethod("set" + StringUtils.capitalize(field.getName()), resultObj.getClass())
.invoke(object, resultObj);

}
}

/**
* 写入时,处理所有 Enum 的填充
*
* @param object 被填充的对象
* @throws Exception 反射异常
*/
public void fillEnumOnWriting(Object object) throws Exception {
if (object == null) {
return;
}
Class clazz = object.getClass();
// 获取所有 ManyToOne注解的Filed
List<Field> result = getFieldsEquals(clazz, Enumerated.class);
for (Field field : result) {
//获取Enum对应的,String类型的变量名
String varName = field.getAnnotation(Enumerated.class).var();

//获取 Enum
Enum enumObj = (Enum) clazz.
getMethod("get" + StringUtils.capitalize(field.getName())).invoke(object);

// 转成 String,插回 varName
String enumString = enumObj.name();
clazz.getMethod("set" + StringUtils.capitalize(varName), String.class)
.invoke(object, enumString);
}
}

/**
* 写入时,处理所有 ManyToOne 的填充
*
* @param object 被填充的对象
* @throws Exception 反射异常
*/
public void fillManyToOneOnWriting(Object object) throws Exception {
if (object == null) {
return;
}
Class clazz = object.getClass();
// 获取所有 ManyToOne注解的Filed
List<Field> result = getFieldsEquals(clazz, ManyToOne.class);
for (Field field : result) {
//获取One端的变量名
String columnName = field.getAnnotation(JoinColumn.class).name();

//获取One的对象
Object targetObj = clazz
.getMethod("get" + StringUtils.capitalize(field.getName()))
.invoke(object);
if (targetObj == null) {
continue;
}
//获取 获取 id 值
int id = (int) targetObj.getClass().
getMethod("getId").invoke(targetObj);

// 插回 columnName

clazz.getMethod("set" + StringUtils.capitalize(columnName), Integer.class)
.invoke(object, id);
}
}

/**
* 读取时填充数据,递归调用上面的方法
* @param object 对象
* @param depth 当前递归深度
* @throws Exception 反射异常
*/
public void fillOnReading(Object object, int depth) throws Exception {
if (object == null) {
return;
}
if (depth <= 0) {
return;
}

// 处理 ManyToOne
fillManyToOneOnReading(object, depth);
// 处理 OneToMany
fillOneToManyOnReading(object, depth);
// 处理 Enumerated
fillEnumOnReading(object);

}

/**
* 写入时填充数据,递归调用上面的方法
* @param object 对象
* @throws Exception 反射异常
*/
public void fillOnWriting(Object object) throws Exception {
if (object == null) {
return;
}
// 处理 Enumerated
fillEnumOnWriting(object);
// 处理 ManyToOne
fillManyToOneOnWriting(object);
}
}

4.在 service 层调用

1
2
@Resource
private MapperFactory mapperFactory;

拿到 mapperFactory ,如果要对 CategoryMaper 进行填充处理的话,就直接用 mapperFactory.getMapper(mapperInterface);即可拿到对应的通用 mapper ,然后和 原来的 mybatis mapper 使用 方法一样。

后记

毫无疑问,这个通用 mapper 还很不完善,效率也比较低,现在的实现只相当于玩具的级别。而且市面上也已经有了一系列 jpa 系统实现,这个通用 mapper 存在的意义也不是十分大。

但是,我们可以通过这个 mapper 理解到泛型、反射的一系列用法,递归的实操中的使用,还可以对 mybatis-generator 有了更深的理解

这个 mapper 也在我的 小小商城-ssm 版中 完整运用了,为此我可以使用 mybatis 而不用写 一行 sql 代码。

时间仓促,涉及到的知识点也太多,文章非常简略,对新手也不是十分友好,在此表示歉意。如果想详细理解这个 通用 mapper ,可以到项目 github 中 查看全部源代码,或者发邮件、留言和我交流 (邮件地址在 Github 首页)。