1. 前言

明天持续搭建咱们的kono Spring Boot脚手架,上一文把国内最风行的ORM框架Mybatis也集成了进去。然而很多时候咱们心愿有一些开箱即用的通用Mapper来简化咱们的开发。我本人尝试实现了一个,接下来我分享一下思路。昨天晚上才写的,审慎用于理论生产开发,然而能够借鉴思路。

Gitee: https://gitee.com/felord/kono day03 分支

GitHub: https://github.com/NotFound40... day03 分支

2. 思路起源

最近在看一些对于Spring Data JDBC的货色,发现它很不错。其中CrudRepository十分神奇,只有ORM接口继承了它就被主动退出Spring IoC,同时也具备了一些根底的数据库操作接口。我就在想能不能把它跟Mybatis联合一下。

其实Spring Data JDBC自身是反对Mybatis的。然而我尝试整合它们之后发现,要做的事件很多,而且须要恪守很多规约,比方MybatisContext的参数上下文,接口名称前缀都有比拟严格的约定,学习应用老本比拟高,不如独自应用Spring Data JDBC爽。然而我还是想要那种通用的CRUD性能啊,所以就开始尝试本人简略搞一个。

3. 一些尝试

最开始能想到的有几个思路然而最终都没有胜利。这里也分享一下,有时候失败也是十分值得借鉴的。

3.1 Mybatis plugin

应用Mybatis的插件性能开发插件,然而钻研了半天发现不可行,最大的问题就是Mapper生命周期的问题。

在我的项目启动的时候Mapper注册到配置中,同时对应的SQL也会被注册到MappedStatement对象中。当执行Mapper的办法时会通过代理来依据名称空间(Namespace)来加载对应的MappedStatement来获取SQL并执行。

而插件的生命周期是在MappedStatement曾经注册的前提下才开始,基本连接不上。

3.2 代码生成器

这个齐全可行,然而造轮子的老本高了一些,而且成熟的很多,理论生产开发中咱们找一个就是了,个人造轮子工夫精力老本比拟高,也没有必要。

3.3 模仿MappedStatement注册

最初还是依照这个方向走,找一个适合的切入点把对应通用MapperMappedStatement注册进去。接下来会具体介绍我是如何实现的。

4. Spring 注册Mapper的机制

在最开始没有Spring Boot的时候,大都是这么注册Mapper的。

  <bean id="baseMapper" class="org.mybatis.spring.mapper.MapperFactoryBean" abstract="true" lazy-init="true">     <property name="sqlSessionFactory" ref="sqlSessionFactory" />   </bean>   <bean id="oneMapper" parent="baseMapper">     <property name="mapperInterface" value="my.package.MyMapperInterface" />   </bean>   <bean id="anotherMapper" parent="baseMapper">     <property name="mapperInterface" value="my.package.MyAnotherMapperInterface" />  </bean>

通过MapperFactoryBean每一个Mybatis Mapper被初始化并注入了Spring IoC容器。所以这个中央来进行通用Mapper的注入是可行的,而且侵入性更小一些。那么它是如何失效的呢?我在大家相熟的@MapperScan中找到了它的身影。上面摘自其源码:

/** * Specifies a custom MapperFactoryBean to return a mybatis proxy as spring bean. * * @return the class of {@code MapperFactoryBean} */Class<? extends MapperFactoryBean> factoryBean() default MapperFactoryBean.class;

也就是说通常@MapperScan会将特定包下的所有Mapper应用MapperFactoryBean批量初始化并注入Spring IoC

5. 实现通用Mapper

明确了Spring 注册Mapper的机制之后就能够开始实现通用Mapper了。

5.1 通用Mapper接口

这里借鉴Spring Data我的项目中的CrudRepository<T,ID>的格调,编写了一个Mapper的父接口CrudMapper<T, PK>,蕴含了四种根本的单表操作。

/** * 所有的Mapper接口都会继承{@code CrudMapper<T, PK>}. * * @param <T>  实体类泛型 * @param <PK> 主键泛型  * @author felord.cn * @since 14 :00 */public interface CrudMapper<T, PK> {    int insert(T entity);    int updateById(T entity);    int deleteById(PK id);    T findById(PK id);}

