1. 程式人生 > >springmvc整合mybatis分頁程式碼示例

springmvc整合mybatis分頁程式碼示例

程式實現思路:
用mybatis的攔截器攔截查詢請求,根據前臺傳遞過來的查詢條數,當前頁數重新組裝資料庫分頁語句,查詢並返結果。

上程式碼:

mybatis_config.xml(放置到src目錄下)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration  
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<plugins>
<plugin interceptor="com.monitor.business.mybatis.core.PagePlugin"> <!--資料庫型別,不同的資料庫分頁SQL略有不同--> <property name="databaseType" value="mysql"/> <!--此處的.*Page$正則用來匹配請求的方法名,凡是方法名以Page結尾的方法全部要被該攔截器攔截處理--> <property name="pageSqlId" value
=".*Page$" />
</plugin> </plugins> </configuration>

applicationContext.xml(將mybatis_config.xml配置到springmvc的配置檔案中)

<!-- myBatis檔案 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    ...其他配置此處略去只演示mybatis_config.xml的配置
    <property
name="configLocation" value="classpath:mybatis_config.xml" />
<!-- 自動掃描entity目錄, 省掉Configuration.xml裡的手工配置 --> ...其他配置此處略去 </bean>

PagePlugin .java(分頁程式碼)

package com.monitor.business.mybatis.core;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Properties;

import org.apache.ibatis.executor.parameter.DefaultParameterHandler;
import org.apache.ibatis.executor.parameter.ParameterHandler;
import org.apache.ibatis.executor.statement.RoutingStatementHandler;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Plugin;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;

import com.monitor.business.jdbc.model.Paginator;
import com.monitor.common.CommonUtils;
import com.monitor.common.ReflectUtil;

/**
 * 
 * 分頁攔截器,用於攔截需要進行分頁查詢的操作,然後對其進行分頁處理。 利用攔截器實現Mybatis分頁的原理:
 * 要利用JDBC對資料庫進行操作就必須要有一個對應的Statement物件
 * ,Mybatis在執行Sql語句前就會產生一個包含Sql語句的Statement物件,而且對應的Sql語句
 * 是在Statement之前產生的,所以我們就可以在它生成Statement之前對用來生成Statement的Sql語句下手
 * 。在Mybatis中Statement語句是通過RoutingStatementHandler物件的
 * prepare方法生成的。所以利用攔截器實現Mybatis分頁的一個思路就是攔截StatementHandler介面的prepare方法
 * ,然後在攔截器方法中把Sql語句改成對應的分頁查詢Sql語句,之後再呼叫
 * StatementHandler物件的prepare方法,即呼叫invocation.proceed()。
 * 對於分頁而言,在攔截器裡面我們還需要做的一個操作就是統計滿足當前條件的記錄一共有多少
 * ,這是通過獲取到了原始的Sql語句後,把它改為對應的統計語句再利用Mybatis封裝好的引數和設
 * 置引數的功能把Sql語句中的引數進行替換,之後再執行查詢記錄數的Sql語句進行總記錄數的統計。
 * 
 */
