前面我们已经介绍了mbatis
的一些源码,这篇博客我们说下mybatis
的插件的书写,首先我们要先了解一下mybatis
的执行流程,具体如下图:
想想我们在执行分页查询的时候,应该在什么时候,虽然上面后三个阶段都可以拦截,但是这儿的分页查询,我们在执行SQL
语句之间进行拦截,所以这儿就在StatementHandler
的阶段拦截。然后我们再想下,执行分页查询的步骤有哪些?具体如下:
第一步 执行一条count语句
第二步 重写sql select * from admin limit start,limit
有了上面的思路,我们开始书写对应的插件,插件默认要实现mybatis
的Interceptor
接口,于是我写出了如下的代码
//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
//插件的核心处理方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象
return Plugin.wrap(target,this);
}
//要传给这个插件的配置信息
@Override
public void setProperties(Properties properties) {
}
}
上面我们书写了一个简单的实现Interceptor
接口的插件类,回到上面的问题,走来第一步要执行一条count
的语句。但是现在我们不知道是对那条语句进行执行count
语句,所以这儿必须要对指定的语句进行拦截,于是我们要加一个变量叫pageSqlId
,用来执行哪些语句进行分页查询。还有就是不同的数据库的分页查询是不一样的。所以我们这儿需要指定一个数据库的类型,用databaseType
变量来指定,这两个变量是在配置文件中配置的。于是我们写出下面的代码
//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
String databaseType = "";
String pageSqlId = "";
//插件的核心处理方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象
return Plugin.wrap(target,this);
}
//要传给这个插件的配置信息
@Override
public void setProperties(Properties properties) {
//从主配置文件中读取对应的配置
this.databaseType = properties.getProperty("databaseType");
this.pageSqlId = properties.getProperty("pageSqlId");
}
}
现在这两个关键的变量已经执行好了。于是我们开始执行count
语句。要执行count
语句的前提就是必须知道原来的SQL
语句。那么我们如何获取呢?我们开始也不清楚,于是我们先写一部分代码,然后debug
,通过反射的方式获取对应的SQL
语句,因为mybatis
没有提供方法直接获取对应的SQL
语句,于是我们写出了下面的代码
//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
String databaseType = "";
String pageSqlId = "";
//插件的核心处理方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象
return Plugin.wrap(target,this);
}
//要传给这个插件的配置信息
@Override
public void setProperties(Properties properties) {
//从主配置文件中读取对应的配置
this.databaseType = properties.getProperty("databaseType");
this.pageSqlId = properties.getProperty("pageSqlId");
}
}
通过debug
的方式运行,运行的结果如下:
你可以发现要获取对应的对象,是多么的难,不过mybatis
提供了对应的反射工具类。可以直接获取到。于是我写出了如下的代码
//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
String databaseType = "";
String pageSqlId = "";
//插件的核心处理方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//固定的写法
MetaObject metaObject = MetaObject.forObject(
statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new
DefaultReflectorFactory());
//通过属性名一个个获取
String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象
return Plugin.wrap(target,this);
}
//要传给这个插件的配置信息
@Override
public void setProperties(Properties properties) {
//从主配置文件中读取对应的配置
this.databaseType = properties.getProperty("databaseType");
this.pageSqlId = properties.getProperty("pageSqlId");
}
}
通过上面的代码我们就拿到执行的SQL
的id
,具体如下图:
于是我们通过正则表达式来筛选正确的SQL
语句。来决定要对哪些SQL
语句进行分页查询,于是我规定了以ByPage
结尾的SQL
语句,我给他进行了分页查询,于是我写出了如下的代码
//args: 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
String databaseType = "";
String pageSqlId = "";
//插件的核心处理方法。
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//固定的写法
MetaObject metaObject = MetaObject.forObject(
statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new
DefaultReflectorFactory());
//通过属性名一个个获取
String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
if (sqlId.matches(pageSqlId)){
ParameterHandler parameterHandler = statementHandler.getParameterHandler();
//原来应该执行的sql
String sql = statementHandler.getBoundSql().getSql();
//执行一条count语句
//拿到数据库连接对象
Connection connection = (Connection) invocation.getArgs()[0];
String countSql = "select count(0) from ("+sql+") a";
System.out.println(countSql);
//渲染参数
PreparedStatement preparedStatement = connection.prepareStatement(countSql);
//条件交给mybatis
parameterHandler.setParameters(preparedStatement);
ResultSet resultSet = preparedStatement.executeQuery();
int count =0;
if (resultSet.next()) {
count = resultSet.getInt(1);
}
resultSet.close();
preparedStatement.close();
}
}
@Override
public Object plugin(Object target) {
//固定的写法,第二个参数表示要代理的对象
return Plugin.wrap(target,this);
}
//要传给这个插件的配置信息
@Override
public void setProperties(Properties properties) {
//从主配置文件中读取对应的配置
this.databaseType = properties.getProperty("databaseType");
this.pageSqlId = properties.getProperty("pageSqlId");
}
}
经过上面的代码,我们就执行了count
的语句,接下来就是进行我们的重写sql select * from admin limit start,limit
,这个分页参数必须要传给我们,因为我们分页的插件并不知道你要显示那一页,每页显示多少条数据。于是我们在接口中传入一个参数Map
用来接收分页参数,同时查询的时候只需要传入当前页还有每页显示的数据。总页数,还有一些其他的变量需要计算,所以我们这儿写了一个分页的工具类。具体的代码如下:
public interface PageMapper {
List<Admin> selectByPage(Map<String,Object> page);
}
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.PageMapper">
<select id="selectByPage" resultType="entity.Admin" parameterType="map">
select * from admin
</select>
</mapper>
public class PageUtil {
private int page;
private int limit;
private int count;
private int start;
public PageUtil() {
}
public PageUtil(int page, int limit) {
this.page = page;
this.limit = limit;
this.start = (page - 1) * limit;
}
public int getPage() {
return page;
}
public void setPage(int page) {
this.page = page;
}
public int getLimit() {
return limit;
}
public void setLimit(int limit) {
this.limit = limit;
}
public int getCount() {
return count;
}
public void setCount(int count) {
this.count = count;
}
public int getStart() {
return start;
}
public void setStart(int start) {
this.start = start;
}
}
有了这个工具类。我们就可以写出对应的分页的SQL
语句了,于是我写出下面的代码
//args : 你需要mybatis传入什么参数给你 type :你需要拦截的对象 method=要拦截的方法
@Intercepts(@Signature(type = StatementHandler.class,method ="prepare",args = {Connection.class,Integer.class}))
public class PagePlugin implements Interceptor {
String databaseType = "";
String pageSqlId = "";
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
MetaObject metaObject = MetaObject.forObject(
statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new
DefaultReflectorFactory());
String sqlId = (String)metaObject.getValue("delegate.mappedStatement.id");
//判断一下是否是分页
//<!--第一步 执行一条count语句-->
//1.1 拿到连接
//1.2 预编译SQL语句 拿到绑定的sql语句
//1.3 执行 count语句 怎么返回你执行count的结果
//<!--第二步 重写sql select * from admin limit start,limit -->
//2.1 怎么知道 start 和limit
//2.2 拼接start 和limit
//2.3 替换原来绑定sql
//拿到原来应该执行的sql
if (sqlId.matches(pageSqlId)){
ParameterHandler parameterHandler = statementHandler.getParameterHandler();
//原来应该执行的sql
String sql = statementHandler.getBoundSql().getSql();
//执行一条count语句
//拿到数据库连接对象
Connection connection = (Connection) invocation.getArgs()[0];
String countSql = "select count(0) from ("+sql+") a";
System.out.println(countSql);
//渲染参数
PreparedStatement preparedStatement = connection.prepareStatement(countSql);
//条件交给mybatis
parameterHandler.setParameters(preparedStatement);
ResultSet resultSet = preparedStatement.executeQuery();
int count =0;
if (resultSet.next()) {
count = resultSet.getInt(1);
}
resultSet.close();
preparedStatement.close();
//获得你传进来的参数对象
Map<String, Object> parameterObject = (Map<String, Object>) parameterHandler.getParameterObject();
//limit page
PageUtil pageUtil = (PageUtil) parameterObject.get("page");
//limit 1 ,10 十条数据 总共可能有100 count 要的是 后面的100
pageUtil.setCount(count);
//拼接分页语句(limit) 并且修改mysql本该执行的语句
String pageSql = getPageSql(sql, pageUtil);
metaObject.setValue("delegate.boundSql.sql",pageSql);
System.out.println(pageSql);
}
//推进拦截器调用链
return invocation.proceed();
}
public String getPageSql(String sql,PageUtil pageUtil){
if(databaseType.equals("mysql")){
return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit();
}else if(databaseType.equals("oracle")){
//拼接oracle的分语句
}
return sql+" limit "+pageUtil.getStart()+","+pageUtil.getLimit();
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target,this);
}
@Override
public void setProperties(Properties properties) {
this.databaseType = properties.getProperty("databaseType");
this.pageSqlId = properties.getProperty("pageSqlId");
}
}
至此整个分页的插件就写完了。我们来测试一下,记住我们要在s
的主配置中文件,注册这个插件,同时配置对应的配置项。具体的如下:
<plugins>
<plugin interceptor="plugin.PagePlugin">
<property name="databaseType" value="mysql"/>
<property name="pageSqlId" value=".*ByPage$"/>
</plugin>
</plugins>
于是我们写出如下测试类。具体的代码如下:
public class Main {
public static void main(String[] args) throws IOException {
String resource = "mybatis.xml";
InputStream inputStream = Resources.getResourceAsStream(resource);
SqlSessionFactory sessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
SqlSession sqlSession = sessionFactory.openSession();
PageUtil pageUtil = new PageUtil(2, 10);
Map<String, Object> page = new HashMap<>();
page.put("page", pageUtil);
PageMapper mapper = sqlSession.getMapper(PageMapper.class);
mapper.selectByPage(page);
sqlSession.close();
}
}
运行结果如下图:
至此整个分页的插件的书写就结束了。下篇博客会讲下mybatis的日志。