四.吃透Mybatis源码-通过分析Pagehelper源码来理解Mybatis的拦截器

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 高可用系列,价值2615元额度,1个月
简介: 面试官:用过pagehelper做分页把,你说一下pagehelper的分页实现原理。额…此时你只能说我不知道。如果你事先看了我接下来的这篇文章,相信你一定也把这个面试题答得很好。

前言

面试官:用过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);
}

测试结果如下
在这里插入图片描述
看到这个结果是不是觉得很有意思,我并没有编写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中,他们的继承体系如下。
    在这里插入图片描述

  3. Object result = invocation.proceed(); 查询总条数,然后把总条数设置给结果对象 page。该方法会触发CachingExecutor#query的调用,方法中会调用 PageSqlSource#getBoundSql 去生成查询count的SQL,而底层最终会调用 SqlParser#getSimpleCountSql 解析查询count的SQL,然后执行count查询。
  4. 然后就是执行分页查询,一样是执行 invocation.proceed() ,在Page中有个countSignal信号来标记是该生成count的sql还是分页sql, 这一次依然会执行CachingExecutor#query,因为countSignal信号的改变,这次会触发MysqlParser#getPageSql 拼接分页的SQL

  5. 最后把条数和列表转到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做增强。该方法有四个地方被调用,分别对应了四个对象的创建
在这里插入图片描述

下面是其他三个对象的创建方法,都在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中的参数值,完成参数的替换

测试效果如下
在这里插入图片描述

说在最后

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

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

相关文章
|
2月前
|
SQL XML Java
mybatis-源码深入分析(一)
mybatis-源码深入分析(一)
|
1月前
|
SQL JSON Java
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
这篇文章介绍了如何在Spring Boot项目中整合MyBatis和PageHelper进行分页操作,并且集成Swagger2来生成API文档,同时定义了统一的数据返回格式和请求模块。
54 1
mybatis使用三:springboot整合mybatis,使用PageHelper 进行分页操作,并整合swagger2。使用正规的开发模式:定义统一的数据返回格式和请求模块
|
2月前
|
SQL Java 数据库连接
解决mybatis-plus 拦截器不生效--分页插件不生效
本文介绍了在使用 Mybatis-Plus 进行分页查询时遇到的问题及解决方法。依赖包包括 `mybatis-plus-boot-starter`、`mybatis-plus-extension` 等,并给出了正确的分页配置和代码示例。当分页功能失效时,需将 Mybatis-Plus 版本改为 3.5.5 并正确配置拦截器。
597 6
解决mybatis-plus 拦截器不生效--分页插件不生效
|
1月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
43 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
1月前
|
Java 数据库连接 mybatis
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
该文档详细介绍了如何在Springboot Web项目中整合Mybatis,包括添加依赖、使用`@MapperScan`注解配置包扫描路径等步骤。若未使用`@MapperScan`,系统会自动扫描加了`@Mapper`注解的接口;若使用了`@MapperScan`,则按指定路径扫描。文档还深入分析了相关源码,解释了不同情况下的扫描逻辑与优先级,帮助理解Mybatis在Springboot项目中的自动配置机制。
127 0
Springboot整合Mybatis,MybatisPlus源码分析,自动装配实现包扫描源码
|
3月前
|
XML Java 数据库连接
mybatis源码研究、搭建mybatis源码运行的环境
这篇文章详细介绍了如何搭建MyBatis源码运行的环境,包括创建Maven项目、导入源码、添加代码、Debug运行研究源码,并提供了解决常见问题的方法和链接到搭建好的环境。
mybatis源码研究、搭建mybatis源码运行的环境
|
3月前
|
Web App开发 前端开发 关系型数据库
基于SpringBoot+Vue+Redis+Mybatis的商城购物系统 【系统实现+系统源码+答辩PPT】
这篇文章介绍了一个基于SpringBoot+Vue+Redis+Mybatis技术栈开发的商城购物系统,包括系统功能、页面展示、前后端项目结构和核心代码,以及如何获取系统源码和答辩PPT的方法。
|
3月前
|
供应链 前端开发 Java
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
该博客文章介绍了一个使用Mybatis、Layui、MVC和JSP技术栈开发的服装库存管理系统,包括注册登录、权限管理、用户和货号管理、库存管理等功能,并提供了源码下载链接。
服装库存管理系统 Mybatis+Layui+MVC+JSP【完整功能介绍+实现详情+源码】
|
3月前
|
缓存 Java 数据库连接
我要手撕mybatis源码
该文章深入分析了MyBatis框架的初始化和数据读写阶段的源码,详细阐述了MyBatis如何通过配置文件解析、建立数据库连接、映射接口绑定、动态代理、查询缓存和结果集处理等步骤实现ORM功能,以及与传统JDBC编程相比的优势。
我要手撕mybatis源码
|
5月前
|
SQL 人工智能 Java
mybatis-plus配置sql拦截器实现完整sql打印
_shigen_ 博主分享了如何在MyBatis-Plus中打印完整SQL,包括更新和查询操作。默认日志打印的SQL用?代替参数,但通过自定义`SqlInterceptor`可以显示详细信息。代码示例展示了拦截器如何替换?以显示实际参数,并计算执行时间。配置中添加拦截器以启用此功能。文章提到了分页查询时的限制,以及对AI在编程辅助方面的思考。
669 5
mybatis-plus配置sql拦截器实现完整sql打印