mybatis的攔截器實現(xiàn)分頁(動態(tài)代理)
攔截sql語句來實現(xiàn)分頁
1.攔截什么樣的對象(以page作為參數(shù)傳入;page對象)
2.攔截對象什么行為
3.什么時候攔截 (在prepareStatement的時候攔截)
代人買票
mybatis獲取statement其實是在statementHandler中,這是一個處理接口,有個prepare方法,返回Statement,這個方法是在BaseStatementHandler中實現(xiàn)的,statement是在instantiateStatement這個方法中獲取的,這個方法是一個抽象方法,看它的PrepareStatementHandler實現(xiàn),在這里邊看到了connection.prepareStatement(sql,PreparedStatement.),也就是和JDBC類似的代碼了,這就是分頁攔截器要攔截的位置了。如何實現(xiàn)攔截呢?mybatis提供了相應的注解:
@Intercepts({@Signature(type=StatementHandler.class,method="prepare",args={Connection.class})})
//成立代購公司 ---- implements Interceptor
public class PageInterceptor implements Interceptor {
①type指向要連接的接口class,這里指向StatementHandler.class, ②Method指向要攔截的方法,這里是prepare
③args[]攔截的方法的參數(shù)類型,這里是Connection.class
這樣就準確描述了要攔截StatementHandler接口下的prepare方法。目標確定,接下來就可以做手腳了,在PrepareStatementHandler拿到sql語句之前將這個sql語句改裝成我們的分頁sql,然后在塞回去,讓程序繼續(xù)執(zhí)行,這樣就成功了。
注意:過早過遲的攔截都不合適。所以在PreparedStatement pstmt=conn.prepareStatement(sql.toString());之前攔截即可(把SQL語句處理再放進去提交)
注冊公司 ------ plugin
申報資產(chǎn) ----- property
<plugins>
<plugin interceptor="com.imooc.interceptor.PageInterceptor">
<property name="test" value="abc"/>
</plugin>
</plugins>
使用資產(chǎn)
public class PageInterceptor implements Interceptor {
private String test;
// 執(zhí)行順序 1
@Override
public void setProperties(Properties properties) {
this.test = properties.getProperty("test");
}
識別哪些是去買票的人
(并不一定就是需要找代購的人,在正式代購的時候會更精確的定位客戶群體)
// 執(zhí)行順序 2
@Override
public Object plugin(Object target) {
System.out.println(this.test);
return Plugin.wrap(target, this);
}
plugin(Object target)方法參數(shù)就是被攔截的對象target,返回的就是滿足條件的代理類,Plugin.wrap(target,this):this也就是自定義攔截器實例,通過獲取注解得到要攔截的類型,比較target的類型與this獲取的要攔截的類型是不是一致,如果滿足條件就獲取代理對象,并執(zhí)行intercept方法,沒有獲取代理對象的將直接返回,不會經(jīng)過intercept方法。
開始代購
// 執(zhí)行順序 3
@Override
public Object intercept(Invocation invocation) throws Throwable {
//攔截器的參數(shù)(Invocation)中保存了攔截器所攔截的所有對象,根據(jù)方法簽名,這里僅僅只是對statementHandler中的關鍵信息進行處理,原理就是使用分頁的sql替換攔截到的原始sql,攔截對象類型是StatementHandler,由方法簽名決定的
StatementHandler statementHandler = (StatementHandler)invocation.getTarget();
//StatementHandler將對配置文件中的sql語句進行處理(sql語句在MappedStatement中),但是在StatementHandler中,所有的對象屬性均為受保護的以及私有的,首先想到的是通過反射讀寫信息,幸好Mybatis已經(jīng)有一個類MetaObject,有個方法 MetaObject.forObject(statementHandler,__,__)可以對注解的攔截方法簽名所對應的對象進行包裝,這樣我們得到的是被包裝的statementHandler
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY, SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY);
MappedStatement mappedStatement = (MappedStatement)metaObject.getValue("delegate.mappedStatement");
// 配置文件中SQL語句的ID
String id = mappedStatement.getId();
//定位更精準的用戶群體(嫌麻煩不愿排隊的)
if(id.matches(".+ByPage$")) {
BoundSql boundSql = statementHandler.getBoundSql();
通過BoundSql獲得原始的sql語句之后,再次使用的是BoundSql的getParameterObject()來獲取配置文件中的參數(shù),因為得到的參數(shù)是一個map,調用對象的get方法得到Page對象,得到page對象之后就可以拼接分頁sql了。metaObject.setValue(“delegate.boundSql.sql”,pageSql)修改原本不可以修改的值,修改原來的屬性值為新的sql。
mybatis通過Invocation這個參數(shù)的proceed()方法交回主權,這個方法的源碼 return method.invoke(target,args)
// 原始的SQL語句
String sql = boundSql.getSql();
// 查詢總條數(shù)的SQL語句
//這里的問題在于sql是否能執(zhí)行以及如何執(zhí)行,需要connection對象,而此對象就是方法簽名的參數(shù),可以通過invocation.getArgs()[0]獲得,然后通過connection.prepareStatement(countSql)將拼接好的sql語句進行預編譯,并執(zhí)行,就可以獲得結果,由于此結果是統(tǒng)計總數(shù)的,只有一條記錄,將此記錄轉換為int類型,并賦值給page對象。
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();
Map<?,?> parameter = (Map<?,?>)boundSql.getParameterObject();
//獲取購票信息
Page page = (Page)parameter.get("page");
if(rs.next()) {
page.setTotalNumber(rs.getInt(1));
}
// 改造后帶分頁查詢的SQL語句
//代購公司 開始購票
String pageSql = sql + " limit " + page.getDbIndex() + "," + page.getDbNumber();
metaObject.setValue("delegate.boundSql.sql", pageSql);
}
return invocation.proceed();
}
代購過程總結:
1.RoutingStatementHandler
2.通過RoutingStatementHandler對象的屬性delegate找到statement實現(xiàn)類BaseStatementHandler
3.通過BaseStatementHandler類的反射得到對象的MappedStatement對象
4.通過MappedStatement的屬性getID得到配置文件sql語句的ID
5.通過BaseStatementHandler屬性的到原始sql語句
6.拼接分頁sql(
1.需要查詢總數(shù)的sql
2.通過攔截Connection對象得到PrepareStatement對象
3.得到對應的參數(shù)
4.把參數(shù)設到prepareStatement對象里的?(該?號在配置文件以#{}形式存在,mybatis會把它轉為?號)
5.執(zhí)行sql語句
6.得到總數(shù)
)
7.把屬性值為新的sql