文章目录
MyBatis10_插件
MyBatis在四大对象的创建过程中,都会有插件进行介入。插件可以利用动态代理机制一层层的包装目标对象,而实现在目标对象执行目标方法之前进行拦截的效果。
MyBatis 允许在已映射语句执行过程中的某一点进行拦截调用。
默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback,getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
1.插件原理
1)、按照插件注解声明,按照插件配置顺序调用插件plugin方法,生成被拦截对象的动态代理。
2)、多个插件依次生成目标对象的代理对象,层层包裹,先声明的先包裹,形成代理链。
3)、目标方法执行时依次从外到内执行插件的intercept方法。
4)、多个插件情况下,我们往往需要在某个插件中分离出目标对象,可以借助MyBatis提供的SystemMetaObject类来进行获取最后一层的h以及target属性的值。
插件原理
在四大对象创建的时候
1、每个创建出来的对象不是直接返回的,而是。
interceptorChain.pluginAll(parameterHandler);
2、获取到所有的Interceptor(拦截器)(插件需要实现的接口)。
调用interceptor.plugin(target);返回target包装后的对象。
3、插件机制,我们可以使用插件为目标对象创建一个代理对象;AOP(面向切面)
我们的插件可以为四大对象创建出代理对象;
代理对象就可以拦截到四大对象的每一个执行;
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
2.插件编写步骤
1、编写Interceptor的实现类。
2、使用@Intercepts注解完成插件签名。
3、将写好的插件注册到全局配置文件中。
/**
* 完成插件签名:告诉MyBatis当前插件用来拦截哪个对象的哪个方法,不然不知道是四个对象中的哪一个方法。
*/
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor{
/**
* intercept:拦截:
* 拦截目标对象的目标方法的执行;
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
/**
* plugin:
* 包装目标对象的:包装:为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
//我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
Object wrap = Plugin.wrap(target, this);
//返回为当前target创建的动态代理
return wrap;
}
/**
* setProperties:
* 将插件注册时 的property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
System.out.println("插件配置的信息:"+properties);
}
}
将插件注册到全局配置文件中
<!--plugins:注册插件 -->
<plugins>
<plugin interceptor="com.atguigu.mybatis.dao.MyFirstPlugin">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</plugin>
<plugin interceptor="com.atguigu.mybatis.dao.MySecondPlugin"></plugin>
</plugins>
随便写个查询测试就可以了。
@Test
public void testMyBatis3Simple() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectByPrimaryKey(1);
System.out.println(employee);
}finally{
openSession.close();
}
}
测试结果
3.多个插件运行流程
在写一个插件
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MySecondPlugin implements Interceptor{
@Override
public Object intercept(Invocation invocation) throws Throwable {
System.out.println("MySecondPlugin...intercept:"+invocation.getMethod());
return invocation.proceed();
}
@Override
public Object plugin(Object target) {
// TODO Auto-generated method stub
System.out.println("MySecondPlugin...plugin:"+target);
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
}
}
将插件注册到全局配置文件中
<!--plugins:注册插件 -->
<plugins>
<plugin interceptor="com.ginger.mybatis.plugin.MyFirstPlugin">
<property name="username" value="root"/>
<property name="password" value="123456"/>
</plugin>
<plugin interceptor="com.ginger.mybatis.plugin.MySecondPlugin"></plugin></plugins>
随便写个查询测试就可以了。
@Test
public void testMyBatis3Simple() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectByPrimaryKey(1);
System.out.println(employee);
}finally{
openSession.close();
}
}
插件封装对象的原理图
4.插件开发
在插件中动态的修改查询条件开始是查询1号员工,在插件中修改为2号员工。
/**
* 完成插件签名:
* 告诉MyBatis当前插件用来拦截哪个对象的哪个方法
*/
@Intercepts(
{
@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)
})
public class MyFirstPlugin implements Interceptor{
/**
* intercept:拦截:
* 拦截目标对象的目标方法的执行;
*/
@Override
public Object intercept(Invocation invocation) throws Throwable {
// TODO Auto-generated method stub
System.out.println("MyFirstPlugin...intercept:"+invocation.getMethod());
//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询2号员工
Object target = invocation.getTarget();
System.out.println("当前拦截到的对象:"+target);
//拿到:StatementHandler==>ParameterHandler===>parameterObject
//使用SystemMetaObject这个类可以拿到target的中的属性
MetaObject metaObject = SystemMetaObject.forObject(target);
Object value = metaObject.getValue("parameterHandler.parameterObject");
System.out.println("sql语句用的参数是:"+value);
//修改完sql语句要用的参数
metaObject.setValue("parameterHandler.parameterObject", 2);
//执行目标方法
Object proceed = invocation.proceed();
//返回执行后的返回值
return proceed;
}
/**
* plugin:
* 包装目标对象的:包装:为目标对象创建一个代理对象
*/
@Override
public Object plugin(Object target) {
// TODO Auto-generated method stub
//我们可以借助Plugin的wrap方法来使用当前Interceptor包装我们目标对象
System.out.println("MyFirstPlugin...plugin:mybatis将要包装的对象"+target);
Object wrap = Plugin.wrap(target, this);
//返回为当前target创建的动态代理
return wrap;
}
/**
* setProperties:
* 将插件注册时 的property属性设置进来
*/
@Override
public void setProperties(Properties properties) {
// TODO Auto-generated method stub
System.out.println("插件配置的信息:"+properties);
}
}
//测试
@Test
public void testMyBatis3Simple() throws IOException {
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
SqlSession openSession = sqlSessionFactory.openSession();
try{
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
Employee employee = employeeMapper.selectByPrimaryKey(1);
System.out.println(employee);
}finally{
openSession.close();
}
}
测试结果
5.PageHelper插件进行分页
PageHelper是MyBatis中非常方便的第三方分页插件。
官方文档:https://pagehelper.github.io/docs/
我们可以对照官方文档的说明,快速的使用插件。
1.PageHelper使用步骤
1.引入分页插件
引入 Jar 包
你可以从下面的地址中下载最新版本的 jar 包
https://oss.sonatype.org/content/repositories/releases/com/github/pagehelper/pagehelper/
http://repo1.maven.org/maven2/com/github/pagehelper/pagehelper/
由于使用了sql 解析工具,你还需要下载 jsqlparser.jar:
http://repo1.maven.org/maven2/com/github/jsqlparser/jsqlparser/0.9.5/
使用 Maven
在 pom.xml 中添加如下依赖:
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>最新版本</version>
</dependency>
2.配置拦截器插件
特别注意,新版拦截器是 com.github.pagehelper.PageInterceptor
。 com.github.pagehelper.PageHelper
现在是一个特殊的 dialect
实现类,是分页插件的默认实现类,提供了和以前相同的用法。
1.在 MyBatis 配置 xml 中配置拦截器插件
<plugins>
<!-- com.github.pagehelper为PageHelper类所在包名 -->
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 使用下面的方式配置参数,后面会有所有的参数介绍 -->
<property name="param1" value="value1"/>
</plugin>
</plugins>
2.在 Spring 配置文件中配置拦截器插件
使用 spring 的属性配置方式,可以使用 plugins 属性像下面这样配置:
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!-- 注意其他配置 -->
<property name="plugins">
<array>
<bean class="com.github.pagehelper.PageInterceptor">
<property name="properties">
<!--使用下面的方式配置参数,一行配置一个 -->
<value>
params=value1
</value>
</property>
</bean>
</array>
</property>
</bean>
3.分页插件参数介绍
分页插件提供了多个可选参数,这些参数使用时,按照上面两种配置方式中的示例配置即可。
分页插件可选参数如下:
- dialect:默认情况下会使用 PageHelper 方式进行分页,如果想要实现自己的分页逻辑,可以实现Dialect(com.github.pagehelper.Dialect) 接口,然后配置该属性为实现类的全限定名称。
下面几个参数都是针对默认 dialect 情况下的参数。使用自定义 dialect 实现时,下面的参数没有任何作用。
1、helperDialect
:分页插件会自动检测当前的数据库链接,自动选择合适的分页方式。 你可以配置helperDialect
属性来指定分页插件使用哪种方言。配置时,可以使用下面的缩写值:
oracle
,mysql
,mariadb
,sqlite
,hsqldb
,postgresql
,db2
,sqlserver
,informix
,h2
,sqlserver2012
,derby
特别注意:使用 SqlServer2012 数据库时,需要手动指定为 sqlserver2012
,否则会使用 SqlServer2005 的方式进行分页。
你也可以实现 AbstractHelperDialect
,然后配置该属性为实现类的全限定名称即可使用自定义的实现方法。
2、offsetAsPageNum
:默认值为 false
,该参数对使用 RowBounds
作为分页参数时有效。 当该参数设置为 true
时,会将 RowBounds
中的 offset
参数当成 pageNum
使用,可以用页码和页面大小两个参数进行分页。
3、rowBoundsWithCount
:默认值为false
,该参数对使用 RowBounds
作为分页参数时有效。 当该参数设置为true
时,使用 RowBounds
分页会进行 count 查询。
4、pageSizeZero
:默认值为 false
,当该参数设置为 true
时,如果 pageSize=0
或者 RowBounds.limit = 0
就会查询出全部的结果(相当于没有执行分页查询,但是返回结果仍然是 Page
类型)。
5、reasonable
:分页合理化参数,默认值为false
。当该参数设置为 true
时,pageNum<=0
时会查询第一页, pageNum>pages
(超过总数时),会查询最后一页。默认false
时,直接根据参数进行查询。
6、params
:为了支持startPage(Object params)
方法,增加了该参数来配置参数映射,用于从对象中根据属性名取值, 可以配置 pageNum,pageSize,count,pageSizeZero,reasonable
,不配置映射的用默认值, 默认值为pageNum=pageNum;pageSize=pageSize;count=countSql;reasonable=reasonable;pageSizeZero=pageSizeZero
。
7、supportMethodsArguments
:支持通过 Mapper 接口参数来传递分页参数,默认值false
,分页插件会从查询方法的参数值中,自动根据上面 params
配置的字段中取值,查找到合适的值时就会自动分页。 使用方法可以参考测试代码中的 com.github.pagehelper.test.basic
包下的 ArgumentsMapTest
和 ArgumentsObjTest
。
8、autoRuntimeDialect
:默认值为 false
。设置为 true
时,允许在运行时根据多数据源自动识别对应方言的分页 (不支持自动选择sqlserver2012
,只能使用sqlserver
),用法和注意事项参考下面的场景五。
9、closeConn
:默认值为 true
。当使用运行时动态数据源或没有设置 helperDialect
属性自动获取数据库类型时,会自动获取一个数据库连接, 通过该属性来设置是否关闭获取的这个连接,默认true
关闭,设置为 false
后,不会关闭获取的连接,这个参数的设置要根据自己选择的数据源来决定。
重要提示:
当 offsetAsPageNum=false
的时候,由于 PageNum
问题,RowBounds
查询的时候 reasonable
会强制为 false
。使用 PageHelper.startPage
方法不受影响。
4.如何选择配置这些参数
单独看每个参数的说明可能是一件让人不爽的事情,这里列举一些可能会用到某些参数的情况。
场景一
如果你仍然在用类似ibatis式的命名空间调用方式,你也许会用到rowBoundsWithCount
, 分页插件对RowBounds
支持和 MyBatis 默认的方式是一致,默认情况下不会进行 count 查询,如果你想在分页查询时进行 count 查询, 以及使用更强大的 PageInfo
类,你需要设置该参数为 true
。
注: PageRowBounds
想要查询总数也需要配置该属性为 true
。
场景二
如果你仍然在用类似ibatis式的命名空间调用方式,你觉得 RowBounds
中的两个参数 offset,limit
不如 pageNum,pageSize
容易理解, 你可以使用 offsetAsPageNum
参数,将该参数设置为 true
后,offset
会当成 pageNum
使用,limit
和 pageSize
含义相同。
场景三
如果觉得某个地方使用分页后,你仍然想通过控制参数查询全部的结果,你可以配置 pageSizeZero
为 true
, 配置后,当 pageSize=0
或者 RowBounds.limit = 0
就会查询出全部的结果。
场景四
如果你分页插件使用于类似分页查看列表式的数据,如新闻列表,软件列表, 你希望用户输入的页数不在合法范围(第一页到最后一页之外)时能够正确的响应到正确的结果页面, 那么你可以配置 reasonable
为 true
,这时如果 pageNum<=0
会查询第一页,如果 pageNum>总页数
会查询最后一页。
场景五
如果你在 Spring 中配置了动态数据源,并且连接不同类型的数据库,这时你可以配置 autoRuntimeDialect
为 true
,这样在使用不同数据源时,会使用匹配的分页进行查询。 这种情况下,你还需要特别注意 closeConn
参数,由于获取数据源类型会获取一个数据库连接,所以需要通过这个参数来控制获取连接后,是否关闭该连接。 默认为 true
,有些数据库连接关闭后就没法进行后续的数据库操作。而有些数据库连接不关闭就会很快由于连接数用完而导致数据库无响应。所以在使用该功能时,特别需要注意你使用的数据源是否需要关闭数据库连接。
当不使用动态数据源而只是自动获取 helperDialect
时,数据库连接只会获取一次,所以不需要担心占用的这一个连接是否会导致数据库出错,但是最好也根据数据源的特性选择是否关闭连接。
3.如何在代码中使用
PageHelper的使用方法在官网中有很详细的介绍,可以参照官网文档来。
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor"></plugin>
</plugins>
@Test
public void test01() throws IOException {
// 1、获取sqlSessionFactory对象
SqlSessionFactory sqlSessionFactory = getSqlSessionFactory();
// 2、获取sqlSession对象
SqlSession openSession = sqlSessionFactory.openSession();
try {
EmployeeMapper employeeMapper = openSession.getMapper(EmployeeMapper.class);
//设置分页 pageNum 页数 pageSize 每页显示大小
Page<Object> page = PageHelper.startPage(1, 3);
//查询所有员工
List<Employee> emps = employeeMapper.selectByExample(null);
// PageInfo 里面封装了分页的所有信息
// navigatePages 页码数量 3
PageInfo<Employee> info = new PageInfo<>(emps, 3);
for (Employee employee : emps) {
System.out.println(employee);
}
/*
System.out.println("当前页码:"+page.getPageNum());
System.out.println("总记录数:"+page.getTotal());
System.out.println("每页的记录数:"+page.getPageSize());
System.out.println("总页码:"+page.getPages());
*/
//------------------------------------------------
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("连续显示的页码:");
int[] nums = info.getNavigatepageNums();
for (int i = 0; i < nums.length; i++) {
System.out.println(nums[i]);
}
} finally {
openSession.close();
}
}
测试结果