龙空技术网

Mybatis插件原理和PageHelper结合实战分页插件(七)

JAVA架构 1847

前言:

目前你们对“ibatismysql分页”大致比较注意,同学们都想要分析一些“ibatismysql分页”的相关知识。那么小编也在网摘上收集了一些关于“ibatismysql分页””的相关资讯,希望姐妹们能喜欢,朋友们快快来了解一下吧!

往期精彩文章:

Mybatis源码分析之SqlSessionFactory(一)

Mybatis源码分析之SqlSession和Excutor(二)

Mybatis源码分析之Mapper执行SQL过程(三)

Mybatis源码分析之Cache一级缓存原理(四)

Mybatis源码分析之Cache二级缓存原理 (五)

mybatis结合redis实战二级缓存(六)

今天和大家分享下mybatis的一个分页插件PageHelper,在讲解PageHelper之前我们需要先了解下mybatis的插件原理。PageHelper

的官方网站:

一、Plugin接口

mybatis定义了一个插件接口org.apache.ibatis.plugin.Interceptor,任何自定义插件都需要实现这个接口PageHelper就实现了改接口

package org.apache.ibatis.plugin;

import java.util.Properties;

/**

* @author Clinton Begin

*/

public interface Interceptor {

Object intercept(Invocation invocation) throws Throwable;

Object plugin(Object target);

void setProperties(Properties properties);

}

1:intercept 拦截器,它将直接覆盖掉你真实拦截对象的方法。

2:plugin方法它是一个生成动态代理对象的方法

3:setProperties它是允许你在使用插件的时候设置参数值。

看下com.github.pagehelper.PageHelper分页的实现了那些

 /**

* Mybatis拦截器方法

*

* @param invocation 拦截器入参

* @return 返回执行结果

* @throws Throwable 抛出异常

*/

public Object intercept(Invocation invocation) throws Throwable {

if (autoRuntimeDialect) {

SqlUtil sqlUtil = getSqlUtil(invocation);

return sqlUtil.processPage(invocation);

} else {

if (autoDialect) {

initSqlUtil(invocation);

}

return sqlUtil.processPage(invocation);

}

}

这个方法获取了是分页核心代码,重新构建了BoundSql对象下面会详细分析

 /**

* 只拦截Executor

*

* @param target

* @return

*/

public Object plugin(Object target) {

if (target instanceof Executor) {

return Plugin.wrap(target, this);

} else {

return target;

}

}

这个方法是正对Executor进行拦截

 /**

* 设置属性值

*

* @param p 属性值

*/

public void setProperties(Properties p) {

checkVersion;

//多数据源时,获取jdbcurl后是否关闭数据源

String closeConn = p.getProperty("closeConn");

//解决#97

if(StringUtil.isNotEmpty(closeConn)){

this.closeConn = Boolean.parseBoolean(closeConn);

}

//初始化SqlUtil的PARAMS

SqlUtil.setParams(p.getProperty("params"));

//数据库方言

String dialect = p.getProperty("dialect");

String runtimeDialect = p.getProperty("autoRuntimeDialect");

if (StringUtil.isNotEmpty(runtimeDialect) && runtimeDialect.equalsIgnoreCase("TRUE")) {

this.autoRuntimeDialect = true;

this.autoDialect = false;

this.properties = p;

} else if (StringUtil.isEmpty(dialect)) {

autoDialect = true;

this.properties = p;

} else {

autoDialect = false;

sqlUtil = new SqlUtil(dialect);

sqlUtil.setProperties(p);

}

}

基本的属性设置

二、Plugin初始化

初始化和所有mybatis的初始化一样的在之前的文章里面已经分析了 《Mybatis源码分析之SqlSessionFactory(一)》

private void pluginElement(XNode parent) throws Exception { 

if (parent != ) {

for (XNode child : parent.getChildren) {

String interceptor = child.getStringAttribute("interceptor");

Properties properties = child.getChildrenAsProperties;

Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance;

interceptorInstance.setProperties(properties);

configuration.addInterceptor(interceptorInstance);

}

}

}

这里是讲多个实例化的插件对象放入configuration,addInterceptor最终存放到一个list里面的,以为这可以同时存放多个Plugin

三、Plugin拦截

插件可以拦截mybatis的4大对象ParameterHandler、ResultSetHandler、StatementHandler、Executor,源码如下图

在Configuration类里面可以找到

PageHelper使用了Executor进行拦截,上面的的源码里面已经可以看到了。

