2024-01-20
原文作者:墨家巨子@俏如来 原文地址: https://blog.csdn.net/u014494148/article/details/122315991

2021一路有你,2022我们继续加油!你的肯定是我最大的动力

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 感谢

前言

面试官:用过pagehelper做分页把,你说一下pagehelper的分页实现原理。额…此时你只能说我不知道。如果你事先看了我接下来的这篇文章,相信你一定也把这个面试题答得很好。

认识Mybatis插件(拦截器)

SpringMVC的拦截器相信大家都清楚,作用是在Controller执行前或执行后拦截器请求从而实现对请求做增强,比如:登录检查等。在mybais中也有拦截器(当然它应该叫插件,我觉得叫拦截器更贴切),它拦截的是发向数据库的请求,达到增强Mybatis的目的,道理都差不多。

下面的文字是Mybatis官方文档对拦截器的描述

MyBatis 允许你在映射语句执行过程中的某一点进行拦截调用。默认情况下,MyBatis 允许使用插件来拦截的方法调用包括:
• Executor(update, query, flushStatements, commit, rollback,getTransaction, close,isClosed)
• ParameterHandler(getParameterObject, setParameters)
• ResultSetHandler(handleResultSets, handleOutputParameters)
• StatementHandler(prepare, parameterize, batch, update, query)
通过 MyBatis 提供的强大机制,使用插件是非常简单的,只需实现 Interceptor 接口,并指定想要拦截的方法签名即可

这句话告诉我们,Mybatis的拦截器会对四个类进行拦截,Executor执行器,ParameterHandler参数处理器,ResultSetHandler结果处理器,StatementHandler 语句处理器。待会儿带大家去看相关的源码。

PageHelper的使用和源码解析

PageHelper是一个分页插件,使用它我们在做分页查询的时候不需要编写查询总条数的SQL,以及查询列表的SQL不需要指定Limit 。我们只需要把分页条件交给PageHelper,它就可以帮我们自动生成查询总条数的SQL,以及在查询列表的SQL后面自动加上 Limit。PageHelper 就是一个Mybatis的拦截器。

下面我们来简单使用一下PageHelper,首先需要导入PageHelper的依赖

    <dependency>
      <groupId>com.github.pagehelper</groupId>
        <artifactId>pagehelper</artifactId>
        <version>4.1.4</version>
    </dependency>

第二步,在mybatis-config.xml配置插件 PageHelper

    <plugins>
        <plugin interceptor="com.github.pagehelper.PageHelper">
           <!-- 指定方言,Mysql数据库 -->
            <property name="dialect" value="mysql"/>
        </plugin>
    </plugins>

第三步,编写查询列表的SQL

    <select id="selectList" resultMap="resultMap">
      select * from student
    </select>

第四步,使用PageHelper查询结果

    @Test
    public void testInterceptor() throws IOException {
        //设置分页信息
        Page page = PageHelper.startPage(1, 10, true);
        //================================================================================
        //加载配置
        InputStream inputStream= Resources.getResourceAsStream("mybatis-config.xml");
        //创建一个sqlSessionFactory
        SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);
        //创建SqlSession
        SqlSession sqlSession = sqlSessionFactory.openSession();
        //Page pageInfo = (Page)sqlSession.selectList("cn.whale.mapper.StudentMapper.selectList");
        StudentMapper mapper = sqlSession.getMapper(StudentMapper.class);
        //==================================================================================
        //结果以Page来接收
        Page pageInfo = (Page) mapper.selectList();
        System.out.println(pageInfo.getTotal());
        pageInfo.getResult().stream().forEach( System.out::println);
    }

测试结果如下

202401201945452391.png
看到这个结果是不是觉得很有意思,我并没有编写select count(0) from student 这条查询总条数的SQL,但是该SQL被执行了,同时还给查询列表的SQL增加了limit 分页条件。

接下来我们分析一下PageHelper的实现原理,首先看一下 com.github.pagehelper.PageHelper 这个类com.github.pagehelper.PageHelper

    @Intercepts({@Signature(
        //拦截器Executor的query方法
        type = Executor.class,
        method = "query",
        //方法的参数,拦截拥有这四个参数的query方法
        args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )})
    public class PageHelper implements Interceptor {
    	//....省略....
    }

首先 PageHelper 实现了 Interceptor接口,Mybatis的拦截器都需要实现该接口。类上通过@Intercepts指定了只需要拦截Executor执行器的 “query” 方法。

