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,隔离机器生成代码和额外增加代码)

当服务层调用的时候,通用 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 生成出来的代码,就会是这样的
undefinedpublic interface CategoryMapper extends BaseMapper<Category, CategoryExample> {}
public class Category extends CategoryExtension implements POJOMapper<CategoryMapper> {}
这样,我们在填充时,读取这个类的时候,就可以通过反射,拿到 <> 中的内容,比如现在知道了一个 product 对象,里面要我们填充一个 Categorty 类,那么我们先来查 Category 类,知道对应的 Mapper 是 CategoryMapper,对应的 Example 是 CategoryEmaple,那么,我们就可以愉快的调用 CategoryMapper 拿到一个 category ,然后插回 product 即可。
3.开发通用 mapper :静态代理+递归填充
我们先通过拿到 mybatis 自带的 sqlSessionTemplate ,需要在 applicationContext.xml 中 注册一下
<bean id="sqlSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate"><constructor-arg index="0" ref="sqlSession"/></bean>
然后我们开发一个 MapperFactory 工厂类,用来在Service层获取我们的通用 mapper,并且从 SqlSessionTemplate 拿到 CateogoryMapper,塞进通用 mapper 里面,最后返回通用 mapper MapperFactory.java
@Componentpublic class MapperFactory {@Resourceprivate 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
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
/*** 通用 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注解的FiledList<Field> result = getFieldsEquals(clazz, ManyToOne.class);for (Field field : result) {//获取外键的表名String joinColumn = field.getAnnotation(JoinColumn.class).name();//获取要填充对象的mapperClass targetMapperClass = getMapperInterfaceByPOJO(field.getType());Object targetMapper = getMapper(targetMapperClass);//获取外键值Integer joinColumnValue = (Integer) clazz.getMethod("get" + StringUtils.capitalize(joinColumn)).invoke(object);if (joinColumnValue == null) {continue;}//配置查询器exampleBaseExample example = getExample(targetMapperClass);Object criteria = example.createCriteria();// 配置criteriacriteria.getClass().getMethod("andIdEqualTo", Integer.class).invoke(criteria, joinColumnValue);//查询,获取结果列表List targetResults = (List) targetMapper.getClass().getMethod("selectByExample", example.getClass()).invoke(targetMapper, example);//判断是否为空 ,不为空插入 filedif (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注解的FiledList<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];//获取要填充对象的mapperClass targetMapperClass = getMapperInterfaceByPOJO(targetClass);Object targetMapper = getMapper(targetMapperClass);//获取外键值Integer joinColumnValue = (Integer) clazz.getMethod("getId").invoke(object);//配置查询器exampleBaseExample example = getExample(targetMapperClass);Object criteria = example.createCriteria();// 配置criteriacriteria.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);}//插入 filedclazz.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注解的FiledList<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,插回 filedEnum 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注解的FiledList<Field> result = getFieldsEquals(clazz, Enumerated.class);for (Field field : result) {//获取Enum对应的,String类型的变量名String varName = field.getAnnotation(Enumerated.class).var();//获取 EnumEnum enumObj = (Enum) clazz.getMethod("get" + StringUtils.capitalize(field.getName())).invoke(object);// 转成 String,插回 varNameString 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注解的FiledList<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);// 插回 columnNameclazz.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;}// 处理 ManyToOnefillManyToOneOnReading(object, depth);// 处理 OneToManyfillOneToManyOnReading(object, depth);// 处理 EnumeratedfillEnumOnReading(object);}/*** 写入时填充数据,递归调用上面的方法* @param object 对象* @throws Exception 反射异常*/public void fillOnWriting(Object object) throws Exception {if (object == null) {return;}// 处理 EnumeratedfillEnumOnWriting(object);// 处理 ManyToOnefillManyToOneOnWriting(object);}}
4.在service层调用
@Resourceprivate MapperFactory mapperFactory;
拿到 mapperFactory ,如果要对CategoryMaper进行填充处理的话,就直接用mapperFactory.getMapper(mapperInterface);即可拿到对应的通用 mapper ,然后和 原来的 mybatis mapper 使用 方法一样。
后记
毫无疑问,这个通用 mapper 还很不完善,效率也比较低,现在的实现只相当于玩具的级别。而且市面上也已经有了一系列 jpa系统实现,这个通用mapper存在的意义也不是十分大。
但是,我们可以通过这个 mapper 理解到泛型、反射的一系列用法,递归的实操中的使用,还可以对mybatis-generator有了更深的理解
这个 mapper 也在我的 小小商城-ssm版中 完整运用了,为此我可以使用 mybatis 而不用写 一行 sql 代码。
时间仓促,涉及到的知识点也太多,文章非常简略,对新手也不是十分友好,在此表示歉意。如果想详细理解这个 通用 mapper ,可以到项目 github 中 查看全部源代码,或者发邮件、留言和我交流 (邮件地址在Github首页)。
