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

相关文章

在Ubuntu上部署SpringBoot应用的操作步骤

《在Ubuntu上部署SpringBoot应用的操作步骤》随着云计算和容器化技术的普及,Linux服务器已成为部署Web应用程序的主流平台之一,Java作为一种跨平台的编程语言,具有广泛的应用场景,本... 目录一、部署准备二、安装 Java 环境1. 安装 JDK2. 验证 Java 安装三、安装 mys

Python中构建终端应用界面利器Blessed模块的使用

《Python中构建终端应用界面利器Blessed模块的使用》Blessed库作为一个轻量级且功能强大的解决方案,开始在开发者中赢得口碑,今天,我们就一起来探索一下它是如何让终端UI开发变得轻松而高... 目录一、安装与配置:简单、快速、无障碍二、基本功能:从彩色文本到动态交互1. 显示基本内容2. 创建链

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT

解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题

《解决mybatis-plus-boot-starter与mybatis-spring-boot-starter的错误问题》本文主要讲述了在使用MyBatis和MyBatis-Plus时遇到的绑定异常... 目录myBATis-plus-boot-starpythonter与mybatis-spring-b

Java汇编源码如何查看环境搭建

《Java汇编源码如何查看环境搭建》:本文主要介绍如何在IntelliJIDEA开发环境中搭建字节码和汇编环境,以便更好地进行代码调优和JVM学习,首先,介绍了如何配置IntelliJIDEA以方... 目录一、简介二、在IDEA开发环境中搭建汇编环境2.1 在IDEA中搭建字节码查看环境2.1.1 搭建步

Redis主从复制实现原理分析

《Redis主从复制实现原理分析》Redis主从复制通过Sync和CommandPropagate阶段实现数据同步,2.8版本后引入Psync指令,根据复制偏移量进行全量或部分同步,优化了数据传输效率... 目录Redis主DodMIK从复制实现原理实现原理Psync: 2.8版本后总结Redis主从复制实

Node.js 中 http 模块的深度剖析与实战应用小结

《Node.js中http模块的深度剖析与实战应用小结》本文详细介绍了Node.js中的http模块,从创建HTTP服务器、处理请求与响应,到获取请求参数,每个环节都通过代码示例进行解析,旨在帮... 目录Node.js 中 http 模块的深度剖析与实战应用一、引言二、创建 HTTP 服务器:基石搭建(一

在C#中合并和解析相对路径方式

《在C#中合并和解析相对路径方式》Path类提供了几个用于操作文件路径的静态方法,其中包括Combine方法和GetFullPath方法,Combine方法将两个路径合并在一起,但不会解析包含相对元素... 目录C#合并和解析相对路径System.IO.Path类幸运的是总结C#合并和解析相对路径对于 C

mysql重置root密码的完整步骤(适用于5.7和8.0)

《mysql重置root密码的完整步骤(适用于5.7和8.0)》:本文主要介绍mysql重置root密码的完整步骤,文中描述了如何停止MySQL服务、以管理员身份打开命令行、替换配置文件路径、修改... 目录第一步:先停止mysql服务,一定要停止!方式一:通过命令行关闭mysql服务方式二:通过服务项关闭

SQL Server数据库磁盘满了的解决办法

《SQLServer数据库磁盘满了的解决办法》系统再正常运行,我还在操作中,突然发现接口报错,后续所有接口都报错了,一查日志发现说是数据库磁盘满了,所以本文记录了SQLServer数据库磁盘满了的解... 目录问题解决方法删除数据库日志设置数据库日志大小问题今http://www.chinasem.cn天发