MyBatis攔截器自定義分頁外掛實現
MyBaits
是一個開源的優秀的持久層框架,SQL語句與程式碼分離,面向配置的程式設計,良好支援複雜資料對映,動態SQL;MyBatis
是支援定製化 SQL、儲存過程以及高階對映的優秀的持久層框架。MyBatis 避免了幾乎所有的 JDBC 程式碼和手動設定引數以及獲取結果集。MyBatis 可以對配置和原生Map使用簡單的 XML 或註解,將介面和 Java 的 POJOs(Plain Old Java Objects,普通的 Java物件)對映成資料庫中的記錄。
通常使用MyBatis
時使用以下幾種形式進行分頁:
- 邏輯分頁:RowBounds
物理分頁: 在SQL裡面使用LIMIT或者使用第三方外掛(PageHelper等)
環境及介紹
匯入核心依賴:
<!-- SpringBoot MyBatis starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
搭建環境步驟可以參考:SpringBoot企業中常用starter
本次主要實現一個介面org.apache.ibatis.plugin.Interceptor
,在介面中有3個方法為:
Object intercept(Invocation invocation) throws Throwable;
Object plugin(Object target);
void setProperties(Properties properties);
- intercept 方法是主要攔截執行方法。
- plugin 方法是決定當前物件是否需要生成代理物件。
setProperties 設定執行時mybatis核心配置引數方法。
JDK
的InvocationHandler
,當我們呼叫ParameterHandler,ResultSetHandler,StatementHandler,Executor
的物件的時候,實際上使用的是Plugin
這個代理類的物件,這個類實現了InvocationHandler介面。
當我們呼叫ParameterHandler,ResultSetHandler,StatementHandler,Executor
的物件的時候,
實際上使用的是Plugin
這個代理類的物件,這個類實現了InvocationHandler
介面。自定義分頁實現
建立Pager
實體物件,用來進行分頁,實體內容如下:
@Data
@ToString
public class Pager {
/*當前頁*/
private int page;
/*每頁大小*/
private int size;
/*總記錄*/
private long total;
/*總頁數*/
private int totalPage;
/*自定義分頁sql*/
private String customSQL;
/*分頁執行時長*/
private long executeTime;
}
這裡customSQL
變數為自定義分頁SQL,很多時候以為sql過於複雜關聯了N張表,獲取了N個欄位會造成查詢時間過於緩慢,加入這個欄位主要是為了在一些複雜的SQL中不暴力使用預設的分頁數量統計,可以自己根據SQL去除不需要的欄位,已經不需要的表連線後的SQL來執行分頁數量的統計。
定義分頁處理介面PagerHandler
,內容如下:
public interface PagerHandler {
/**
* 獲取sql執行引數
* @param boundSql
* @return
*/
public Pager getPager(BoundSql boundSql);
/**
* 執行分頁
*
* @param pager
* @param boundSql
* @param connection
* @param metaObject
* @return
* @throws SQLException
*/
public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException;
}
建立MyBats攔截器實現分頁PagerMyBatisInterceptor
,該類實現介面org.apache.ibatis.plugin.Interceptor
和我們自己定義的PagerHandler
,重寫介面中方法。
我們還需要告訴MyBatis具體在什麼地點進行攔截,使用@Intercepts
來標註:
@Intercepts(value = {
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
主要攔截StatementHandler
中prepare
編譯引數方法該方法需要傳入引數型別為Connection.class, Integer.class
,在MyBatis 3.4.1
版本下引數只有一個Connection
在類中定義一些常量:
private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class);
private final int CONNECTION_INDEX = 0; //連線引數索引
private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();//預設反射工廠
private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement";//反射值獲取路徑
private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler";//反射值獲取路徑
具體攔截器內容:
@Component
@Intercepts(value = {
@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class PagerMyBatisInterceptor implements PagerHandler, Interceptor {
private final Logger log = LoggerFactory.getLogger(PagerMyBatisInterceptor.class);
private final int CONNECTION_INDEX = 0;
private final DefaultReflectorFactory DEFAULT_REFLECTOR_FACTORY = new DefaultReflectorFactory();
private final String DELEGATE_MAPPED_STATEMENT = "delegate.mappedStatement";
private final String DELEGATE_PARAMETER_HANDLER = "delegate.parameterHandler";
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
//資料庫連線
Connection connection = (Connection) args[CONNECTION_INDEX];
//負責處理Mybatis與JDBC之間Statement的互動
StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//MetaObject是Mybatis提供的一個用於方便、優雅訪問物件屬性的物件,通過它可以簡化程式碼、不需要try/catch各種reflect異常,同時它支援對JavaBean、Collection、Map三種類型物件的操作。
MetaObject metaObject = MetaObject.forObject(statementHandler, SystemMetaObject.DEFAULT_OBJECT_FACTORY,
SystemMetaObject.DEFAULT_OBJECT_WRAPPER_FACTORY, DEFAULT_REFLECTOR_FACTORY);
//MappedStatement表示的是XML中的一個SQL
MappedStatement mappedStatement = (MappedStatement) metaObject.getValue(DELEGATE_MAPPED_STATEMENT);
//SqlCommandType代表SQL型別
SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
//BoundSql則是其儲存Sql語句的物件
BoundSql boundSql = statementHandler.getBoundSql();
//分頁物件
Pager pager = getPager(boundSql);
if (sqlCommandType.compareTo(SqlCommandType.SELECT) == 0 && pager != null) {
executer(pager, boundSql, connection, metaObject);
//執行查詢
int left = (pager.getPage() - 1) * pager.getSize();
int right = pager.getSize();
String rewriteSql = boundSql.getSql() + " LIMIT " + left + "," + right;
metaObject.setValue("boundSql.sql", rewriteSql);
}
long startTime = System.currentTimeMillis();
Object proceed = invocation.proceed();
long endTime = System.currentTimeMillis();
log.info("SQL TYPE [{}] , SQL EXECUTE TIME [{}] SQL:\n{}", sqlCommandType, startTime - endTime, boundSql.getSql().toUpperCase());
return proceed;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
//TODO 設定mybatis引數
}
@Override
public Pager getPager(BoundSql boundSql) {
Object parameterObject = boundSql.getParameterObject();
if (parameterObject instanceof Pager) {
return (Pager) parameterObject;
} else if (parameterObject instanceof Map) {
Map<String, Object> paramMap = (Map<String, Object>) parameterObject;
Iterator<String> keys = paramMap.keySet().iterator();
while (keys.hasNext()) {
String key = keys.next();
Object obj = paramMap.get(key);
if (obj instanceof Pager) {
return (Pager) obj;
}
}
}
return null;
}
@Override
public Pager executer(Pager pager, BoundSql boundSql, Connection connection, MetaObject metaObject) throws SQLException {
if (pager.getPage() == 0) {
pager.setPage(0);
}
if (pager.getSize() == 0) {
pager.setSize(0);
}
if (pager.getCustomSQL() == null) {
//如果自己沒有定義分頁SQL,那麼使用預設暴力分頁
pager.setCustomSQL("SELECT COUNT(1) FROM (" + boundSql.getSql() + " ) tmp_table");
}
// 預編譯
PreparedStatement prepareStatement = connection.prepareStatement(pager.getCustomSQL());
// 預編譯執行
ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue(DELEGATE_PARAMETER_HANDLER);
parameterHandler.setParameters(prepareStatement); // 給sql語句設定引數
long startTime = System.currentTimeMillis();
ResultSet resultSet = prepareStatement.executeQuery();
long endTime = System.currentTimeMillis();
log.info("sql execute time {} sql:\n{}", startTime - endTime, pager.getCustomSQL().toUpperCase());
if (resultSet.next()) {
long total = (long) resultSet.getObject(1);// 總記錄數量
int totalPageNum = (int) ((total + pager.getSize() - 1) / pager.getSize());
pager.setTotal(total);
pager.setTotalPage(totalPageNum);
pager.setExecuteTime(startTime - endTime);
}
return pager;
}
}
通過方法getPager
獲取到pager物件,如果是Map引數那麼就優先第一個通過引用的傳遞在BoundSql
物件中獲取到,然後執行分頁獲取裡面的值進行計算,通過引用物件返回總記錄數,總頁數等。
在SpringBoot中如果需要使攔截器生效只需要在型別使用@Component
將該類交給Spring IOC
管理即可,至於攔截器順序如:
有攔截器 PagerMyBatisInterceptor
與 OneInterceptor
,想要分頁攔截器作為第二個攔截器只需要在類上標註@ConditionalOnBean(OneInterceptor)
即可,在第一個攔截器例項化後再例項化第二個攔截器.
Pager使用方式
List<Map<String, Object>> selectUser(Pager pager);
List<Map<String, Object>> selectUser(Map<String,Object> paramMap);
建立一個Pager物件傳入即可。
本文原始碼地址:https://github.com/450255266/open-doubi/tree/master/spring-boot/custom-mybatis-pa