拦截器接口源码如下

    public interface Interceptor {
      //拦截器方法,拦截器的核心方法
      Object intercept(Invocation invocation) throws Throwable;
      //该方法的参数 target就是拦截器的目标类,plugin方法中需要对target做增强
      Object plugin(Object target);
      //设置属性,Properties是mybatis-config.xml对拦截器配置中的属性
      void setProperties(Properties properties);
    
    }

我们看一下 PageHelper 的三个方法,先看plugin方法 :com.github.pagehelper.PageHelper#plugin

    public Object plugin(Object target) {
    		//如果是Executor就对target做增强,否则不做任何处理,直接返回target
            if (target instanceof Executor) {
               //把target交给Plugin.wrap ,this就是PageHelper拦截器类
                return Plugin.wrap(target, this);
            } else {
                return target;
            }
        }

如果target是Executor就对target做增强,否则不做任何处理,直接返回target,看一下 Plugin.wrap做了什么

    public class Plugin implements InvocationHandler {
      //原生对象
      private final Object target;
      //拦截器
      private final Interceptor interceptor;
      //方法
      private final 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;
      }
      //对target做增强,因为pageHelper只是拦截Executor,所以target是 Executor比如CacheingExector, interceptor是拦截器类即:PageHelper
    public static Object wrap(Object target, Interceptor interceptor) {
       //1.拿到拦截器类上的@Intercepts 指定的方法签名@Signature,即:要拦截器什么方法比如:query方法
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        //拿到目标类的class,比如:org.apache.ibatis.executor.CachingExecutor
        Class<?> type = target.getClass();
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > 0) {
          //2.使用JDK动态代理为target生成代理类
          return Proxy.newProxyInstance(
              type.getClassLoader(),
              interfaces,
              //3.Plugin是一个 InvocationHandler
              new Plugin(target, interceptor, signatureMap));
        }
        return target;
      }
     //对象执行会被invoke方法拦截到 ,比如在执行CachingExecutor#query方法的时候会被invoke方法拦截
     @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
          //4.拿到执行到Method
          Set<Method> methods = signatureMap.get(method.getDeclaringClass());
          //5.判断当前方法是否包含在要拦截的方法中
          if (methods != null && methods.contains(method)) {
           //6.如果方法需要被拦截,调用拦截器的intercept方法,也就是PageHelper的intercept方法
           //把拦截的原生对象,方法对象,参数对象封装成Invocation
            return interceptor.intercept(new Invocation(target, method, args));
          }
          //7.如果方法没有被拦截,就直接调用
          return method.invoke(target, args);
        } catch (Exception e) {
          throw ExceptionUtil.unwrapThrowable(e);
        }
      }

有学习过JDK动态代理的同学是能够看得懂上面的代码,使用Proxy为被拦截的对象生成代理类,并解析拦截器上的@Intercepts的@Signature属性来得到要拦截的方法。

当对象方法被执行就会被Plugin#invoke拦截到,Plugin是一个InvocationHandler,其中的invoke会拦截器对象方法的调用。在invoke方法中判断了方法是否需要拦截,如果需要调用interceptor.intercept对原生对象做增强。如果方法不需要拦截就直接调用方法,不走拦截器。

接下来代码走到 com.github.pagehelper.PageHelper#intercept

    //Invocation 是对方法调用的封装,其中包括:target类 ;method ;arg参数
    public Object intercept(Invocation invocation) throws Throwable { 
            //自动获取方言
            if (autoRuntimeDialect) {
                SqlUtil sqlUtil = getSqlUtil(invocation);
                return sqlUtil.processPage(invocation);
            } else {
                 //自动获取dialect,如果没有setProperties或setSqlUtilConfig,也可以正常进行
                if (autoDialect) {
                    initSqlUtil(invocation);
                }
                //1.重点看这里,使用SqlUtil处理分页
                return sqlUtil.processPage(invocation);
            }
        }
      //处理分页
      public Object processPage(Invocation invocation) throws Throwable {
            try {
                //2.处理分页
                Object result = _processPage(invocation);
                return result;
            } finally {
                //处理分页数据,分页数据是保存在SqlUtil中的 ThreadLocal<Page>中
                clearLocalPage();
            }
        }
    
    
    
      private Object _processPage(Invocation invocation) throws Throwable {
           //方法参数
            final Object[] args = invocation.getArgs();
            Page page = null;
            //支持方法参数时,会先尝试获取Page
            if (supportMethodsArguments) {
                page = getPage(args);
            }
            //分页信息,里面包括了pageNum和pageSize.默认是0到Integer的最大值
            RowBounds rowBounds = (RowBounds) args[2];
            //支持方法参数时,如果page == null就说明没有分页条件,不需要分页查询
            //是否支持接口参数来传递分页参数,默认false。就是不支持我们自己传入分页参数
            if ((supportMethodsArguments && page == null)
                    //当不支持分页参数时,判断LocalPage和RowBounds判断是否需要分页
                    || (!supportMethodsArguments && SqlUtil.getLocalPage() == null && rowBounds == RowBounds.DEFAULT)) {
                return invocation.proceed();
            } else {
                //不支持分页参数时,page==null,这里需要获取
                if (!supportMethodsArguments && page == null) {
                	//【重点】我们自己没有传入分页对象,所以代码会走这里
                    page = getPage(args);
                }
                //【重点】 处理分页查询,该方法中会先查询count,再查询list
                return doProcessPage(invocation, page, args);
            }
        }

