MyBatis實現(xiàn)自定義MyBatis插件的流程詳解
初識插件
我們在執(zhí)行查詢的時候,如果sql沒有加上分頁條件,數據量過大的話會造成內存溢出,因此我們可以通過MyBatis提供的插件機制來攔截sql,并進行sql改寫。MyBatis的插件是通過動態(tài)代理來實現(xiàn)的,并且會形成一個插件鏈。原理類似于攔截器,攔截我們需要處理的對象,進行自定義邏輯后,返回一個代理對象,進行下一個攔截器的處理。
我們先來看下一個簡單插件的模板,首先要實現(xiàn)一個Interceptor接口,并實現(xiàn)三個方法。并加上@Intercepts注解。接下來我們以分頁插件為例將對每個細節(jié)進行講解。
/** * @ClassName : PagePlugin * @Description : 分頁插件 * @Date: 2020/12/29 */ @Intercepts({}) public class PagePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.properties = properties; } }
攔截對象
在進行插件創(chuàng)建的時候,需要指定攔截對象。@Intercepts
注解指定需要攔截的方法簽名,內容是個Signature
類型的數組,而Signature
就是對攔截對象的描述。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface Intercepts { /** * Returns method signatures to intercept. * * @return method signatures */ Signature[] value(); }
Signature 需要指定攔截對象中方法的信息的描述。
@Documented @Retention(RetentionPolicy.RUNTIME) @Target({}) public @interface Signature { /** * 對象類型 */ Class<?> type(); /** * 方法名 */ String method(); /** * 參數類型 */ Class<?>[] args(); }
在MyBatis中,我們只能對以下四種類型的對象進行攔截
- ParameterHandler : 對sql參數進行處理
- ResultSetHandler : 對結果集對象進行處理
- StatementHandler : 對sql語句進行處理
- Executor : 執(zhí)行器,執(zhí)行增刪改查
現(xiàn)在我們需要對sql進行改寫,因此可以需要攔截Executor的query方法進行攔截
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class})})
攔截實現(xiàn)
每個插件除了指定攔截的方法后,還需要實現(xiàn)Interceptor
接口。Interceptor
接口有以下三個方法。其中intercept是我們必須要實現(xiàn)的方法,在這里面我們需要實現(xiàn)自定義邏輯。其它兩個方法給出了默認實現(xiàn)。
public interface Interceptor { /** * 進行攔截處理 * @param invocation * @return * @throws Throwable */ Object intercept(Invocation invocation) throws Throwable; /** * 返回代理對象 * @param target * @return */ default Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 設置配置屬性 * @param properties */ default void setProperties(Properties properties) { // NOP } }
因此我們實現(xiàn)intercept方法即可,因為我們要改寫查詢sql語句,因此需要攔截Executor的query方法,然后修改RowBounds參數中的limit
,如果limit大于1000,我們強制設置為1000。
@Slf4j @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})}) public class PagePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); RowBounds rowBounds = (RowBounds)args[2]; log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds)); if(rowBounds != null){ if(rowBounds.getLimit() > 1000){ Field field = rowBounds.getClass().getDeclaredField("limit"); field.setAccessible(true); field.set(rowBounds, 1000); } }else{ rowBounds = new RowBounds(0 ,100); args[2] = rowBounds; } log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds)); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.properties = properties; } }
加載流程
以上我們已經實現(xiàn)了一個簡單的插件,在執(zhí)行查詢的時候對query方法進行攔截,并且修改分頁參數。但是我們現(xiàn)在還沒有進行插件配置,只有配置了插件,MyBatis才能啟動過程中加載插件。
xml配置插件
在mybatis-config.xml
中添加plugins標簽,并且配置我們上面實現(xiàn)的plugin
<plugins> <plugin interceptor="com.example.demo.mybatis.PagePlugin"> </plugin> </plugins>
XMLConfigBuilder加載插件
在啟動流程中加載插件中使用到SqlSessionFactoryBuilder的build方法,其中XMLConfigBuilder這個解析器中的parse()方法就會讀取plugins標簽下的插件,并加載Configuration中的InterceptorChain中。
// SqlSessionFactoryBuilder public SqlSessionFactory build(InputStream inputStream, String environment, Properties properties) { SqlSessionFactory var5; try { XMLConfigBuilder parser = new XMLConfigBuilder(inputStream, environment, properties); var5 = this.build(parser.parse()); } catch (Exception var14) { throw ExceptionFactory.wrapException("Error building SqlSession.", var14); } finally { ErrorContext.instance().reset(); try { inputStream.close(); } catch (IOException var13) { } } return var5; }
可見XMLConfigBuilder這個parse()方法就是解析xml中配置的各個標簽。
// XMLConfigBuilder public Configuration parse() { if (parsed) { throw new BuilderException("Each XMLConfigBuilder can only be used once."); } parsed = true; parseConfiguration(parser.evalNode("/configuration")); return configuration; } private void parseConfiguration(XNode root) { try { // issue #117 read properties first // 解析properties節(jié)點 propertiesElement(root.evalNode("properties")); Properties settings = settingsAsProperties(root.evalNode("settings")); loadCustomVfs(settings); loadCustomLogImpl(settings); typeAliasesElement(root.evalNode("typeAliases")); // 記載插件 pluginElement(root.evalNode("plugins")); objectFactoryElement(root.evalNode("objectFactory")); objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); reflectorFactoryElement(root.evalNode("reflectorFactory")); settingsElement(settings); // read it after objectFactory and objectWrapperFactory issue #631 environmentsElement(root.evalNode("environments")); databaseIdProviderElement(root.evalNode("databaseIdProvider")); typeHandlerElement(root.evalNode("typeHandlers")); mapperElement(root.evalNode("mappers")); } catch (Exception e) { throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); } } XMLConfigBuilder 的pluginElement就是遍歷plugins下的plugin加載到interceptorChain中。 // XMLConfigBuilder private void pluginElement(XNode parent) throws Exception { if (parent != null) { // 遍歷每個plugin插件 for (XNode child : parent.getChildren()) { // 讀取插件的實現(xiàn)類 String interceptor = child.getStringAttribute("interceptor"); // 讀取插件配置信息 Properties properties = child.getChildrenAsProperties(); // 創(chuàng)建interceptor對象 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).getDeclaredConstructor().newInstance(); interceptorInstance.setProperties(properties); // 加載到interceptorChain鏈中 configuration.addInterceptor(interceptorInstance); } } }
InterceptorChain
是一個interceptor集合
,相當于是一層層包裝,后一個插件就是對前一個插件的包裝,并返回一個代理對象。
public class InterceptorChain { private final List<Interceptor> interceptors = new ArrayList<>(); // 生成代理對象 public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; } // 將插件加到集合中 public void addInterceptor(Interceptor interceptor) { interceptors.add(interceptor); } public List<Interceptor> getInterceptors() { return Collections.unmodifiableList(interceptors); } }
創(chuàng)建插件對象
因為我們需要對攔截對象進行攔截,并進行一層包裝返回一個代理類,那是什么時候進行處理的呢?以Executor為例,在創(chuàng)建Executor對象的時候,會有以下代碼。
// Configuration public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; if (ExecutorType.BATCH == executorType) { executor = new BatchExecutor(this, transaction); } else if (ExecutorType.REUSE == executorType) { executor = new ReuseExecutor(this, transaction); } else { executor = new SimpleExecutor(this, transaction); } if (cacheEnabled) { executor = new CachingExecutor(executor); } // 創(chuàng)建插件對象 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
創(chuàng)建完Executor對象后,就會調用interceptorChain.pluginAll()
方法,實際調用的是每個Interceptor的plugin()方法
。plugin()就是對目標對象的一個代理,并且生成一個代理對象返回。而Plugin.wrap()
就是進行包裝的操作。
// Interceptor /** * 返回代理對象 * @param target * @return */ default Object plugin(Object target) { return Plugin.wrap(target, this); }
Plugin的wrap()主要進行了以下步驟:
- 獲取攔截器攔截的方法,以攔截對象為key,攔截方法集合為value
- 獲取目標對象的class對,比如Executor對象
- 如果攔截器中攔截的對象包含目標對象實現(xiàn)的接口,則返回攔截的接口
- 創(chuàng)建代理類Plugin對象,Plugin實現(xiàn)了
InvocationHandler
接口,最終對目標對象的調用都會調用Plugin的invocate
方法。
// Plugin public static Object wrap(Object target, Interceptor interceptor) { // 獲取攔截器攔截的方法,以攔截對象為key,攔截方法為value Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲取目標對象的class對象 Class<?> type = target.getClass(); // 如果攔截器中攔截的對象包含目標對象實現(xiàn)的接口,則返回攔截的接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); // 如果對目標對象進行了攔截 if (interfaces.length > 0) { // 創(chuàng)建代理類Plugin對象 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; }
例子
我們已經了解MyBatis插件的配置,創(chuàng)建,實現(xiàn)流程,接下來就以一開始我們提出的例子來介紹實現(xiàn)一個插件應該做哪些。
確定攔截對象
因為我們要對查詢sql分頁參數進行改寫,因此可以攔截Executor的query方法,并進行分頁參數的改寫
@Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})})
實現(xiàn)攔截接口
實現(xiàn)Interceptor
接口,并且實現(xiàn)intercept
實現(xiàn)我們的攔截邏輯
@Slf4j @Intercepts({@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class , ResultHandler.class})}) public class PagePlugin implements Interceptor { private Properties properties; @Override public Object intercept(Invocation invocation) throws Throwable { Object[] args = invocation.getArgs(); RowBounds rowBounds = (RowBounds)args[2]; log.info("執(zhí)行前, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds)); if(rowBounds != null){ if(rowBounds.getLimit() > 1000){ Field field = rowBounds.getClass().getDeclaredField("limit"); field.setAccessible(true); field.set(rowBounds, 1000); } }else{ rowBounds = new RowBounds(0 ,100); args[2] = rowBounds; } log.info("執(zhí)行后, rowBounds = [{}]", JSONUtil.toJsonStr(rowBounds)); return invocation.proceed(); } @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(Properties properties) { this.properties = properties; } }
配置插件
在mybatis-config.xml
中配置以下插件
<plugins> <plugin interceptor="com.example.demo.mybatis.PagePlugin"> </plugin> </plugins>
測試
TTestUserMapper.java
新增selectByPage方法
List<TTestUser> selectByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);
mapper/TTestUserMapper.xml
新增對應的sql
<select id="selectByPage" resultMap="BaseResultMap"> select <include refid="Base_Column_List" /> from t_test_user <if test="offset != null"> limit #{offset}, #{pageSize} </if> </select>
最終測試代碼,我們沒有在查詢的時候指定分頁參數。
public static void main(String[] args) { try { // 1. 讀取配置 InputStream inputStream = Resources.getResourceAsStream("mybatis-config.xml"); // 2. 創(chuàng)建SqlSessionFactory工廠 SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); // 3. 獲取sqlSession SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.SIMPLE); // 4. 獲取Mapper TTestUserMapper userMapper = sqlSession.getMapper(TTestUserMapper.class); // 5. 執(zhí)行接口方法 List<TTestUser> list2 = userMapper.selectByPage(null, null); System.out.println("list2="+list2.size()); // 6. 提交事物 sqlSession.commit(); // 7. 關閉資源 sqlSession.close(); inputStream.close(); } catch (Exception e){ log.error(e.getMessage(), e); } }
最終打印的日志如下,我們可以看到rowBounds
已經被我們強制修改了只能查處1000條數據。
10:11:49.313 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行前, rowBounds = [{"offset":0,"limit":2147483647}] 10:11:58.015 [main] INFO com.example.demo.mybatis.PagePlugin - 執(zhí)行后, rowBounds = [{"offset":0,"limit":1000}] 10:12:03.211 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection 10:12:04.269 [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 749981943. 10:12:04.270 [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Setting autocommit to false on JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@2cb3d0f7] 10:12:04.283 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Preparing: select id, member_id, real_name, nickname, date_create, date_update, deleted from t_test_user 10:12:04.335 [main] DEBUG com.example.demo.dao.TTestUserMapper.selectByPage - ==> Parameters: list2=1000
以上就是MyBatis實現(xiàn)自定義MyBatis插件的流程詳解的詳細內容,更多關于MyBatis自定義MyBatis插件的資料請關注腳本之家其它相關文章!
相關文章
ProtoStuff不支持BigDecimal序列化及反序列化詳解
這篇文章主要為大家介紹了ProtoStuff不支持BigDecimal序列化/反序列化,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-08-08SpringCloud?OpenFeign?服務調用傳遞?token的場景分析
這篇文章主要介紹了SpringCloud?OpenFeign?服務調用傳遞?token的場景分析,本篇文章簡單介紹?OpenFeign?調用傳遞?header?,以及多線程環(huán)境下可能會出現(xiàn)的問題,其中涉及到?ThreadLocal?的相關知識,需要的朋友可以參考下2022-07-07