四、Mybatis学习实践-分页查询插件

仓库

文章涉及的代码都将统一存放到此仓库,文章涉及的案例代码存放在包:com.hzchendou.blog.demo.example下

代码地址:Gitee

分支:lesson4

涉及案例:Case4

简介

Mybatis是一款优秀ORM框架,开发者只需要关注SQL,处理记录结果映射规则,同时Mybatis提供了许多功能特性帮助开发者编写高性能程序,本文将介绍Mybatis插件机制,实现分页查询功能。

插件

Mybatis提供了插件机制,可以让开发者更加灵活地使用Mybatis框架。插件接口Interceptor定义如下:

public interface Interceptor {
   插件方法入口,在执行目标方法之前先调用intercept方法
  Object intercept(Invocation invocation) throws Throwable;
   对目标对象进行包装,底层使用了JDK的Proxy方法创建代理对象,实现方法拦截
  default Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  /// 可以给插件对象设置属性,方便在连接方法中使用
  default void setProperties(Properties properties) {
    // NOP
  }
}

 关于插件,我们需要知道Mybatis为我们提供了哪些扩展点,以及插件实现方法拦截的原理是什么。

插件扩展点

插件需要在Myabtis配置文件中进行配置,然后才能被Mybatis框架识别,如下所示:

<configuration>
  <!-- 省略无关配置 -->
  <!-- 插件配置 -->
  <plugins>
    <plugin interceptor="com.hzchendou.blog.demo.interceptor.xxxxxxxx"/>
  </plugins>
  <!-- 省略无关配置 -->
</configuration>

配置文件中的插件将统一注册到Configuration配置对象中的interceptorChain属性

public class InterceptorChain {

  private final List<Interceptor> interceptors = new ArrayList<>();
  /// 使用插件对目标进行包装,实现方法拦截
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
  /// 注册插件
  public void addInterceptor(Interceptor interceptor) {
    interceptors.add(interceptor);
  }
  /// 获取所有插件
  public List<Interceptor> getInterceptors() {
    return Collections.unmodifiableList(interceptors);
  }

}

然后会在合适的时机调用pluginAll方法对目标对象进行代理,实现方法调用拦截,通过查看原代码,我们发现Interceptor会对四类组件实施代理操作:

  • ParameterHandler,对输入参数进行处理,包含两个方法
    • getParameterObject,获取参数对象
    • setParameters,将参数配置到预处理器中,构建完整的SQL语句
  • ResultSetHandler,对返回结果进行处理,将数据库记录转化为Java中的对象
    • handleResultSets,处理SQL结果,以List方式返回查询结果
    • handleCursorResultSets,处理SQL结果,以Cursor(游标)方式返回查询结果
    • handleOutputParameters,处理存储过程结果,不返回数据
  • StatementHandler,处理SQL语句,可以对输入参数进行处理, 实现参数化操作,所有的操作最后都是委托StatementHandler执行,底层调用JDBC的Statement对象
    • prepare,预处理,获取Statement对象
    • parameterize,参数化处理
    • batch,执行批处理
    • update 更新操作
    • query 查询操作
    • queryCursor 查询操作,返回游标
    • getBoundSql 获取SQL语句操作对象
    • getParameterHandler,获取参数处理器
  • Executor,主要涉及数据库操作,增删改查,以及事务操作,提交回滚等,设计内容较多不便写,有兴趣可以查看代码

上述所有操作都能够使用Mybatis的插件机制进行拦截,每一个插件可以拦截一个或者多个方法,按照编程中的单一职责原则,建议一个拦截器实现一种方法,方便维护,下面我们来讨论一下拦截是如何实现的

插件方法拦截原理

我们知道Java提供了动态代理以及反射机制,Mybatis的插件就是用这两个技术实现的,首先我们定义一个插件:

@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class,
        Integer.class})})
public class LogInterceptor implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        log.info("拦截StatementHandler.prepare方法, {}", invocation);
        return invocation.proceed();
    }
}

可以看出这个插件主要实现日志打印功能,在执行目标方法前打印日志,我们实现了Interceptor接口,同时使用了注解@Intercepts

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Intercepts {
  /**
   * Returns method signatures to intercept.
   *
   * @return method signatures
   */
  Signature[] value();
}

