tags: [小小商城, SSM]
categories: [技术实战]


提要

  本文是 小小商城-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,隔离机器生成代码和额外增加代码

SSM开发 - 实现 Mybatis 的通用 Mapper - 图1

  当服务层调用的时候,通用 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. undefined
  2. public interface CategoryMapper extends BaseMapper<Category, CategoryExample> {
  3. }
  1. public class Category extends CategoryExtension implements POJOMapper<CategoryMapper> {
  2. }

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

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

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

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

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

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

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

  1. public class Mapper extends Mapper4ORM {
  2. private int defaultTraversalDepth = 2;
  3. public Object selectByPrimaryKey(Integer id) throws Exception {
  4. return selectByPrimaryKey(id, defaultTraversalDepth);
  5. }
  6. public Object selectByPrimaryKey(Integer id, Integer depth) throws Exception {
  7. Object object = mapper.getClass().getMethod("selectByPrimaryKey", Integer.class).invoke(mapper, id);
  8. fillOnReading(object, depth);
  9. return object;
  10. }
  11. public int insert(Object object) throws Exception {
  12. fillOnWriting(object);
  13. return (int) mapper.getClass().getMethod("insert", object.getClass()).invoke(mapper, object);
  14. }
  15. public int insertSelective(Object object) throws Exception {
  16. fillOnWriting(object);
  17. return (int) mapper.getClass().getMethod("insertSelective", Object.class).invoke(mapper, object);
  18. }
  19. public int updateByPrimaryKeySelective(Object object) throws Exception {
  20. fillOnWriting(object);
  21. return (int) mapper.getClass().
  22. getMethod("updateByPrimaryKeySelective", object.getClass()).invoke(mapper, object);
  23. }
  24. public int updateByPrimaryKey(Object object) throws Exception {
  25. fillOnWriting(object);
  26. return (int) mapper.getClass().getMethod("updateByPrimaryKey", object.getClass()).invoke(mapper, object);
  27. }
  28. public List selectByExample(Object example) throws Exception {
  29. return selectByExample(example, defaultTraversalDepth);
  30. }
  31. public List selectByExample(Object example, int depth) throws Exception {
  32. List result = (List) mapper.getClass().getMethod("selectByExample", example.getClass()).invoke(mapper, example);
  33. for (int i = 0; i < result.size(); i++) {
  34. Object item = result.get(i);
  35. fillOnReading(item, depth);
  36. result.set(i, item);
  37. }
  38. return result;
  39. }
  40. }

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

  1. /**
  2. * 通用 Mapper | 核心,处理一对多,多对一的插入
  3. */
  4. @SuppressWarnings("unchecked")
  5. public class Mapper4ORM {
  6. Object mapper;
  7. private Class mapperInterface;
  8. private SqlSessionTemplate sqlSessionTemplate;
  9. void setSqlSessionTemplate(SqlSessionTemplate sqlSessionTemplate) {
  10. this.sqlSessionTemplate = sqlSessionTemplate;
  11. }
  12. void setMybatisMapper(Class mapperInterface) throws Exception {
  13. this.mapperInterface = mapperInterface;
  14. mapper = getMapper(mapperInterface);
  15. }
  16. public Object getMapper(Class mapperInterface) throws Exception {
  17. return sqlSessionTemplate.getMapper(mapperInterface);
  18. }
  19. public BaseExample getExample(Class mapperInterface) throws Exception {
  20. ParameterizedType t = (ParameterizedType) mapperInterface.getGenericInterfaces()[0];
  21. Class exampleClass = (Class) t.getActualTypeArguments()[1];
  22. return (BaseExample) exampleClass.newInstance();
  23. }
  24. public Class getMapperInterfaceByPOJO(Class POJOClass) throws Exception {
  25. ParameterizedType t = (ParameterizedType) POJOClass.getGenericInterfaces()[0];
  26. return (Class) t.getActualTypeArguments()[0];
  27. }
  28. /**
  29. * 获取一个类里的,有指定annotation的,所有 Filed
  30. *
  31. * @param objectClass 一个类
  32. * @param annotationClass 指定的 annotation
  33. * @return 所有的 Filed
  34. */
  35. List<Field> getFieldsEquals(Class objectClass, Class annotationClass) {
  36. if (objectClass == null) {
  37. return null;
  38. }
  39. List<Field> fields = new ArrayList<>();
  40. for (Class temp = objectClass; temp != Object.class; temp = temp.getSuperclass()) {
  41. fields.addAll(Arrays.asList(temp.getDeclaredFields()));
  42. }
  43. List<Field> result = new ArrayList<>();
  44. for (Field field : fields) {
  45. if (field.getAnnotation(annotationClass) != null)
  46. result.add(field);
  47. }
  48. return result;
  49. }
  50. /**
  51. * 读取时,处理所有 多对一 的填充
  52. *
  53. * @param object 被填充的对象
  54. * @param depth 当前深度
  55. * @throws Exception 反射异常
  56. */
  57. public void fillManyToOneOnReading(Object object, int depth) throws Exception {
  58. if (object == null) {
  59. return;
  60. }
  61. Class clazz = object.getClass();
  62. // 获取所有 ManyToOne注解的Filed
  63. List<Field> result = getFieldsEquals(clazz, ManyToOne.class);
  64. for (Field field : result) {
  65. //获取外键的表名
  66. String joinColumn = field.getAnnotation(JoinColumn.class).name();
  67. //获取要填充对象的mapper
  68. Class targetMapperClass = getMapperInterfaceByPOJO(field.getType());
  69. Object targetMapper = getMapper(targetMapperClass);
  70. //获取外键值
  71. Integer joinColumnValue = (Integer) clazz.
  72. getMethod("get" + StringUtils.capitalize(joinColumn)).invoke(object);
  73. if (joinColumnValue == null) {
  74. continue;
  75. }
  76. //配置查询器example
  77. BaseExample example = getExample(targetMapperClass);
  78. Object criteria = example.createCriteria();
  79. // 配置criteria
  80. criteria.getClass().getMethod("andIdEqualTo", Integer.class).invoke(criteria, joinColumnValue);
  81. //查询,获取结果列表
  82. List targetResults = (List) targetMapper.getClass().getMethod("selectByExample", example.getClass()).
  83. invoke(targetMapper, example);
  84. //判断是否为空 ,不为空插入 filed
  85. if (targetResults.size() > 0) {
  86. Object targetResult = targetResults.get(0);
  87. fillOnReading(targetResult, depth - 1);
  88. clazz.getMethod("set" + StringUtils.capitalize(field.getName()), targetResult.getClass())
  89. .invoke(object, targetResult);
  90. }
  91. }
  92. }
  93. /**
  94. * 读取时,处理所有 一对多 的填充
  95. *
  96. * @param object 被填充的对象
  97. * @param depth 当前深度
  98. * @throws Exception 反射异常
  99. */
  100. public void fillOneToManyOnReading(Object object, int depth) throws Exception {
  101. if (object == null) {
  102. return;
  103. }
  104. Class clazz = object.getClass();
  105. // 获取所有 ManyToOne注解的Filed
  106. List<Field> result = getFieldsEquals(clazz, OneToMany.class);
  107. for (Field field : result) {
  108. //获取外键的表名
  109. String joinColumn = field.getAnnotation(JoinColumn.class).name();
  110. //得到其Generic的类型
  111. Type genericType = field.getGenericType();
  112. ParameterizedType pt = (ParameterizedType) genericType;
  113. //得到List泛型里的目标类型对象
  114. Class targetClass = (Class) pt.getActualTypeArguments()[0];
  115. //获取要填充对象的mapper
  116. Class targetMapperClass = getMapperInterfaceByPOJO(targetClass);
  117. Object targetMapper = getMapper(targetMapperClass);
  118. //获取外键值
  119. Integer joinColumnValue = (Integer) clazz.
  120. getMethod("getId").invoke(object);
  121. //配置查询器example
  122. BaseExample example = getExample(targetMapperClass);
  123. Object criteria = example.createCriteria();
  124. // 配置criteria
  125. criteria.getClass().getMethod("and" + StringUtils.capitalize(joinColumn) + "EqualTo", Integer.class).invoke(criteria, joinColumnValue);
  126. //查询,获取结果列表
  127. List targetResults = (List) targetMapper.getClass().getMethod("selectByExample", example.getClass()).
  128. invoke(targetMapper, example);
  129. for (int i = 0; i < targetResults.size(); i++) {
  130. Object item = targetResults.get(i);
  131. fillOnReading(item, depth - 1);
  132. targetResults.set(i, item);
  133. }
  134. //插入 filed
  135. clazz.getMethod("set" + StringUtils.capitalize(field.getName()), List.class)
  136. .invoke(object, targetResults);
  137. }
  138. }
  139. /**
  140. * 读取时,处理所有 Enum 的填充
  141. *
  142. * @param object 被填充的对象
  143. * @throws Exception 反射异常
  144. */
  145. public void fillEnumOnReading(Object object) throws Exception {
  146. if (object == null) {
  147. return;
  148. }
  149. Class clazz = object.getClass();
  150. // 获取所有 ManyToOne注解的Filed
  151. List<Field> result = getFieldsEquals(clazz, Enumerated.class);
  152. for (Field field : result) {
  153. //获取Enum对应的,String类型的变量名
  154. String varName = field.getAnnotation(Enumerated.class).var();
  155. //获取值
  156. String enumString = (String) clazz.
  157. getMethod("get" + StringUtils.capitalize(varName)).invoke(object);
  158. // 转成Enum,插回 filed
  159. Enum resultObj = Enum.valueOf((Class<Enum>) field.getType(), enumString);
  160. clazz.getMethod("set" + StringUtils.capitalize(field.getName()), resultObj.getClass())
  161. .invoke(object, resultObj);
  162. }
  163. }
  164. /**
  165. * 写入时,处理所有 Enum 的填充
  166. *
  167. * @param object 被填充的对象
  168. * @throws Exception 反射异常
  169. */
  170. public void fillEnumOnWriting(Object object) throws Exception {
  171. if (object == null) {
  172. return;
  173. }
  174. Class clazz = object.getClass();
  175. // 获取所有 ManyToOne注解的Filed
  176. List<Field> result = getFieldsEquals(clazz, Enumerated.class);
  177. for (Field field : result) {
  178. //获取Enum对应的,String类型的变量名
  179. String varName = field.getAnnotation(Enumerated.class).var();
  180. //获取 Enum
  181. Enum enumObj = (Enum) clazz.
  182. getMethod("get" + StringUtils.capitalize(field.getName())).invoke(object);
  183. // 转成 String,插回 varName
  184. String enumString = enumObj.name();
  185. clazz.getMethod("set" + StringUtils.capitalize(varName), String.class)
  186. .invoke(object, enumString);
  187. }
  188. }
  189. /**
  190. * 写入时,处理所有 ManyToOne 的填充
  191. *
  192. * @param object 被填充的对象
  193. * @throws Exception 反射异常
  194. */
  195. public void fillManyToOneOnWriting(Object object) throws Exception {
  196. if (object == null) {
  197. return;
  198. }
  199. Class clazz = object.getClass();
  200. // 获取所有 ManyToOne注解的Filed
  201. List<Field> result = getFieldsEquals(clazz, ManyToOne.class);
  202. for (Field field : result) {
  203. //获取One端的变量名
  204. String columnName = field.getAnnotation(JoinColumn.class).name();
  205. //获取One的对象
  206. Object targetObj = clazz
  207. .getMethod("get" + StringUtils.capitalize(field.getName()))
  208. .invoke(object);
  209. if (targetObj == null) {
  210. continue;
  211. }
  212. //获取 获取 id 值
  213. int id = (int) targetObj.getClass().
  214. getMethod("getId").invoke(targetObj);
  215. // 插回 columnName
  216. clazz.getMethod("set" + StringUtils.capitalize(columnName), Integer.class)
  217. .invoke(object, id);
  218. }
  219. }
  220. /**
  221. * 读取时填充数据,递归调用上面的方法
  222. * @param object 对象
  223. * @param depth 当前递归深度
  224. * @throws Exception 反射异常
  225. */
  226. public void fillOnReading(Object object, int depth) throws Exception {
  227. if (object == null) {
  228. return;
  229. }
  230. if (depth <= 0) {
  231. return;
  232. }
  233. // 处理 ManyToOne
  234. fillManyToOneOnReading(object, depth);
  235. // 处理 OneToMany
  236. fillOneToManyOnReading(object, depth);
  237. // 处理 Enumerated
  238. fillEnumOnReading(object);
  239. }
  240. /**
  241. * 写入时填充数据,递归调用上面的方法
  242. * @param object 对象
  243. * @throws Exception 反射异常
  244. */
  245. public void fillOnWriting(Object object) throws Exception {
  246. if (object == null) {
  247. return;
  248. }
  249. // 处理 Enumerated
  250. fillEnumOnWriting(object);
  251. // 处理 ManyToOne
  252. fillManyToOneOnWriting(object);
  253. }
  254. }

4.在service层调用

  1. @Resource
  2. private MapperFactory mapperFactory;

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

后记

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

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

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

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