我看下上图newExecutor方法

executor = (Executor) interceptorChain.pluginAll(executor);

这个是生产一个代理对象,生产了代理对象就运行带invoke方法

四、Plugin运行

mybatis自己带了Plugin方法,源码如下

public class Plugin implements InvocationHandler {

private Object target;

private Interceptor interceptor;

private Map<Class<?>, Set<Method>> signatureMap;

private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) {

this.target = target;

this.interceptor = interceptor;

this.signatureMap = signatureMap;

}

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;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

try {

Set<Method> methods = signatureMap.get(method.getDeclaringClass);

if (methods != && methods.contains(method)) {

return interceptor.intercept(new Invocation(target, method, args));

}

return method.invoke(target, args);

} catch (Exception e) {

throw ExceptionUtil.unwrapThrowable(e);

}

}

private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {

Intercepts interceptsAnnotation = interceptor.getClass.getAnnotation(Intercepts.class);

// issue #251

if (interceptsAnnotation == ) {

throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass.getName);

}

Signature sigs = interceptsAnnotation.value;

Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>;

for (Signature sig : sigs) {

Set<Method> methods = signatureMap.get(sig.type);

if (methods == ) {

methods = new HashSet<Method>;

signatureMap.put(sig.type, methods);

}

try {

Method method = sig.type.getMethod(sig.method, sig.args);

methods.add(method);

} catch (NoSuchMethodException e) {

throw new PluginException("Could not find method on " + sig.type + " named " + sig.method + ". Cause: " + e, e);

}

}

return signatureMap;

}

private static Class<?> getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) {

Set<Class<?>> interfaces = new HashSet<Class<?>>;

while (type != ) {

for (Class<?> c : type.getInterfaces) {

if (signatureMap.containsKey(c)) {

interfaces.add(c);

}

}

type = type.getSuperclass;

}

return interfaces.toArray(new Class<?>[interfaces.size()]);

}

}

wrap方法是为了生成一个动态代理类。

invoke方法是代理绑定的方法,该方法首先判定签名类和方法是否存在,如果不存在则直接反射调度被拦截对象的方法,如果存在则调度插件的interceptor方法,这时候会初始化一个Invocation对象

我们在具体看下PageHelper,当执行到invoke后程序将跳转到PageHelper.intercept

 public Object intercept(Invocation invocation) throws Throwable {

if (autoRuntimeDialect) {

SqlUtil sqlUtil = getSqlUtil(invocation);

return sqlUtil.processPage(invocation);

} else {

if (autoDialect) {

initSqlUtil(invocation);

}

return sqlUtil.processPage(invocation);

}

}

我们在来看sqlUtil.processPage方法

 /**

* Mybatis拦截器方法,这一步嵌套为了在出现异常时也可以清空Threadlocal

*

* @param invocation 拦截器入参

* @return 返回执行结果

* @throws Throwable 抛出异常

*/

public Object processPage(Invocation invocation) throws Throwable {

try {

Object result = _processPage(invocation);

return result;

} finally {

clearLocalPage;

}

}

继续跟进

 /**

* Mybatis拦截器方法

*

* @param invocation 拦截器入参

* @return 返回执行结果

* @throws Throwable 抛出异常

*/

private Object _processPage(Invocation invocation) throws Throwable {

final Object args = invocation.getArgs;

Page page = ;

//支持方法参数时,会先尝试获取Page

if (supportMethodsArguments) {

page = getPage(args);

}

//分页信息

RowBounds rowBounds = (RowBounds) args[2];

//支持方法参数时,如果page == 就说明没有分页条件,不需要分页查询

if ((supportMethodsArguments && page == )

//当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页

|| (!supportMethodsArguments && SqlUtil.getLocalPage == && rowBounds == RowBounds.DEFAULT)) {

return invocation.proceed;

} else {

//不支持分页参数时,page==,这里需要获取

if (!supportMethodsArguments && page == ) {

page = getPage(args);

}

return doProcessPage(invocation, page, args);

}

}

这些都只是分装page方法,真正的核心是doProcessPage

 /**

* Mybatis拦截器方法

*

* @param invocation 拦截器入参

* @return 返回执行结果

* @throws Throwable 抛出异常

*/

