在日常開發中,小伙伴們多多少少都有用過 MyBatis 插件,松哥猜測大家用的最多的就是 MyBatis 的分頁插件!不知道小伙伴們有沒有想過有一天自己也來開發一個 MyBatis 插件?
其實自己動手擼一個 MyBatis 插件并不難,今天松哥就把手帶大家擼一個 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 }}
這個接口中就三個方法,第一個方法必須實現,后面兩個方法都是可選的。三個方法作用分別如下:
<plugins> <plugin interceptor="org.javaboy.mybatis03.plugin.CamelInterceptor"> <property name="xxx" value="xxx"/> </plugin></plugins>
攔截器定義好了后,攔截誰?
這個就需要攔截器簽名來完成了!
攔截器簽名是一個名為 @Intercepts 的注解,該注解中可以通過 @Signature 配置多個簽名。@Signature 注解中則包含三個屬性:
一個簡單的簽名可能像下面這樣:
@Intercepts(@Signature( type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class}))public class CamelInterceptor implements Interceptor { //...}
根據前面的介紹,被攔截的對象主要有如下四個:
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);}
各方法含義分別如下:
ParameterHandler
public interface ParameterHandler { Object getParameterObject(); void setParameters(PreparedStatement ps) throws SQLException;}
各方法含義分別如下:
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;}
各方法含義分別如下:
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();}
各方法含義分別如下:
defaultExecutorType=”BATCH”
,執行數據操作時該方法會被調用。在開發一個具體的插件時,我們應當根據自己的需求來決定到底攔截哪個方法。
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 中方法的調用方式如下:
@Testpublic 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 自帶的內存分頁之后,接下來我們就可以來看看如何自定義分頁插件了。
首先要聲明一下,這里松哥帶大家自定義 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(); }}
這是我們今天定義的核心代碼,涉及到的知識點松哥來給大家一個一個剖析。
invocation.getArgs()
獲取攔截方法的參數,獲取到的是一個數組,正常來說這個數組的長度為 4。數組第一項是一個 MappedStatement,我們在 Mapper.xml 中定義的各種操作節點和 SQL,都被封裝成一個個的 MappedStatement 對象了;數組第二項就是所攔截方法的具體參數,也就是你在 Mapper 接口中定義的方法參數;數組的第三項是一個 RowBounds 對象,我們在 Mapper 接口中定義方法時不一定使用了 RowBounds 對象,如果我們沒有定義 RowBounds 對象,系統會給我們提供一個默認的 RowBounds.DEFAULT;數組第四項則是一個處理返回值的 ResultHandler。return invocation.proceed();
,讓方法繼續往下走就行了。在前面的代碼中,我們一共在兩個地方重新組織了 SQL,一個是查詢總記錄數的時候,另一個則是分頁的時候,都是通過 boundSql.getSql() 獲取到 Mapper.xml 中的 SQL 然后進行改裝,有的小伙伴在 Mapper.xml 中寫 SQL 的時候不注意,結尾可能加上了 ;
,這會導致分頁插件重新組裝的 SQL 運行出錯,這點需要注意。松哥在 GitHub 上看到的其他 MyBatis 分頁插件也是一樣的,Mapper.xml 中 SQL 結尾不能有 ;
。
如此之后,我們的分頁插件就算是定義成功了。
接下來我們對我們的分頁插件進行一個簡單測試。
首先我們需要在全局配置中配置分頁插件,配置方式如下:
<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>
最后我們進行測試:
@Testpublic 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 進行測試,如下:
@Testpublic 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() 方法我們就可以獲取到總記錄數。
好啦,今天主要和小伙伴們分享了我們如何自己開發一個 MyBatis 插件,插件功能其實都是次要的,最主要是希望小伙伴們能夠理解 MyBatis 的工作流程。
本文鏈接:http://www.www897cc.com/showinfo-26-80839-0.html手把手教你開發 MyBatis 分頁插件
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 什么是單元測試,它和集成測試有什么區別?