在 PageHelper#intercept 拦截方法中调用了 SqlUtil.processPage 来处理分页,该方法中会判断是否支持接口传入分页对象来分页,默认是supportMethodsArguments=false。代码会来到 com.github.pagehelper.SqlUtil#getPage 获取分页对象Page

    public Page getPage(Object[] args) {
    		//【重要】 这里是拿到分页信息,使用的一个ThreadLocal<Page>来存储的
            Page page = getLocalPage();
            ...省略...
            //分页合理化
            if (page.getReasonable() == null) {
                page.setReasonable(reasonable);
            }
            //当设置为true的时候,如果pagesize设置为0(或RowBounds的limit=0),就不执行分页,返回全部结果
            if (page.getPageSizeZero() == null) {
                page.setPageSizeZero(pageSizeZero);
            }
            return page;
        }

在SqlUtil#getPage中,如果我们自己没有在方法中传入分页对象,那么会从 com.github.pagehelper.SqlUtil#LOCAL_PAGE 获取分页对象,它是一个ThreadLocal<Page> , 那这个分页对象是在什么时候保存进去的呢?就是在我们最开始测试代码中执行Page page = PageHelper.startPage(1, 10, true); 的时候,就会把pageNum和pageSize封装成 Page对象存储到SqlUtil中的一个ThreadLocal<Page>中。