前面的逻辑都会围绕这个接口开展。当具体的Mapper继承这个接口后,实体类泛型 T 和主键泛型PK就曾经确定了。咱们须要拿到T的具体类型并把其成员属性封装为SQL,并定制MappedStatement

5.2 Mapper的元数据解析封装

为了简化代码,实体类做了一些常见的规约:

  • 实体类名称的下划线格调就是对应的表名,例如 UserInfo的数据库表名就是user_info
  • 实体类属性的下划线格调就是对应数据库表的字段名称。而且实体内所有的属性都有对应的数据库字段,其实能够实现疏忽。
  • 如果对应Mapper.xml存在对应的SQL,该配置疏忽。

因为主键属性必须有显式的标识能力取得,所以申明了一个主键标记注解:

/** * Demarcates an identifier. * * @author felord.cn */@Retention(RetentionPolicy.RUNTIME)@Target(value = { FIELD, METHOD, ANNOTATION_TYPE })public @interface PrimaryKey {}

而后咱们申明一个数据库实体时这样就行了:

/** * @author felord.cn * @since 15:43 **/@Datapublic class UserInfo implements Serializable {    private static final long serialVersionUID = -8938650956516110149L;    @PrimaryKey    private Long userId;    private String name;    private Integer age;}

而后就能够这样编写对用的Mapper了。

public interface UserInfoMapper extends CrudMapper<UserInfo,String> {}

上面就要封装一个解析这个接口的工具类CrudMapperProvider了。它的作用就是解析UserInfoMapper这些Mapper,封装MappedStatement。为了便于了解我通过举例对解析Mapper的过程进行阐明。

