MyBatis3源码深度解析(二十三)MyBatis拦截器的原理及应用(二)自定义分页插件、慢SQL插件

本文主要是介绍MyBatis3源码深度解析(二十三)MyBatis拦截器的原理及应用(二)自定义分页插件、慢SQL插件,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

文章目录

    • 前言
    • 9.2 自定义一个MyBatis分页插件
      • 9.2.1 自定义分页插件的编写
      • 9.2.2 自定义分页插件的使用
    • 9.3 自定义慢SQL统计插件
    • 9.4 小结

前言

分页查询在日常开发中非常常见,实现的方式一般有两种:

第一种是从数据库中查询出所有满足条件的数据,然后通过应用程序进行分页处理,这种方式在数据量过大时效率比较低,而且可能会造成内存溢出,所以不太常用。

第二种是通过数据库提供的分页语句进行物理分页,这种该方式效率较高且查询数据量较少,所以是一种比较查用的分页方式。

本节基于数据库物理分页的方式编写一个MyBatis分页插件。

9.2 自定义一个MyBatis分页插件

9.2.1 自定义分页插件的编写

由于面向对象设计原则中提倡面向接口编程,因此首先可以编写一个接口,定义有关分页的一些基本方法。例如编写一个Paginable接口:

public interface Paginable<T> {/** 总记录数 */int getTotalCount();/** 总页数 */int getTotalPage();/** 每页记录数 */int getPageSize();/** 当前页号 */int getPageNo();}

然后编写一个Page类实现Paginable接口,来具体描述分页信息:

