Mybatis[4] - 配置文件 - plugins

博由

    接着配置文件的问题,在描述完了如何使用typeHandler之后,接下来本文主要讲述如何使用plugins相关内容,以及从源码角度分析其基本原理。

plugins是什么?

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

案例(实现分页)

案例: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);
}

自定义Interceptor

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

@Intercepts(value={
    @Signature(
        type = Executor.class, // 只能是: StatementHandler | ParameterHandler | ResultSetHandler | Executor 类或者子类
        method = "query", // 表示:拦截Executor的query方法
        args = {  // query 有很多的重载方法,需要通过方法签名来指定具体拦截的是那个方法
                MappedStatement.class,
                Object.class,
                RowBounds.class,
                ResultHandler.class
        }
        /**
         * type:标记需要拦截的类
         * method: 标记是拦截类的那个方法
         * args: 标记拦截类方法的具体那个引用(尤其是重载时)
         */
    )})
public class LogPlugin implements Interceptor{

    /**
     * 具体拦截的实现逻辑
     * @param invocation
     * @return
     * @throws Throwable
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {

        System.out.println("----------- intercept query start.... ---------");

        // 调用方法,实际上就是拦截的方法
        Object result = invocation.proceed();

        System.out.println("----------- intercept query end.... ---------");

        return result;
    }

    // 插入插件
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this); // 调用Plugin工具类,创建当前的类的代理类
    }

    // 设置插件属性
    @Override
    public void setProperties(Properties properties) {

    }
}

配置插件

<plugins>
<plugin interceptor="com.plugins.interceptors.LogPlugin" />    
</plugins>

完成上述两个步骤就可以使用插件了,接下来我们具体到分页案例来进行。

分页实践

分页插件
分析:
1. 分页封装对象:Pager;
2. ThreadLocal存放每个线程的分页对象;
3. 分页操作是两个步骤:
   3.1 分页查询,需要进行拼装limit sql;
   3.2 总数量查询,需要进行数量查询内置操作;
4. 可以拦截StatementHandler的prepare方法,对SQL信息进行修改并重新组装,然后进行查询相关操作。
分页封装类
package com.plugins.entity;

import java.io.Serializable;
import java.util.List;

/**
 * 分页类
 * Created by wangzhiping on 17/3/10.
 */
public class Pager<T> implements Serializable{

    /**
     * 开始位置
     */
    private int startPos;

    /**
     * 当前页码
     */
    private int curPage;

    /**
     * 每页大小
     */
    private int pageSize;

    /**
     * 每一页的数据
     */
    private List<T> datas;

    /**
     * 总页数
     */
    private int totalPage;

    /**
     * 总数量
     */
    private int totalCount;

    public Pager(int curPage, int pageSize) {
        this.curPage = curPage;
        this.pageSize = pageSize;

        this.startPos = (this.curPage - 1) * this.pageSize;
    }

    public int getStartPos() {
        return startPos;
    }

    public void setStartPos(int startPos) {
        this.startPos = startPos;
    }

    public int getCurPage() {
        return curPage;
    }

    public void setCurPage(int curPage) {
        this.curPage = curPage;
    }

    public int getPageSize() {
        return pageSize;
    }

    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }

    public List<T> getDatas() {
        return datas;
    }

    public void setDatas(List<T> datas) {
        this.datas = datas;
    }

    public int getTotalPage() {
        return totalPage;
    }

    public void setTotalPage(int totalPage) {
        this.totalPage = totalPage;
    }

    public int getTotalCount() {
        return totalCount;
    }

    public void setTotalCount(int totalCount) {
        this.totalCount = totalCount;
        this.totalPage = (this.totalCount - 1) / this.pageSize + 1;
    }

    @Override
    public String toString() {
        return "Pager{" +
                "startPos=" + startPos +
                ", curPage=" + curPage +
                ", pageSize=" + pageSize +
                ", datas=" + datas +
                ", totalPage=" + totalPage +
                ", totalCount=" + totalCount +
                '}';
    }
}
分页插件
拦截StatementHandler-prepare方法
@Intercepts(value={
@Sigunature(
    type = StatementHandler.class, // 拦截目标类
    method = "prepare", // 目标类的目标方法
    args = { // prepare参数列表的参数类型
        Connection.class, 
        Integer.class
    }
)
})
prepare code <源码>
Statement prepare(
    Connection connection, 
    Integer transactionTimeout
)throws SQLException;
实际上述的拦截就是拦截的StatementHandler -> prepare method
拦截实现分析
处理链路
|--- StatementHandler
|--- --- RoutingStatementHandler
|--- --- BaseStatementHandler
|--- --- --- PreparedStatementHandler
路由处理(RoutingStatementHandler)
switch (ms.getStatementType()) {
      case STATEMENT:
        delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      case PREPARED:
        delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); // 路由到Prepared
        break;
      case CALLABLE:
        delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql);
        break;
      default:
        throw new ExecutorException("Unknown statement type: " + ms.getStatementType());
    }
