一、插件原理
在四大对象创建的时候,有以下几个特性:
- 每个创建出来的对象不是直接返回的,而是
interceptorChain.pluginAll(parameterHandler);
- 该方法获取到所有的
Interceptor
(插件需要实现的接口),调用interceptor.plugin(target);
返回target
包装后的对象。
public Object pluginAll(Object target) {
Interceptor interceptor;
for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
interceptor = (Interceptor)var2.next();
}
return target;
}
因此,我们可以使用插件为目标对象创建一个代理对象,类似于AOP(面向切面编程)。我们的插件可以为四大对象创建出代理对象,代理对象就可以拦截到四大对象中每一个的执行。
二、插件编写(单个插件)
1、编写Interceptor
的实现类
package com.cerr.dao;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
public class MyFirstPlugin implements Interceptor {
//拦截目标对象的目标方法的执行
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("intercept..."+invocation);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
//包装目标对象,为目标对象创建一个代理对象
public Object plugin(Object target) {
System.out.println("plugin..."+target);
//借助这个方法来使用当前Interceptor包装我们目标对象
Object wrap = Plugin.wrap(target,this);
//返回当前target创建的动态代理对象
return wrap;
}
//将插件注册时的property属性设置进去
public void setProperties(Properties properties) {
System.out.println("插件配置的信息"+properties);
}
}
2、使用@Intercepts
注解完成插件的签名
在编写的插件类上标注@Intercepts
,标注后的插件类如下:
package com.cerr.dao;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
//完成插件签名:告诉MyBatis当前插件来用拦截哪个对象的哪个方法
@Intercepts({
@Signature(type = StatementHandler.class,method = "parameterize",
args = java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor {
//拦截目标对象的目标方法的执行
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("intercept..."+invocation);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
//包装目标对象,为目标对象创建一个代理对象
public Object plugin(Object target) {
System.out.println("plugin..."+target);
//借助这个方法来使用当前Interceptor包装我们目标对象
Object wrap = Plugin.wrap(target,this);
//返回当前target创建的动态代理对象
return wrap;
}
//将插件注册时的property属性设置进去
public void setProperties(Properties properties) {
System.out.println("插件配置的信息"+properties);
}
}
3、将写好的插件注册到全局配置文件中
在全局配置文件中使用<plugins>
标签来注册:
<plugins>
<plugin interceptor="com.cerr.dao.MyFirstPlugin"></plugin>
</plugins>
此时我们就定义了一个拦截StatementHandler
对象的parameterize
方法的拦截器。
三、多个插件时的执行顺序
我们再编写一个插件类MySecondPlugin
,也是和上面的插件类一样拦截的同一个对象的同一个方法。代码如下:
package com.cerr.dao;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.plugin.*;
import java.util.Properties;
@Intercepts({
@Signature(type = StatementHandler.class,method = "parameterize",
args = java.sql.Statement.class)
})
public class MySecondPlugin implements Interceptor {
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MySecondPlugin..intercept:"+invocation);
return invocation.proceed();
}
public Object plugin(Object target) {
System.out.println("MySecondPlugin...plugin:"+target);
return Plugin.wrap(target,this);
}
public void setProperties(Properties properties) {
}
}
在全局配置文件中配置:
<plugins>
<plugin interceptor="com.cerr.dao.MyFirstPlugin"></plugin>
<plugin interceptor="com.cerr.dao.MySecondPlugin"></plugin>
</plugins>
配置后我们随便找一个测试方法运行,例如:
@Test
public void testSimple() throws IOException {
SqlSessionFactory factory = getSqlSessionFactory();
SqlSession session = factory.openSession();
try{
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
List< Employee > list = mapper.selectByExample(null);
}finally {
session.close();
}
}
结果如图1所示:
从上图可以看出,在生成代理对象时,是先创建第一个插件类的代理对象,再创建第二个插件类的代理对象;但是在拦截目标方法的时候,则是先执行第二个插件类,再执行第一个插件类。
因此我们可以得出以下结论:创建动态代理的时候,是按照插件配置的顺序层层创建代理对象的。执行目标方法的时候,按照逆序顺序执行。
我们可以将该过程类比为如下的模型,首先创建的StatementHandler
,然后再创建MyFirstPlugin
代理对象,然后再创建了MySecondPlugin
代理对象,其三者的关系是晚创建的包含早创建的,在执行目标方法的时候自然而然是从外向里执行。
四、使用PageInterceptor插件
1、导包
需要两个包,可以在GitHub上面下载:点此下载
2、在全局配置文件中注册PageInterceptor
插件
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
3、编码
可以使用Page
对象:
@Test
public void test() throws IOException {
SqlSessionFactory factory = getSqlSessionFactory();
SqlSession session = factory.openSession();
try{
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
//第一个参数是页码,第二个是每页的记录数
Page <Object> page = PageHelper.startPage(1,5);
List <Employee> list = mapper.getEmps();
System.out.println("当前页码:"+page.getPageNum());
System.out.println("总记录数:"+page.getTotal());
System.out.println("每页的记录数:"+page.getPageSize());
System.out.println("总页码:"+page.getPages());
}finally {
session.close();
}
}
可以使用Page
对象来获取关于分页的数据,例如当前页码、总记录数等等。PageHelper.startPage(1,5)
表示显示第一页,然后每页有5条记录数。
@Test
public void test() throws IOException {
SqlSessionFactory factory = getSqlSessionFactory();
SqlSession session = factory.openSession();
try{
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
//第一个参数是页码,第二个是每页的记录数
PageHelper.startPage(1,5);
List <Employee> list = mapper.getEmps();
PageInfo<Employee> info = new PageInfo <>(list);
System.out.println("当前页码:"+info.getPageNum());
System.out.println("总记录数:"+info.getTotal());
System.out.println("每页的记录数:"+info.getPageSize());
System.out.println("总页码:"+info.getPages());
System.out.println("是否第一页:"+info.isIsFirstPage());
System.out.println("是否最后一页:"+info.isIsLastPage());
}finally {
session.close();
}
}
也可以使用PageInfo
对象来获取分页的数据,跟上面代码差不多。
五、使用BatchExecutor进行批量操作
在通过SqlSessionFactory
获取SqlSession
的时候传入一个参数即可,即:
SqlSessionFactory factory = getSqlSessionFactory();
//可以执行批量操作的SqlSession
SqlSession session = factory.openSession(ExecutorType.BATCH);
设置了该参数之后,现在该SqlSession
就是可以批量操作的了:
@Test
public void testBatch() throws IOException {
SqlSessionFactory factory = getSqlSessionFactory();
//可以执行批量操作的SqlSession
SqlSession session = factory.openSession(ExecutorType.BATCH);
try{
EmployeeMapper mapper = session.getMapper(EmployeeMapper.class);
for (int i = 0;i < 10000;++i){
mapper.addEmps(new Employee(null,"a","a","a"));
}
session.commit();
}finally {
session.close();
}
}
六、自定义TypeHandler来处理枚举类型
我们在Employee
类中添加一个EmpStatus
字段,用来保存该类的状态,该字段是一个枚举类:
package com.cerr.mybatis;
public enum EmpStatus {
LOGIN,LOGOUT,REMOVE;
}
MyBatis在处理枚举类型的时候有两个TypeHandler
,一个是EnumTypeHandler
,另一个是EnumOrdinalTypeHandler
。
-
EnumTypeHandler
:保存枚举对象的时候默认保存的是枚举对象的名字 -
EnumOrdinalTypeHandler
:保存枚举对象的时候默认保存的是枚举对象的索引。
我们现在想自己来处理枚举类型,就需要自定义TypeHandler来实现,自定义的类需要实现TypeHandler
接口或者继承BaseTypeHandler
。
我们首先将EmpStatus
来改造一下,因为我们想在保存进数据库的时候存状态码,然后获取的时候获取的是该对象的信息,因此需要增加两个字段msg
和code
,再增加对应的构造方法、getter
和setter
方法后:
package com.cerr.mybatis;
public enum EmpStatus {
LOGIN(100,"用户登录"),LOGOUT(200,"用户登出"),REMOVE(300,"用户不存在");
private Integer code;
private String msg;
public static EmpStatus getEmpStatusByCode(Integer code){
switch (code){
case 100:
return LOGIN;
case 200:
return LOGOUT;
case 300:
return REMOVE;
default:
return LOGOUT;
}
}
EmpStatus(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
1、自定义TypeHandler类
package com.cerr.mybatis.dao;
import com.cerr.mybatis.EmpStatus;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
/**
* 实现TypeHandler接口或者继承BaseTypeHandler
*/
public class MyEnumEmpStatusTypeHandler implements TypeHandler<EmpStatus> {
//定义数据如何保存到数据库中
@Override
public void setParameter(PreparedStatement preparedStatement, int i, EmpStatus empStatus, JdbcType jdbcType) throws SQLException {
preparedStatement.setString(i,empStatus.getCode().toString());
}
@Override
public EmpStatus getResult(ResultSet resultSet, String s) throws SQLException {
//根据从数据库中拿到的状态码返回枚举对象
int code = resultSet.getInt(s);
return EmpStatus.getEmpStatusByCode(code);
}
@Override
public EmpStatus getResult(ResultSet resultSet, int i) throws SQLException {
//根据从数据库中拿到的状态码返回枚举对象
int code = resultSet.getInt(i);
return EmpStatus.getEmpStatusByCode(code);
}
@Override
public EmpStatus getResult(CallableStatement callableStatement, int i) throws SQLException {
//根据从数据库中拿到的状态码返回枚举对象
int code = callableStatement.getInt(i);
return EmpStatus.getEmpStatusByCode(code);
}
}
实现TypeHandler
后的方法中,setParameter()
定义了数据如何保存在数据库中,直接使用PreparedStatement
设置参数的方法将我们想保存的数据添加为参数,对于剩下的三个方法定义从数据库拿到数据后如何处理,我们将数据库中保存的状态码拿出来后,调用我们编写的EmpStatus.getEmpStatusByCode(code)
方法返回该枚举对象的信息。
2、在全局配置文件中注册该TypeHandler
语法格式如下:
<typeHandlers>
<!-- 配置自定义的类型处理器 -->
<typeHandler handler="自定义的TypeHandler全类名" javaType="指定要处理的类"/>
</typeHandlers>
在此处我们的配置如下:
<typeHandlers>
<!-- 配置自定义的类型处理器 -->
<typeHandler handler="com.cerr.mybatis.dao.MyEnumEmpStatusTypeHandler" javaType="com.cerr.mybatis.EmpStatus"/>
</typeHandlers>
当然了我们除了在全局配置文件中使用javaType
来指定哪个类被我们自定义的类型处理器处理之外,我们还可以在sql映射文件中配置:
- 对于
<insert>
标签,我们直接在传参时加上typeHandler
属性即可,例如#{empStatus,typeHandler=com.cerr.mybatis.dao.MyEnumEmpStatusTypeHandler}
。 - 对于
<select>
标签,我们可以在定义<resultMap>
的时候,对于枚举属性,加上一个typeHandler
属性并指定为我们自定义的类型处理器即可。 - 但是,如果使用这种方法指定使用类型处理器的话,比如保证
<insert>
和<select>
标签使用的是同一个类型处理器。