关于java:手把手教你开发-MyBatis-插件

小伙伴们元宵节高兴,记得吃元宵哦~

在日常开发中,小伙伴们多多少少都有用过 MyBatis 插件,松哥猜想大家用的最多的就是 MyBatis 的分页插件!不晓得小伙伴们有没有想过有一天本人也来开发一个 MyBatis 插件?

其实本人入手撸一个 MyBatis 插件并不难,明天松哥就把手带大家撸一个 MyBatis 插件!

1.MyBatis 插件接口

即便你没开发过 MyBatis 插件,预计也能猜出来,MyBatis 插件是通过拦截器来起作用的,MyBatis 框架在设计的时候,就曾经为插件的开发预留了相干接口,如下:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }

  default void setProperties(Properties properties) {
    // NOP
  }

}

这个接口中就三个办法,第一个办法必须实现,前面两个办法都是可选的。三个办法作用别离如下:

  1. intercept:这个就是具体的拦挡办法,咱们自定义 MyBatis 插件时,个别都须要重写该办法,咱们插件所实现的工作也都是在该办法中实现的。
  2. plugin:这个办法的参数 target 就是拦截器要拦挡的对象,一般来说咱们不须要重写该办法。Plugin.wrap 办法会主动判断拦截器的签名和被拦挡对象的接口是否匹配,如果匹配,才会通过动静代理拦挡指标对象。
  3. setProperties:这个办法用来传递插件的参数,能够通过参数来扭转插件的行为。咱们定义好插件之后,须要对插件进行配置,在配置的时候,能够给插件设置相干属性,设置的属性能够通过该办法获取到。插件属性设置像上面这样:
<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
        <property name="xxx" value="xxx"/>
    </plugin>
</plugins>

2.MyBatis 拦截器签名

拦截器定义好了后,拦挡谁?

这个就须要拦截器签名来实现了!

拦截器签名是一个名为 @Intercepts 的注解,该注解中能够通过 @Signature 配置多个签名。@Signature 注解中则蕴含三个属性:

  • type: 拦截器须要拦挡的接口,有 4 个可选项,别离是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler。
  • method: 拦截器所拦挡接口中的办法名,也就是后面四个接口中的办法名,接口和办法要对应上。
  • args: 拦截器所拦挡办法的参数类型,通过办法名和参数类型能够锁定惟一一个办法。

一个简略的签名可能像上面这样:

@Intercepts(@Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
    //...
}

3.被拦挡的对象

依据后面的介绍,被拦挡的对象次要有如下四个:

Executor

public interface Executor {

  ResultHandler NO_RESULT_HANDLER = null;

  int update(MappedStatement ms, Object parameter) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;

  <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;

  <E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;

  List<BatchResult> flushStatements() throws SQLException;

  void commit(boolean required) throws SQLException;

  void rollback(boolean required) throws SQLException;

  CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);

  boolean isCached(MappedStatement ms, CacheKey key);

  void clearLocalCache();

  void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);

  Transaction getTransaction();

  void close(boolean forceRollback);

  boolean isClosed();

  void setExecutorWrapper(Executor executor);

}

各办法含意别离如下:

  • update:该办法会在所有的 INSERT、 UPDATE、 DELETE 执行时被调用,如果想要拦挡这些操作,能够通过该办法实现。
  • query:该办法会在 SELECT 查询方法执行时被调用,办法参数携带了很多有用的信息,如果须要获取,能够通过该办法实现。
  • queryCursor:当 SELECT 的返回类型是 Cursor 时,该办法会被调用。
  • flushStatements:当 SqlSession 办法调用 flushStatements 办法或执行的接口办法中带有 @Flush 注解时该办法会被触发。
  • commit:当 SqlSession 办法调用 commit 办法时该办法会被触发。
  • rollback:当 SqlSession 办法调用 rollback 办法时该办法会被触发。
  • getTransaction:当 SqlSession 办法获取数据库连贯时该办法会被触发。
  • close:该办法在懒加载获取新的 Executor 后会被触发。
  • isClosed:该办法在懒加载执行查问前会被触发。

ParameterHandler

public interface ParameterHandler {

  Object getParameterObject();

  void setParameters(PreparedStatement ps) throws SQLException;

}

各办法含意别离如下:

  • getParameterObject:在执行存储过程解决出参的时候该办法会被触发。
  • setParameters:设置 SQL 参数时该办法会被触发。

ResultSetHandler

public interface ResultSetHandler {

  <E> List<E> handleResultSets(Statement stmt) throws SQLException;

  <E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;

  void handleOutputParameters(CallableStatement cs) throws SQLException;

}

各办法含意别离如下:

  • handleResultSets:该办法会在所有的查询方法中被触发(除去返回值类型为 Cursor<E> 的查询方法),一般来说,如果咱们想对查问后果进行二次解决,能够通过拦挡该办法实现。
  • handleCursorResultSets:当查询方法的返回值类型为 Cursor<E> 时,该办法会被触发。
  • handleOutputParameters:应用存储过程解决出参的时候该办法会被调用。

StatementHandler

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();

}