拿到Page分页对象后,代码来到com.github.pagehelper.SqlUtil#doProcessPage ,处理分页查询,该方法中会先查询count,再查询list

     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)) {
            	//[重要]1.处理MappedStatment,因为要生成一个count语句所以该方法中会
            	//新建count查询和分页查询的MappedStatement ,id会加上_COUNT,比如:
            	//cn.whale.StudentMapper.selectList会变成cn.whale.StudentMapper.selectList_COUNT
                processMappedStatement(ms);
            }
            //设置当前的parser,后面每次使用前都会set,ThreadLocal的值不会产生不良影响
            //【重要】 给 PageSqlSource设置parse。PageSqlSource表示从 XML 文件或注释读取的映射语句的内容
            //PageSqlSource中有一个 ThreadLocal<Parser> ,Parser是pagehelper提供的SQL解析器比如:MysqlParser
            ((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(null);
                    //拿到原本的SQL
                    BoundSql boundSql = ms.getBoundSql(args[1]);
                    //设置分页,信息,啊page总的开始也和每页条数设置到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;
        }

方法稍微比较复杂,总共做了这些事情

  1. processMappedStatement(ms); 新建count查询和分页查询的MappedStatement
  2. ((PageSqlSource)ms.getSqlSource()).setParser(parser); 给 PageSqlSource设置parse。PageSqlSource表示从 XML 文件或注释读取的映射语句的内容,PageSqlSource中有一个 ThreadLocal<Parser> ,Parser是Pagehelper提供的SQL解析器比如:MysqlParser
        public class MysqlParser extends AbstractParser {
        
        @Override
        public String getPageSql(String sql) {
            StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
            sqlBuilder.append(sql);
            //处理SQL的分页条件【重要重要重要】
            sqlBuilder.append(" limit ?,?");
            return sqlBuilder.toString();
        }
在MysqlParser中处理了分页SQL,那么对于查询Count Sql是在哪儿生成的呢?是在com.github.pagehelper.parser.`SqlParser#getSimpleCountSql`中
        public String getSimpleCountSql(final String sql) {
            isSupportedSql(sql);
            StringBuilder stringBuilder = new StringBuilder(sql.length() + 40);
            stringBuilder.append("select count(0) from (");
            stringBuilder.append(sql);
            stringBuilder.append(") tmp_count");
            return stringBuilder.toString();
        }
SqlParser存储在MysqlParser的父类AbstractParser中,他们的继承体系如下。  


![202401201945461092.png][]
  1. Object result = invocation.proceed(); 查询总条数,然后把总条数设置给结果对象 page。该方法会触发CachingExecutor#query的调用,方法中会调用 PageSqlSource#getBoundSql 去生成查询count的SQL,而底层最终会调用 SqlParser#getSimpleCountSql 解析查询count的SQL,然后执行count查询。
  2. 然后就是执行分页查询,一样是执行 invocation.proceed() ,在Page中有个countSignal信号来标记是该生成count的sql还是分页sql, 这一次依然会执行CachingExecutor#query,因为countSignal信号的改变,这次会触发MysqlParser#getPageSql 拼接分页的SQL
  3. 最后把条数和列表转到Page中返回,Page继承了ArrayList,对中总条数和数据列表进行了封装
        public class Page<E> extends ArrayList<E> {
        private static final long serialVersionUID = 1L;
        
        /**
         * 页码,从1开始
         */
        private int pageNum;
        /**
         * 页面大小
         */
        private int pageSize;
        /**
         * 起始行
         */
        private int startRow;
        /**
         * 末行
         */
        private int endRow;
        /**
         * 总数
         */
        private long total;
        /**
         * 总页数
         */
        private int pages;
        /**
         * 包含count查询
         */
        private boolean count;
        //列表就是自己
        public List<E> getResult() {
                return this;
            }

内容比较多,还是要稍微总结一下

  • 首先我们通过 PageHelper.startPage(1, 10, true) 来指定分页信息,该分页会被封装成Page对象,保存到SqlUtil#LOCAL_PAGE 一个ThreadLocal总,方便后面查询的时候使用
  • PageHelper提供了拦截器类 com.github.pagehelper.PageHelper 它是 Interceptor的子类 ,它通过 @Intercepts(@Signature 注解来指定只需要拦截器 Executor 的 query 方法。
  • 在PageHelper中的plugin方法中为原生类也就是 Executor 生成代理。代理类通过Plugin#wrap来做增强,Plugin实现了InvocationHandler(JDK动态代理) ,复写了invoke方法。当 Executor被执行的时候会被invoke方法拦截。
  • 在Plugin#invoke方法中会判断当前方法是否满足拦截条件(query方法),如果需要拦截就会执行 PageHelper#intercept方法去执行查询。
  • 在PageHelper#intercept方法中会从SqlUtil#LOCAL_PAGE中拿到Page分页对象,然后会根据MappedStatement生成count和list的MappedStatement
  • 接着会给MappedStatement的PageSqlSource设置MysqlParser解析器,在MysqlParser#getPageSql 中负责给SQL增加分页条件 limit。在MysqlParser的父类AbstractParser中包含了一个SqlParser ,它提供了getSimpleCountSql方法负责拼接查询count的sql。
  • 当PageHelper#intercept 分页拦截器方法被执行,会先执行查询count的SQL,底层调用了SqlParser#getSimpleCountSql 来生成count的SQL。然后会执行查询list的SQL,底层调用了MysqlParser#getPageSql来获取给list增加分页limit。
  • 最后把count和list封装到Page对象返回,Page本身继承了ArrayList。

Mybatis中的拦截器执行流程

上面我们详细分析了PageHelper的执行原理,现在我们来分析一下Mybatis是如何执行拦截器的。在第一章我们知道,mybatis-config.xml中配置的<plugin interceptor="com.github.pagehelper.PageHelper"> 在执行SqlSessionFactoryBuilder.builder的时候,会通过XMLConfigBuilder解析<Plugins/> ,然后添加到Configuration中的InterceptorChain拦截器链中 ,见:org.apache.ibatis.builder.xml.XMLConfigBuilder#pluginElement

      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).newInstance();
            //设置属性
            interceptorInstance.setProperties(properties);
            //添加到拦截器链
            configuration.addInterceptor(interceptorInstance);
          }
        }
      }

上面我们有介绍过,拦截器可以拦截Executor 执行器;ParameterHandler 参数处理器;ResultSetHandler 结果处理器;StatementHandler 语句执行器。在创建这四种对象的时候会把对象交给 interceptor.plugin 方法做处理。我们一个一个看,首先是在创建 Executor的时候Configuration#newExecutor

    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 {
          executor = new SimpleExecutor(this, transaction);
        }
        if (cacheEnabled) {
          executor = new CachingExecutor(executor);
        }
        //重点,把CachingExecutor 交给 interceptorChain去处理
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
      }

