Mybatis插件机制及实现分页插件

一、Mybatis插件机制简介

Mybatis在四大组件(Executor,StatementHandler,ParameterHandler,ResultSetHandler)处都提供了插件机制。Mybatis通过对四大核心对象进行拦截,并增强对象的功能。本质上是通过动态代理来实现的,也就是说实际上,四大组件的对象最后都是代理对象。

1. Mybatis允许拦截的方法如下

  • 执行器Executor的update, query, commit, rollback等方法

  • SQL语法构建器StatementHandler的prepare, parameterize, batch, update, query等方法

  • 参数处理器ParameterHandler的getParameterObject, setParameterObject等方法

  • 结果处理器ResultSetHandler的handleResultSets, handlerOutputPameters等方法

2. Mybatis插件原理

在四大组件创建时,每个对象创建之后不是直接返回,而是通过调用interceptorChain.pluginAll(handler)之后再返回。

Mybatis插件机制及实现分页插件

 

public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
  ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
  parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
  return parameterHandler;
}

interceptorChain保存了所有的拦截器,这是在Mybatis初始化的时候创建的。

pluginAll方法就是遍历interceptorChain中的每一个inteceptor,执行它的plugin方法: 

  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }

 

二、实现分页插件

如果我们要拦截四大组件的某个方法,可以通过@Intercepts, @Signature注解来实现,例如拦截Executor对象的query方法可以这么实现:

@Intercepts({
    @Signature(
        type=Executor.class,
        method="query",
        args={MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
    )
})
public class InterceptExecutorQuery implements Interceptor{
    //......
}

然后在sqlMapConfig.xml中配置这个插件。

我们来实现一个Mybatis的分页插件,这个插件的用法是这样的:

    public Page<GJAJEntity> findPage(GJAJEntity leaveEntity, int pageNum) {
        PageHelper.startPage(pageNum, Constant.pageSize);
        caseDao.queryList(leaveEntity);
        return PageHelper.endPage();
    }
  • 调用PageHelper.startPage(pageNum, pageSize)方法,开始分页
  • 执行查询操作
  • 调用PageHelper.endPage()方法,结束分页,返回分页对象

 

1. 首先就是PageHelper这个类要实现Interceptor接口,并且标记拦截StatementHandler的prepare方法以及ResultSetHandler的handleResultSets方法

@Intercepts(
        {
                @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class}),
                @Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})
        }
)
public class PageHelper implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        return null;
    }

    @Override
    public Object plugin(Object target) {
        return null;
    }

    @Override
    public void setProperties(Properties properties) {

    }
}

2. 实现startPage和endPage方法

    /** 
     * 开始分页 
     * @param pageNum 
     * @param pageSize 
     */  
    public static void startPage(int pageNum, int pageSize) {  
        localPage.set(new Page(pageNum, pageSize));  
    }  
  
    /** 
     * 结束分页并返回结果,该方法必须被调用,否则localPage会一直保存下去,直到下一次startPage 
     * @return 
     */  
    public static Page endPage() {  
        Page page = localPage.get();  
        localPage.remove();  
        return page;  
    }  

这个localPage对象其实就是一个ThreadLocal对象,里面存储的是一个Page对象,Page对象里面保存了查询结果:

public static final ThreadLocal<Page> localPage = new ThreadLocal<Page>();  

3. 实现Interceptor接口的plugin方法

    /** 
     * 只拦截这两种类型的 
     * <br>StatementHandler 
     * <br>ResultSetHandler 
     * @param target 
     * @return 
     */  
    @Override  
    public Object plugin(Object target) {  
        if (target instanceof StatementHandler || target instanceof ResultSetHandler) {
            return Plugin.wrap(target, this);
        } else {  
            return target;  
        }  
    }  