各办法含意别离如下:

  • prepare:该办法在数据库执行前被触发。
  • parameterize:该办法在 prepare 办法之后执行,用来解决参数信息。
  • batch:如果 MyBatis 的全剧配置中配置了 defaultExecutorType=”BATCH”,执行数据操作时该办法会被调用。
  • update:更新操作时该办法会被触发。
  • query:该办法在 SELECT 办法执行时会被触发。
  • queryCursor:该办法在 SELECT 办法执行时,并且返回值为 Cursor 时会被触发。

在开发一个具体的插件时,咱们该当依据本人的需要来决定到底拦挡哪个办法。

4.开发分页插件

4.1 内存分页

MyBatis 中提供了一个不太好用的内存分页性能,就是一次性把所有数据都查问进去,而后在内存中进行分页解决,这种分页形式效率很低,基本上没啥用,然而如果咱们想要自定义分页插件,就须要对这种分页形式有一个简略理解。

内存分页的应用形式如下,首先在 Mapper 中增加 RowBounds 参数,如下:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}

而后在 XML 文件中定义相干 SQL:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>

能够看到,在 SQL 定义时,压根不必管分页的事件,MyBatis 会查问到所有的数据,而后在内存中进行分页解决。

Mapper 中办法的调用形式如下:

@Test
public void test3() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    RowBounds rowBounds = new RowBounds(1,2);
    List<User> list = userMapper.getAllUsersByPage(rowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
}

构建 RowBounds 时传入两个参数,别离是 offset 和 limit,对应分页 SQL 中的两个参数。也能够通过 RowBounds.DEFAULT 的形式构建一个 RowBounds 实例,这种形式构建进去的 RowBounds 实例,offset 为 0,limit 则为 Integer.MAX_VALUE,也就相当于不分页。

这就是 MyBatis 中提供的一个很不实用的内存分页性能。

理解了 MyBatis 自带的内存分页之后,接下来咱们就能够来看看如何自定义分页插件了。

4.2 自定义分页插件

首先要申明一下,这里松哥带大家自定义 MyBatis 分页插件,次要是想通过这个货色让小伙伴们理解自定义 MyBatis 插件的一些条条框框,理解整个自定义插件的流程,分页插件并不是咱们的目标,自定义分页插件只是为了让大家的学习过程变得乏味一些而已。

接下来咱们就来开启自定义分页插件之旅。

首先咱们须要自定义一个 RowBounds,因为 MyBatis 原生的 RowBounds 是内存分页,并且没有方法获取到总记录数(个别分页查问的时候咱们还须要获取到总记录数),所以咱们自定义 PageRowBounds,对原生的 RowBounds 性能进行加强,如下:

public class PageRowBounds extends RowBounds {
    private Long total;

    public PageRowBounds(int offset, int limit) {
        super(offset, limit);
    }

    public PageRowBounds() {
    }

    public Long getTotal() {
        return total;
    }

    public void setTotal(Long total) {
        this.total = total;
    }
}

能够看到,咱们自定义的 PageRowBounds 中减少了 total 字段,用来保留查问的总记录数。

接下来咱们自定义拦截器 PageInterceptor,如下:

@Intercepts(@Signature(
        type = Executor.class,
        method = "query",
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PageInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0];
        Object parameterObject = args[1];
        RowBounds rowBounds = (RowBounds) args[2];
        if (rowBounds != RowBounds.DEFAULT) {
            Executor executor = (Executor) invocation.getTarget();
            BoundSql boundSql = ms.getBoundSql(parameterObject);
            Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
            Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
            if (rowBounds instanceof PageRowBounds) {
                MappedStatement countMs = newMappedStatement(ms, Long.class);
                CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
                String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
                BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
                Set<String> keySet = additionalParameters.keySet();
                for (String key : keySet) {
                    countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                }
                List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql);
                Long count = (Long) countQueryResult.get(0);
                ((PageRowBounds) rowBounds).setTotal(count);
            }
            CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
            pageKey.update("RowBounds");
            String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
            BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
            Set<String> keySet = additionalParameters.keySet();
            for (String key : keySet) {
                pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
            }
            List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
            return list;
        }
        //不须要分页,间接返回后果
        return invocation.proceed();
    }

    private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) {
        MappedStatement.Builder builder = new MappedStatement.Builder(
                ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
        );
        ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
        builder.resource(ms.getResource())
                .fetchSize(ms.getFetchSize())
                .statementType(ms.getStatementType())
                .timeout(ms.getTimeout())
                .parameterMap(ms.getParameterMap())
                .resultSetType(ms.getResultSetType())
                .cache(ms.getCache())
                .flushCacheRequired(ms.isFlushCacheRequired())
                .useCache(ms.isUseCache())
                .resultMaps(Arrays.asList(resultMap));
        if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
            StringBuilder keyProperties = new StringBuilder();
            for (String keyProperty : ms.getKeyProperties()) {
                keyProperties.append(keyProperty).append(",");
            }
            keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
            builder.keyProperty(keyProperties.toString());
        }
        return builder.build();
    }
}