实际处理(PreparedStatementHandler|BaseStatementHandler )

BaseStatementHandler.java

@Override
  public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException {
    ErrorContext.instance().sql(boundSql.getSql());
    Statement statement = null;
    try {
      // 实例化Statement
      statement = instantiateStatement(connection);
      // 设置操作超时时间
      setStatementTimeout(statement, transactionTimeout);
      // 设置获取大小
      setFetchSize(statement);
      return statement;
    } catch (SQLException e) {
      closeStatement(statement);
      throw e;
    } catch (Exception e) {
      closeStatement(statement);
      throw new ExecutorException("Error preparing statement.  Cause: " + e, e);
    }
  }

PreparedStatementHandler.java

@Override
  protected Statement instantiateStatement(Connection connection) throws SQLException {
    //调用Connection.prepareStatement
    String sql = boundSql.getSql();
    if (mappedStatement.getKeyGenerator() instanceof Jdbc3KeyGenerator) {
      String[] keyColumnNames = mappedStatement.getKeyColumns();
      if (keyColumnNames == null) {
        return connection.prepareStatement(sql, PreparedStatement.RETURN_GENERATED_KEYS);
      } else {
        return connection.prepareStatement(sql, keyColumnNames);
      }
    } else if (mappedStatement.getResultSetType() != null) {
      return connection.prepareStatement(sql, mappedStatement.getResultSetType().getValue(), ResultSet.CONCUR_READ_ONLY);
    } else {
      return connection.prepareStatement(sql);
    }
  }
  // SQL参数化处理
  @Override
  public void parameterize(Statement statement) throws SQLException {
    //调用ParameterHandler.setParameters
    parameterHandler.setParameters((PreparedStatement) statement);
  }
如何操作达到分页操作

获取SQL信息(BoundSql)

| --- RoutingStatementHandler(delegate)
| --- --- delegate = PreparedStatementHandler
| --- --- PreparedStatementHandler -> BaseStatementHandler

包含了BoundSql、MappedStatement等

protected final Configuration configuration;
protected final ObjectFactory objectFactory;
protected final TypeHandlerRegistry typeHandlerRegistry;
protected final ResultSetHandler resultSetHandler;
protected final ParameterHandler parameterHandler;
protected final Executor executor;
protected final MappedStatement mappedStatement;
protected final RowBounds rowBounds;
protected BoundSql boundSql;

获取StatementId(MappedStatement)

我们需要获取BaseStatementHandler中的mappedStatement属性,但是这些属性都是protected,没哟继承结构无法直接访问,在mybatis可以通过metaObject来访问。
// 实际上就是获取某个目标对象的属性操作
MetaObject metaObject = MetaObject.forObject(
handler,
SystemMetaObject.DEFAULT_OBJECT_FACTORY,             SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,
new DefaultReflectorFactory()
); 

// metaObject.getValue获取属性(ognl表达式获取)
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");

执行SQL的数量查询

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

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

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

注入limit sql

// 修改SQL
String pageSql = sql + " LIMIT " + pager.getStartPos() + ", " + pager.getPageSize();
// 重新设定BoundSql的SQL属性
metaObject.setValue("delegate.boundSql.sql", pageSql);
代码实现
@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();

            // 获取当前线程分页信息
            Pager<?> 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.plugins.interceptors.ThreadLocalPagePlugin" />
    </plugins>
Mapper.xml
<select id="findByPage" resultType="User">
        SELECT *
        FROM user
</select>
单元测试
@Test
public void testQueryPageByPlugin() {

        SqlSession session = instance.getSession();

        Pager<User> pager = new Pager<User>(1, 10);
        ThreadLocalUtil.threadLocal.set(pager);

        List<User> users = session.selectList(User.class.getName() + ".findByPage");
        pager.setDatas(users);

        System.out.print(pager);

    }

总结

通过实现一个简单分页的代码而言,要想真正了解plugin的使用,需要真正了解StatementHandler|ParameterHandler|ResultSetHandler|Executor
这四个类的执行链路和原理,只有了解到这些你才能知道,在哪里拦截,拦截什么,怎么拦截。

项目地址

branch v1.4: https://github.com/wzpthq/csdn_mybatis.git

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值