/**
* @ClassName: PagePlugin
* @classDescription:mybatis分頁
* @author 雲鶴公子 & Z.J
* @createTime 2016-7-29 下午3:07:00
*
*/ 
@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) })
public class PagePlugin implements Interceptor {
    private String databaseType;// 資料庫型別,不同的資料庫有不同的分頁方法
    private String pageSqlId;
    /*
     * 對於StatementHandler其實只有兩個實現類,一個是RoutingStatementHandler,
     * 另一個是抽象類BaseStatementHandler,
     * BaseStatementHandler有三個子類,分別是SimpleStatementHandler
     * ,PreparedStatementHandler和CallableStatementHandler,
     * SimpleStatementHandler是用於處理Statement的
     * ,PreparedStatementHandler是處理PreparedStatement的
     * ,而CallableStatementHandler是
     * 處理CallableStatement的。Mybatis在進行Sql語句處理的時候都是建立的RoutingStatementHandler
     * ,而在RoutingStatementHandler裡面擁有一個 StatementHandler型別的delegate屬性,
     * RoutingStatementHandler會依據Statement的不同建立對應的BaseStatementHandler
     * ,即SimpleStatementHandler、
     * PreparedStatementHandler或CallableStatementHandler
     * ,在RoutingStatementHandler裡面所有StatementHandler介面方法的實現都是呼叫的delegate對應的方法
     * 。 我們在PageInterceptor類上已經用@
     * Signature標記了該Interceptor只攔截StatementHandler介面的prepare方法
     * ,又因為Mybatis只有在建立RoutingStatementHandler的時候
     * 是通過Interceptor的plugin方法進行包裹的
     * ,所以我們這裡攔截到的目標物件肯定是RoutingStatementHandler物件。
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
        //通過反射獲取到當前RoutingStatementHandler物件的delegate屬性
        StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
        /*
         * 獲取到當前StatementHandler的boundSql,
         * 這裡不管是呼叫handler.getBoundSql()還是直接呼叫delegate.getBoundSql()結果是一樣的,因為之前已經說過了
         * RoutingStatementHandler實現的所有StatementHandler介面方法裡面都是呼叫的delegate對應的方法。
         */
        BoundSql boundSql = delegate.getBoundSql();
        // 拿到當前繫結Sql的引數物件,就是我們在呼叫對應的Mapper對映語句時所傳入的引數物件
        Object obj = boundSql.getParameterObject();
        MetaObject metaStatementHandler = MetaObject.forObject(handler); 
        // 分離代理物件鏈(由於目標類可能被多個攔截器攔截,從而形成多次代理,通過下面的兩次迴圈  可以分離出最原始的的目標類)  
         while (metaStatementHandler.hasGetter("h")) {  
             Object object = metaStatementHandler.getValue("h");  
             metaStatementHandler = MetaObject.forObject(object);  
         }  
         // 分離最後一個代理物件的目標類  
         while (metaStatementHandler.hasGetter("target")) {  
             Object object = metaStatementHandler.getValue("target");  
             metaStatementHandler = MetaObject.forObject(object);  
         }
         MappedStatement mds = (MappedStatement)metaStatementHandler.getValue("delegate.mappedStatement");  
        //分頁攔截以Page結尾的方法
         if (mds.getId().matches(pageSqlId)) { 
             Paginator<?> page = (Paginator<?>) obj;
             if(page.isPageFlag()){
            //通過反射獲取delegate父類BaseStatementHandler的mappedStatement屬性
            MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
            //攔截到的prepare方法引數是一個Connection物件
            Connection connection = (Connection) invocation.getArgs()[0];
            // 獲取當前要執行的Sql語句,也就是我們直接在Mapper對映語句中寫的Sql語句
            String sql = boundSql.getSql();
            // 給當前的page引數物件設定總記錄數
            this.setTotalRecord(page, mappedStatement, connection);
            // 獲取分頁Sql語句
            String pageSql = this.getPageSql(page, sql);
            // 利用反射設定當前BoundSql對應的sql屬性為我們建立好的分頁Sql語句
            ReflectUtil.setFieldValue(boundSql, "sql", pageSql);
             }
        }
        return invocation.proceed();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {
        this.databaseType = properties.getProperty("databaseType");
        this.pageSqlId=properties.getProperty("pageSqlId");
    }

    /**
     * 根據page物件獲取對應的分頁查詢Sql語句,這裡只做了兩種資料庫型別,Mysql和Oracle 其它的資料庫都 沒有進行分頁
     * 
     * @param page
     *            分頁物件
     * @param sql
     *            原sql語句
     * @return
     */
    private String getPageSql(Paginator<?> page, String sql) {
        StringBuffer sqlBuffer = new StringBuffer(sql);
        if ("mysql".equalsIgnoreCase(databaseType)) {
            return getMysqlPageSql(page, sqlBuffer);
        } else if ("oracle".equalsIgnoreCase(databaseType)) {
            return getOraclePageSql(page, sqlBuffer);
        }
        return sqlBuffer.toString();
    }

