本章主要描述 MyBatis 插件模塊的原理,從以下兩點出發:
- MyBatis 是如何加載插件配置的?
- MyBatis 是如何實現用戶使用自定義攔截器對 SQL 語句執行過程中的某一點進行攔截的?
示例準備
首先準備兩個攔截器示例,代碼如下。
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})}) public class AInterceptor implements Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(AInterceptor.class); /** * 執行攔截邏輯的方法 * * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { LOGGER.info("--------------執行攔截器A前--------------"); Object obj = invocation.proceed(); LOGGER.info("--------------執行攔截器A後--------------"); return obj; } /** * 決定是否觸發intercept()方法 * * @param target * @return */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 根據配置初始化Interceptor對象 * * @param properties */ @Override public void setProperties(Properties properties) { } }
@Intercepts({ @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}), @Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class})}) public class BInterceptor implements Interceptor { private static final Logger LOGGER = LoggerFactory.getLogger(BInterceptor.class); /** * 執行攔截邏輯的方法 * * @param invocation * @return * @throws Throwable */ @Override public Object intercept(Invocation invocation) throws Throwable { LOGGER.info("--------------執行攔截器B前--------------"); Object obj = invocation.proceed(); LOGGER.info("--------------執行攔截器B後--------------"); return obj; } /** * 決定是否觸發intercept()方法 * * @param target * @return */ @Override public Object plugin(Object target) { return Plugin.wrap(target, this); } /** * 根據配置初始化Interceptor對象 * * @param properties */ @Override public void setProperties(Properties properties) { } }
MyBatis 配置文件 mybatis-config.xml 增加 plugin 配置。
<plugins> <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.AInterceptor"/> <plugin interceptor="com.yjw.mybatis.test.mybatis.plugin.BInterceptor"/> </plugins>
加載插件配置
在 MyBatis 初始化時,會通過 XMLConfigBuilder#pluginElement 方法解析 mybatis-config.xml 配置文件中定義的 <plugin> 節點,得到相應的 Interceptor 對象,最後將 Interceptor 對象添加到 Configuration.interceptorChain 字段中保存。源碼如下所示。
private void pluginElement(XNode parent) throws Exception { if (parent != null) { for (XNode child : parent.getChildren()) { String interceptor = child.getStringAttribute("interceptor"); Properties properties = child.getChildrenAsProperties(); // 創建Interceptor對象 Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); interceptorInstance.setProperties(properties); // 保存到Configuration.interceptorChain字段中 configuration.addInterceptor(interceptorInstance); } } } public void addInterceptor(Interceptor interceptor) { interceptorChain.addInterceptor(interceptor); }
攔截過程
繼續介紹 MyBatis 的攔截器如何對 Exector、StatementHandler、ParameterHandler、ResultSetHandler 進行攔截。
在 MyBatis 中使用的這四類對象,都是通過 Configuration 創建的,方法如下圖所示。如果配置了自定義攔截器,則會在該系列方法中,通過 InterceptorChain.pluginAll() 方法爲目標對象創建代理對象,所以通過 Configuration.new*() 系列方法得到的對象實際是一個代理對象。
以 newExecutor() 方法爲例進行分析,其他方法原理類似,newExecutor() 方法的具體實現如下所示。
public Executor newExecutor(Transaction transaction, ExecutorType executorType) { executorType = executorType == null ? defaultExecutorType : executorType; executorType = executorType == null ? ExecutorType.SIMPLE : executorType; Executor executor; // 默認是SIMPLE 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); } // 通過InterceptorChain.pluginAll()方法創建Exector代理對象 executor = (Executor) interceptorChain.pluginAll(executor); return executor; }
在 InterceptorChain.pluginAll() 方法會遍歷 interceptors 集合,並調用每個 interceptor 的 plugin() 方法創建代理對象,具體實現如下所示。
public Object pluginAll(Object target) { for (Interceptor interceptor : interceptors) { target = interceptor.plugin(target); } return target; }
一般我們自定義攔截器的 plugin 方法,會使用 MyBatis 提供的 Plugin 工具類,它實現了 InvocationHandler 接口,並提供了 wrap() 靜態方法用於創建代理對象,Plugin.wrap() 方法的具體實現如下所示。
public static Object wrap(Object target, Interceptor interceptor) { // 獲取用戶自定義Interceptor中@Signature註解的信息 // getSignatureMap()方法負責處理@Signature註解 Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor); // 獲取目標類型 Class<?> type = target.getClass(); // 獲取目標類型實現的接口 Class<?>[] interfaces = getAllInterfaces(type, signatureMap); if (interfaces.length > 0) { // 使用JDK動態代理的方式創建代理對象 return Proxy.newProxyInstance( type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)); } return target; } private static Class<?>[] getAllInterfaces(Class<?> type, Map<Class<?>, Set<Method>> signatureMap) { Set<Class<?>> interfaces = new HashSet<Class<?>>(); while (type != null) { for (Class<?> c : type.getInterfaces()) { if (signatureMap.containsKey(c)) { interfaces.add(c); } } type = type.getSuperclass(); } return interfaces.toArray(new Class<?>[interfaces.size()]); }
示例中 Exector 存在兩個攔截器 AInterceptor 和 BInterceptor,在執行 InterceptorChain.pluginAll() 方法的時候,傳給 getAllInterfaces() 方法的 type 字段第一次是 CacheExector 對象,第二次是 CacheExector 的代理對象,因爲生成的代理對象也繼承 Exector 接口,signatureMap.containsKey(c) 可以獲得值,繼續生成代理的代理對象,結構如下圖所示。
在 Plugin.invoke() 方法中,會將當前調用方法與 signatureMap 集合中記錄的方法信息進行比較,如果當前調用的方法是需要被攔截的方法,則調用其 intercept() 方法進行處理,如果不能被攔截則直接調用 target 的相應方法。Plugin.invoke() 方法的具體實現如下所示。
@Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { // 獲取當前方法所在類或接口中,可被當前 Interceptor攔截的方法 Set<Method> methods = signatureMap.get(method.getDeclaringClass()); // 如果當前調用的方法需要被攔截,則調用interceptor.intercept()方法進行攔截處理 if (methods != null && methods.contains(method)) { return interceptor.intercept(new Invocation(target, method, args)); } // 如果當前調用的方法不能被攔截,則調用target對象的相應方法 return method.invoke(target, args); } catch (Exception e) { throw ExceptionUtil.unwrapThrowable(e); } }
Interceptor.intercept() 方法的參數是 Invocation 對象,其中封裝了目標對象、目標方法以及調用目標方法的參數,並提供了 process() 方法調用目標方法,如下所示。
public Object proceed() throws InvocationTargetException, IllegalAccessException { return method.invoke(target, args); }
需要注意的是,在 Interceptor.intercept() 方法中執行完攔截處理之後,如果需要調用目標方法,則通過 Invocation.process() 方法實現。
根據上面的分析,就不難理解示例的如下輸出日誌了,同時配置文件中插件的執行順序也清楚了。
[main] DEBUG org.apache.ibatis.logging.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------執行攔截器B前-------------- [main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------執行攔截器A前-------------- [main] DEBUG org.apache.ibatis.transaction.jdbc.JdbcTransaction - Opening JDBC Connection [main] DEBUG org.apache.ibatis.datasource.pooled.PooledDataSource - Created connection 360067785. [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Preparing: select id, name, sex, selfcard_no, note from t_student where id = ? [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - ==> Parameters: 1(Long) [main] DEBUG com.yjw.mybatis.dao.StudentMapper.selectByPrimaryKey - <== Total: 1 [main] INFO com.yjw.mybatis.test.mybatis.plugin.AInterceptor - --------------執行攔截器A後-------------- [main] INFO com.yjw.mybatis.test.mybatis.plugin.BInterceptor - --------------執行攔截器B後-------------- Student [Hash = 550752602, id=1, name=張三, sex=1, selfcardNo=111, note=zhangsan]
MyBatis 源碼篇