ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

Mybatis分页插件PageHelper的原理

2021-09-28 11:02:43  阅读:148  来源: 互联网

标签:插件 executor 分页 Object PageHelper 拦截器 Executor Mybatis parameter


Mybatis分页插件PageHelper的原理

Mybatis的查询流程

在原生的mybatis中我们是通过SqlSessionFactory.openSession()方法获取一个SqlSession对象,然后通过这个对象提供的select,insert,delete,update方法执行sql语句。那么SqlSession内部是怎么实现的呢?

Mybatis的查询接口Executor

我们以SqlSession的selectOne()方法为例,进行讲解, 下面是selectOne()方法的调用流程

	public <T> T selectOne(String statement) {
        return this.selectOne(statement, (Object)null);
    }
    
    public <T> T selectOne(String statement, Object parameter) {
        List<T> list = this.selectList(statement, parameter);
        if (list.size() == 1) {
            return list.get(0);
        } else if (list.size() > 1) {
            throw new TooManyResultsException("Expected one result (or null) to be returned by selectOne(), but found: " + list.size());
        } else {
            return null;
        }
    }
    
	public <E> List<E> selectList(String statement, Object parameter) {
        return this.selectList(statement, parameter, RowBounds.DEFAULT);
    }

	//关键的地方
	public <E> List<E> selectList(String statement, Object parameter, RowBounds rowBounds) {
        List var5;
        try {
        	//这里是去mybatis的配置类Configuration中去获取MappedStatement对象。
        	//在mybatis解析mapper文件时,会把文件的select,delete,update,insert这些标签的内容解析成MappedStatement对象
            MappedStatement ms = this.configuration.getMappedStatement(statement);
            //这里可以看到是通过Executor负责执行sql的,SqlSession只是对其做了一层封装
            var5 = this.executor.query(ms, this.wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER);
        } catch (Exception var9) {
            throw ExceptionFactory.wrapException("Error querying database.  Cause: " + var9, var9);
        } finally {
            ErrorContext.instance().reset();
        }

        return var5;
    }

可以看到真正执行sql的是Executor。那么我们可以思考一下,分页插件是不是就是对Executor做了文章,才实现的分页呢?

Mybatis的拦截器插件

经过上面的分析,我们找到了可以对Executor做文章的拦截器插件。
前面说到SqlSession是对Executor做的一层封装,那么我们先找到是如何创建SqlSession的

	public SqlSession openSession() {
        return this.openSessionFromDataSource(this.configuration.getDefaultExecutorType(), (TransactionIsolationLevel)null, false);
    }


	private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
        Transaction tx = null;

        DefaultSqlSession var8;
        try {
            Environment environment = this.configuration.getEnvironment();
            TransactionFactory transactionFactory = this.getTransactionFactoryFromEnvironment(environment);
            tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
            //创建Executor的方法
            Executor executor = this.configuration.newExecutor(tx, execType);
            var8 = new DefaultSqlSession(this.configuration, executor, autoCommit);
        } catch (Exception var12) {
            this.closeTransaction(tx);
            throw ExceptionFactory.wrapException("Error opening session.  Cause: " + var12, var12);
        } finally {
            ErrorContext.instance().reset();
        }

        return var8;
    }

可以看到Executor是在Configuration的newExecutor()方法中创建的

public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        //根据executorType来创建对应的executor
        if (ExecutorType.BATCH == executorType) {
            executor = new BatchExecutor(this, transaction);
        } else if (ExecutorType.REUSE == executorType) {
            executor = new ReuseExecutor(this, transaction);
        } else {
            executor = new SimpleExecutor(this, transaction);
        }

        if (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }
		//这里是遍历所有的拦截器,如果有拦截器对Executor进行了拦截,就会返回一个代理类,分页插件就是利用这个实现分页的
        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }

