1. 程式人生 > >mybatis分頁攔截器的實現

mybatis分頁攔截器的實現

import java.lang.reflect.Field;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;
import java.util.Properties;

import org.apache.ibatis.executor.Executor;
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.scripting.defaults.DefaultParameterHandler;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import utn.app.daobase.model.PageInfo;

/**  
 *  
 * 分頁攔截器,用於攔截需要進行分頁查詢的操作,然後對其進行分頁處理。  
 * 利用攔截器實現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語句進行總記錄數的統計。  
 *  
 */    
@Intercepts({ @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }),
        @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class MybatisSpringPageInterceptor implements Interceptor {
    private static final Logger log = LoggerFactory.getLogger(MybatisSpringPageInterceptor.class);

    public static final String MYSQL = "mysql";
    public static final String ORACLE = "oracle";

    protected String databaseType;// 資料庫型別,不同的資料庫有不同的分頁方法

    @SuppressWarnings("rawtypes")
    protected ThreadLocal<PageInfo> pageThreadLocal = new ThreadLocal<PageInfo>();

    public String getDatabaseType() {
        return databaseType;
    }

    public void setDatabaseType(String databaseType) {
        if (!databaseType.equalsIgnoreCase(MYSQL) && !databaseType.equalsIgnoreCase(ORACLE)) {
            throw new PageNotSupportException("Page not support for the type of database, database type [" + databaseType + "]");
        }
        this.databaseType = databaseType;
    }

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

    public void setProperties(Properties properties) {
        String databaseType = properties.getProperty("databaseType");
        if (databaseType != null) {
            setDatabaseType(databaseType);
        }
    }
    /*對於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對應的方法*/
    @SuppressWarnings({ "unchecked", "rawtypes" })
    public Object intercept(Invocation invocation) throws Throwable {
        if (invocation.getTarget() instanceof StatementHandler) { // 控制SQL和查詢總數的地方
            PageInfo page = pageThreadLocal.get();
            if (page == null) { //不是分頁查詢
                return invocation.proceed();
            }

            RoutingStatementHandler handler = (RoutingStatementHandler) invocation.getTarget();
            StatementHandler delegate = (StatementHandler) ReflectUtil.getFieldValue(handler, "delegate");
            BoundSql boundSql = delegate.getBoundSql();

            Connection connection = (Connection) invocation.getArgs()[0];
            prepareAndCheckDatabaseType(connection); // 準備資料庫型別

            if (page.getTotalPage() > -1) {
                if (log.isTraceEnabled()) {
                    log.trace("已經設定了總頁數, 不需要再查詢總數.");
                }
            } else {
                Object parameterObj = boundSql.getParameterObject();
                MappedStatement mappedStatement = (MappedStatement) ReflectUtil.getFieldValue(delegate, "mappedStatement");
                queryTotalRecord(page, parameterObj, mappedStatement, connection);
            }

            String sql = boundSql.getSql();
            String pageSql = buildPageSql(page, sql);
            if (log.isDebugEnabled()) {
                log.debug("分頁時, 生成分頁pageSql: " + pageSql);
            }
            ReflectUtil.setFieldValue(boundSql, "sql", pageSql);

            return invocation.proceed();
        } else { // 查詢結果的地方
            // 獲取是否有分頁Page物件
            PageInfo<?> page = findPageObject(invocation.getArgs()[1]);
            if (page == null) {
                if (log.isTraceEnabled()) {
                    log.trace("沒有Page物件作為引數, 不是分頁查詢.");
                }
                return invocation.proceed();
            } else {
                if (log.isTraceEnabled()) {
                    log.trace("檢測到分頁Page物件, 使用分頁查詢.");
                }
            }
            //設定真正的parameterObj
            invocation.getArgs()[1] = extractRealParameterObject(invocation.getArgs()[1]);

            pageThreadLocal.set(page);
            try {
                Object resultObj = invocation.proceed(); // Executor.query(..)
                if (resultObj instanceof List) {
                    /* @SuppressWarnings({ "unchecked", "rawtypes" }) */
                    page.setResults((List) resultObj);
                }
                return resultObj;
            } finally {
                pageThreadLocal.remove();
            }
        }
    }

    protected PageInfo<?> findPageObject(Object parameterObj) {
        if (parameterObj instanceof PageInfo<?>) {
            return (PageInfo<?>) parameterObj;
        } else if (parameterObj instanceof Map) {
            for (Object val : ((Map<?, ?>) parameterObj).values()) {
                if (val instanceof PageInfo<?>) {
                    return (PageInfo<?>) val;
                }
            }
        }
        return null;
    }

    /**
     * <pre>
     * 把真正的引數物件解析出來
     * Spring會自動封裝zhe這個引數物件為Map<String, Object>物件
     * 對於通過@Param指定key值引數我們不做處理,因為XML檔案需要該KEY值
     * 而對於沒有@Param指定時,Spring會使用0,1作為主鍵
     * 對於沒有@Param指定名稱的引數,一般XML檔案會直接對真正的引數物件解析,
     * 此時解析出真正的引數作為根物件
     * </pre>
     * @param parameterObj
     * @return
     */
    protected Object extractRealParameterObject(Object parameterObj) {
        if (parameterObj instanceof Map<?, ?>) {
            Map<?, ?> parameterMap = (Map<?, ?>) parameterObj;
            if (parameterMap.size() == 2) {
                boolean springMapWithNoParamName = true;
                for (Object key : parameterMap.keySet()) {
                    if (!(key instanceof String)) {
                        springMapWithNoParamName = false;
                        break;
                    }
                    String keyStr = (String) key;
                    if (!"0".equals(keyStr) && !"1".equals(keyStr)) {
                        springMapWithNoParamName = false;
                        break;
                    }
                }
                if (springMapWithNoParamName) {
                    for (Object value : parameterMap.values()) {
                        if (!(value instanceof PageInfo<?>)) {
                            return value;
                        }
                    }
                }
            }
        }
        return parameterObj;
    }

    protected void prepareAndCheckDatabaseType(Connection connection) throws SQLException {
        if (databaseType == null) {
            String productName = connection.getMetaData().getDatabaseProductName();
            if (log.isTraceEnabled()) {
                log.trace("Database productName: " + productName);
            }
            productName = productName.toLowerCase();
            if (productName.indexOf(MYSQL) != -1) {
                databaseType = MYSQL;
            } else if (productName.indexOf(ORACLE) != -1) {
                databaseType = ORACLE;
            } else {
                throw new PageNotSupportException("Page not support for the type of database, database product name [" + productName + "]");
            }
            if (log.isInfoEnabled()) {
                log.info("自動檢測到的資料庫型別為: " + databaseType);
            }
        }
    }

    /**
     * <pre>
     * 生成分頁SQL
     * </pre>
     *
     * @param page
     * @param sql
     * @return
     */
    protected String buildPageSql(PageInfo<?> page, String sql) {
        if (MYSQL.equalsIgnoreCase(databaseType)) {
            return buildMysqlPageSql(page, sql);
        } else if (ORACLE.equalsIgnoreCase(databaseType)) {
            return buildOraclePageSql(page, sql);
        }
        return sql;
    }

    /**
     * <pre>
     * 生成Mysql分頁查詢SQL
     * </pre>
     *
     * @param page
     * @param sql
     * @return
     */
    protected String buildMysqlPageSql(PageInfo<?> page, String sql) {
        // 計算第一條記錄的位置,Mysql中記錄的位置是從0開始的。
        int offset = (page.getPageNo() - 1) * page.getPageSize();
        return new StringBuilder(sql).append(" limit ").append(offset).append(",").append(page.getPageSize()).toString();
    }

    /**
     * <pre>
     * 生成Oracle分頁查詢SQL
     * </pre>
     *
     * @param page
     * @param sql
     * @return
     */
    protected String buildOraclePageSql(PageInfo<?> page, String sql) {
        // 計算第一條記錄的位置,Oracle分頁是通過rownum進行的,而rownum是從1開始的
        int offset = (page.getPageNo() - 1) * page.getPageSize() + 1;
        StringBuilder sb = new StringBuilder(sql);
        sb.insert(0, "select u.*, rownum r from (").append(") u where rownum < ").append(offset + page.getPageSize());
        sb.insert(0, "select * from (").append(") where r >= ").append(offset);
        return sb.toString();
    }

    /**
     * <pre>
     * 查詢總數
     * </pre>
     *
     * @param page
     * @param parameterObject
     * @param mappedStatement
     * @param connection
     * @throws SQLException
     */
    protected void queryTotalRecord(PageInfo<?> page, Object parameterObject, MappedStatement mappedStatement, Connection connection) throws SQLException {
        BoundSql boundSql = mappedStatement.getBoundSql(page);
        //獲取到我們自己寫在Mapper對映語句中對應的Sql語句 
        String sql = boundSql.getSql();
        //通過查詢Sql語句獲取到對應的計算總記錄數的sql語句  
        String countSql = this.buildCountSql(sql);
        if (log.isDebugEnabled()) {
            log.debug("分頁時, 生成countSql: " + countSql);
        }
        //通過BoundSql獲取對應的引數對映
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        //利用Configuration、查詢記錄數的Sql語句countSql、引數對映關係parameterMappings和引數物件page建立查詢記錄數對應的BoundSql物件。
        BoundSql countBoundSql = new BoundSql(mappedStatement.getConfiguration(), countSql, parameterMappings, parameterObject);
        //通過mappedStatement、引數物件page和BoundSql物件countBoundSql建立一個用於設定引數的ParameterHandler物件
        ParameterHandler parameterHandler = new DefaultParameterHandler(mappedStatement, parameterObject, 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()) {
                long totalRecord = rs.getLong(1);
                //給當前的引數page物件設定總記錄數 
                page.setTotalRecord(totalRecord);
            }
        } finally {
            if (rs != null)
                try {
                    rs.close();
                } catch (Exception e) {
                    if (log.isWarnEnabled()) {
                        log.warn("關閉ResultSet時異常.", e);
                    }
                }
            if (pstmt != null) {
                try {
                    pstmt.close();
                } catch (Exception e) {
                    if (log.isWarnEnabled()) {
                        log.warn("關閉PreparedStatement時異常.", e);
                    }
                }
            }
        }
    }

    /**
     * 根據原Sql語句獲取對應的查詢總記錄數的Sql語句
     *
     * @param sql
     * @return
     */
    protected String buildCountSql(String sql) {
        int index = sql.toLowerCase().indexOf("from");
        return "select count(*) " + sql.substring(index);
    }

    /**
     * 利用反射進行操作的一個工具類
     *
     */
    private static class ReflectUtil {
        /**
         * 利用反射獲取指定物件的指定屬性
         *
         * @param obj 目標物件
         * @param fieldName 目標屬性
         * @return 目標屬性的值
         */
        public static Object getFieldValue(Object obj, String fieldName) {
            Object result = null;
            Field field = ReflectUtil.getField(obj, fieldName);
            if (field != null) {
                field.setAccessible(true);
                try {
                    result = field.get(obj);
                } catch (IllegalArgumentException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
            return result;
        }

        /**
         * 利用反射獲取指定物件裡面的指定屬性
         *
         * @param obj 目標物件
         * @param fieldName 目標屬性
         * @return 目標欄位
         */
        private static Field getField(Object obj, String fieldName) {
            Field field = null;
            for (Class<?> clazz = obj.getClass(); clazz != Object.class; clazz = clazz.getSuperclass()) {
                try {
                    field = clazz.getDeclaredField(fieldName);
                    break;
                } catch (NoSuchFieldException e) {
                    // 這裡不用做處理,子類沒有該欄位可能對應的父類有,都沒有就返回null。
                }
            }
            return field;
        }

        /**
         * 利用反射設定指定物件的指定屬性為指定的值
         *
         * @param obj 目標物件
         * @param fieldName 目標屬性
         * @param fieldValue 目標值
         */
        public static void setFieldValue(Object obj, String fieldName, String fieldValue) {
            Field field = ReflectUtil.getField(obj, fieldName);
            if (field != null) {
                try {
                    field.setAccessible(true);
                    field.set(obj, fieldValue);
                } catch (IllegalArgumentException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
            }
        }
    }

    public static class PageNotSupportException extends RuntimeException {

        /** serialVersionUID*/
        private static final long serialVersionUID = 1L;

        public PageNotSupportException() {
            super();
        }

        public PageNotSupportException(String message, Throwable cause) {
            super(message, cause);
        }

        public PageNotSupportException(String message) {
            super(message);
        }

        public PageNotSupportException(Throwable cause) {
            super(cause);
        }
    }
}

 
別忘了在mybatis配置檔案中這個配置
<!-- 配置管理器 -->
<configuration>
	<properties>  
        <property name="dialect" value="mysql" />  
    </properties>
    <settings>
        <setting name="logImpl" value="STDOUT_LOGGING" />
    </settings>
	<plugins>  
        <plugin interceptor="utn.app.daobase.interceptor.MybatisSpringPageInterceptor"></plugin>
    </plugins>
</configuration>



相關推薦

mybatis攔截實現

import java.lang.reflect.Field; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql

mybatismybatis攔截搭配bootstrap-table使用

提前說明:   這一種方式已被我自己pass掉了,已經被新的方式迭代了。但是記錄下自己曾經的成果還是有必要的,而且裡面的思想還是不變的,另外技術不就是在不斷地迭代中升級嗎。千萬不要想著一步完美,那樣會讓你止步不前。 業務說明:   前臺bootstrap-table外掛進行資料展示;後端SSM架子接收引

MyBatis功能的實現(陣列、sql攔截,RowBounds

前言:學習hibernate & mybatis等持久層框架的時候,不外乎對資料庫的增刪改查操作。而使用最多的當是資料庫的查詢操

mybatis -mysql -攔截

1 攔截器:jsqlparser-0.9.4.jar /*  * The MIT License (MIT)  *  * Copyright (c) 2014 [email protected]  *  * Permis

基於Mybatis外掛PageHelper實現功能

使用PageHelper外掛實現分頁功能 分頁的功能幾乎是所有專案必備的功能,在SSM(spring 、springmvc、mybatis)組織的專案中如何實現分頁呢? 下面介紹一種基於mybatis的分頁外掛PageHelper來幫助我們實現分頁的功能。

SpringMVC+MyBatis外掛簡單實現

一、封裝分頁page類 package com.framework.common.page.impl; import java.io.Serializable; import com.framework.common.page.IPage; /*

MyBatis的簡單實現

使用spring+springmvc+mybatis實現簡單的分頁查詢 spring+springmvc+mybatis的整合配置就不在贅述了 1.需要下載pagehelper-3.2.1.jar

myBatis學習筆記(10)——使用攔截實現查詢

條件 iba execute rri itl alias property gen func 1. Page package com.sm.model; import java.util.List; public class Page<T&g

利用Mybatis攔截實現查詢

手寫Mybatis攔截器 版本 Spring Boot 2.0.3.RELEASE Mybatis自定義攔截器 如果有閱讀過我之前一篇部落格 Hibernate 重新整理上下文 的朋友應該還記得 Hibernate 的上下文中可以新增自定義的事件監聽器。當初是為

Mybatis實現的方法(攔截+pageHelper)

一、攔截器實現 1.原理 在mybatis 執行過程中攔截執行物件,獲得sql資訊,將分頁資訊新增到sql語句中,然後放行mybatis的執行過程 2.瞭解一點mybatis原始碼 首先我們需要明白要攔截的物件是處理物件(Statement),攔截的時機應該是sql執

通過spring抽象路由數據源+MyBatis攔截實現數據庫自動讀寫分離

註入 兩個 -- 事情 rem 使用註解 connect key值 -m 前言 之前使用的讀寫分離的方案是在mybatis中配置兩個數據源,然後生成兩個不同的SqlSessionTemplate然後手動去識別執行sql語句是操作主庫還是從庫。如下圖所示: 好處是,你

mybatis攔截實現查看sql執行效率

nth 內部 設置 work check sel nts mage view package cc.zeelan.common.utils; import java.lang.reflect.Field; import java.sql.Statement; impor

MyBatis精通之路之功能的實現

avi 一個 冗余 details 結果 nature ann email ret MyBatis精通之路之分頁功能的實現(數組分頁、sql分頁、攔截器,RowBounds分頁) 原創 2017年04月27日 21:34:48 標簽: mybatis / java /

SpringBoot+Mybatis配置Pagehelper插件實現自動

pri ldb tar pos method prop 註意 dss stk SpringBoot+Mybatis配置Pagehelper分頁插件實現自動分頁 **SpringBoot+Mybatis使用Pagehelper分頁插件自動分頁,非常好用,不用在自己去計算和組裝

代理模式的實際運用-以mybatis攔截實現原理為例

之前在寫mybatis攔截器的時候,因為不懂原理,琢磨了很久,不知道怎麼寫,在網上找了很多資料,才知道mybatis的攔截器主要還是通過代理實現的,而且我在之前的博文中剛好學習了代理模式。更精細的是,在mybatis對代理的應用上,不管是封裝易用性,減少程式碼耦合度上,都可以讓我之前寫的

基於mybatis攔截實現資料許可權

資料許可權是很多系統常見的功能,實現的方式也是很多的,最近在做專案的時候,自己基於mybatis攔截器做了一個數據許可權的功能。 **功能設計 a)  需要做資料許可權功能的表加上一個許可權id欄位。 許可權id可以不僅僅是組織,還可以是其他自定義的欄位,用來做資料許可權,

Mybatis攔截實現)通用mapper及全ORM實現(四)

到目前為止,我們通過mybatis的攔截器完成了一些基礎mapper的功能,接下來我們來看看如何能夠實現一個物件關係對映的全ORM操作。 實體與表、列的定義已經完成了,那剩下要做的就是: 1、定義如何通過物件方式編寫sql語句 2、把查詢物件轉譯成sql去查詢 3、把查詢結

Mybatis攔截實現)通用mapper及全ORM實現(五)-- springboot+mybatis多資料來源設定

本篇實際上和mybatisext專案並沒有太大關係了,但在實際專案中脫離不開多個數據源,尤其是主從分離,同樣網上一些資料大同小異而且大部分並不能真正解決問題,所以單獨提出來說一下 假設我們就是要解決一個主從分離,資料來源定義在了application.properties中

Springboot2(22)Mybatis攔截實現

原始碼地址 springboot2教程系列 MyBatis提供了一種外掛(plugin)的功能,雖然叫做外掛,但其實這是攔截器功能 MyBatis 允許攔截的介面 MyBatis 允許你在已對映語句執行過程中的某一點進行攔截呼叫。預設情況下,

SpringBoot整合Mybatis自定義攔截實現拼接sql和修改

一、應用場景 1.分頁,如com.github.pagehelper的分頁外掛實現; 2.攔截sql做日誌監控; 3.統一對某些sql進行統一條件拼接,類似於分頁。 二、MyBatis的攔截器簡介 然後我們要知道攔截器攔截什麼樣的物件,攔截物件的什麼行為,什麼時候攔截? &n