4. 实现Interceptor接口的intercept方法

    @Override  
    public Object intercept(Invocation invocation) throws Throwable {
        if (localPage.get() == null) {  
            return invocation.proceed();  
        }  
        if (invocation.getTarget() instanceof StatementHandler) {
            StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
            MetaObject metaStatementHandler = SystemMetaObject.forObject(statementHandler);
            // 分离代理对象链(由于目标类可能被多个拦截器拦截,从而形成多次代理,通过下面的两次循环  
            // 可以分离出最原始的的目标类)  
            while (metaStatementHandler.hasGetter("h")) {  
                Object object = metaStatementHandler.getValue("h");  
                metaStatementHandler = SystemMetaObject.forObject(object);
            }  
            // 分离最后一个代理对象的目标类  
            while (metaStatementHandler.hasGetter("target")) {  
                Object object = metaStatementHandler.getValue("target");  
                metaStatementHandler = SystemMetaObject.forObject(object);
            }  
            MappedStatement mappedStatement = (MappedStatement) metaStatementHandler.getValue("delegate.mappedStatement");
            //分页信息if (localPage.get() != null) {  
            Page page = localPage.get();
            //Mybatis中最终执行的sql封装成了一个BoundSql对象,先获取到这个BoundSql对象  
            BoundSql boundSql = (BoundSql) metaStatementHandler.getValue("delegate.boundSql");
            // 然后获取到sql语句。分页参数作为参数对象parameterObject的一个属性  
            String sql = boundSql.getSql();  
            // 重写sql  
            String pageSql = buildPageSqlMysql(sql, page);
            //重写分页sql ,再把这个sql语句赋值给BoundSql对象中的sql属性
            metaStatementHandler.setValue("delegate.boundSql.sql", pageSql);  
            Connection connection = (Connection) invocation.getArgs()[0];  
            // 重设分页参数里的总页数等  
            setPageParameter(sql, connection, mappedStatement, boundSql, page);  
            // 将执行权交给下一个拦截器  
            return invocation.proceed();  
        } else if (invocation.getTarget() instanceof ResultSetHandler) {
            //拦截ResultSetHandler的handleResultSets方法,直接返回给Page
            Object result = invocation.proceed();  
            Page page = localPage.get();  
            page.setResult((List) result);  
            return result;  
        }  
        return null;  
    }  
  

/**
     * 修改原SQL为分页SQL mysql
     * @param sql
     * @param page
     * @return
     */
    private String buildPageSqlMysql(String sql, Page page) {
        StringBuilder pageSql = new StringBuilder(200);
        pageSql.append(sql);
        pageSql.append(" LIMIT ").append(page.getStartRow()).append(",").append(page.getEndRow()-page.getStartRow());
        return pageSql.toString();
    }
  
    /** 
     * 获取总记录数 
     * @param sql 
     * @param connection 
     * @param mappedStatement 
     * @param boundSql 
     * @param page 
     */  
    private void setPageParameter(String sql, Connection connection, MappedStatement mappedStatement,
                                  BoundSql boundSql, Page page) {
        // 记录总记录数  
        String countSql = "select count(0) from (" + sql + ")as total";
        PreparedStatement countStmt = null;  
        ResultSet rs = null;  
        try {  
            countStmt = connection.prepareStatement(countSql);  
            BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql,
                    boundSql.getParameterMappings(), boundSql.getParameterObject());  
            setParameters(countStmt, mappedStatement, countBS, boundSql.getParameterObject());  
            rs = countStmt.executeQuery();  
            int totalCount = 0;  
            if (rs.next()) {  
                totalCount = rs.getInt(1);  
            }  
            page.setTotal(totalCount);
            int totalPage = totalCount / page.getPageSize() + ((totalCount % page.getPageSize() == 0) ? 0 : 1);  
            page.setPages(totalPage);  
        } catch (SQLException e) {  
            logger.error("Ignore this exception", e);  
        } finally {  
            try {  
            	if(rs != null){
            		rs.close();  
            	}
            } catch (SQLException e) {  
                logger.error("Ignore this exception", e);  
            }  
            try {  
                countStmt.close();  
            } catch (SQLException e) {  
                logger.error("Ignore this exception", e);  
            }  
        }  
    }  
  
    /** 
     * 代入参数值 
     * @param ps 
     * @param mappedStatement 
     * @param boundSql 
     * @param parameterObject 
     * @throws SQLException 
     */  
    private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql,
                               Object parameterObject) throws SQLException {  
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler.setParameters(ps);  
    } 

5. 使用PageHelper

    public Page<GJAJEntity> findPage(GJAJEntity leaveEntity, int pageNum) {
        PageHelper.startPage(pageNum, Constant.pageSize);
        caseDao.queryList(leaveEntity);
        return PageHelper.endPage();
    }

可以看到只要执行startPage和endPage两个方法,PageHelper本身不需要和数据发生耦合。

 

上一篇:为渠道转型而生的华为“云管理网络”


下一篇:袋鼠云旗下新公司云掣科技启航,深耕云MSP业务助推企业数字化转型