本文主要是介绍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插件的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!