MybatisPlugin实现及原理

Plugins

简单理解为拦截器,既然是拦截器说白了一般都是动态代理来实现对目标方法的拦截,在前后做一些操作。
在mybatis将这种东西,称之为plugin,配置在mybatis-config.xml配置文件中,通过 标签配置。在mybatis中,可以被拦截的目标主要是:
1. StatementHandler;
2. ParameterHandler;
3. ResultSetHandler;
4. Executor;

这里实现 ByPage后缀的查询,自动执行分页操作;不需要显性的limit SQL操作;

interceptor
通过实现Interceptor接口,来自定义plugin

public interface Interceptor {
  // 拦截逻辑,参数是代理类
  Object intercept(Invocation invocation) throws Throwable;
  // 加载插件,一般使用Plugin.wrap(target, this);加载当前插件
  Object plugin(Object target);
  // 初始化属性
  void setProperties(Properties properties);
}

可以通过implements Interceptor来自定义plugin,但是仅仅这样是不行的,额外需要通过@Inteceptors和@Signature源注解来指定拦截器需要拦截的目标(类、方法、参数);

@Intercepts(value = {@Signature(
        type = StatementHandler.class,
        method = "prepare",
        args = {
                Connection.class,
                Integer.class
        }
)})
public class ThreadLocalPagePlugin implements Interceptor {

    /**
     * 这个方法是实际的拦截逻辑,我们的目的是在这里来实现分页,需要达到什么程度的使用。
     * 假设从ThreadLocal获取分页信息,来进行分页操作;
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        // 获取目标对象,注意StatementHandler中的属性都是protected
        // 不能直接访问,因此需要通过其他的方式来获取,就是MetaObject
        // 其基本实现是BaseStatementHandler其中最重要的属性是MappedStatment
        // 包含了SQL相关信息

        // 实际返回的是RoutingStatementHandler
        StatementHandler handler = (StatementHandler) invocation.getTarget();

        // 获取指定对象的元信息
        MetaObject metaObject = MetaObject.forObject(
                handler,
                SystemMetaObject.DEFAULT_OBJECT_FACTORY,
                SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
                new DefaultReflectorFactory()
        );

        // 然后就可以通过MetaObject获取对象的属性
        // 获取RoutingStatementHandler->PrepareStatementHandler->BaseStatementHandler中的mappedStatement
        // mappedStatement 包含了Sql的信息
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

        // 获取statement id
        String statementId = mappedStatement.getId();

        // 会拦截每个属性
        if (statementId.endsWith("ByPage")){
            // ByPage 表示的是分页查询
            BoundSql boundSql = handler.getBoundSql();

            String sql = boundSql.getSql();

            // 获取当前线程分页信息
            Page<?> pager =  ThreadLocalUtil.threadLocal.get();


            String countSql = "SELECT COUNT(*) " + sql.substring(sql.indexOf("from"));

            Connection conn = (Connection) invocation.getArgs()[0];
            PreparedStatement ps = conn.prepareStatement(countSql);

            // 获取参数处理器来处理参数
            ParameterHandler ph = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            ph.setParameters(ps);

            // 执行查询
            ResultSet rs = ps.executeQuery();
            if(rs.next()){
                pager.setTotalCount(rs.getInt(1));
            }

            String pageSql = sql + " LIMIT " + pager.getStartPos() + ", " + pager.getPageSize();

            metaObject.setValue("delegate.boundSql.sql", pageSql);
        }


        return invocation.proceed();
    }

    // 指定需要拦截的对象
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 初始化属性
    @Override
    public void setProperties(Properties properties) {

    }
}

增加配置:

<plugins>
        <plugin interceptor="com.test.plugin.ThreadLocalPagePlugin" >
        </plugin>
    </plugins>

测试代码:

    Page<User> pager = new Page<User>(1, 2);
    ThreadLocalUtil.threadLocal.set(pager);

    List<User> list = getSqlSession().getMapper(UserMapper.class).selectUserByPage();
    Page page = ThreadLocalUtil.threadLocal.get();
    System.out.println(list.size()+page.toString());

打印出分页相关信息。这里没有将查询结果设置到 page 对象中。

----------- intercept query end… ---------testPro
2Pager{startPos=0, curPage=1, pageSize=2, datas=null, totalPage=3, totalCount=5}

我们再增加一个处理查询结果的Plugin代码如下,该代码拦截结果处理的方法,并将处理后的结果设置到我们的 Page对象中

@Intercepts(value = {@Signature(
        type = ResultSetHandler.class,
        method = "handleResultSets",
        args = {
                Statement.class
        }
)})
public class ThreadLocalPagePluginAfter implements Interceptor {

    /**
     * 这个方法是实际的拦截逻辑,我们的目的是在这里来实现分页,需要达到什么程度的使用。
     * 假设从ThreadLocal获取分页信息,来进行分页操作;
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object rr = invocation.proceed();
        Page page =ThreadLocalUtil.threadLocal.get();
        page.setDatas((List) rr);
        return rr;
    }

    // 指定需要拦截的对象
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    // 初始化属性
    @Override
    public void setProperties(Properties properties) {

    }
}

配置plugin

执行测试代码:

 public static void main(String[] args) throws Exception {
        SqlSession sesion =  getSqlSession();
        UserMapper mapper= sesion.getMapper(UserMapper.class);
        Page<User> page = new Page<>(1,2);
        ThreadLocalUtil.threadLocal.set(page);
        mapper.getUsersByPage();
        System.out.println(page);
    }

可以看到 我们拿到了分页的完整信息

11:22:20.796 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==>  Preparing: SELECT COUNT(*) from user 
11:22:20.822 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Parameters: 
11:22:20.857 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==>  Preparing: select * from user LIMIT 0, 2 
11:22:20.858 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - ==> Parameters: 
11:22:20.863 [main] DEBUG c.t.m.m.UserMapper.getUsersByPage - <==      Total: 2
Pager{startPos=0, curPage=1, pageSize=2, datas=[com.test.mybatis.beans.User@5b8dfcc1, com.test.mybatis.beans.User@2f9f7dcf], totalPage=7502, totalCount=15003}

Plugin实现原理
1.我们我们在配置文件中声明定义了plugin 如:

 <plugin interceptor="com.test.plugin.ThreadLocalPagePluginAfter" > </plugin>

2.当Mybaitis 启动加载配置信息时将配置的plugin加载到 Configuration 属性 中
在这里插入图片描述
3、随后在创建 一下四个接口的实现类之后会调用 InterceptorChain 的 pluginAll方法,这里以 Exexutor 为例如截图:
1. StatementHandler;
2. ParameterHandler;
3. ResultSetHandler;
4. Executor;
在这里插入图片描述

4、pluginAll方法 调用所有plugin 对象的 plugin方法 如下图:
在这里插入图片描述
以我们实现的plugin 为例 可以看到 plugin 方法只有 一行 调用 Plugin.wrap参数target是我们们的 Executor实例或代理对象,

return Plugin.wrap(target, this);

wrap 方法中判断我们定义的plugin 是否对目标对象( Executor实例或代理对象)的接口做了拦截,如果做了拦截 就用当前我们定义的Interceptor 构造一个plugin 对 目标对象做代理,否则不做代理。
在这里插入图片描述
经过上述过程如果 我们定义的 多个plugin 对某个接口做了拦截,就会依次生成对象的代理。我们这里的两个plugin 分别 对 Executor接口和resultSetHandler接口做了代理,所以创建 executor对象后就会有一个plugin 对 这个对象做代理。resultSetHandler同理。所以如果定义了 plugin 那么我们得到的 对象其实是代理对象。

  • 4
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

catch that elf

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值