mybatis插件原理

  MyBatis 通过提供插件机制,让我们可以根据自己的需要去增强MyBatis 的功能。MyBatis 的插件可以在不修改原来的代码的情况下,通过拦截的方式,改变四大核心对象的行为,比如处理参数,处理SQL,处理结果。

  不修改对象的代码,怎么对对象的行为进行修改,比如说在原来的方法前面做一点事情,在原来的方法后面做一点事情?很容易能想到用代理模式,这个也确实是MyBatis 插件的原理。

  我们可以定义很多的插件,那么这种所有的插件会形成一个链路,插件是层层拦截的,我们又需要用到另一种设计模式——责任链模式。

  mybatis内部对于插件的处理确实使用的代理模式,既然是代理模式,应该了解MyBatis 允许哪些对象的哪些方法允许被拦截,并不是每一个运行的节点都是可以被修改的。

  Executor 会拦截到CachingExcecutor 或者BaseExecutor。因为创建Executor 时是先创建CachingExcecutor,再包装拦截。可以通过mybatis的分页插件来看看整个插件从包装拦截器链到执行拦截器链的过程。

@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 MyPageInterceptor implements Interceptor {


    // 用于覆盖被拦截对象的原有方法(在调用代理对象Plugin 的invoke()方法时被调用)
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        System.out.println("将逻辑分页改为物理分页");
        Object[] args = invocation.getArgs();
        MappedStatement ms = (MappedStatement) args[0]; // MappedStatement
        BoundSql boundSql = ms.getBoundSql(args[1]); // Object parameter
        RowBounds rb = (RowBounds) args[2]; // RowBounds
        // RowBounds为空,无需分页
        if (rb == RowBounds.DEFAULT) {
            return invocation.proceed();
        }// 在SQL后加上limit语句
        String sql = boundSql.getSql();
        String limit = String.format("LIMIT %d,%d", rb.getOffset(), rb.getLimit());
        sql = sql + " " + limit;

        // 自定义sqlSource
        SqlSource sqlSource = new StaticSqlSource(ms.getConfiguration(), sql, boundSql.getParameterMappings());

        // 修改原来的sqlSource
        Field field = MappedStatement.class.getDeclaredField("sqlSource");
        field.setAccessible(true);
        field.set(ms, sqlSource);

        // 执行被拦截方法
        return invocation.proceed();
    }

    // target 是被拦截对象,这个方法的作用是给被拦截对象生成一个代理对象,并返回它
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }


    // 设置参数
    @Override
    public void setProperties(Properties properties) {
    }
}

  插件注册,在mybatis-config.xml 中注册插件:

<plugins>
  <plugin interceptor="com.github.pagehelper.PageInterceptor">
    <property name="offsetAsPageNum" value="true"/>
      ……后面全部省略……
  </plugin>
</plugins>

  拦截签名跟参数的顺序有严格要求,如果按照顺序找不到对应方法会抛出异常:

    org.apache.ibatis.exceptions.PersistenceException:
            ### Error opening session.  Cause: org.apache.ibatis.plugin.PluginException: Could not find method on interface org.apache.ibatis.executor.Executor named query

  MyBatis 启动时扫描<plugins> 标签, 注册到Configuration 对象的 InterceptorChain 中。property 里面的参数,会调用setProperties()方法处理。

代理和拦截是怎么实现的?

  上面提到的可以被代理的四大对象都是什么时候被代理的呢?Executor 是openSession() 的时候创建的; StatementHandler 是SimpleExecutor.doQuery()创建的;里面包含了处理参数的ParameterHandler 和处理结果集的ResultSetHandler 的创建,创建之后即调用InterceptorChain.pluginAll(),返回层层代理后的对象。代理是由Plugin 类创建。在我们重写的 plugin() 方法里面可以直接调用returnPlugin.wrap(target, this);返回代理对象。

  因为代理类是Plugin,所以最后调用的是Plugin 的invoke()方法。它先调用了定义的拦截器的intercept()方法。可以通过invocation.proceed()调用到被代理对象被拦截的方法。

  调用流程时序图:

PageHelper 原理:

  先来看一下分页插件的简单用法:

PageHelper.startPage(1, 3);
List<Blog> blogs = blogMapper.selectBlogById2(blog);
PageInfo page = new PageInfo(blogs, 3);

  插件机制上面已经介绍过了,自然的会想到其所涉及的核心类 :PageInterceptor。拦截的是Executor 的两个query()方法,要实现分页插件的功能,肯定是要对我们写的sql进行改写,那么一定是在 intercept 方法中进行操作的,会发现这么一行代码:

 String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);

  调用到 AbstractHelperDialect 中的  getPageSql 方法:

public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        // 获取sql
        String sql = boundSql.getSql();
        //获取分页参数对象
        Page page = this.getLocalPage();
        return this.getPageSql(sql, page, pageKey);
    }

  这里可以看到会去调用 this.getLocalPage():

public <T> Page<T> getLocalPage() {
  return PageHelper.getLocalPage();
}
//线程独享
protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();
public static <T> Page<T> getLocalPage() {
  return (Page)LOCAL_PAGE.get();
}

  可以发现这里是调用的是PageHelper的一个本地线程变量中的一个 Page对象,从其中获取所设置的  PageSize 与 PageNum,那么是怎么设置值的呢?

PageHelper.startPage(1, 3);

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, Boolean reasonable, Boolean pageSizeZero) {
        Page<E> page = new Page(pageNum, pageSize, count);
        page.setReasonable(reasonable);
        page.setPageSizeZero(pageSizeZero);
        Page<E> oldPage = getLocalPage();
        if (oldPage != null && oldPage.isOrderByOnly()) {
            page.setOrderBy(oldPage.getOrderBy());
     }
        //设置页数,行数信息
        setLocalPage(page);
        return page;
}

protected static void setLocalPage(Page page) {
        //设置值
        LOCAL_PAGE.set(page);
}

  在调用 PageHelper.startPage(1, 3); 的时候,系统会调用 LOCAL_PAGE.set(page) 进行设置,从而在分页插件中可以获取到这个本地变量对象中的参数进行 SQL 的改写,由于改写有很多实现,这里用的Mysql的实现:

  在这里会发现分页插件改写SQL的核心代码:

public String getPageSql(String sql, Page page, CacheKey pageKey) {
        StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
        sqlBuilder.append(sql);
        if (page.getStartRow() == 0) {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getPageSize());
        } else {
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow());
        }

        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
}

  PageHelper 就是这么一步一步的改写了我们的SQL 从而达到一个分页的效果。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值