注解只有一个属性,定义如下:

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface Signature {
  ///目标类型
  Class<?> type();

   方法名
  String method();

  ///输入参数类型
  Class<?>[] args();
}

Signature包含三个属性,目标类型、方法名称、方法输入参数,通过这三个属性可以唯一确定一个方法,这三个属性的组合可以称为方法签名,通过方法签名我们可以知道需要拦截哪个方法,Mybatis调用Inteceptor中的plugin方法生成代理对象:

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

方法中调用了Plugin中的wrap方法,Plugin实现了InvocationHandler接口,我们首先来看一下wrap方法

public static Object wrap(Object target, Interceptor interceptor) {
    /// 解析@Intercepts注解信息,生成需要拦截方法集合
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    Class<?> type = target.getClass();
    /// 查找目标类中的哪些接口需要被代理,实现方法拦截
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    if (interfaces.length > 0) {
       使用JDK中的Proxy生成代理对象
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
   return target;
}

生成的代理对象会调用Plugin的invoke方法进行拦截

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  try {
    /// 判断当前方法是否在插件配置的拦截方法列表中
    Set<Method> methods = signatureMap.get(method.getDeclaringClass());
    if (methods != null && methods.contains(method)) {
      /// 存在执行插件intercept方法进行拦截
      return interceptor.intercept(new Invocation(target, method, args));
    }
    /// 不存在直接执行目标方法
    return method.invoke(target, args);
  } catch (Exception e) {
    throw ExceptionUtil.unwrapThrowable(e);
  }
}

至此我们揭秘了插件运行原理

分页插件

分页查询是很常见的数据库查询操作,我们可以使用Mybatis的插件机制完成分页SQL查询,分页操作主要包含两个SQL语句:

  • 查询分页数据记录
  • 查询总记录数

我们可以使用Mybatis插件机制在查询分页数据的时候自动完成数据记录数查询操作,拦截StatementHandler.prepare方法,在此方法中查询数据记录数

public Object intercept(Invocation invocation) throws Throwable {
    log.info("拦截StatementHandler.prepare方法, {}", invocation);
    ///得到代理目标对象
    StatementHandler statementHandler = realTarget(invocation.getTarget());
    ///MetaObject主要用于设置或者是获取对象属性
    MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
    MappedStatement mappedStatement = (MappedStatement) metaObject
            .getValue("delegate.mappedStatement");
    // 1、先判断是不是SELECT操作  (跳过存储过程和非Select操作)
    if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
            || StatementType.CALLABLE == mappedStatement.getStatementType()) {
        return invocation.proceed();
    }
    statementHandler = metaObject.hasGetter("delegate") ? (StatementHandler) metaObject
            .getValue("delegate") : statementHandler;
    if (!(statementHandler instanceof CallableStatementHandler)) {
        BoundSql boundSql = (BoundSql) metaObject.getValue(DELEGATE_BOUNDSQL);
        Object parameterObejct = boundSql.getParameterObject();
        String sql = boundSql.getSql();
        /// 1、查看是否有分页参数
        if (parameterObejct != null && parameterObejct instanceof PageVO) {
            /// 2、在原先SQL语句中加入limit分页参数
            PageVO param = (PageVO) parameterObejct;
            int offset = (param.getPage() - 1) * param.getSize();
            int size = param.getSize();
            sql = sql + " limit " + offset + "," + size;
            /// 3、将带分页参数的SQL语句替换原先SQL语句
            metaObject.setValue(DELEGATE_BOUNDSQL_SQL, sql);
            log.info("完成分页SQL配置, {}", sql);
             4、组装记录数查询SQL,5、将总记录数保存到PageVO对象中
            Connection connection = (Connection)invocation.getArgs()[0];
            countTotal(param, parameterObejct, statementHandler, connection, mappedStatement);
            log.info("完成分页总记录数查询, {}", param);
        }
    }

    return invocation.proceed();
}

实现思路如下:

  • 1、当是Select操作,并且输入参数为PageVO类型时认为是分页查询,否则不查询记录数
  • 2、同时对原先查询操作拼接limit 分页参数
  • 3、将拼接后带有分页参数的SQL语句替换原先查询语句,然后执行原先查询操作
  • 4、查询记录数SQL通过拼接完成
    • 将原先的SQL进行切分,保留from关键字之后的内容
    • 在分割后的SQL语句前加入“select count(1) ”语句,拼接成完整的查询记录数SQL
  • 5、然后执行查询操作,将查询后的记录数保存到输入参数PageVO中