    /**
     * 獲取Mysql資料庫的分頁查詢語句
     * 
     * @param page
     *            分頁物件
     * @param sqlBuffer
     *            包含原sql語句的StringBuffer物件
     * @return Mysql資料庫分頁語句
     */
    private String getMysqlPageSql(Paginator<?> page, StringBuffer sqlBuffer) {
        // 計算第一條記錄的位置,Mysql中記錄的位置是從0開始的。
        int offset = (page.getPageNum() - 1) * page.getPageSize();
        page.setPage(page.getPageNum());
        sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize());
        return sqlBuffer.toString();
    }

    /**
     * 獲取Oracle資料庫的分頁查詢語句
     * 
     * @param page
     *            分頁物件
     * @param sqlBuffer
     *            包含原sql語句的StringBuffer物件
     * @return Oracle資料庫的分頁查詢語句
     */
    private String getOraclePageSql(Paginator<?> page, StringBuffer sqlBuffer) {
        // 計算第一條記錄的位置,Oracle分頁是通過rownum進行的,而rownum是從1開始的
        int offset = (page.getPageNum() - 1) * page.getPageSize() + 1;
        sqlBuffer.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize());
        sqlBuffer.insert(0, "select * from (").append(") where r >= ").append(offset);
        // 上面的Sql語句拼接之後大概是這個樣子:
        // select * from (select u.*, rownum r from (select * from t_user) u
        // where rownum < 31) where r >= 16
        return sqlBuffer.toString();
    }

    /**
     * 給當前的引數物件page設定總記錄數
     * 
     * @param page
     *            Mapper對映語句對應的引數物件
     * @param mappedStatement
     *            Mapper對映語句
     * @param connection
     *            當前的資料庫連線
     */
    private void setTotalRecord(Paginator<?> page, MappedStatement mappedStatement, Connection connection) {
        // 獲取對應的BoundSql,這個BoundSql其實跟我們利用StatementHandler獲取到的BoundSql是同一個物件。
        // delegate裡面的boundSql也是通過mappedStatement.getBoundSql(paramObj)方法獲取到的。
        BoundSql boundSql = mappedStatement.getBoundSql(page);
        // 獲取到我們自己寫在Mapper對映語句中對應的Sql語句
        String sql = boundSql.getSql();
        // 通過查詢Sql語句獲取到對應的計算總記錄數的sql語句
        String countSql = this.getCountSql(sql);
        // 通過BoundSql獲取對應的引數對映
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        // 利用Configuration、查詢記錄數的Sql語句countSql、引數對映關係parameterMappings和引數物件page建立查詢記錄數對應的BoundSql物件。
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, page);
        // 通過mappedStatement、引數物件page和BoundSql物件countBoundSql建立一個用於設定引數的ParameterHandler物件
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, page, countBoundSql);
        // 通過connection建立一個countSql對應的PreparedStatement物件。
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        try {
            pstmt = connection.prepareStatement(countSql);
            // 通過parameterHandler給PreparedStatement物件設定引數
            parameterHandler.setParameters(pstmt);
            // 之後就是執行獲取總記錄數的Sql語句和獲取結果了。
            rs = pstmt.executeQuery();
            if (rs.next()) {
                int totalRecord = rs.getInt(1);
                // 給當前的引數page物件設定總記錄數
                page.setTotalRecord(totalRecord);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        } finally {
            try {
                if (rs != null)
                    rs.close();
                if (pstmt != null)
                    pstmt.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 根據原Sql語句獲取對應的查詢總記錄數的Sql語句
     * 
     * @param sql
     * @return
     */
    private String getCountSql(String sql) {

        return "select count(1) from (" + sql + ") countNum";
    }
}

Demo下載地址: