在前面往数据库中批量插入了大量数据,那么如何将这些数据进行分页展示呢?这里就需要介绍Mybatis中的拦截器了,通过拦截器可以实现数据的分页(在数据库中物理分页方式读取数据)。 本篇主要介绍以下几点内容:1、Mybatis的四大对象、插件原理及接口;2、Mybatis的插件开发过程;3、代理模式的介绍;4、如何使用PageHelper插件来实现分页功能。

Mybatis的四大对象

Mybatis对持久层的操作就是借助于四大核心对象,分别是:ParameterHandlerResultSetHandlerStatementHandlerExecutor

其中ParameterHandler用于处理SQL的参数对象;ResultSetHandler用于处理SQL的返回结果集;StatementHandler是数据库的处理对象,用于执行SQL语句;Executor是Mybatis的执行器,用于执行增删改查操作。

MyBatis支持用插件对四大核心对象进行拦截,对MyBatis来说插件就是拦截器,用来增强核心对象的功能,增强功能本质上是借助于底层的动态代理实现的,换句话说MyBatis中的四大对象都是代理对象。可以把拦截器理解为Servlet,对传递过来的请求进行拦截过滤和封装。

Mybatis插件原理

Mybatis的插件借助于责任链模式进行拦截处理,同时使用动态代理对目标对象进行包装达到拦截的目的,然后作用于Mybatis的作用域对象之上。那么插件具体是如何拦截并附加额外的功能的呢?我们以ParameterHandler 为例进行说明:

1
2
3
4
5
public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object object, BoundSql sql, InterceptorChain interceptorChain){
ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement,object,sql);
parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
return parameterHandler;
}

接着再来看看这个pluginAll方法的源码,可以发现其实就是对每个拦截器对象都调用plugin方法,然后将生成的target对象返回过去。

1
2
3
4
5
6
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}

也就是说interceptorChain中保存了所有的拦截器(interceptors),Mybatis初始化时创建的。接着调用拦截器链中的拦截器依次的对目标进行拦截或增强,interceptor.plugin(target)中的target可以理解为Mybatis中的四大对象,返回的target是被重重代理后的对象。

Mybatis插件接口—Interceptor

如何让一个类变成一个插件呢?很简单,只需让该类实现Interceptor接口(org.apache.ibatis.plugin.Interceptor包内的Interceptor接口),并重写其中的InterceptpluginsetProperties方法。

其中Intercept方法是插件的核心方法,是拦截目标对象的目标方法;plugin方法,用于生成target代理对象;setProperties方法用于传递插件所需参数。

注意使用之前的mybatis_insertdata项目进行代码的演示。

mybatis-config.xml文件中新增plugins配置:

1
2
3
4
5
<plugins>
<plugin interceptor="com.envy.interceptor.FirstInterceptor" >
<property name="hello" value="world"></property>
</plugin>
</plugins>

com.envy.interceptor包中新建一个FirstInterceptor.java文件,里面的代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.envy.interceptor;

import org.apache.ibatis.executor.resultset.ResultSetHandler;
import org.apache.ibatis.plugin.*;

import java.beans.Statement;
import java.util.Properties;


//插件签名,告诉mybatis当前插件用来拦截哪个对象的哪个方法
@Intercepts({@Signature(type = ResultSetHandler.class,method ="handleResultSets",args = Statement.class)})
public class FirstInterceptor implements Interceptor {

//拦截目标对象的目标方法
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("拦截的目标对象:"+invocation.getTarget());
invocation.proceed(); //执行目标对象中的目标方法
return null;
}

//包装目标对象,用于给目标对象创建代理对象
public Object plugin(Object o) {
System.out.println("将要包装的目标对象为:"+o);
return Plugin.wrap(o,this); //返回被代理的对象
}


//用于获取mybatis-config.xml中配置的plugin参数
public void setProperties(Properties properties) {
System.out.println("插件配置的初始化参数为:"+properties);
}
}

新建一个查询方法,因为只有查询方法才会返回ResultSet。在PersonMapper.java文件中新增addPersons方法:

1
2
//测试Interceptor接口
Person getPersonById(Integer id);

PersonMapper.xml文件中新增代码如下:

1
2
3
4
<!--测试Interceptor接口-->
<select id="getPersonById" resultType="Person">
select * from person where id=#{id}
</select>

ParameterTest.java文件中新增selectPersonById方法:

1
2
3
4
5
6
7
8
9
10
    //测试Interceptor接口
@Test
public void selectPersonById(){
SqlSession sqlSession = getSqlSessionFactory().openSession();
PersonMapper personMapper = sqlSession.getMapper(PersonMapper.class);
Person person = personMapper.getPersonById(1001);
System.out.println(person);
sqlSession.commit();
sqlSession.close();
}

运行结果如下:

1
2
3
4
5
6
7
8
9
插件配置的初始化参数为:{hello=world}
将要包装的目标对象为:org.apache.ibatis.executor.CachingExecutor@32709393
将要包装的目标对象为:org.apache.ibatis.scripting.defaults.DefaultParameterHandler@394e1a0f
将要包装的目标对象为:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@167fdd33
将要包装的目标对象为:org.apache.ibatis.executor.statement.RoutingStatementHandler@4d95d2a2
拦截的目标对象:org.apache.ibatis.executor.resultset.DefaultResultSetHandler@167fdd33
拦截的目标对象的方法是:public abstract java.util.List org.apache.ibatis.executor.resultset.ResultSetHandler.handleResultSets(java.sql.Statement) throws java.sql.SQLException
拦截的目标对象的参数是[Ljava.lang.Object;@376b4233
Person{id=1001, username='envy', email='envy@qq.com', gender='男', departmentId=null, department=null}

可以发现这个将要包装的目标对象为一共输出了4次,因为Mybatis拦截器会默认去拦截前面所说的四大对象。

插件开发的过程是:确定拦截的签名–>实现拦截方法–>配置和运行。

多插件开发过程

多插件开发过程中需要注意两点:1、创建代理对象时,按照插件配置的顺序进行包装;2、执行目标方法后,是按照代理的逆向进行执行:

1
2
3
4
5
6
7
<plugins>
<plugin interceptor="com.envy.interceptor.FirstInterceptor" >
</plugin>

<plugin interceptor="com.envy.interceptor.SecondtInterceptor" >
</plugin>
</plugins>

也就是说执行到拦截目标对象的目标方法intercept处就不是FirstInterceptor在前面了,而是先执行SecondtInterceptor(注意这里的前提是各个拦截器的签名是一致的)。

针对上述情况,接下来简单总结一下:1、遵循插件尽量不使用的原则,因为会修改底层设计;2、插件是生成层层代理对象的责任链模式,使用反射机制实现;3、插件的编写要考虑全面,特别是多个插件层层代理的时候。

分页

分页原理

分页通常分为两种:内存分页物理分页

内存分页:从数据库中一次性将数据读取到内存,根据页面显示的条数去内存中查询。虽然占用了较大的内存空间,但是减少了与数据库的交互,数据发生改变,数据库的最新状态不能实时反映到操作中,实时性差。

物理分页:将整个大的ResultSet转换成一个个小的ResultSet,降低了内存的开销,增加了与数据库的交互,能够获取数据库的最新状态,实时性强。(实际开发使用

MySQL中实现分页的方式为:

1
2
3
4
select * from person limit 0,10; /*第一页*/
select * from person limit 10,10; /*第二页*/
select * from person limit 20,10; /*第三页*/
select * from person limit 30,10; /*第四页*/

从上面可以分析出规律:开始记录索引的规律(当前页-1)*每页的条数;一共有多少页:总记录%条数==0?(总记录%条数):(总记录%条数+1)。同时分页主要注意三个参数:当前页、每页的条数、总记录数。

使用pageHelper插件进行分页

点击这里了解更多关于pageHelper插件的信息。