public CrudMapperProvider(Class<? extends CrudMapper<?, ?>> mapperInterface) {    // 拿到 具体的Mapper 接口  如 UserInfoMapper    this.mapperInterface = mapperInterface;    Type[] genericInterfaces = mapperInterface.getGenericInterfaces();    // 从Mapper 接口中获取 CrudMapper<UserInfo,String>    Type mapperGenericInterface = genericInterfaces[0];    // 参数化类型    ParameterizedType genericType = (ParameterizedType) mapperGenericInterface;      // 参数化类型的目标是为了解析出 [UserInfo,String]    Type[] actualTypeArguments = genericType.getActualTypeArguments();    // 这样就拿到实体类型 UserInfo    this.entityType = (Class<?>) actualTypeArguments[0];    // 拿到主键类型 String    this.primaryKeyType = (Class<?>) actualTypeArguments[1];    // 获取所有实体类属性  原本打算采纳内省形式获取    Field[] declaredFields = this.entityType.getDeclaredFields();    // 解析主键    this.identifer = Stream.of(declaredFields)            .filter(field -> field.isAnnotationPresent(PrimaryKey.class))            .findAny()            .map(Field::getName)            .orElseThrow(() -> new IllegalArgumentException(String.format("no @PrimaryKey found in %s", this.entityType.getName())));    // 解析属性名并封装为下划线字段 排除了动态属性  其它没有深刻 后续有须要可申明一个疏忽注解用来疏忽字段    this.columnFields = Stream.of(declaredFields)            .filter(field -> !Modifier.isStatic(field.getModifiers()))            .collect(Collectors.toList());    // 解析表名    this.table = camelCaseToMapUnderscore(entityType.getSimpleName()).replaceFirst("_", "");}

拿到这些元数据之后就是生成四种SQL了。咱们冀望的SQL,以UserInfoMapper为例是这样的:

#  findByIdSELECT user_id, name, age FROM user_info WHERE (user_id = #{userId})#  insertINSERT INTO user_info (user_id, name, age) VALUES (#{userId}, #{name}, #{age})#  deleteById DELETE FROM user_info WHERE (user_id = #{userId})#  updateByIdUPDATE user_info SET  name = #{name}, age = #{age} WHERE (user_id = #{userId})

Mybatis提供了很好的SQL工具类来生成这些SQL:

 String findSQL = new SQL()                .SELECT(COLUMNS)                .FROM(table)                .WHERE(CONDITION)                .toString();String insertSQL = new SQL()                .INSERT_INTO(table)                .INTO_COLUMNS(COLUMNS)                .INTO_VALUES(VALUES)                .toString();                String deleteSQL = new SQL()                .DELETE_FROM(table)                .WHERE(CONDITION).toString();                 String updateSQL = new SQL().UPDATE(table)                .SET(SETS)                .WHERE(CONDITION).toString();                

咱们只须要把后面通过反射获取的元数据来实现SQL的动态创建就能够了。以insert办法为例:

/** * Insert. * * @param configuration the configuration */private void insert(Configuration configuration) {    String insertId = mapperInterface.getName().concat(".").concat("insert");     // xml配置中曾经注册就跳过  xml中的优先级最高    if (existStatement(configuration,insertId)){        return;    }    // 生成数据库的字段列表    String[] COLUMNS = columnFields.stream()            .map(Field::getName)            .map(CrudMapperProvider::camelCaseToMapUnderscore)            .toArray(String[]::new);    // 对应的值 用 #{} 包裹    String[] VALUES = columnFields.stream()            .map(Field::getName)            .map(name -> String.format("#{%s}", name))            .toArray(String[]::new);    String insertSQL = new SQL()            .INSERT_INTO(table)            .INTO_COLUMNS(COLUMNS)            .INTO_VALUES(VALUES)            .toString();    Map<String, Object> additionalParameters = new HashMap<>();    // 注册    doAddMappedStatement(configuration, insertId, insertSQL, SqlCommandType.INSERT, entityType, additionalParameters);}

这里还有一个很重要的货色,每一个MappedStatement都有一个全局惟一的标识,Mybatis的默认规定是Mapper的全限定名用标点符号 . 拼接上对应的办法名称。例如 cn.felord.kono.mapperClientUserRoleMapper.findById。这些实现之后就是定义本人的MapperFactoryBean了。

5.3 自定义MapperFactoryBean

一个最佳的切入点是在Mapper注册后进行MappedStatement的注册。咱们能够继承MapperFactoryBean重写其checkDaoConfig办法利用CrudMapperProvider来注册MappedStatement

    @Override    protected void checkDaoConfig() {        notNull(super.getSqlSessionTemplate(), "Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required");        Class<T> mapperInterface = super.getMapperInterface();        notNull(mapperInterface, "Property 'mapperInterface' is required");        Configuration configuration = getSqlSession().getConfiguration();        if (isAddToConfig()) {            try {                // 判断Mapper 是否注册                if (!configuration.hasMapper(mapperInterface)) {                    configuration.addMapper(mapperInterface);                }                // 只有继承了CrudMapper 再进行切入                if (CrudMapper.class.isAssignableFrom(mapperInterface)) {                    // 一个注册SQL映射的机会                    CrudMapperProvider crudMapperProvider = new CrudMapperProvider(mapperInterface);                    // 注册 MappedStatement                    crudMapperProvider.addMappedStatements(configuration);                }            } catch (Exception e) {                logger.error("Error while adding the mapper '" + mapperInterface + "' to configuration.", e);                throw new IllegalArgumentException(e);            } finally {                ErrorContext.instance().reset();            }        }    }

5.4 启用通用Mapper

因为咱们笼罩了默认的MapperFactoryBean所以咱们要显式申明启用自定义的MybatisMapperFactoryBean,如下:

@MapperScan(basePackages = {"cn.felord.kono.mapper"},factoryBean = MybatisMapperFactoryBean.class)

而后一个通用Mapper性能就实现了。

5.5 我的项目地位

这只是本人的一次小尝试,我曾经独自把这个性能抽出来了,有趣味可自行参考钻研。

  • GitHub: https://github.com/NotFound40...
  • Gitee: https://gitee.com/felord/myba...

6. 总结

胜利的关键在于对Mybatis中一些概念生命周期的把控。其实大多数框架如果须要魔改时都遵循了这一个思路:把流程搞清楚,找一个适合的切入点把自定义逻辑嵌进去。本次DEMO不会合并的主分支,因为这只是一次尝试,还不足以使用于实际,你能够抉择其它出名的框架来做这些事件。多多关注并反对:码农小胖哥 分享更多开发中的事件。

关注公众号:Felordcn 获取更多资讯

集体博客:https://felord.cn