private Page doProcessPage(Invocation invocation, Page page, Object[] args) throws Throwable {

//保存RowBounds状态

RowBounds rowBounds = (RowBounds) args[2];

//获取原始的ms

MappedStatement ms = (MappedStatement) args[0];

//判断并处理为PageSqlSource

if (!isPageSqlSource(ms)) {

processMappedStatement(ms);

}

//设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响

((PageSqlSource)ms.getSqlSource).setParser(parser);

try {

//忽略RowBounds-否则会进行Mybatis自带的内存分页

args[2] = RowBounds.DEFAULT;

//如果只进行排序 或 pageSizeZero的判断

if (isQueryOnly(page)) {

return doQueryOnly(page, invocation);

}

//简单的通过total的值来判断是否进行count查询

if (page.isCount) {

page.setCountSignal(Boolean.TRUE);

//替换MS

args[0] = msCountMap.get(ms.getId);

//查询总数

Object result = invocation.proceed;

//还原ms

args[0] = ms;

//设置总数

page.setTotal((Integer) ((List) result).get(0));

if (page.getTotal == 0) {

return page;

}

} else {

page.setTotal(-1l);

}

//pageSize>0的时候执行分页查询,pageSize<=0的时候不执行相当于可能只返回了一个count

if (page.getPageSize > 0 &&

((rowBounds == RowBounds.DEFAULT && page.getPageNum > 0)

|| rowBounds != RowBounds.DEFAULT)) {

//将参数中的MappedStatement替换为新的qs

page.setCountSignal;

BoundSql boundSql = ms.getBoundSql(args[1]);

args[1] = parser.setPageParameter(ms, args[1], boundSql, page);

page.setCountSignal(Boolean.FALSE);

//执行分页查询

Object result = invocation.proceed;

//得到处理结果

page.addAll((List) result);

}

} finally {

((PageSqlSource)ms.getSqlSource).removeParser;

}

//返回结果

return page;

}

上面的有两个 Object result = invocation.proceed执行,第一个是执行统计总条数,第二个是执行执行分页的查询的数据

里面用到了代理。最终第一回返回一个总条数,第二个把分页的数据得到。

五:PageHelper使用

以上讲解了Mybatis的插件原理和PageHelper相关的内部实现,下面具体讲讲PageHelper使用

1:先增加maven依赖:

<dependency> 

<groupId>com.github.pagehelper</groupId>

<artifactId>pagehelper</artifactId>

<version>4.1.6</version>

</dependency

2:配置configuration.xml文件加入如下配置(plugins应该在environments的上面 )

<plugins>

<!-- PageHelper4.1.6 -->

<plugin interceptor="com.github.pagehelper.PageHelper">

<property name="dialect" value="mysql"/>

<property name="offsetAsPageNum" value="false"/>

<property name="rowBoundsWithCount" value="false"/>

<property name="pageSizeZero" value="true"/>

<property name="reasonable" value="false"/>

<property name="supportMethodsArguments" value="false"/>

<property name="returnPageInfo" value="none"/>

</plugin>

</plugins>

相关字段说明可以查看SqlUtilConfig源码里面都用说明

注意配置的时候顺序不能乱了否则报错

Caused by: org.apache.ibatis.builder.BuilderException: Error creating document instance. Cause: org.xml.sax.SAXParseException; lineNumber: 57; columnNumber: 17; 元素类型为 "configuration" 的内容必须匹配 "(properties?,settings?,typeAliases?,typeHandlers?,objectFactory?,objectWrapperFactory?,plugins?,environments?,databaseIdProvider?,mappers?)"。

at org.apache.ibatis.parsing.XPathParser.createDocument(XPathParser.java:259)

at org.apache.ibatis.parsing.XPathParser.<init>(XPathParser.java:120)

at org.apache.ibatis.builder.xml.XMLConfigBuilder.<init>(XMLConfigBuilder.java:66)

at org.apache.ibatis.session.SqlSessionFactoryBuilder.build(SqlSessionFactoryBuilder.java:49)

... 2 more

意思是配置里面的节点顺序是properties->settings->typeAliases->typeHandlers->objectFactory->objectWrapperFactory->plugins->environments->databaseIdProvider->mappers plugins应该在environments之前objectWrapperFactory之后 这个顺序不能乱了

3:具体使用

1:分页

 SqlSession sqlSession = sessionFactory.openSession;

UserMapper userMapper = sqlSession.getMapper(UserMapper.class);

PageHelper.startPage(1,10,true); //第一页 每页显示10条

Page<User> page=userMapper.findUserAll;

2:不分页

 PageHelper.startPage(1,-1,true);

3:查询总条数

PageInfo<User> info=new PageInfo<>(userMapper.findUserAll);

标签: #ibatismysql分页