關于Mybatis插件,大部分人都知道,也都使用過,但很多時候,我們僅僅是停留在表面上,知道Mybatis插件可以在DAO層進行攔截,如列印執行的SQL語句日志,做一些權限控制,分頁等功能;
但對其内部實作機制,涉及的軟體設計模式,程式設計思想往往沒有深入的了解。
本篇案例将幫助讀者對Mybatis插件的使用場景,實作機制,以及其中涉及的程式設計思想進行一個小結,希望對以後的程式設計開發工作有所幫助。
Mybatis插件适用場景:
- 分頁功能
mybatis的分頁預設是基于記憶體分頁的(查出所有,再截取),資料量大的情況下效率較低,不過使用mybatis插件可以改變該行為,隻需要攔截StatementHandler類的prepare方法,改變要執行的SQL語句為分頁語句即可;
- 公共字段統一指派
一般業務系統都會有建立者,建立時間,修改者,修改時間四個字段,對于這四個字段的指派,實際上可以在DAO層統一攔截處理,可以用mybatis插件攔截Executor類的update方法,對相關參數進行統一指派即可;
- 性能監控
對于SQL語句執行的性能監控,可以通過攔截Executor類的update, query等方法,用日志記錄每個方法執行的時間;
- 其它
其實mybatis擴充性還是很強的,基于插件機制,基本上可以控制SQL執行的各個階段,如執行階段,參數處理階段,文法建構階段,結果集處理階段,具體可以根據項目業務來實作對應業務邏輯。
Mybatis插件實際上就是一個攔截器,應用代理模式,在方法級别上進行攔截。
1.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<E> 的查詢方法),一般來說,如果我們想對查詢結果進行二次處理,可以通過攔截該方法實作。
- handleCursorResultSets:當查詢方法的傳回值類型為 Cursor<E> 時,該方法會被觸發。
- 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 自定義分頁插件
參考:
https://segmentfault.com/a/1190000039305062?utm_source=tag-newest
https://www.cnblogs.com/chenpi/p/10498921.html
https://www.jianshu.com/p/cdff18f8cb97
https://segmentfault.com/a/1190000021220956