很早有這個想法,但具體的實現一直沒去作,網上正好找到2篇,怕之後找不到,特意記錄一下,原文地址:javascript
https://my.oschina.net/gaoguofan/blog/753406html
https://my.oschina.net/dendy/blog/385575java
MyBatis 分頁攔截器實現mysql
攔截器的一個做用就是咱們能夠攔截某些方法的調用,咱們能夠選擇在這些被攔截的方法執行先後加上某些邏輯,也能夠在執行這些被攔截的方法時執行本身的邏輯而再也不執行被攔截的方法。Mybatis攔截器設計的一個初衷就是爲了供用戶在某些時候能夠實現本身的邏輯而沒必要去動Mybatis固有的邏輯。打個比方,對於Executor,Mybatis中有幾種實現:BatchExecutor、ReuseExecutor、SimpleExecutor和CachingExecutor。這個時候若是你以爲這幾種實現對於Executor接口的query方法都不能知足你的要求,那怎麼辦呢?是要去改源碼嗎?固然不。咱們能夠創建一個Mybatis攔截器用於攔截Executor接口的query方法,在攔截以後實現本身的query方法邏輯,以後能夠選擇是否繼續執行原來的query方法。ajax
對於攔截器Mybatis爲咱們提供了一個Interceptor接口,經過實現該接口就能夠定義咱們本身的攔截器。咱們先來看一下這個接口的定義:算法
package org.apache.ibatis.plugin; import java.util.Properties; public interface Interceptor { Object intercept(Invocation invocation) throws Throwable; Object plugin(Object target); void setProperties(Properties properties); }
咱們能夠看到在該接口中一共定義有三個方法,intercept、plugin和setProperties。plugin方法是攔截器用於封裝目標對象的,經過該方法咱們能夠返回目標對象自己,也能夠返回一個它的代理。當返回的是代理的時候咱們能夠對其中的方法進行攔截來調用intercept方法,固然也能夠調用其餘方法,這點將在後文講解。setProperties方法是用於在Mybatis配置文件中指定一些屬性的。
定義本身的Interceptor最重要的是要實現plugin方法和intercept方法,在plugin方法中咱們能夠決定是否要進行攔截進而決定要返回一個什麼樣的目標對象。而intercept方法就是要進行攔截的時候要執行的方法。
對於plugin方法而言,其實Mybatis已經爲咱們提供了一個實現。Mybatis中有一個叫作Plugin的類,裏面有一個靜態方法wrap(Object target,Interceptor interceptor),經過該方法能夠決定要返回的對象是目標對象仍是對應的代理。這裏咱們先來看一下Plugin的源碼:spring
package org.apache.ibatis.plugin; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import org.apache.ibatis.reflection.ExceptionUtil; public class Plugin implements InvocationHandler { private Object target; private Interceptor interceptor; private Map<Class<?>, Set<Method>> signatureMap; private Plugin(Object target, Interceptor interceptor, Map<Class<?>, Set<Method>> signatureMap) { this.target = target; this.interceptor = interceptor; this.signatureMap = signatureMap; } public static Object wrap(Object target, Interceptor interceptor) { Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); Class<?> type = target.getClass(); Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { Set<Method> methods = signatureMap.get(method.getDeclaringClass()); if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } } private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) { Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class); if (interceptsAnnotation == null) { // issue #251 throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName()); } Signature[] sigs = interceptsAnnotation.value(); Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>(); for (Signature sig : sigs) { Set<Method> methods = signatureMap.get(sig.type()); if (methods == null) { methods = new HashSet<Method>(); signatureMap.put(sig.type(), methods); } try { Method method = sig.type().getMethod(sig.method(), sig.args()); methods.add(method); } catch (NoSuchMethodException e) { throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e); } } return signatureMap; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); } }
咱們先看一下Plugin的wrap方法,它根據當前的Interceptor上面的註解定義哪些接口須要攔截,而後判斷當前目標對象是否有實現對應須要攔截的接口,若是沒有則返回目標對象自己,若是有則返回一個代理對象。而這個代理對象的InvocationHandler正是一個Plugin。因此當目標對象在執行接口方法時,若是是經過代理對象執行的,則會調用對應InvocationHandler的invoke方法,也就是Plugin的invoke方法。因此接着咱們來看一下該invoke方法的內容。這裏invoke方法的邏輯是:若是當前執行的方法是定義好的須要攔截的方法,則把目標對象、要執行的方法以及方法參數封裝成一個Invocation對象,再把封裝好的Invocation做爲參數傳遞給當前攔截器的intercept方法。若是不須要攔截,則直接調用當前的方法。Invocation中定義了定義了一個proceed方法,其邏輯就是調用當前方法,因此若是在intercept中須要繼續調用當前方法的話能夠調用invocation的procced方法。
這就是Mybatis中實現Interceptor攔截的一個思想,若是用戶以爲這個思想有問題或者不能徹底知足你的要求的話能夠經過實現本身的Plugin來決定何時須要代理何時須要攔截。如下講解的內容都是基於Mybatis的默認實現即經過Plugin來管理Interceptor來說解的。
對於實現本身的Interceptor而言有兩個很重要的註解,一個是@Intercepts,其值是一個@Signature數組。@Intercepts用於代表當前的對象是一個Interceptor,而@Signature則代表要攔截的接口、方法以及對應的參數類型。來看一個自定義的簡單Interceptor:sql
package com.tiantian.mybatis.interceptor; import java.sql.Connection; import java.util.Properties; import org.apache.ibatis.executor.Executor; import org.apache.ibatis.executor.statement.StatementHandler; import org.apache.ibatis.mapping.MappedStatement; 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.session.ResultHandler; import org.apache.ibatis.session.RowBounds; @Intercepts( { @Signature(method = "query", type = Executor.class, args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }), @Signature(method = "prepare", type = StatementHandler.class, args = { Connection.class }) }) public class MyInterceptor implements Interceptor { public Object intercept(Invocation invocation) throws Throwable { Object result = invocation.proceed(); System.out.println("Invocation.proceed()"); return result; } public Object plugin(Object target) { return Plugin.wrap(target, this); } public void setProperties(Properties properties) { String prop1 = properties.getProperty("prop1"); String prop2 = properties.getProperty("prop2"); System.out.println(prop1 + "------" + prop2); } }
首先看setProperties方法,這個方法在Configuration初始化當前的Interceptor時就會執行,這裏只是簡單的取兩個屬性進行打印。
其次看plugin方法中咱們是用的Plugin的邏輯來實現Mybatis的邏輯的。
接着看MyInterceptor類上咱們用@Intercepts標記了這是一個Interceptor,而後在@Intercepts中定義了兩個@Signature,即兩個攔截點。第一個@Signature咱們定義了該Interceptor將攔截Executor接口中參數類型爲MappedStatement、Object、RowBounds和ResultHandler的query方法;第二個@Signature咱們定義了該Interceptor將攔截StatementHandler中參數類型爲Connection的prepare方法。
最後再來看一下intercept方法,這裏咱們只是簡單的打印了一句話,而後調用invocation的proceed方法,使當前方法正常的調用。
對於這個攔截器,Mybatis在註冊該攔截器的時候就會利用定義好的n個property做爲參數調用該攔截器的setProperties方法。以後在新建可攔截對象的時候會調用該攔截器的plugin方法來決定是返回目標對象自己仍是代理對象。對於這個攔截器而言,當Mybatis是要Executor或StatementHandler對象的時候就會返回一個代理對象,其餘都是原目標對象自己。而後當Executor代理對象在執行參數類型爲MappedStatement、Object、RowBounds和ResultHandler的query方法或StatementHandler代理對象在執行參數類型爲Connection的prepare方法時就會觸發當前的攔截器的intercept方法進行攔截,而執行這兩個接口對象的其餘方法時都只是作一個簡單的代理。數據庫
註冊攔截器是經過在Mybatis配置文件中plugins元素下的plugin元素來進行的。一個plugin對應着一個攔截器,在plugin元素下面咱們能夠指定若干個property子元素。Mybatis在註冊定義的攔截器時會先把對應攔截器下面的全部property經過Interceptor的setProperties方法注入給對應的攔截器。因此,咱們能夠這樣來註冊咱們在前面定義的MyInterceptor:apache
<?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> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <plugins> <plugin interceptor="com.tiantian.mybatis.interceptor.MyInterceptor"> <property name="prop1" value="prop1"/> <property name="prop2" value="prop2"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers> </configuration>
Mybatis攔截器只能攔截四種類型的接口:Executor、StatementHandler、ParameterHandler和ResultSetHandler。這是在Mybatis的Configuration中寫死了的,若是要支持攔截其餘接口就須要咱們重寫Mybatis的Configuration。Mybatis能夠對這四個接口中全部的方法進行攔截。
下面將介紹一個Mybatis攔截器的實際應用。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語句這個看起來很簡單,而事實上來講的話就沒那麼直觀,由於包括sql等其餘屬性在內的多個屬性都沒有對應的方法能夠直接取到,它們對外部都是封閉的,是對象的私有屬性,因此這裏就須要引入反射機制來獲取或者更改對象的私有屬性的值了。對於分頁而言,在攔截器裏面咱們經常還須要作的一個操做就是統計知足當前條件的記錄一共有多少,這是經過獲取到了原始的Sql語句後,把它改成對應的統計語句再利用Mybatis封裝好的參數和設置參數的功能把Sql語句中的參數進行替換,以後再執行查詢記錄數的Sql語句進行總記錄數的統計。先來看一個咱們對分頁操做封裝的一個實體類Page:
import java.util.HashMap; import java.util.List; import java.util.Map; /** * 對分頁的基本數據進行一個簡單的封裝 */ public class Page<T> { private int pageNo = 1;//頁碼,默認是第一頁 private int pageSize = 15;//每頁顯示的記錄數,默認是15 private int totalRecord;//總記錄數 private int totalPage;//總頁數 private List<T> results;//對應的當前頁記錄 private Map<String, Object> params = new HashMap<String, Object>();//其餘的參數咱們把它分裝成一個Map對象 public int getPageNo() { return pageNo; } public void setPageNo(int pageNo) { this.pageNo = pageNo; } public int getPageSize() { return pageSize; } public void setPageSize(int pageSize) { this.pageSize = pageSize; } public int getTotalRecord() { return totalRecord; } public void setTotalRecord(int totalRecord) { this.totalRecord = totalRecord; //在設置總頁數的時候計算出對應的總頁數,在下面的三目運算中加法擁有更高的優先級,因此最後能夠不加括號。 int totalPage = totalRecord%pageSize==0 ? totalRecord/pageSize : totalRecord/pageSize + 1; this.setTotalPage(totalPage); } public int getTotalPage() { return totalPage; } public void setTotalPage(int totalPage) { this.totalPage = totalPage; } public List<T> getResults() { return results; } public void setResults(List<T> results) { this.results = results; } public Map<String, Object> getParams() { return params; } public void setParams(Map<String, Object> params) { this.params = params; } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("Page [pageNo=").append(pageNo).append(", pageSize=") .append(pageSize).append(", results=").append(results).append( ", totalPage=").append(totalPage).append( ", totalRecord=").append(totalRecord).append("]"); return builder.toString(); } }
對於須要進行分頁的Mapper映射,咱們會給它傳一個Page對象做爲參數,咱們能夠看到Page對象裏面包括了一些分頁的基本信息,這些信息咱們能夠在攔截器裏面用到,而後咱們把除分頁的基本信息之外的其餘參數用一個Map對象進行包裝,這樣在Mapper映射語句中的其餘參數就能夠從Map中取值了。接着來看一下咱們的PageInterceptor的定義,對於PageInterceptor我就不作過多的說明,代碼裏面附有很詳細的註釋信息:
package com.tiantian.mybatis.interceptor; 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.Properties; 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 com.tiantian.mybatis.model.Page; /** * * 分頁攔截器,用於攔截須要進行分頁查詢的操做,而後對其進行分頁處理。 * 利用攔截器實現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}) }) public class PageInterceptor implements Interceptor { private String databaseType;//數據庫類型,不一樣的數據庫有不一樣的分頁方法 /** * 攔截後要執行的方法 */ public Object intercept(Invocation invocation) throws Throwable { //對於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對象。 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(); //這裏咱們簡單的經過傳入的是Page對象就認定它是須要進行分頁操做的。 if (obj instanceof Page<?>) { Page<?> page = (Page<?>) obj; //經過反射獲取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(); } /** * 攔截器對應的封裝原始對象的方法 */ public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 設置註冊攔截器時設定的屬性 */ public void setProperties(Properties properties) { this.databaseType = properties.getProperty("databaseType"); } /** * 根據page對象獲取對應的分頁查詢Sql語句,這裏只作了兩種數據庫類型,Mysql和Oracle * 其它的數據庫都 沒有進行分頁 * * @param page 分頁對象 * @param sql 原sql語句 * @return */ private String getPageSql(Page<?> 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(Page<?> page, StringBuffer sqlBuffer) { //計算第一條記錄的位置,Mysql中記錄的位置是從0開始的。 int offset = (page.getPageNo() - 1) * page.getPageSize(); sqlBuffer.append(" limit ").append(offset).append(",").append(page.getPageSize()); return sqlBuffer.toString(); } /** * 獲取Oracle數據庫的分頁查詢語句 * @param page 分頁對象 * @param sqlBuffer 包含原sql語句的StringBuffer對象 * @return Oracle數據庫的分頁查詢語句 */ private String getOraclePageSql(Page<?> page, StringBuffer sqlBuffer) { //計算第一條記錄的位置,Oracle分頁是經過rownum進行的,而rownum是從1開始的 int offset = (page.getPageNo() - 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(Page<?> 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 + ")"; } /** * 利用反射進行操做的一個工具類 * */ 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(); } } } } }
接着咱們在Mybatis的配置文件裏面註冊該攔截器:
<?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> <properties resource="config/jdbc.properties"></properties> <typeAliases> <package name="com.tiantian.mybatis.model"/> </typeAliases> <plugins> <plugin interceptor="com.tiantian.mybatis.interceptor.PageInterceptor"> <property name="databaseType" value="Oracle"/> </plugin> </plugins> <environments default="development"> <environment id="development"> <transactionManager type="JDBC" /> <dataSource type="POOLED"> <property name="driver" value="${jdbc.driver}" /> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> </dataSource> </environment> </environments> <mappers> <mapper resource="com/tiantian/mybatis/mapper/UserMapper.xml"/> </mappers>
這樣咱們的攔截器就已經定義而且配置好了,接下來咱們就來測試一下。假設在咱們的UserMapper.xml中有以下這樣一個Mapper映射信息:
<select id="findPage" resultType="User" parameterType="page"> select * from t_user </select>
那咱們就能夠這樣來測試它:
SqlSession sqlSession = sqlSessionFactory.openSession(); try { UserMapper userMapper = sqlSession.getMapper(UserMapper.class); Page<User> page = new Page<User>(); page.setPageNo(2); List<User> users = userMapper.findPage(page); page.setResults(users); System.out.println(page); } finally { sqlSession.close(); }
============分界線========================================
============分界線========================================
============分界線========================================
============分界線========================================
最新項目用到springMVC和mybatis,分頁其實用一個RowBounds能夠實現,可是高級查詢很差封裝, 通過反覆測試,總算搞出來了,感受封裝的不是很好,有待優化和提升!
原理:利用mybatis自定義插件功能,自定義一個攔截器,攔截須要分頁的sql,並想辦法經過BoundSql對象進行處理,大體分8步:
一、得到BoundSql對象
二、獲取原始的寫在配置文件中的SQL
三、攔截到mapper中定義的執行查詢方法中的參數
四、解析參數,獲取高級查詢參數信息
五、解析參數,獲取查詢限制條件
六、根據四、5中的參數拼裝並從新生成SQL語句
七、將SQL設置回BoundSql對象中
八、完成。
攔截器:
package com.wtas.page.interceptor; 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 java.util.Set; import javax.xml.bind.PropertyException; import org.apache.ibatis.executor.ErrorContext; import org.apache.ibatis.executor.ExecutorException; import org.apache.ibatis.executor.statement.BaseStatementHandler; 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.mapping.ParameterMode; 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 org.apache.ibatis.reflection.property.PropertyTokenizer; import org.apache.ibatis.scripting.xmltags.ForEachSqlNode; import org.apache.ibatis.session.Configuration; import org.apache.ibatis.type.TypeHandler; import org.apache.ibatis.type.TypeHandlerRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.wtas.page.PageContext; import com.wtas.page.Pager; import com.wtas.page.Query; import com.wtas.utils.SystemUtil; /** * 查詢分頁攔截器,用戶攔截SQL,並加上分頁的參數和高級查詢條件 * * @author dendy * */ @Intercepts({ @Signature(type = StatementHandler.class, method = "prepare", args = { Connection.class }) }) public class PaginationInterceptor implements Interceptor { private final Logger logger = LoggerFactory .getLogger(PaginationInterceptor.class); private String dialect = ""; // 暫時不須要這個參數,如今根據參數類型來判斷是不是分頁sql // private String pageMethodPattern = ""; public Object intercept(Invocation ivk) throws Throwable { if (!(ivk.getTarget() instanceof RoutingStatementHandler)) { return ivk.proceed(); } RoutingStatementHandler statementHandler = (RoutingStatementHandler) ivk .getTarget(); BaseStatementHandler delegate = (BaseStatementHandler) SystemUtil .getValueByFieldName(statementHandler, "delegate"); MappedStatement mappedStatement = (MappedStatement) SystemUtil .getValueByFieldName(delegate, "mappedStatement"); // BoundSql封裝了sql語句 BoundSql boundSql = delegate.getBoundSql(); // 得到查詢對象 Object parameterObject = boundSql.getParameterObject(); // 根據參數類型判斷是不是分頁方法 if (!(parameterObject instanceof Query)) { return ivk.proceed(); } logger.debug(" beginning to intercept page SQL..."); Connection connection = (Connection) ivk.getArgs()[0]; String sql = boundSql.getSql(); Query query = (Query) parameterObject; // 查詢參數對象 Pager pager = null; // 查詢條件Map Map<String, Object> conditions = query.getQueryParams(); pager = query.getPager(); // 拼裝查詢條件 if (conditions != null) { Set<String> keys = conditions.keySet(); Object value = null; StringBuffer sb = new StringBuffer(); boolean first = true; for (String key : keys) { value = conditions.get(key); if (first) { sb.append(" where ").append(key).append(value); first = !first; } else { sb.append(" and ").append(key).append(value); } } sql += sb.toString(); } // 獲取查詢數來的總數目 String countSql = "SELECT COUNT(0) FROM (" + sql + ") AS tmp "; PreparedStatement countStmt = connection.prepareStatement(countSql); BoundSql countBS = new BoundSql(mappedStatement.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject); setParameters(countStmt, mappedStatement, countBS, parameterObject); ResultSet rs = countStmt.executeQuery(); int count = 0; if (rs.next()) { count = rs.getInt(1); } rs.close(); countStmt.close(); // 設置總記錄數 pager.setTotalResult(count); // 設置總頁數 pager.setTotalPage((count + pager.getShowCount() - 1) / pager.getShowCount()); // 放到做用於 PageContext.getInstance().set(pager); // 拼裝查詢參數 String pageSql = generatePageSql(sql, pager); SystemUtil.setValueByFieldName(boundSql, "sql", pageSql); logger.debug("generated pageSql is : " + pageSql); return ivk.proceed(); } /** * setting parameters * * @param ps * @param mappedStatement * @param boundSql * @param parameterObject * @throws SQLException */ private void setParameters(PreparedStatement ps, MappedStatement mappedStatement, BoundSql boundSql, Object parameterObject) throws SQLException { ErrorContext.instance().activity("setting parameters") .object(mappedStatement.getParameterMap().getId()); List<ParameterMapping> parameterMappings = boundSql .getParameterMappings(); if (parameterMappings != null) { Configuration configuration = mappedStatement.getConfiguration(); TypeHandlerRegistry typeHandlerRegistry = configuration .getTypeHandlerRegistry(); MetaObject metaObject = parameterObject == null ? null : configuration.newMetaObject(parameterObject); for (int i = 0; i < parameterMappings.size(); i++) { ParameterMapping parameterMapping = parameterMappings.get(i); if (parameterMapping.getMode() != ParameterMode.OUT) { Object value; String propertyName = parameterMapping.getProperty(); PropertyTokenizer prop = new PropertyTokenizer(propertyName); if (parameterObject == null) { value = null; } else if (typeHandlerRegistry .hasTypeHandler(parameterObject.getClass())) { value = parameterObject; } else if (boundSql.hasAdditionalParameter(propertyName)) { value = boundSql.getAdditionalParameter(propertyName); } else if (propertyName .startsWith(ForEachSqlNode.ITEM_PREFIX) && boundSql.hasAdditionalParameter(prop.getName())) { value = boundSql.getAdditionalParameter(prop.getName()); if (value != null) { value = configuration.newMetaObject(value) .getValue( propertyName.substring(prop .getName().length())); } } else { value = metaObject == null ? null : metaObject .getValue(propertyName); } @SuppressWarnings("unchecked") TypeHandler<Object> typeHandler = (TypeHandler<Object>) parameterMapping .getTypeHandler(); if (typeHandler == null) { throw new ExecutorException( "There was no TypeHandler found for parameter " + propertyName + " of statement " + mappedStatement.getId()); } typeHandler.setParameter(ps, i + 1, value, parameterMapping.getJdbcType()); } } } } /** * 生成Sql語句 * * @param sql * @param page * @return */ private String generatePageSql(String sql, Pager page) { if (page != null && (dialect != null || !dialect.equals(""))) { StringBuffer pageSql = new StringBuffer(); if ("mysql".equals(dialect)) { pageSql.append(sql); pageSql.append(" LIMIT " + page.getCurrentResult() + "," + page.getShowCount()); } else if ("oracle".equals(dialect)) { pageSql.append("SELECT * FROM (SELECT t.*,ROWNUM r FROM ("); pageSql.append(sql); pageSql.append(") t WHERE r <= "); pageSql.append(page.getCurrentResult() + page.getShowCount()); pageSql.append(") WHERE r >"); pageSql.append(page.getCurrentResult()); } return pageSql.toString(); } else { return sql; } } public Object plugin(Object arg0) { return Plugin.wrap(arg0, this); } public void setProperties(Properties p) { dialect = p.getProperty("dialect"); if (dialect == null || dialect.equals("")) { try { throw new PropertyException("dialect property is not found!"); } catch (PropertyException e) { e.printStackTrace(); } } // pageMethodPattern = p.getProperty("pageMethodPattern"); if (dialect == null || dialect.equals("")) { try { throw new PropertyException( "pageMethodPattern property is not found!"); } catch (PropertyException e) { e.printStackTrace(); } } } }
查詢對象的封裝:
一、map封裝查詢條件
二、pager對象封裝查詢限制條件,就是MySql中limit後的參數等附加信息
package com.wtas.page; /** * 分頁描述信息 * * @author dendy * */ public class Pager { // 每一頁的顯示條數 private int showCount; // 總的頁數 private int totalPage; // 查詢的數據總條數 private int totalResult; // 當前頁 private int currentPage; // 從第幾條開始獲取數據 @SuppressWarnings("unused") private int currentResult; public Pager() { this(1); } public Pager(int currentPage) { // 默認每頁顯示10條記錄 this(currentPage, 10); } public Pager(int currentPage, int showCount) { this.currentPage = currentPage; if (showCount > 0) { this.showCount = showCount; } // 錯誤處理 if (this.currentPage < 1) { this.currentPage = 1; } } //只列出關鍵的getter和setter…… public int getTotalPage() { // 分頁算法,計算總頁數 return this.totalPage; } public int getCurrentResult() { // 計算從第幾條獲取數據 return (currentPage - 1) * showCount; } }
package com.wtas.page; import java.util.Map; /** * 封裝查詢蠶食和查詢條件 * * @author dendy * */ public class Query { private Map<String, Object> queryParams; private Pager pager; public Map<String, Object> getQueryParams() { return queryParams; } public void setQueryParams(Map<String, Object> queryParams) { this.queryParams = queryParams; } //省略getter和setter }
控制層關鍵代碼:
/** * 分頁時獲取全部的學生 * * @return */ @RequestMapping("pageStus") @ResponseBody public List<User> pageAllStudents(HttpServletRequest req) { try { Query query = new Query(); Pager pager = new Pager(); Map<String, Object> queryParams = new HashMap<String, Object>(); // 獲取分頁參數 String showCount = req.getParameter("showCount"); String currentPage = req.getParameter("currentPage"); if (StringUtils.hasLength(showCount)) { pager.setShowCount(Integer.parseInt(showCount)); } if (StringUtils.hasLength(currentPage)) { pager.setCurrentPage(Integer.parseInt(currentPage)); } // 高級查詢條件:學生真實姓名 String trueNameForQuery = req.getParameter("trueNameForQuery"); if (StringUtils.hasLength(trueNameForQuery)) { queryParams.put(" u.REAL_NAME like ", "'%" + trueNameForQuery + "%'"); } query.setPager(pager); query.setQueryParams(queryParams); List<User> users = userService.pageUsersByRole(query); // req.setAttribute("pager", PageContext.getInstance().get()); return users; } catch (Exception e) { LOG.error("getAllStudents error : " + e.getMessage()); } return null; } @RequestMapping("getPager") @ResponseBody public Pager getPager() { return PageContext.getInstance().get(); }
dao中的方法:
/** * 級聯查詢全部某一角色的用戶信息,帶分頁 * * @param roleValue * @param page * @return */ List<User> pageUsers(Object query);
dao的Mappder.xml定義:
<select id="pageUsers" resultMap="userMapping" parameterType="hashMap"> SELECT DISTINCT u.* FROM T_USER u LEFT JOIN T_REL_USER_ROLE ur ON u.id=ur.user_id LEFT JOIN T_ROLE r ON ur.role_id=r.id </select>
頁面經過javascript來異常發送請求獲取數據,關鍵代碼:
/** * 處理分頁 * * @param curPage * @param id */ function page(curPage, id) { if(curPage <= 0){ curPage = 1; } var trueNameForQuery = $("#findByTrueNameInput").val().trim(); var url = path + "/studygroup/pageStus.do"; var thCss = "class='s-th-class'"; var tdCss = "class='s-td-class'"; $.ajax({ type : "POST", url : url, dataType : "json", data : { "id" : id, "currentPage" : curPage, "trueNameForQuery" : trueNameForQuery }, success : function(data) { var json = eval(data); var res = "<tr><th " + thCss + ">選擇</th>" + "<th " + thCss + ">用戶名</th>" + "<th " + thCss + ">真實姓名</th>" + "<th " + thCss + ">性別</th>" + "<th " + thCss + ">學校</th>" + "<th " + thCss + ">年級</th>" + "<th " + thCss + ">班級</th></tr>"; for ( var i = 0; i < json.length; i++) { var userId = json[i].id; var name = json[i].name; var trueName = json[i].trueName; var sex = json[i].sex; var school = ""; if (json[i].school) { school = json[i].school.name; } var grade = ""; if (json[i].grade) { grade = json[i].grade.name; } var clazz = ""; if (json[i].clazz) { clazz = json[i].clazz.name; } res += "<tr><td align='center' " + tdCss + "><input type='checkbox' value='" + userId + "' /></td>" + "<td align='center' " + tdCss + ">" + (name || "") + "</td>" + "<td align='center' " + tdCss + ">" + (trueName || "") + "</td>" + "<td align='center' " + tdCss + ">" + (sex == 1 ? '女' : '男' || "") + "</td>" + "<td align='center' " + tdCss + ">" + school + "</td>" + "<td align='center' " + tdCss + ">" + grade + "</td>" + "<td align='center' " + tdCss + ">" + clazz + "</td>" + "</td></tr>"; } $("#inviteStudentsTbl").html(res); // 每次加載完成都要刷新分頁欄數據 freshPager(id); } }); } /** * 從新獲取分頁對象,刷新分頁工具欄 */ function freshPager(id){ var url = path + "/studygroup/getPager.do"; var studyGroupId = id; $.ajax({ type : "POST", url : url, dataType : "json", success : function (data) { var pager = eval(data); var currentPage = pager.currentPage; // var currentResult = pager.currentResult; // var showCount = pager.showCount; var totalPage = pager.totalPage; // var totalResult = pager.totalResult; var prePage = currentPage - 1; var nextPage = currentPage + 1; if (prePage <= 0) { prePage = 1; } if (nextPage > totalPage) { nextPage = totalPage; } $("#topPageId").attr("href", "javascript:page(1, " + studyGroupId + ");"); $("#prefixPageId").attr("href", "javascript:page(" + prePage + ", " + studyGroupId + ");"); $("#nextPageId").attr("href", "javascript:page(" + nextPage + ", " + studyGroupId + ");"); $("#endPageId").attr("href", "javascript:page(" + totalPage + ", " + studyGroupId + ");"); $("#curPageId").html(currentPage); $("#totalPageId").html(totalPage); } }); } /** * 按真實姓名搜索 */ function findByTrueName() { page(1, studyGroupId); }
end.
————————————————————————————————————————————————
應網友須要,貼上SystemUtil的代碼:
package com.common.utils; import java.lang.reflect.Field; import javax.servlet.http.HttpSession; import com.common.consts.SystemConst; import com.wtas.sys.domain.User; /** * 系統工具類,定義系統經常使用的工具方法 * * @author dendy * */ public class SystemUtil { private SystemUtil() { } /** * 獲取系統訪問的相對路徑,如:/WTAS * * @return */ public static String getContextPath() { return System.getProperty(SystemConst.SYSTEM_CONTEXT_PATH_KEY); } /** * 修改一個bean(源)中的屬性值,該屬性值從目標bean獲取 * * @param dest * 目標bean,其屬性將被複制到源bean中 * @param src * 須要被修改屬性的源bean * @param filtNullProps * 源bean的null屬性是否覆蓋目標的屬性<li>true : 源bean中只有爲null的屬性纔會被覆蓋<li>false * : 無論源bean的屬性是否爲null,均覆蓋 * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void copyBean(Object dest, Object src, boolean filtNullProps) throws IllegalArgumentException, IllegalAccessException { if (dest.getClass() == src.getClass()) { // 目標bean的全部字段 Field[] destField = dest.getClass().getDeclaredFields(); // 源bean的全部字段 Field[] srcField = src.getClass().getDeclaredFields(); for (int i = 0; i < destField.length; i++) { String destFieldName = destField[i].getName(); String destFieldType = destField[i].getGenericType().toString(); for (int n = 0; n < srcField.length; n++) { String srcFieldName = srcField[n].getName(); String srcFieldType = srcField[n].getGenericType() .toString(); // String srcTypeName = // srcField[n].getType().getSimpleName(); if (destFieldName.equals(srcFieldName) && destFieldType.equals(srcFieldType)) { destField[i].setAccessible(true); srcField[n].setAccessible(true); Object srcValue = srcField[n].get(src); Object destValue = destField[i].get(dest); if (filtNullProps) { // 源bean中的屬性已經非空,則不覆蓋 if (srcValue == null) { srcField[n].set(src, destValue); } } else { srcField[n].set(dest, srcValue); } } } } } } /** * 根據字段的值獲取該字段 * * @param obj * @param fieldName * @return */ public static Field getFieldByFieldName(Object obj, String fieldName) { for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass .getSuperclass()) { try { return superClass.getDeclaredField(fieldName); } catch (NoSuchFieldException e) { } } return null; } /** * 獲取對象某一字段的值 * * @param obj * @param fieldName * @return * @throws SecurityException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static Object getValueByFieldName(Object obj, String fieldName) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field field = getFieldByFieldName(obj, fieldName); Object value = null; if (field != null) { if (field.isAccessible()) { value = field.get(obj); } else { field.setAccessible(true); value = field.get(obj); field.setAccessible(false); } } return value; } /** * 向對象的某一字段上設置值 * * @param obj * @param fieldName * @param value * @throws SecurityException * @throws NoSuchFieldException * @throws IllegalArgumentException * @throws IllegalAccessException */ public static void setValueByFieldName(Object obj, String fieldName, Object value) throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field field = obj.getClass().getDeclaredField(fieldName); if (field.isAccessible()) { field.set(obj, value); } else { field.setAccessible(true); field.set(obj, value); field.setAccessible(false); } } /** * 從session中獲取當前登陸用戶 * * @param session * @return */ public static User getLoginUser(HttpSession session) { return (User) session.getAttribute(SystemConst.USER_IN_SESSION); } /** * @Description 設置更新信息後的登陸用戶給session * @param user 登陸用戶 * @param session session */ public static void setUser(User user, HttpSession session) { session.setAttribute(SystemConst.USER_IN_SESSION, user); } }