使用方法
1.添加Maven依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
<version>5.0.0</version>
</dependency>
2.在 Mybatis-Config.xml中配置PageHelper插件参数
<plugins>
<plugin interceptor="com.github.pagehelper.PageInterceptor">
<!-- 4.0.0以后版本可以不设置该参数 ,可以自动识别
<property name="dialect" value="mysql"/> -->
<!-- 该参数默认为false -->
<!-- 设置为true时,会将RowBounds第一个参数offset当成pageNum页码使用 -->
<!-- 和startPage中的pageNum效果一样-->
<property name="offsetAsPageNum" value="true"/>
<!-- 该参数默认为false -->
<!-- 设置为true时,使用RowBounds分页会进行count查询 -->
<property name="rowBoundsWithCount" value="true"/>
<!-- 设置为true时,如果pageSize=0或者RowBounds.limit = 0就会查询出全部的结果 -->
<!-- (相当于没有执行分页查询,但是返回结果仍然是Page类型)-->
<property name="pageSizeZero" value="true"/>
<!-- 3.3.0版本可用 - 分页参数合理化,默认false禁用 -->
<!-- 启用合理化时,如果pageNum<1会查询第一页,如果pageNum>pages会查询最后一页 -->
<!-- 禁用合理化时,如果pageNum<1或pageNum>pages会返回空数据 -->
<property name="reasonable" value="true"/>
<!-- 3.5.0版本可用 - 为了支持startPage(Object params)方法 -->
<!-- 增加了一个`params`参数来配置参数映射,用于从Map或ServletRequest中取值 -->
<!-- 可以配置pageNum,pageSize,count,pageSizeZero,reasonable,orderBy,不配置映射的用默认值 -->
<!-- 不理解该含义的前提下,不要随便复制该配置 -->
<property name="params" value="pageNum=start;pageSize=limit;"/>
<!-- 支持通过Mapper接口参数来传递分页参数 -->
<property name="supportMethodsArguments" value="true"/>
<!-- always总是返回PageInfo类型,check检查返回类型是否为PageInfo,none返回Page -->
<property name="returnPageInfo" value="check"/>
</plugin>
</plugins>
注意:plugins插件的配置在 settings之后 在environments之前,否则会导致配置文件无法解析
3.代码
PageHelper.startPage(pn, 10); //pageNumber, pageSize,第几页,每页几条
List<Employee> emps = employeeService.getAll();
PageInfo page = new PageInfo(emps, 10);
return Msg.success().add("pageInfo", page);
原理
从配置文件可以看出,pageHelper分页插件的核心类就是com.github.pagehelper.PageInterceptor
package com.github.pagehelper;
import com.github.pagehelper.cache.Cache;
import com.github.pagehelper.cache.CacheFactory;
import com.github.pagehelper.util.MSUtils;
import com.github.pagehelper.util.StringUtil;
import org.apache.ibatis.cache.CacheKey;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* Mybatis - 通用分页拦截器<br/>
* 项目地址 : http://git.oschina.net/free/Mybatis_PageHelper
*
* @author liuzh/abel533/isea533
* @version 5.0.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
public class PageInterceptor implements Interceptor {
//缓存count查询的ms
protected Cache<CacheKey, MappedStatement> msCountMap = null;
private Dialect dialect;
private String default_dialect_class = "com.github.pagehelper.PageHelper";
private Field additionalParametersField;
@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) {
...
}
}
主要是实现了org.apache.ibatis.plugin.Interceptor接口。其中主要由三个方法
- intercept 执行拦截内容的地方,拦截目标对象的目标方法的执行
- plugin 决定是否触发intercept()方法,包装目标对象,包装就是为目标对象创建一个代理对象
- setProperties 给自定义的拦截器传递xml配置的属性参数。将插件注册时的property属性设置进来
package org.apache.ibatis.plugin;
import java.util.Properties;
/**
* @author Clinton Begin
*/
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
}
}
还需要注意的是PageInterceptor的@Intercepts注解,其指定了拦截的对象和方法。在默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
- Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
- ParameterHandler (getParameterObject, setParameters)
- ResultSetHandler (handleResultSets, handleOutputParameters)
- StatementHandler (prepare, parameterize, batch, update, query)
@Intercepts(
{
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
}
)
了解完了PageHelper的核心类,我们再来看看它是如何运行的。
1.加载
首先,在myBatis生成sqlSessionFactory会话工厂时,加载了mybatis-config.xml配置文件中plugin节点
XMLConfigBuilder.java
private void pluginElement(XNode parent) throws Exception {
if (parent != null) {
for (XNode child : parent.getChildren()) {
String interceptor = child.getStringAttribute("interceptor");
Properties properties = child.getChildrenAsProperties();
Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance();
interceptorInstance.setProperties(properties);
configuration.addInterceptor(interceptorInstance);
}
}
}
循环读取所有的interceptor节点,并强转成Interceptor对象。塞到一个InterceptorChain容器中进行管理
InterceptorChain.java
public class InterceptorChain {
private final List<Interceptor> interceptors = new ArrayList<>();
public void addInterceptor(Interceptor interceptor) {
interceptors.add(interceptor);
}
通过类名就可以看出,mybaits管理所有的插件使用了责任链的设计模式
通过代码使用分页看出,它没有修改原有的代码,那它是怎么做到改变和增强对象的行为的呢?另外,如果我有多个插件,拦截同一个类时,它是如何做到层层拦截的呢?
大胆猜测一下,通过代理模式实现改变和增强对象;通过责任链模式实现层层拦截。
现在,我们进入代码看一看是不是和我们猜测的一样。
2.拦截
在@Intercepts注解中看到分页插件拦截的对象是Execut下执行器,而mybatis的执行器是在创建会话sqlSession创建的
Configuration.java
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
executorType = executorType == null ? defaultExecutorType : executorType;
executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
Executor executor;
if (ExecutorType.BATCH == executorType) {
executor = new BatchExecutor(this, transaction);
} else if (ExecutorType.REUSE == executorType) {
executor = new ReuseExecutor(this, transaction);
} else {
// 默认 SimpleExecutor
executor = new SimpleExecutor(this, transaction);
}
// 二级缓存开关,settings 中的 cacheEnabled 默认是 true
if (cacheEnabled) {
executor = new CachingExecutor(executor);
}
// 植入插件的逻辑,至此,四大对象已经全部拦截完毕
executor = (Executor) interceptorChain.pluginAll(executor);
return executor;
}
InterceptorChain.java
public Object pluginAll(Object target) {
for (Interceptor interceptor : interceptors) {
target = interceptor.plugin(target);
}
return target;
}
如猜测的那样,果然使用责任链模式管理所有plugin。
按照猜测,interceptor.plugin(target)方法就是用来创建代理对象的,点进去看看
Interceptor.java
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
Plugin.java
public static Object wrap(Object target, Interceptor interceptor) {
Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
Class<?> type = target.getClass();
Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
if (interfaces.length > 0) {
return Proxy.newProxyInstance(
type.getClassLoader(),
interfaces,
new Plugin(target, interceptor, signatureMap));
}
return target;
}
确实如猜测的那样,通过Plugin封装一个wrap方法,生成一个jdk动态代理类。
在Proxy.newProxyInstance方法中,共有三个参数:
- ClassLoader loader 类加载器
- Class<?>[] interfaces 被代理类的所有接口信息
- InvocationHandler h 处理类,实现InvocationHandler接口中invoke方法。
所以接下来我们要关注的就是Plugin类中的invoke()方法。
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
//获取被代理类的所有方法
Set<Method> methods = signatureMap.get(method.getDeclaringClass());
//与拦截的方法相匹配
if (methods != null && methods.contains(method)) {
//走插件流程
return interceptor.intercept(new Invocation(target, method, args));
}
//继续走原来的流程
return method.invoke(target, args);
} catch (Exception e) {
throw ExceptionUtil.unwrapThrowable(e);
}
}
这样,我们就回到了一开始拦截器intercept()的方法。在这里,它封装了一个Invocation类,也可以理解为被代理类的封装。
public class Invocation {
private final Object target;
private final Method method;
private final Object[] args;
public Invocation(Object target, Method method, Object[] args) {
this.target = target;
this.method = method;
this.args = args;
}
public Object proceed() throws InvocationTargetException, IllegalAccessException {
//被代理类本来的方法
return method.invoke(target, args);
}
}
也就是说,我们只需要执行proceed()方法,就相当于执行了被代理类本来的方法。这样,我们在自己写插件类的时候可以在处理完成后调用proceed()方法就可以了。
总结
Mybatis插件关键对象:
- Interceptor接口:自定义拦截器(实现类)
- InterceptorChain:存放插件的容器
- Plugin:h对象,提供创建代理类的方法
- Invocation:对被代理类的封装
插件的工作流程图:
当有多个插件时:
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/12932.html