小伙伴們元宵節快樂,記得吃元宵哦~
在日常開發中,小伙伴們多多少少都有用過 MyBatis 插件,松哥猜測大家用的最多的就是 MyBatis 的分頁插件!不知道小伙伴們有沒有想過有一天自己也來開發一個 MyBatis 插件?
其實自己動手擼一個 MyBatis 插件并不難,今天松哥就把手帶大家擼一個 MyBatis 插件!
1.MyBatis 插件介面
即使你沒開發過 MyBatis 插件,估計也能猜出來,MyBatis 插件是通過攔截器來起作用的,MyBatis 框架在設計的時候,就已經為插件的開發預留了相關介面,如下:
public interface Interceptor {
Object intercept(Invocation invocation) throws Throwable;
default Object plugin(Object target) {
return Plugin.wrap(target, this);
}
default void setProperties(Properties properties) {
// NOP
}
}
這個介面中就三個方法,第一個方法必須實作,后面兩個方法都是可選的,三個方法作用分別如下:
-
intercept:這個就是具體的攔截方法,我們自定義 MyBatis 插件時,一般都需要重寫該方法,我們插件所完成的作業也都是在該方法中完成的,
-
plugin:這個方法的引數 target 就是攔截器要攔截的物件,一般來說我們不需要重寫該方法,Plugin.wrap 方法會自動判斷攔截器的簽名和被攔截物件的介面是否匹配,如果匹配,才會通過動態代理攔截目標物件,
-
setProperties:這個方法用來傳遞插件的引數,可以通過引數來改變插件的行為,我們定義好插件之后,需要對插件進行配置,在配置的時候,可以給插件設定相關屬性,設定的屬性可以通過該方法獲取到,插件屬性設定像下面這樣:
<plugins>
<plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor">
<property name="xxx" value="xxx"/>
</plugin>
</plugins>
2.MyBatis 攔截器簽名
攔截器定義好了后,攔截誰?
這個就需要攔截器簽名來完成了!
攔截器簽名是一個名為 @Intercepts 的注解,該注解中可以通過 @Signature 配置多個簽名,@Signature 注解中則包含三個屬性:
- type: 攔截器需要攔截的介面,有 4 個可選項,分別是:Executor、ParameterHandler、ResultSetHandler 以及 StatementHandler,
- method: 攔截器所攔截介面中的方法名,也就是前面四個介面中的方法名,介面和方法要對應上,
- args: 攔截器所攔截方法的引數型別,通過方法名和引數型別可以鎖定唯一一個方法,
一個簡單的簽名可能像下面這樣:
@Intercepts(@Signature(
type = ResultSetHandler.class,
method = "handleResultSets",
args = {Statement.class}
))
public class CamelInterceptor implements Interceptor {
//...
}
3.被攔截的物件
根據前面的介紹,被攔截的物件主要有如下四個:
Executor
public interface Executor {
ResultHandler NO_RESULT_HANDLER = null;
int update(MappedStatement ms, Object parameter) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException;
<E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException;
<E> Cursor<E> queryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds) throws SQLException;
List<BatchResult> flushStatements() throws SQLException;
void commit(boolean required) throws SQLException;
void rollback(boolean required) throws SQLException;
CacheKey createCacheKey(MappedStatement ms, Object parameterObject, RowBounds rowBounds, BoundSql boundSql);
boolean isCached(MappedStatement ms, CacheKey key);
void clearLocalCache();
void deferLoad(MappedStatement ms, MetaObject resultObject, String property, CacheKey key, Class<?> targetType);
Transaction getTransaction();
void close(boolean forceRollback);
boolean isClosed();
void setExecutorWrapper(Executor executor);
}
各方法含義分別如下:
- update:該方法會在所有的 INSERT、 UPDATE、 DELETE 執行時被呼叫,如果想要攔截這些操作,可以通過該方法實作,
- query:該方法會在 SELECT 查詢方法執行時被呼叫,方法引數攜帶了很多有用的資訊,如果需要獲取,可以通過該方法實作,
- queryCursor:當 SELECT 的回傳型別是 Cursor 時,該方法會被呼叫,
- flushStatements:當 SqlSession 方法呼叫 flushStatements 方法或執行的介面方法中帶有 @Flush 注解時該方法會被觸發,
- commit:當 SqlSession 方法呼叫 commit 方法時該方法會被觸發,
- rollback:當 SqlSession 方法呼叫 rollback 方法時該方法會被觸發,
- getTransaction:當 SqlSession 方法獲取資料庫連接時該方法會被觸發,
- close:該方法在懶加載獲取新的 Executor 后會被觸發,
- isClosed:該方法在懶加載執行查詢前會被觸發,
ParameterHandler
public interface ParameterHandler {
Object getParameterObject();
void setParameters(PreparedStatement ps) throws SQLException;
}
各方法含義分別如下:
- getParameterObject:在執行存盤程序處理出參的時候該方法會被觸發,
- setParameters:設定 SQL 引數時該方法會被觸發,
ResultSetHandler
public interface ResultSetHandler {
<E> List<E> handleResultSets(Statement stmt) throws SQLException;
<E> Cursor<E> handleCursorResultSets(Statement stmt) throws SQLException;
void handleOutputParameters(CallableStatement cs) throws SQLException;
}
各方法含義分別如下:
- handleResultSets:該方法會在所有的查詢方法中被觸發(除去回傳值型別為 Cursor 的查詢方法),一般來說,如果我們想對查詢結果進行二次處理,可以通過攔截該方法實作,
- handleCursorResultSets:當查詢方法的回傳值型別為 Cursor 時,該方法會被觸發,
- handleOutputParameters:使用存盤程序處理出參的時候該方法會被呼叫,
StatementHandler
public interface StatementHandler {
Statement prepare(Connection connection, Integer transactionTimeout)
throws SQLException;
void parameterize(Statement statement)
throws SQLException;
void batch(Statement statement)
throws SQLException;
int update(Statement statement)
throws SQLException;
<E> List<E> query(Statement statement, ResultHandler resultHandler)
throws SQLException;
<E> Cursor<E> queryCursor(Statement statement)
throws SQLException;
BoundSql getBoundSql();
ParameterHandler getParameterHandler();
}
各方法含義分別如下:
- prepare:該方法在資料庫執行前被觸發,
- parameterize:該方法在 prepare 方法之后執行,用來處理引數資訊,
- batch:如果 MyBatis 的全劇配置中配置了
defaultExecutorType=”BATCH”,執行資料操作時該方法會被呼叫, - update:更新操作時該方法會被觸發,
- query:該方法在 SELECT 方法執行時會被觸發,
- queryCursor:該方法在 SELECT 方法執行時,并且回傳值為 Cursor 時會被觸發,
在開發一個具體的插件時,我們應當根據自己的需求來決定到底攔截哪個方法,
4.開發分頁插件
4.1 記憶體分頁
MyBatis 中提供了一個不太好用的記憶體分頁功能,就是一次性把所有資料都查詢出來,然后在記憶體中進行分頁處理,這種分頁方式效率很低,基本上沒啥用,但是如果我們想要自定義分頁插件,就需要對這種分頁方式有一個簡單了解,
記憶體分頁的使用方式如下,首先在 Mapper 中添加 RowBounds 引數,如下:
public interface UserMapper {
List<User> getAllUsersByPage(RowBounds rowBounds);
}
然后在 XML 檔案中定義相關 SQL:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
select * from user
</select>
可以看到,在 SQL 定義時,壓根不用管分頁的事情,MyBatis 會查詢到所有的資料,然后在記憶體中進行分頁處理,
Mapper 中方法的呼叫方式如下:
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
RowBounds rowBounds = new RowBounds(1,2);
List<User> list = userMapper.getAllUsersByPage(rowBounds);
for (User user : list) {
System.out.println("user = " + user);
}
}
構建 RowBounds 時傳入兩個引數,分別是 offset 和 limit,對應分頁 SQL 中的兩個引數,也可以通過 RowBounds.DEFAULT 的方式構建一個 RowBounds 實體,這種方式構建出來的 RowBounds 實體,offset 為 0,limit 則為 Integer.MAX_VALUE,也就相當于不分頁,
這就是 MyBatis 中提供的一個很不實用的記憶體分頁功能,
了解了 MyBatis 自帶的記憶體分頁之后,接下來我們就可以來看看如何自定義分頁插件了,
4.2 自定義分頁插件
首先要宣告一下,這里松哥帶大家自定義 MyBatis 分頁插件,主要是想通過這個東西讓小伙伴們了解自定義 MyBatis 插件的一些條條框框,了解整個自定義插件的流程,分頁插件并不是我們的目的,自定義分頁插件只是為了讓大家的學習程序變得有趣一些而已,
接下來我們就來開啟自定義分頁插件之旅,
首先我們需要自定義一個 RowBounds,因為 MyBatis 原生的 RowBounds 是記憶體分頁,并且沒有辦法獲取到總記錄數(一般分頁查詢的時候我們還需要獲取到總記錄數),所以我們自定義 PageRowBounds,對原生的 RowBounds 功能進行增強,如下:
public class PageRowBounds extends RowBounds {
private Long total;
public PageRowBounds(int offset, int limit) {
super(offset, limit);
}
public PageRowBounds() {
}
public Long getTotal() {
return total;
}
public void setTotal(Long total) {
this.total = total;
}
}
可以看到,我們自定義的 PageRowBounds 中增加了 total 欄位,用來保存查詢的總記錄數,
接下來我們自定義攔截器 PageInterceptor,如下:
@Intercepts(@Signature(
type = Executor.class,
method = "query",
args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}
))
public class PageInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameterObject = args[1];
RowBounds rowBounds = (RowBounds) args[2];
if (rowBounds != RowBounds.DEFAULT) {
Executor executor = (Executor) invocation.getTarget();
BoundSql boundSql = ms.getBoundSql(parameterObject);
Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
additionalParametersField.setAccessible(true);
Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
if (rowBounds instanceof PageRowBounds) {
MappedStatement countMs = newMappedStatement(ms, Long.class);
CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql);
String countSql = "select count(*) from (" + boundSql.getSql() + ") temp";
BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List<Object> countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], countKey, countBoundSql);
Long count = (Long) countQueryResult.get(0);
((PageRowBounds) rowBounds).setTotal(count);
}
CacheKey pageKey = executor.createCacheKey(ms, parameterObject, rowBounds, boundSql);
pageKey.update("RowBounds");
String pageSql = boundSql.getSql() + " limit " + rowBounds.getOffset() + "," + rowBounds.getLimit();
BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject);
Set<String> keySet = additionalParameters.keySet();
for (String key : keySet) {
pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
}
List list = executor.query(ms, parameterObject, RowBounds.DEFAULT, (ResultHandler) args[3], pageKey, pageBoundSql);
return list;
}
//不需要分頁,直接回傳結果
return invocation.proceed();
}
private MappedStatement newMappedStatement(MappedStatement ms, Class<Long> longClass) {
MappedStatement.Builder builder = new MappedStatement.Builder(
ms.getConfiguration(), ms.getId() + "_count", ms.getSqlSource(), ms.getSqlCommandType()
);
ResultMap resultMap = new ResultMap.Builder(ms.getConfiguration(), ms.getId(), longClass, new ArrayList<>(0)).build();
builder.resource(ms.getResource())
.fetchSize(ms.getFetchSize())
.statementType(ms.getStatementType())
.timeout(ms.getTimeout())
.parameterMap(ms.getParameterMap())
.resultSetType(ms.getResultSetType())
.cache(ms.getCache())
.flushCacheRequired(ms.isFlushCacheRequired())
.useCache(ms.isUseCache())
.resultMaps(Arrays.asList(resultMap));
if (ms.getKeyProperties() != null && ms.getKeyProperties().length > 0) {
StringBuilder keyProperties = new StringBuilder();
for (String keyProperty : ms.getKeyProperties()) {
keyProperties.append(keyProperty).append(",");
}
keyProperties.delete(keyProperties.length() - 1, keyProperties.length());
builder.keyProperty(keyProperties.toString());
}
return builder.build();
}
}
這是我們今天定義的核心代碼,涉及到的知識點松哥來給大家一個一個剖析,
- 首先通過 @Intercepts 注解配置攔截器簽名,從 @Signature 的定義中我們可以看到,攔截的是 Executor#query 方法,該方法有一個多載方法,通過 args 指定了方法引數,進而鎖定了多載方法(實際上該方法的另一個多載方法我們沒法攔截,那個是 MyBatis 內部呼叫的,這里不做討論),
- 將查詢操作攔截下來之后,接下來我們的操作主要在 PageInterceptor#intercept 方法中完成,該方法的引數重包含了攔截物件的諸多資訊,
- 通過
invocation.getArgs()獲取攔截方法的引數,獲取到的是一個陣列,正常來說這個陣列的長度為 4,陣列第一項是一個 MappedStatement,我們在 Mapper.xml 中定義的各種操作節點和 SQL,都被封裝成一個個的 MappedStatement 物件了;陣列第二項就是所攔截方法的具體引數,也就是你在 Mapper 介面中定義的方法引數;陣列的第三項是一個 RowBounds 物件,我們在 Mapper 介面中定義方法時不一定使用了 RowBounds 物件,如果我們沒有定義 RowBounds 物件,系統會給我們提供一個默認的 RowBounds.DEFAULT;陣列第四項則是一個處理回傳值的 ResultHandler, - 接下來判斷上一步提取到的 rowBounds 物件是否不為 RowBounds.DEFAULT,如果為 RowBounds.DEFAULT,說明用戶不想分頁;如果不為 RowBounds.DEFAULT,則說明用戶想要分頁,如果用戶不想分頁,則直接執行最后的
return invocation.proceed();,讓方法繼續往下走就行了, - 如果需要進行分頁,則先從 invocation 物件中取出執行器 Executor、BoundSql 以及通過反射拿出來 BoundSql 中保存的額外引數(如果我們使用了動態 SQL,可能會存在該引數),BoundSql 中封裝了我們執行的 Sql 以及相關的引數,
- 接下來判斷 rowBounds 是否是 PageRowBounds 的實體,如果是,說明除了分頁查詢,還想要查詢總記錄數,如果不是,則說明 rowBounds 可能是 RowBounds 實體,此時只要分頁即可,不用查詢總記錄數,
- 如果需要查詢總記錄數,則首先呼叫 newMappedStatement 方法構造出一個新的 MappedStatement 物件出來,這個新的 MappedStatement 物件的回傳值是 Long 型別的,然后分別創建查詢的 CacheKey、拼接查詢的 countSql,再根據 countSql 構建出 countBoundSql,再將額外引數添加進 countBoundSql 中,最后通過 executor.query 方法完成查詢操作,并將查詢結果賦值給 PageRowBounds 中的 total 屬性,
- 接下來進行分頁查詢,有了第七步的介紹之后,分頁查詢就很簡單了,這里就不細說了,唯一需要強調的是,當我們啟動了這個分頁插件之后,MyBatis 原生的 RowBounds 記憶體分頁會變成物理分頁,原因就在這里我們修改了查詢 SQL,
- 最后將查詢結果回傳,
在前面的代碼中,我們一共在兩個地方重新組織了 SQL,一個是查詢總記錄數的時候,另一個則是分頁的時候,都是通過 boundSql.getSql() 獲取到 Mapper.xml 中的 SQL 然后進行改裝,有的小伙伴在 Mapper.xml 中寫 SQL 的時候不注意,結尾可能加上了 ;,這會導致分頁插件重新組裝的 SQL 運行出錯,這點需要注意,松哥在 GitHub 上看到的其他 MyBatis 分頁插件也是一樣的,Mapper.xml 中 SQL 結尾不能有 ;,
如此之后,我們的分頁插件就算是定義成功了,
5.測驗
接下來我們對我們的分頁插件進行一個簡單測驗,
首先我們需要在全域配置中配置分頁插件,配置方式如下:
<plugins>
<plugin interceptor="org.javaboy.mybatis03.plugin.PageInterceptor"></plugin>
</plugins>
接下來我們在 Mapper 中定義查詢介面:
public interface UserMapper {
List<User> getAllUsersByPage(RowBounds rowBounds);
}
接下來定義 UserMapper.xml,如下:
<select id="getAllUsersByPage" resultType="org.javaboy.mybatis03.model.User">
select * from user
</select>
最后我們進行測驗:
@Test
public void test3() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
List<User> list = userMapper.getAllUsersByPage(new RowBounds(1,2));
for (User user : list) {
System.out.println("user = " + user);
}
}
這里在查詢時,我們使用了 RowBounds 物件,就只會進行分頁,而不會統計總記錄數,需要注意的時,此時的分頁已經不是記憶體分頁,而是物理分頁了,這點我們從列印出來的 SQL 中也能看到,如下:

可以看到,查詢的時候就已經進行了分頁了,
當然,我們也可以使用 PageRowBounds 進行測驗,如下:
@Test
public void test4() {
UserMapper userMapper = sqlSessionFactory.openSession().getMapper(UserMapper.class);
PageRowBounds pageRowBounds = new PageRowBounds(1, 2);
List<User> list = userMapper.getAllUsersByPage(pageRowBounds);
for (User user : list) {
System.out.println("user = " + user);
}
System.out.println("pageRowBounds.getTotal() = " + pageRowBounds.getTotal());
}
此時通過 pageRowBounds.getTotal() 方法我們就可以獲取到總記錄數,
6.小結
好啦,今天主要和小伙伴們分享了我們如何自己開發一個 MyBatis 插件,插件功能其實都是次要的,最主要是希望小伙伴們能夠理解 MyBatis 的作業流程,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/265439.html
標籤:其他
上一篇:Codeforces Global Round 13 D. Zookeeper and The Infinite Zoo(思維,位運算)