我们来看下pluginAll方法的执行流程

	public Object pluginAll(Object target) {
        Interceptor interceptor;
        //遍历拦截器,调用拦截器的plugin方法,如果该拦截器对目标对象target进行了拦截,则会返回一个代理对象(这里可以了解,如果多个拦截器对同一个目标对象进行拦截,则会出现一个代理类内部嵌套一个代理类)
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }


	default Object plugin(Object target) {
		//调用Plugin的wrap方法进行判断,生成代理类
        return Plugin.wrap(target, this);
    }


	public static Object wrap(Object target, Interceptor interceptor) {
		//对拦截器进行解析,获取拦截器对哪些类的哪些方法进行了拦截,放到Map中
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        //获取目标对象的class对象,这里是Executor
        Class<?> type = target.getClass();
        //判断是否对目标对象进行了拦截
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        //interfaces.length > 0说明对目标对象进行可拦截,就生成代理类返回。没有拦截,直接返回目标对象本身
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

上面对mybatis的拦截器进行了分析。那么分页插件有没有定义拦截器呢?答案是肯定的

分页插件的实现:PageInterceptor

//由@Intercepts来定义对哪些类的哪些方法进行拦截
@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 {
    protected Cache<CacheKey, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;

    public PageInterceptor() {
    }
	
	//执行拦截器逻辑的方法
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement)args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds)args[2];
            ResultHandler resultHandler = (ResultHandler)args[3];
            Executor executor = (Executor)invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }

            List resultList;
            //判断是否跳过分页,就是去当前线程获取Page对象,如果没有获取到就返回true,跳过
            //我们调用PageHelper.startPage(int pageNum, int pageSize)时,就会创建Page对象并和当前线程进行绑定
            if (this.dialect.skip(ms, parameter, rowBounds)) {
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
                Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                    CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
                    countKey.update("_Count");
                    MappedStatement countMs = (MappedStatement)this.msCountMap.get(countKey);
                    if (countMs == null) {
                        countMs = MSUtils.newCountMappedStatement(ms);
                        this.msCountMap.put(countKey, countMs);
                    }
					//获取查询总数的sql语句
                    String countSql = this.dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
                    //创建BoundSql,用于查询总数
                    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
                    Iterator var16 = additionalParameters.keySet().iterator();

                    while(var16.hasNext()) {
                        String key = (String)var16.next();
                        countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
					//查询总数sql
                    Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                    Long count = (Long)((List)countResultList).get(0);
                    //如果查询总数count返回的是0,就进入这个分支,返回一个空集合
                    if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                        Object var18 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                        return var18;
                    }
                }
				
				//判断Page对象的pageSize是否>0,如果pageSize<=0,进入到这个分支
                if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                } else {
                    parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
                    //获取分页查询的sql
                    String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
                    //创建分页查询的BoundSql对象
                    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
                    Iterator var25 = additionalParameters.keySet().iterator();

                    while(true) {
                        if (!var25.hasNext()) {
                        	//执行分页查询
                            resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
                            break;
                        }

                        String key = (String)var25.next();
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                }
            }
            
			//把查询结果放到Page中
            Object var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
            return var22;
        } finally {
        	//把当前线程和Page进行解绑
            this.dialect.afterAll();
        }
    }
	
	//判断拦截器是否对目标对象进行拦截的方法
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
	
	//对拦截器设置一些参数
    public void setProperties(Properties properties) {
        this.msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = this.default_dialect_class;
        }

        try {
            Class<?> aClass = Class.forName(dialectClass);
            this.dialect = (Dialect)aClass.newInstance();
        } catch (Exception var5) {
            throw new PageException(var5);
        }

        this.dialect.setProperties(properties);

        try {
            this.additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            this.additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException var4) {
            throw new PageException(var4);
        }
    }

这就是分页插件定义的分页插件,实现了mybatis的插件接口Interceptor,可以看到类上的@Intercepts注解,是对Executor的query方法进行了拦截,当执行Executor的query方法时,就会进入到这个拦截器的intercept方法执行,就是在这个方法中完成了分页的逻辑。

分页插件是如何获取我们的pageNum和pageSize参数的

当我们调用PageHelper.startPage(int pageNum, int pageSize)方法时,它是怎么获取我们给的分页参数呢?下面是startPage的调用流程

	public static <E> Page<E> startPage(int pageNum, int pageSize) {
        return startPage(pageNum, pageSize, true);
    }

    public static <E> Page<E> startPage(int pageNum, int pageSize, boolean count) {
        Page<E> page = new Page(pageNum, pageSize, count);
        setLocalPage(page);
        return page;
    }

    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }
    
	//把Page对象和当前线程绑定的ThreadLocal
	protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

可以看到分页插件是把我们的分页参数放到Page对象中,并把Page对象和当前线程绑定,这样就能在分页插件中获取到我们的分页参数了。

总结

分页插件是利用了mybatis提供的拦截器功能,定义一个分页拦截器PageInterceptor实现了Interceptor接口,并对查询接口Executor的query方法进行了拦截。当执行Executor的query方法时,会进入到PageInterceptor的intercept方法执行分页的逻辑,根据我们的数据库环境,生成对应的查询总条数的sql和分页sql语句,完成分页逻辑后把分页对象Page返回给我们。

标签:插件,executor,分页,Object,PageHelper,拦截器,Executor,Mybatis,parameter
来源: https://blog.csdn.net/qq_42722652/article/details/120504688

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有