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 生成出来的代码,就会是这样的
undefined
public 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
@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
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注解的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层调用

 @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首页)。