下面是pluginAll方法的代码org.apache.ibatis.plugin.InterceptorChain#pluginAll

    public class InterceptorChain {
    
      private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    
      public Object pluginAll(Object target) {
        for (Interceptor interceptor : interceptors) {
          //把要拦截的对象交给interceptor.plugin
          target = interceptor.plugin(target);
        }
        return target;
      }
      ...省略...

比如对于PageHelper来说plugin方法就是通过代理的方式对target做增强。该方法有四个地方被调用,分别对应了四个对象的创建

202401201945466373.png

下面是其他三个对象的创建方法,都在Configuration中

     public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        //交给拦截器去做处理
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
      }
    
      public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
          ResultHandler resultHandler, BoundSql boundSql) {
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        //交给拦截器去做处理
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
      }
    
      public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        //交给拦截器去做处理
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
      }

那么拦截器是在什么时候执行intercept方法的呢?因为在拦截器的interceptor.plugin方法中我们为要拦截的对象做了代理,参考PageHelper。以CachingExecutor 为例,当CachingExecutor#query被调用就会触发 Plugin#invoke ,因为它本身是一个InvocationHandler。在invoke方法中会真正触发interceptor.intercept 方法的调用。

定义拦截器做参数增强

我们这里来做一个案例,自定义拦截器给SQL传入一个条件参数把默认的参数替换掉,我的SQL如下

        <select id="selectByUsername" resultMap="resultMap">
            select * from student where username = #{username}
        </select>

我的测试代码如下

    List<Student> students = 
             sqlSession.selectList("cn.whale.mapper.StudentMapper.selectByUsername","ls");
    students.stream().forEach( System.out::println);

记住我上面的参数传入的是“ls” , 然后定义拦截器

    //拦截StatementHandler的prepare方法
    @Intercepts(value = { @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class , Integer.class})})
    public class MyInterceptor implements Interceptor {
    
        private Properties properties;
    
        @Override
        public Object intercept(Invocation invocation) throws Throwable {
            //获取statementHandler
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            //获取绑定sql
            BoundSql boundSql = statementHandler.getBoundSql();
    
            //获取参数
            List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
            //筛选动态list参数,并加密后重新赋值
            for (int i = 0; i < parameterMappings.size(); i++) {
                ParameterMapping parameterMapping = parameterMappings.get(i);
                if (parameterMapping.getMode() != ParameterMode.OUT) {
                    String propertyName = parameterMapping.getProperty();
                    System.out.println("参数名 = "+propertyName);
    
                    Object parameterObject = boundSql.getParameterObject();
                    System.out.println("原本的参数 = "+parameterObject);
                    //替换参数值
                    parameterObject = properties.getProperty("username");
                    System.out.println("替换后的参数 = "+parameterObject);
                    boundSql.setAdditionalParameter(propertyName,parameterObject );
                }
            }
            return invocation.proceed();
        }
    
        @Override
        public Object plugin(Object target) {
            //只是对ParameterHandler做增强
            if (target instanceof StatementHandler) {
                return Plugin.wrap(target, this);
            } else {
                return target;
            }
        }
    
        @Override
        public void setProperties(Properties properties) {
            //加载配置
            this.properties = properties;
        }
    }

解释一下这个拦截器

  1. 拦截器上拦截的是StatementHandler 的 prepare 方法,拦截该方法可以对参数做修改
  2. 先在 plugin方法中我做了判断,只对 StatementHandler 做拦截,然后使用 Plugin.wrap 对 StatementHandler生成代理。
  3. 在 setProperties 方法中我们把拦截器的配置属性保存给Properties成员变量
  4. 在 intercept 方法中,我们拿到 boundSql,把Properties中的username值覆盖给boundSql中的参数值,完成参数的替换

测试效果如下

202401201945473324.png

说在最后

其实我个人并不是很喜欢pagehelper这种分页插件,虽然可以自动生成分页sql来减少我们的工作量,但是它从某种程度上来说会影响性能。一是生成SQL需要时间,二是它自动生成的查询count的sql是根据查询list的sql来的,比如:查询list的sql关联了10张表,那么查询count的sql也会关联10张表。可能这并不是最优的SQL,所以我还是喜欢自己写分页查询自己控制SQL。

文章结束,喜欢就给我去点个五星好评吧。2021一路有你,2022我们继续加油!你的肯定是我最大的动力

博主在参加博客之星评比,点击链接 , https://bbs.csdn.net/topics/603957267 疯狂打Call!五星好评 感谢。

阅读全文