运行

在博客系统中分页查询文章列表记录,对应Mapper接口:

/**
  * 分页查询
  */
List<BlogDO> selectPage(PageVO param);

 对应的XML中的SQL语句:

<select id="selectPage" resultMap="blogDO">
   SELECT <include refid="blog_columns"/> FROM blog
</select>

分页查询结果需要在业务层进行组装,代码如下

public void run() throws IOException {
    /// 使用Java 7提供的 try-with-resource写法,让JVM自动完成资源关闭操作
    try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
         获取Mapper接口示例(底层使用了JDK的Proxy.newProxyInstance方法创建代理)
        BlogMapper blogMapper = sqlSession.getMapper(BlogMapper.class);
        PageVO<BlogDO> result = selectPage(blogMapper, 1, 2);
        log.info("分页查询博客文章列表信息, {}", result);
    }
}

private PageVO<BlogDO> selectPage(BlogMapper blogMapper, int page, int size) {
    PageVO<BlogDO> param = new PageVO();
    param.setPage(page);
    param.setSize(size);
    /// 执行查询操作,并将记录数保存到PageVO对象中
    List<BlogDO> blogs = blogMapper.selectPage(param);
    param.setRecords(blogs);
    param.setPageSize(blogs == null ? 0 : blogs.size());
    return param;
}

运行结果如下:

1.进入到分页插件方法
22:54:48.351 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 拦截StatementHandler.prepare方法, org.apache.ibatis.plugin.Invocation@16f7c8c1
 完成带分页参数的SQL语句组装
22:54:48.354 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 完成分页SQL配置, SELECT  
    id, `title`, `author_id`, `tags`, `status`
    FROM blog limit 0,2
22:54:48.354 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==>  Preparing: select count(1) from blog limit 0,2
22:54:48.368 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Parameters: 
 完成记录数查询,总记录数为4
22:54:48.377 [main] INFO com.hzchendou.blog.demo.interceptor.PaginationInterceptor - 完成分页总记录数查询, PageVO(page=1, size=2, total=4, pageSize=null, records=null)
22:54:48.377 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==>  Preparing: SELECT id, `title`, `author_id`, `tags`, `status` FROM blog limit 0,2
22:54:48.378 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - ==> Parameters: 
22:54:48.380 [main] DEBUG com.hzchendou.blog.demo.mapper.BlogMapper.selectPage - <==      Total: 2
/// 可以看出分页查询成功执行,总记录数:4,分页查询到2条记录
22:54:48.382 [main] INFO com.hzchendou.blog.demo.example.Case4 - 分页查询博客文章列表信息, PageVO(page=1, size=2, total=4, pageSize=2, records=[BlogDO(id=1, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID), BlogDO(id=2, title=时间海绵博文, authorId=1, tags=博文、时间海绵, status=VALID)])
22:54:48.390 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Resetting autocommit to true on JDBC Connection [org.sqlite.SQLiteConnection@4d1b0d2a]
22:54:48.390 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Closing JDBC Connection [org.sqlite.SQLiteConnection@4d1b0d2a]
22:54:48.390 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Returned connection 1293618474 to pool.

总结

mybtais提供了插件机制,开发者可以使用插件实现功能扩展

  • 插件机制针对的是4种组件,可以拦截4种组件中定义的方法,实现功能扩展
    • ParameterHandler,参数处理器
    • ResultSetHandler,结果集处理器
    • StatementHandler,SQL语句执行器
    • Executor,执行器,包括SQL执行、事务管理
  • 插件实现的基础是代理和反射技术
    • 插件实例需要实现Interceptor接口
    • 需要使用注解声明拦截的方法
    • 在代理类Plugin中实现拦截调用逻辑
    • 在目标方法前执行Interceptor中的intercept方法

联系方式

技术更新换代速度很快,我们无法在有限时间掌握全部知识,但我们可以在他人的基础上进行快速学习,学习也是枯燥无味的,加入我们学习牛人经验:

QQ:901856121

点击:加群讨论 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值