这是咱们明天定义的外围代码,波及到的知识点松哥来给大家一个一个分析。

  1. 首先通过 @Intercepts 注解配置拦截器签名,从 @Signature 的定义中咱们能够看到,拦挡的是 Executor#query 办法,该办法有一个重载办法,通过 args 指定了办法参数,进而锁定了重载办法(实际上该办法的另一个重载办法咱们没法拦挡,那个是 MyBatis 外部调用的,这里不做探讨)。
  2. 将查问操作拦挡下来之后,接下来咱们的操作次要在 PageInterceptor#intercept 办法中实现,该办法的参数重蕴含了拦挡对象的诸多信息。
  3. 通过 invocation.getArgs() 获取拦挡办法的参数,获取到的是一个数组,失常来说这个数组的长度为 4。数组第一项是一个 MappedStatement,咱们在 Mapper.xml 中定义的各种操作节点和 SQL,都被封装成一个个的 MappedStatement 对象了;数组第二项就是所拦挡办法的具体参数,也就是你在 Mapper 接口中定义的办法参数;数组的第三项是一个 RowBounds 对象,咱们在 Mapper 接口中定义方法时不肯定应用了 RowBounds 对象,如果咱们没有定义 RowBounds 对象,零碎会给咱们提供一个默认的 RowBounds.DEFAULT;数组第四项则是一个解决返回值的 ResultHandler。
  4. 接下来判断上一步提取到的 rowBounds 对象是否不为 RowBounds.DEFAULT,如果为 RowBounds.DEFAULT,阐明用户不想分页;如果不为 RowBounds.DEFAULT,则阐明用户想要分页,如果用户不想分页,则间接执行最初的 return invocation.proceed();,让办法持续往下走就行了。
  5. 如果须要进行分页,则先从 invocation 对象中取出执行器 Executor、BoundSql 以及通过反射拿进去 BoundSql 中保留的额定参数(如果咱们应用了动静 SQL,可能会存在该参数)。BoundSql 中封装了咱们执行的 Sql 以及相干的参数。
  6. 接下来判断 rowBounds 是否是 PageRowBounds 的实例,如果是,阐明除了分页查问,还想要查问总记录数,如果不是,则阐明 rowBounds 可能是 RowBounds 实例,此时只有分页即可,不必查问总记录数。
  7. 如果须要查问总记录数,则首先调用 newMappedStatement 办法结构出一个新的 MappedStatement 对象进去,这个新的 MappedStatement 对象的返回值是 Long 类型的。而后别离创立查问的 CacheKey、拼接查问的 countSql,再依据 countSql 构建出 countBoundSql,再将额定参数增加进 countBoundSql 中。最初通过 executor.query 办法实现查问操作,并将查问后果赋值给 PageRowBounds 中的 total 属性。
  8. 接下来进行分页查问,有了第七步的介绍之后,分页查问就很简略了,这里就不细说了,惟一须要强调的是,当咱们启动了这个分页插件之后,MyBatis 原生的 RowBounds 内存分页会变成物理分页,起因就在这里咱们批改了查问 SQL。
  9. 最初将查问后果返回。

在后面的代码中,咱们一共在两个中央从新组织了 SQL,一个是查问总记录数的时候,另一个则是分页的时候,都是通过 boundSql.getSql() 获取到 Mapper.xml 中的 SQL 而后进行改装,有的小伙伴在 Mapper.xml 中写 SQL 的时候不留神,结尾可能加上了 ;,这会导致分页插件从新组装的 SQL 运行出错,这点须要留神。松哥在 GitHub 上看到的其余 MyBatis 分页插件也是一样的,Mapper.xml 中 SQL 结尾不能有 ;

如此之后,咱们的分页插件就算是定义胜利了。

5.测试

接下来咱们对咱们的分页插件进行一个简略测试。

首先咱们须要在全局配置中配置分页插件,配置形式如下:

<plugins>
    <plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>

接下来咱们在 Mapper 中定义查问接口:

public interface UserMapper {
    List<User> getAllUsersByPage(RowBounds rowBounds);
}

接下来定义 UserMapper.xml,如下:

<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
    select * from user
</select>

最初咱们进行测试:

@Test
public void test3() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    List<User> list = userMapper.getAllUsersByPage(new RowBounds(1,2));
    for (User user : list) {
        System.out.println("user = " + user);
    }
}

这里在查问时,咱们应用了 RowBounds 对象,就只会进行分页,而不会统计总记录数。须要留神的时,此时的分页曾经不是内存分页,而是物理分页了,这点咱们从打印进去的 SQL 中也能看到,如下:

能够看到,查问的时候就曾经进行了分页了。

当然,咱们也能够应用 PageRowBounds 进行测试,如下:

@Test
public void test4() {
    UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
    PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
    List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
    for (User user : list) {
        System.out.println("user = " + user);
    }
    System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}

此时通过 pageRowBounds.getTotal() 办法咱们就能够获取到总记录数。

6.小结

好啦,明天次要和小伙伴们分享了咱们如何本人开发一个 MyBatis 插件,插件性能其实都是主要的,最次要是心愿小伙伴们可能了解 MyBatis 的工作流程。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理