public class Page<T> implements Paginable<T> {/** 当前页面,默认第1页 */private int pageNo = 1;/** 每页记录数,默认10条 */private int pageSize = 10;/** 总记录数 */private int totalCount = 0;/** 总页数 */private int totalPage = 0;/** 查询时间戳 */private long timestamp = 0;/** 是否全量更新,若true,则会更新totalCount */private boolean full = false;@Overridepublic int getTotalCount() {return totalCount;}public void setTotalCount(int totalCount) {this.totalCount = totalCount;// 设置总记录数totalCount时,根据一定的规则计算出总页数totalPageint totalPage = totalCount % pageSize == 0 ? totalCount / pageSize : totalCount / pageSize + 1;this.setTotalPage(totalPage);}// 其他getter、setter方法 ......
}

分页插件的基本思路是:在SQL语句执行前,从目标对象中将SQL取出来,替换成能查询总记录数或进行分页的SQL语句,再放回目标对象中。

既然要提取和重新设置目标对象的属性,那么可以先定义一个工具类,通过反射机制提取和设置Java对象的属性。

public abstract class ReflectionUtils {/*** 利用反射获取指定对象的指定属性** @param target 目标对象* @param fieldName 目标属性* @return 目标属性的值*/public static Object getFieldValue(Object target, String fieldName) {Object result = null;Field field = ReflectionUtils.getField(target, fieldName);if (field != null) {field.setAccessible(true);try {result = field.get(target);} catch (Exception e) {e.printStackTrace();}}return result;}/*** 利用反射获取指定对象里面的指定属性** @param target*            目标对象* @param fieldName*            目标属性* @return 目标字段*/private static Field getField(Object target, String fieldName) {Field field = null;for (Class<?> clazz = target.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {try {field = clazz.getDeclaredField(fieldName);break;} catch (NoSuchFieldException e) {// ignore}}return field;}/*** 利用反射设置指定对象的指定属性为指定的值** @param target 目标对象* @param fieldName 目标属性* @param fieldValue 目标值*/public static void setFieldValue(Object target, String fieldName, String fieldValue) {Field field = ReflectionUtils.getField(target, fieldName);if (field != null) {try {field.setAccessible(true);field.set(target, fieldValue);} catch (Exception e) {e.printStackTrace();}}}
}

接下来正式编写分页插件:

// 拦截StatementHandler接口的prepare()方法
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class PageInterceptor implements Interceptor {// 数据库类型private String databaseType;@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {this.databaseType = properties.getProperty("databaseType");}@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 获取拦截的目标对象RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();// 获取目标对象组合的StatementHandler对象(delegate属性)StatementHandler delegate = (StatementHandler) ReflectionUtils.getFieldValue(handler, "delegate");// 获取StatementHandler对象中封装的BoundSql对象BoundSql boundSql = delegate.getBoundSql();// 获取BoundSql对象中的参数对象Object parameterObject = boundSql.getParameterObject();// 如果参数对象是Page类,才进行增强逻辑if(parameterObject instanceof Page<?>) {Page<?> page = (Page<?>) parameterObject;// 获取StatementHandler对象中封装的MappedStatement对象MappedStatement mappedStatement = (MappedStatement) ReflectionUtils.getFieldValue(delegate, "mappedStatement");// 获取目标方法的参数之一:Connection对象Connection connection = (Connection) invocation.getArgs()[0];// 获取BoundSql对象中的SQL语句String sql = boundSql.getSql();System.out.println("原SQL语句:" + sql);if(page.isFull()) {// 获取记录总数this.setTotalCount(page, mappedStatement, connection);}page.setTimestamp(System.currentTimeMillis());// 根据原SQL语句和page对象的信息,获取分页SQLString pageSql = this.getPageSql(page, sql);System.out.println("分页SQL语句:" + pageSql);// 替换BoundSql对象中的SQL语句ReflectionUtils.setFieldValue(boundSql, "sql", pageSql);}// 继续指定原目标方法return invocation.proceed();}private String getPageSql(Page<?> page, String sql) {// todoreturn null;}private void setTotalCount(Page<?> page, MappedStatement mappedStatement, Connection connection) {// todo}}

MyBatis自定义插件类都必须实现Interceptor接口,还需要通过@Intercepts注解配置对哪些组件的哪些方法进行拦截。

在本案例中,指定对StatementHandler对象的prepare()方法进行拦截,因此在调用StatementHandler对象的prepare()方法之前,会调用PageInterceptor对象的intercept()方法。

在PageInterceptor对象的intercept()方法中,拦截逻辑的大致流程是:

(1)如果参数对象是Page类型(或是Page类的子类),则进入分页逻辑,通过反射机制获取BoundSql对象,从该对象中提取出要执行的SQL语句和参数对象;
(2)如果全量更新配置为true,则调用setTotalCount()方法,将原SQL语句转换为查总记录数的SQL语句,查询出记录总数;
(3)接着调用getPageSql()方法将原SQL语句转换为对应数据库类型格式的分页SQL语句;
(4)将分页SQL语句放回目标方法中,执行目标方法,底层则会执行分页SQL语句。

需要注意的是,进入分页逻辑的条件是:参数对象是Page类型。因此,Mapper方法的参数对象必须继承Page类。

PageInterceptor类中还有getPageSql()方法、setTotalCount()方法未完成,下面继续:

/*** 给当前的参数对象page设置总记录数** @param page            Mapper映射语句对应的参数对象* @param mappedStatement Mapper映射语句* @param connection      当前的数据库连接*/
private void setTotalCount(Page<?> page, MappedStatement mappedStatement, Connection connection) {// 获取原SQL语句BoundSql boundSql = mappedStatement.getBoundSql(page);String sql = boundSql.getSql();// 根据原SQL语句获取对应的查询记录总数的SQL语句String countSql = "select count(1) " + sql.substring(sql.toLowerCase().indexOf("from"));System.out.println("查询记录总数的SQL语句:" + countSql);List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();// 重新封装一个BoundSql对象和ParameterHandler对象BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);PreparedStatement pstmt = null;ResultSet rs = null;try {// 执行查询记录总数的语句pstmt = connection.prepareStatement(countSql);parameterHandler.setParameters(pstmt);rs = pstmt.executeQuery();if(rs.next()) {int totalCount = rs.getInt(1);System.out.println("获得记录总数 = " + totalCount);// 设置记录总数的同时已设置了总页数page.setTotalCount(totalCount);}} catch (SQLException e) {e.printStackTrace();} finally {if(rs != null) {try {rs.close();} catch (SQLException e) {e.printStackTrace();}}if(pstmt != null) {try {pstmt.close();} catch (SQLException e) {e.printStackTrace();}}}
}

setTotalCount()方法的实现可知,查询记录总数是一次额外的查询,直接使用JDBC API操作数据库。 这也很好理解,因为用户并没有编写查询记录总数的SQL语句,但这个查询又是有需要的。

/*** 根据page对象获取对应的分页查询Sql语句,* 这里只做了三种数据库类型,Mysql、Oracle、HSQLDB* 其它的数据库都没有进行分页** @param page 分页对象* @param sql  原始sql语句* @return*/
private String getPageSql(Page<?> page, String sql) {StringBuffer sqlBuffer = new StringBuffer(sql);if ("mysql".equalsIgnoreCase(databaseType)) {return getMysqlPageSql(page, sqlBuffer);} else if ("oracle".equalsIgnoreCase(databaseType)) {return getOraclePageSql(page, sqlBuffer);} else if ("hsqldb".equalsIgnoreCase(databaseType)) {return getHSQLDBPageSql(page, sqlBuffer);}return sqlBuffer.toString();
}/*** 获取Mysql数据库的分页查询语句** @param page      分页对象* @param sqlBuffer 包含原sql语句的StringBuffer对象* @return Mysql数据库分页语句*/
private String getMysqlPageSql(Page<?> page, StringBuffer sqlBuffer) {int offset = (page.getPageNo() - 1) * page.getPageSize();sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize());return sqlBuffer.toString();
}/*** 获取Oracle数据库的分页查询语句** @param page      分页对象* @param sqlBuffer 包含原sql语句的StringBuffer对象* @return Oracle数据库的分页查询语句*/
private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) {int offset = (page.getPageNo() - 1) * page.getPageSize() + 1;sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize());sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset);return sqlBuffer.toString();
}/*** 获取HSQLDB数据库的分页查询语句** @param page      分页对象* @param sqlBuffer 包含原sql语句的StringBuffer对象* @return Oracle数据库的分页查询语句*/
private String getHSQLDBPageSql(Page<?> page, StringBuffer sqlBuffer) {int offset = (page.getPageNo() - 1) * page.getPageSize() + 1;return "select limit " + offset + " " + page.getPageSize() + " * from (" + sqlBuffer.toString() + " )";
}

自定义的分页插件支持3种数据库厂商,分别是MySQL、Oracle、HSQLDB,它们的分页查询语句各不相同。

到此为止,分页插件已经编写好了。接下来编写测试代码,看看在实际开发中如何使用该插件。

9.2.2 自定义分页插件的使用

自定义插件后,需要在MyBatis主配置文件中对插件进行注册:

<!--mybatis-config.xml-->
<plugins><plugin interceptor="com.star.mybatis.page.PageInterceptor"><property name="databaseType" value="mysql"/></plugin>
</plugins>

上面的配置中,通过databaseType属性指定数据库类型为MySQL。

由于仅当参数对象是Page的子类时才会执行分页逻辑,因此需要编写一个UserQuery类继承Page类:

public class UserQuery extends Page<User> {
}

接下来是Mapper方法:

public interface UserMapper {@Select("select * from user")List<User> selectUserPage(UserQuery userQuery);
}

Mapper方法selectUserPage()中编写的SQL语句,只是一个简单的SELECT语句,并没有涉及到分页查询。具体的分页操作都是由自定义的插件来完成的。

最后,编写单元测试调用Mapper方法:

@Test
public void testPage() throws IOException {Reader reader = Resources.getResourceAsReader("mybatis-config.xml");SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(reader);SqlSession sqlSession = sqlSessionFactory.openSession();UserMapper userMapper = sqlSession.getMapper(UserMapper.class);UserQuery userQuery = new UserQuery();userQuery.setPageNo(1);userQuery.setPageSize(2);userQuery.setFull(true);List<User> userList = userMapper.selectUserPage(userQuery);userList.forEach(System.out::println);
}

运行单元测试,控制台打印相关信息:

原SQL语句:select * from user
查询记录总数的SQL语句:select count(1) from user
获得记录总数 = 3
分页SQL语句:select * from user limit 0,2
User{id=1, name='孙悟空', age=1500, phone='18705464523', birthday=Thu Jan 01 00:00:00 CST 1}
User{id=2, name='猪八戒', age=1000, phone='15235468789', birthday=Fri Mar 10 00:00:00 CST 500}

由结果可知,自定义的分页插件会根据原SQL语句,构建出对应的查询记录总数SQL语句,以及分页查询SQL语句,使得最终查询的结果是分页后的数据。

9.3 自定义慢SQL统计插件

在实际项目中,有时会因为各种原因导致SQL执行耗时过长,从而影响服务性能。为了对耗时过长的SQL语句进行优化,就需要先把SQL语句找出来。

利用MyBatis插件功能,可以把执行时间超过某个设定的值的SQL语句输出到日志中,从而更有针对性地对SQL语句进行优化。

下面编写一个SlowSqlInterceptor类实现Interceptor接口,通过@Intercepts注解配置拦截StatementHandler对象的query()update()batch()方法:

@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class}),@Signature(type = StatementHandler.class, method = "update", args = {Statement.class}),@Signature(type = StatementHandler.class, method = "batch", args = {Statement.class})
})
public class SlowSqlInterceptor implements Interceptor {// 超时时长(秒)private Integer limitSecond;@Overridepublic Object plugin(Object target) {return Plugin.wrap(target, this);}@Overridepublic void setProperties(Properties properties) {String limitSecond = (String) properties.get("limitSecond");this.limitSecond = Integer.parseInt(limitSecond);}@Overridepublic Object intercept(Invocation invocation) throws Throwable {// 记录开始执行时间long beginTimeMillis = System.currentTimeMillis();StatementHandler statementHandler = (StatementHandler) invocation.getTarget();try {// 执行SQL语句return invocation.proceed();} finally {// 记录结束执行时间long endTimeMillis = System.currentTimeMillis();// 计算执行时间long costTimeMillis = endTimeMillis - beginTimeMillis;if (costTimeMillis > limitSecond * 1000) {BoundSql boundSql = statementHandler.getBoundSql();System.out.println("SQL语句【" + boundSql.getSql() + "】,执行耗时:" + costTimeMillis + "ms");}}}}

intercept()方法中,分别记录SQL语句执行前后的时间,如果两者的差超过了配置好的超时时间(limitSecond属性),则打印出这条SQL语句及其耗时。(为演示方便,这里只是将SQL语句打印在控制台,实际项目中可以输出到日志文件中)

最后,在MyBatis主配置文件中对插件进行注册:

<!--mybatis-config.xml-->
<plugins><plugin interceptor="com.star.mybatis.page.PageInterceptor"><property name="databaseType" value="mysql"/></plugin><plugin interceptor="com.star.mybatis.page.SlowSqlInterceptor"><property name="limitSecond" value="1"/></plugin>
</plugins>

再次执行【9.2 分页插件】的单元测试代码。为了模拟出慢SQL的效果,可以借助IDE在SlowSqlInterceptor类的intercept()方法中的return invocation.proceed();这一行代码上打一个断点,稍停几秒再放通。

控制台打印结果:

SQL语句【select * from user limit 0,2】,执行耗时:22120ms

这样,一个检验慢SQL的插件也生效了。

9.4 小结

第九章到此就梳理完毕了,本章的主题是:MyBatis拦截器原理及应用。回顾一下本章的梳理的内容:

(二十二)拦截器的实现原理与执行过程
(二十三)自定义分页插件、慢SQL插件

更多内容请查阅分类专栏:MyBatis3源码深度解析

第十章主要学习:MyBatis级联映射与懒加载。主要内容包括:

