前文,我們演示了物理分頁的Sql實現方式,這種方式使得我們每次在編寫查詢服務時,不斷的重複造輪子。這樣的代碼實現方式就顯得十分的笨拙了。本文是Mybatis分頁查詢的最後一片內容,我們將介紹基於攔截器的,精巧的實現方式。在閱讀這篇文章之前,強烈建議各位看官能夠先閱讀上文。這樣就能對下文我們提及的各種對象及他們之間的關係有一個清晰的關係。好了,廢話不多講,開始我們的正文部分吧。
準備工作:
a.操作系統 :win7 x64
b.基本軟件:MySQL,Mybatis,SQLyog
-------------------------------------------------------------------------------------------------------------------------------------
【本文只作爲原理分析及簡單分頁功能實現。實際生產環節,建議各位讀者選擇已經廣泛應用的第三方jar包或平臺性支持的實現】
-------------------------------------------------------------------------------------------------------------------------------------
1.創建本文我們將使用的工程Mybatis12,工程結構圖如下:【重點文件我們給出,其他配置文件請讀者參考前文工程】
2.管理分頁的對象PagePOJO的具體內容如下:【這裏我們給出基本分頁的示例,更多需求請讀者自行完成】
-
package com.csdn.ingo.entity;
-
-
-
-
-
-
public class PagePOJO {
-
private int totalNumber;
-
private int currentPage;
-
private int totalPage;
-
private int pageSize;
-
private int startIndex;
-
private int totalSelect;
-
-
public void setTotalNumber(int totalNumber) {
-
this.totalNumber = totalNumber;
-
this.count();
-
}
-
-
-
public PagePOJO(int totalNumber, int currentPage, int totalPage, int pageSize, int startIndex, int totalSelect) {
-
super();
-
this.totalNumber = totalNumber;
-
this.currentPage = currentPage;
-
this.totalPage = totalPage;
-
this.pageSize = pageSize;
-
this.startIndex = startIndex;
-
this.totalSelect = totalSelect;
-
}
-
public void count(){
-
int totalPageTemp = this.totalNumber/this.pageSize;
-
int plus = (this.totalNumber%this.pageSize)==0?0:1;
-
totalPageTemp = totalPageTemp+plus;
-
if(totalPageTemp<=0){
-
totalPageTemp=1;
-
}
-
this.totalPage = totalPageTemp;
-
-
if(this.totalPage<this.currentPage){
-
this.currentPage = this.totalPage;
-
}
-
if(this.currentPage<1){
-
this.currentPage=1;
-
}
-
this.startIndex = (this.currentPage-1)*this.pageSize;
-
this.totalSelect = this.pageSize;
-
}
-
}
3.新增單元測試方法,如下:【測試數據讀者可以自行更換】
-
@Test
-
public void testSelectPage() {
-
try {
-
-
-
PagePOJO page = new PagePOJO(5, 1, 1, 3, 1, 4);
-
Map<String,Object> params = new HashMap<String,Object>();
-
params.put("page", page);
-
UserInfoDao userInfo = sqlSession.getMapper(UserInfoDao.class);
-
List<UserInfo> re = userInfo.selectByPage(params);
-
System.out.println(re);
-
} catch (Exception e) {
-
e.printStackTrace();
-
}
-
}
-------------------------------------------------------------------------------------------------------------------------------------
上面與分頁有關的,比較簡單的內容我們先提供給大家。下面我們來詳細的解釋,攔截器實現分頁的原理,各位看官睜大眼睛啊!
-------------------------------------------------------------------------------------------------------------------------------------
分頁開始之前的問題:
- Mybatis如何找到我們新增的攔截服務。
- 自定義的攔截服務應該在什麼時間攔截查詢動作。即什麼時間截斷Mybatis執行流。
- 自定義的攔截服務應該攔截什麼樣的對象。不能攔截什麼樣的對象。
- 自定義的攔截服務攔截的對象應該具有什麼動作才能被攔截。
- 自定義的攔截服務如何獲取上下文中傳入的參數信息。
- 如何把簡單查詢,神不知鬼不覺的,無侵入性的替換爲分頁查詢語句。
- 最後,攔截器應該如何交還被截斷的Mybatis執行流。
帶着這些問題,我們來看看我們自定義的攔截服務是如何實現的。
-------------------------------------------------------------------------------------------------------------------------------------------------------
1.首先,我們看看查詢語句的時序圖,如下:
2.回顧一下,Mybatis允許我們能夠進行切入的點,如下:【上文的運行細節圖中也有描述,具體內容請參考前文。】
【老外其實從命名上已經給我們了充足的信息,我們結合時序圖已經能夠猜到其大概的含義及執行時間,鑑於篇幅的關係,具體的功能及作用,請參考前文詳細內容。】
【結論】從上面的1.2兩步,我們知道分頁攔截的合理時機是在StatementHandler中。
3.現在,來看看這個StatementHandler的具體內容,如下:
【成員方法】
【類間關係】
這裏請讀者回顧上文中講述的查詢執行流,即這裏將會執行prepare(Connection)方法,如下:
-
@Override
-
public Statement prepare(Connection connection) throws SQLException {
-
ErrorContext.instance().sql(boundSql.getSql());
-
Statement statement = null;
-
try {
-
statement = instantiateStatement(connection);
-
setStatementTimeout(statement);
-
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);
-
}
-
}
執行到instantiateStatement(connection);如下:【PreparedStatementHandler中】
-
@Override
-
protected Statement instantiateStatement(Connection connection) throws SQLException {
-
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);
-
}
-
}
【重點!!!】【這裏我們就發現真正執行查詢語句的地方connection.prepareStatement(...)】
由此,就解釋了爲什麼在StatementHandler中進行攔截。
4.於是,我們可以得到攔截器的對應註解內容如下:【繼承自Mybatis的Interceptor,必須實現3個方法,如下:】
-
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
5.現在,自定義的攔截器的基本結構如下:
-
package com.csdn.ingo.interceptor;
-
-
-
-
-
-
-
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
-
public class PageInterceptor implements Interceptor{
-
-
-
-
-
public Object intercept(Invocation invocation) throws Throwable {
-
-
}
-
-
-
-
-
public Object plugin(Object target) {
-
-
}
-
-
-
-
-
public void setProperties(Properties properties) {
-
-
}
-
-
}
6.我們先看看攔截器上面基本結構中需要攔截的對象的具體方法實現:
-
-
-
-
public Object plugin(Object target) {
-
return Plugin.wrap(target, this);
-
}
接下來,深入的看看wrap方法,如下:
-
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;
-
}
接着,再看看getSignatureMap(interceptor);的詳細內容,如下:
-
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
-
Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(Intercepts.class);
-
-
-
return signatureMap;
-
}
由此看到:annotation註解等其他相關內容都放在Map中返回。最後,判斷出是否生成代理對象。
-------------------------------------------------------------------------------------------------------------------------------------
7.現在,我們已經成功的攔截到了目標對象,然後,就開始要改變查詢過程了。在這裏,出現了兩個基本的問題
- 如何獲取原始的查詢語句,即mapper文件中的sql語句。
- 如何獲取分頁信息。
其實,在方法intercept(Invocation invocation)的參數中,已經包含了StatementHandler的信息,我們需要做的就是取出其中的信息,操作步驟如下:
a.先取出StatementHandler,如下:
-
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
·關於StatementHandler其有兩個實現,【見上文】,默認情況下,程序會執行BaseStatementHandler下的內容。如下:
其中的mappedStatement中就包含着我們需要找的sql信息。如下:【如果讀者已經看過前面的配置詳解部分的內容,已經不會感到陌生】
現在,我們已經找到了我們想要的內容的具體位置,那我們應該怎樣取出來呢?此時應該有看官大喊一聲:這還不簡單!可是我們來看看這部分的源碼內容,如下:
-
protected final MappedStatement mappedStatement;
【注意】
其在BaseStatementHandler中是protected的,我們在沒有繼承等關係的條件下,是無法直接取出來的。
因此,我們就需要引入一個Mybatis已經實現了的對象:MetaObject。關於這個對象,可以先暫時的理解爲幫助我們獲取或設置該對象的原本不可訪問的屬性。
在3.3.1版本中,其內部細節如下:
這裏,我們先只用到了public static MetaObject forObject(.....) ,其他內容有興趣的讀者可以自行學習。
於是,我們操作的對象就由StatementHandler的實例,變爲MetaObject的實例,如下:
-
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
-
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
-
現在對象已經獲取了,就要獲取其中的值了,在具體操作之前,我們再來梳理一下這裏的類間關係,如下:【這裏需要閱讀過前文程序執行流程,或者之前已有了解】
現在,我們終於可以去獲取其屬性值了,獲取的方法爲參數名,OGNL表達式。具體內容如下:
-
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
-
String id = mappedStatement.getId();
8.獲得目標對象之後,現在要做進一步判斷,即,當前被代理的對象是普通查詢,還是分頁查詢。這裏具體方式有很多,我們給出一種簡單實現,更多實現及用法就行讀者自行嘗試吧。
如,我們在這裏定義,id中(即mapper文件中sql語句的唯一id)以ByPage字符串結尾的,我們就認爲該sql語句按照分頁插敘來執行。具體的代碼試下如下:
-
if(id.matches(".+ByPage$")){
-
-
}
-------------------------------------------------------------------------------------------------------------------------------------
9.通過上面一系列的步驟,我們已經成功的捕獲了目標對象。接下來,我們就要開始加入我們“神不知鬼不覺‘”的代碼了。
【具體分爲下面的幾個步驟】
- 獲取原始sql語句
- 執行滿足一定條件的查詢,查詢出結果總數,用於計算分頁頁面總數
- 獲取將dao層傳入的分頁參數
- 將獲取分頁查詢的參數,用於重新拼裝分頁查詢語句。
- 查詢結束之後,交還程序執行流,退出攔截器
這裏仍然需要前面執行流程的知識,默認情況下,執行的是PreparedStatementHandler。因此,我們可以從PreparedStatementHandler中尋找需要的sql語句。具體見上文第3步。這裏僅給出省略內容,如下:
-
@Override
-
protected Statement instantiateStatement(Connection connection) throws SQLException {
-
String sql = boundSql.getSql();
-
-
}
由此,我們發現Sql語句隱藏在boundSql中。回頭看看,已經獲得了的StatementHandler,其具體內容我們也在上文截圖給大家。
於是,就得到了下面的代碼:
-
BoundSql boundSql = statementHandler.getBoundSql();
-
Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject();
現在,各位看官可以尖叫了!!!所有的材料已經準備齊了。
-------------------------------------------------------------------------------------------------------------------------------------
10.查詢總數對應的Mysql語句。【其他數據庫原理一致,讀者自行完成即可】
-
String sql = boundSql.getSql();
-
String countSql = "select count(*)from ("+sql+")a";
【注意:這裏我們已經獲得了傳入參數與完整的sql語句,因此,select count還有更加高效的寫法,請讀者自行完成。】
11.接下來就是執行查詢總數的語句。其總數的值用於完成分頁頁面數量控制。執行方法如下:
【回顧我們的註解,args配置】
-
Connection connection = (Connection) invocation.getArgs()[0];
-
-
PreparedStatement countStatement = connection.prepareStatement(countSql);
-
-
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
-
-
parameterHandler.setParameters(countStatement);
-
ResultSet rs = countStatement.executeQuery();
-
-
if(rs.next()){
-
page.setTotalNumber(rs.getInt(1));
-
}
到此,select count就執行完畢了。
11.分頁的查詢語句爲:【其他數據庫原理一致,讀者自行完成即可】
-
String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect();
12.查詢總數,分頁查詢的語句已經準備完成,如何替換原來的查詢語句呢?再回頭看看MetaObject對象,剛纔,我們說它提供給我們獲取或設置該對象的原本不可訪問的屬性。因此,就來利用它實現替換sql語句的功能。如下:【備註:這句話具體解釋與上文getValue一致,請參考上文即可】
-
metaObject.setValue("delegate.boundSql.sql", pageSql);
到此,我們成功的把原有的簡單查詢語句替換爲分頁查詢語句了,現在是時候將程序的控制權交還給Mybatis了,具體代碼如下:
-
return invocation.proceed();
13.最後一步,自定義的攔截器需要交給Mybatis管理,這樣才能使得Mybatis的執行與攔截器的執行結合在一起,即,攔截器需要註冊到mybatis-config配置文件中。具體內容如下:
-
<?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="jdbc.properties"/>
-
<settings>
-
<setting name="logImpl" value="LOG4J"/>
-
</settings>
-
<typeAliases>
-
<package name="com.csdn.ingo.entity"/>
-
</typeAliases>
-
<plugins>
-
<plugin interceptor="com.csdn.ingo.interceptor.PageInterceptor"></plugin>
-
</plugins>
-
<environments default="development">
-
<environment id="development">
-
<transactionManager type="JDBC" />
-
<dataSource type="POOLED">
-
<property name="driver" value="${jdbc.driverClassName}" />
-
<property name="url" value="${jdbc.url}" />
-
<property name="username" value="${jdbc.username}" />
-
<property name="password" value="${jdbc.password}" />
-
</dataSource>
-
</environment>
-
</environments>
-
<mappers>
-
<mapper resource="mappers/UserInfoMapper.xml"/>
-
</mappers>
-
</configuration>
----------------------------------------------------------------------------------------------------------------------------------
上面是所有的原理內容,我們再給出完整的代碼供大家學習
----------------------------------------------------------------------------------------------------------------------------------
14.PageInterceptor的完整內容如下:
-
package com.csdn.ingo.interceptor;
-
-
import java.sql.Connection;
-
import java.sql.PreparedStatement;
-
import java.sql.ResultSet;
-
import java.util.Map;
-
import java.util.Properties;
-
-
import org.apache.ibatis.executor.parameter.ParameterHandler;
-
import org.apache.ibatis.executor.statement.StatementHandler;
-
import org.apache.ibatis.mapping.BoundSql;
-
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.reflection.DefaultReflectorFactory;
-
import org.apache.ibatis.reflection.MetaObject;
-
import org.apache.ibatis.reflection.ReflectorFactory;
-
import org.apache.ibatis.reflection.SystemMetaObject;
-
-
import com.csdn.ingo.entity.PagePOJO;
-
-
-
-
-
-
-
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
-
public class PageInterceptor implements Interceptor{
-
-
-
-
-
public Object intercept(Invocation invocation) throws Throwable {
-
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
-
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY,new DefaultReflectorFactory());
-
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
-
String id = mappedStatement.getId();
-
if(id.matches(".+ByPage$")){
-
-
BoundSql boundSql = statementHandler.getBoundSql();
-
Map<String,Object> params = (Map<String,Object>)boundSql.getParameterObject();
-
PagePOJO page = (PagePOJO)params.get("page");
-
String sql = boundSql.getSql();
-
String countSql = "select count(*)from ("+sql+")a";
-
Connection connection = (Connection) invocation.getArgs()[0];
-
PreparedStatement countStatement = connection.prepareStatement(countSql);
-
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
-
parameterHandler.setParameters(countStatement);
-
ResultSet rs = countStatement.executeQuery();
-
if(rs.next()){
-
page.setTotalNumber(rs.getInt(1));
-
}
-
String pageSql = sql+" limit "+page.getStartIndex()+","+page.getTotalSelect();
-
metaObject.setValue("delegate.boundSql.sql", pageSql);
-
}
-
return invocation.proceed();
-
}
-
-
-
-
-
public Object plugin(Object target) {
-
return Plugin.wrap(target, this);
-
}
-
-
-
-
-
public void setProperties(Properties properties) {
-
-
}
-
}
15.Mapper.xml文件中的sql語句如下:
-
<select id="selectByPage" parameterType="Map" resultMap="UserInfoResult">
-
select * from userinfo
-
</select>
【注意:】
這裏的sql語句就是簡單查詢
傳入參數爲Map,供多參,條件查詢的等場景使用。【最好這麼使用,因爲攔截器只有1個】
16.執行單元測試方法,看看查詢結果,如下:
如果看到類似上文輸出,表明已經成功執行了分頁查詢!掌聲!!!
----------------------------------------------------------------------------------------------------------------------------------
至此, Mybatis最入門---分頁查詢(攔截器分頁原理及實現)結束
備註:
1.本文僅作爲原理解釋,實際應用時,建議各位看官最好使用更加嚴謹的第三方jar包,或者平臺性的支持。
2.上文未給出的代碼,在前文中均有給出,請讀者自行查閱前文。