  • MyBatis级联映射原理;
  • MyBatis懒加载机制。

这篇关于MyBatis3源码深度解析(二十三)MyBatis拦截器的原理及应用(二)自定义分页插件、慢SQL插件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/849070

相关文章

MySQL查询JSON数组字段包含特定字符串的方法

《MySQL查询JSON数组字段包含特定字符串的方法》在MySQL数据库中,当某个字段存储的是JSON数组,需要查询数组中包含特定字符串的记录时传统的LIKE语句无法直接使用,下面小编就为大家介绍两种... 目录问题背景解决方案对比1. 精确匹配方案(推荐)2. 模糊匹配方案参数化查询示例使用场景建议性能优

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

从原理到实战深入理解Java 断言assert

《从原理到实战深入理解Java断言assert》本文深入解析Java断言机制,涵盖语法、工作原理、启用方式及与异常的区别,推荐用于开发阶段的条件检查与状态验证,并强调生产环境应使用参数验证工具类替代... 目录深入理解 Java 断言(assert):从原理到实战引言:为什么需要断言?一、断言基础1.1 语

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

mysql表操作与查询功能详解

《mysql表操作与查询功能详解》本文系统讲解MySQL表操作与查询,涵盖创建、修改、复制表语法,基本查询结构及WHERE、GROUPBY等子句,本文结合实例代码给大家介绍的非常详细,感兴趣的朋友跟随... 目录01.表的操作1.1表操作概览1.2创建表1.3修改表1.4复制表02.基本查询操作2.1 SE

MySQL中的锁机制详解之全局锁,表级锁,行级锁

《MySQL中的锁机制详解之全局锁,表级锁,行级锁》MySQL锁机制通过全局、表级、行级锁控制并发,保障数据一致性与隔离性,全局锁适用于全库备份,表级锁适合读多写少场景,行级锁(InnoDB)实现高并... 目录一、锁机制基础:从并发问题到锁分类1.1 并发访问的三大问题1.2 锁的核心作用1.3 锁粒度分

MySQL数据库中ENUM的用法是什么详解

《MySQL数据库中ENUM的用法是什么详解》ENUM是一个字符串对象,用于指定一组预定义的值,并可在创建表时使用,下面:本文主要介绍MySQL数据库中ENUM的用法是什么的相关资料,文中通过代码... 目录mysql 中 ENUM 的用法一、ENUM 的定义与语法二、ENUM 的特点三、ENUM 的用法1

Python中re模块结合正则表达式的实际应用案例

《Python中re模块结合正则表达式的实际应用案例》Python中的re模块是用于处理正则表达式的强大工具,正则表达式是一种用来匹配字符串的模式,它可以在文本中搜索和匹配特定的字符串模式,这篇文章主... 目录前言re模块常用函数一、查看文本中是否包含 A 或 B 字符串二、替换多个关键词为统一格式三、提

MySQL count()聚合函数详解

《MySQLcount()聚合函数详解》MySQL中的COUNT()函数,它是SQL中最常用的聚合函数之一,用于计算表中符合特定条件的行数,本文给大家介绍MySQLcount()聚合函数,感兴趣的朋... 目录核心功能语法形式重要特性与行为如何选择使用哪种形式?总结深入剖析一下 mysql 中的 COUNT