建議收藏,MyBatis插件原理詳解
插件原理分析
mybatis插件涉及到的幾個類:
我將以 Executor 為例,分析 MyBatis 是如何為 Executor 實例植入插件的。Executor 實例是在開啟 SqlSession 時被創(chuàng)建的,因此,我們從源頭進行分析。先來看一下 SqlSession 開啟的過程。
- public SqlSession openSession() {
- return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);
- }
- private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
- Transaction tx = null;
- try {
- // 省略部分邏輯
- // 創(chuàng)建 Executor
- final Executor executor = configuration.newExecutor(tx, execType);
- return new DefaultSqlSession(configuration, executor, autoCommit);
- }
- catch (Exception e) {...}
- finally {...}
- }
Executor 的創(chuàng)建過程封裝在 Configuration 中,我們跟進去看看看。
- // Configuration類中
- public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
- executorType = executorType == null ? defaultExecutorType : executorType;
- executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
- Executor executor;
- // 根據(jù) executorType 創(chuàng)建相應的 Executor 實例
- if (ExecutorType.BATCH == executorType) {...}
- else if (ExecutorType.REUSE == executorType) {...}
- else {
- executor = new SimpleExecutor(this, transaction);
- }
- if (cacheEnabled) {
- executor = new CachingExecutor(executor);
- }
- // 植入插件
- executor = (Executor) interceptorChain.pluginAll(executor);
- return executor;
- }
如上,newExecutor 方法在創(chuàng)建好 Executor 實例后,緊接著通過攔截器鏈 interceptorChain 為 Executor 實例植入代理邏輯。那下面我們看一下 InterceptorChain 的代碼是怎樣的。
- public class InterceptorChain {
- private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
- public Object pluginAll(Object target) {
- // 遍歷攔截器集合
- for (Interceptor interceptor : interceptors) {
- // 調(diào)用攔截器的 plugin 方法植入相應的插件邏輯
- target = interceptor.plugin(target);
- }
- return target;
- }
- /** 添加插件實例到 interceptors 集合中 */
- public void addInterceptor(Interceptor interceptor) {
- interceptors.add(interceptor);
- }
- /** 獲取插件列表 */
- public List<Interceptor> getInterceptors() {
- return Collections.unmodifiableList(interceptors);
- }
- }
上面的for循環(huán)代表了只要是插件,都會以責任鏈的方式逐一執(zhí)行(別指望它能跳過某個節(jié)點),所謂插件,其實就類似于攔截器。
這里就用到了責任鏈設計模式,責任鏈設計模式就相當于我們在OA系統(tǒng)里發(fā)起審批,領導們一層一層進行審批。
以上是 InterceptorChain 的全部代碼,比較簡單。它的 pluginAll 方法會調(diào)用具體插件的 plugin 方法植入相應的插件邏輯。如果有多個插件,則會多次調(diào)用 plugin 方法,最終生成一個層層嵌套的代理類。形如下面:
當 Executor 的某個方法被調(diào)用的時候,插件邏輯會先行執(zhí)行。執(zhí)行順序由外而內(nèi),比如上圖的執(zhí)行順序為 plugin3 → plugin2 → Plugin1 → Executor。
plugin 方法是由具體的插件類實現(xiàn),不過該方法代碼一般比較固定,所以下面找個示例分析一下。
- // TianPlugin類
- public Object plugin(Object target) {
- return Plugin.wrap(target, this);
- }
- //Plugin
- public static Object wrap(Object target, Interceptor interceptor) {
- /*
- * 獲取插件類 @Signature 注解內(nèi)容,并生成相應的映射結構。形如下面:
- * {
- * Executor.class : [query, update, commit],
- * ParameterHandler.class : [getParameterObject, setParameters]
- * }
- */
- Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
- Class<?> type = target.getClass();
- // 獲取目標類實現(xiàn)的接口
- Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
- if (interfaces.length > 0) {
- // 通過 JDK 動態(tài)代理為目標類生成代理類
- return Proxy.newProxyInstance(
- type.getClassLoader(),
- interfaces,
- new Plugin(target, interceptor, signatureMap));
- }
- return target;
- }
如上,plugin 方法在內(nèi)部調(diào)用了 Plugin 類的 wrap 方法,用于為目標對象生成代理。Plugin 類實現(xiàn)了 InvocationHandler 接口,因此它可以作為參數(shù)傳給 Proxy 的 newProxyInstance 方法。
到這里,關于插件植入的邏輯就分析完了。接下來,我們來看看插件邏輯是怎樣執(zhí)行的。
執(zhí)行插件邏輯
Plugin 實現(xiàn)了 InvocationHandler 接口,因此它的 invoke 方法會攔截所有的方法調(diào)用。invoke 方法會對所攔截的方法進行檢測,以決定是否執(zhí)行插件邏輯。該方法的邏輯如下:
- //在Plugin類中
- public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- try {
- /*
- * 獲取被攔截方法列表,比如:
- * signatureMap.get(Executor.class),可能返回 [query, update, commit]
- */
- Set<Method> methods = signatureMap.get(method.getDeclaringClass());
- // 檢測方法列表是否包含被攔截的方法
- if (methods != null && methods.contains(method)) {
- // 執(zhí)行插件邏輯
- return interceptor.intercept(new Invocation(target, method, args));
- }
- // 執(zhí)行被攔截的方法
- return method.invoke(target, args);
- } catch (Exception e) {
- throw ExceptionUtil.unwrapThrowable(e);
- }
- }
invoke 方法的代碼比較少,邏輯不難理解。首先,invoke 方法會檢測被攔截方法是否配置在插件的 @Signature 注解中,若是,則執(zhí)行插件邏輯,否則執(zhí)行被攔截方法。插件邏輯封裝在 intercept 中,該方法的參數(shù)類型為 Invocation。Invocation 主要用于存儲目標類,方法以及方法參數(shù)列表。下面簡單看一下該類的定義。
- public class Invocation {
- private final Object target;
- private final Method method;
- private final Object[] args;
- public Invocation(Object target, Method method, Object[] args) {
- this.target = target;
- this.method = method;
- this.args = args;
- }
- // 省略部分代碼
- public Object proceed() throws InvocationTargetException, IllegalAccessException {
- //反射調(diào)用被攔截的方法
- return method.invoke(target, args);
- }
- }
關于插件的執(zhí)行邏輯就分析到這,整個過程不難理解,大家簡單看看即可。
自定義插件
下面為了讓大家更好的理解Mybatis的插件機制,我們來模擬一個慢sql監(jiān)控的插件。
- /**
- * 慢查詢sql 插件
- */
- @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
- public class SlowSqlPlugin implements Interceptor {
- private long slowTime;
- //攔截后需要處理的業(yè)務
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- //通過StatementHandler獲取執(zhí)行的sql
- StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
- BoundSql boundSql = statementHandler.getBoundSql();
- String sql = boundSql.getSql();
- long start = System.currentTimeMillis();
- //結束攔截
- Object proceed = invocation.proceed();
- long end = System.currentTimeMillis();
- long f = end - start;
- System.out.println(sql);
- System.out.println("耗時=" + f);
- if (f > slowTime) {
- System.out.println("本次數(shù)據(jù)庫操作是慢查詢,sql是:");
- System.out.println(sql);
- }
- return proceed;
- }
- //獲取到攔截的對象,底層也是通過代理實現(xiàn)的,實際上是拿到一個目標代理對象
- @Override
- public Object plugin(Object target) {
- //觸發(fā)intercept方法
- return Plugin.wrap(target, this);
- }
- //設置屬性
- @Override
- public void setProperties(Properties properties) {
- //獲取我們定義的慢sql的時間閾值slowTime
- this.slowTime = Long.parseLong(properties.getProperty("slowTime"));
- }
- }
然后把這個插件類注入到容器中。
然后我們來執(zhí)行查詢的方法。
耗時28秒的,大于我們定義的10毫秒,那這條SQL就是我們認為的慢SQL。
通過這個插件,我們就能很輕松的理解setProperties()方法是做什么的了。
回顧分頁插件
也是實現(xiàn)mybatis接口Interceptor。
- @SuppressWarnings({"rawtypes", "unchecked"})
- @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 PageInterceptor implements Interceptor {
- @Override
- public Object intercept(Invocation invocation) throws Throwable {
- ...
- }
intercept方法中
- //AbstractHelperDialect類中
- @Override
- public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
- String sql = boundSql.getSql();
- Page page = getLocalPage();
- //支持 order by
- String orderBy = page.getOrderBy();
- if (StringUtil.isNotEmpty(orderBy)) {
- pageKey.update(orderBy);
- sql = OrderByParser.converToOrderBySql(sql, orderBy);
- }
- if (page.isOrderByOnly()) {
- return sql;
- }
- //獲取分頁sql
- return getPageSql(sql, page, pageKey);
- }
- //模板方法模式中的鉤子方法
- public abstract String getPageSql(String sql, Page page, CacheKey pageKey);
AbstractHelperDialect類的實現(xiàn)類有如下(也就是此分頁插件支持的數(shù)據(jù)庫就以下幾種):
我們用的是MySQL。這里也有與之對應的。
- @Override
- public String getPageSql(String sql, Page page, CacheKey pageKey) {
- StringBuilder sqlBuilder = new StringBuilder(sql.length() + 14);
- sqlBuilder.append(sql);
- if (page.getStartRow() == 0) {
- sqlBuilder.append(" LIMIT ? ");
- } else {
- sqlBuilder.append(" LIMIT ?, ? ");
- }
- pageKey.update(page.getPageSize());
- return sqlBuilder.toString();
- }
到這里我們就知道了,它無非就是在我們執(zhí)行的SQL上再拼接了Limit罷了。同理,Oracle也就是使用rownum來處理分頁了。下面是Oracle處理分頁
- @Override
- public String getPageSql(String sql, Page page, CacheKey pageKey) {
- StringBuilder sqlBuilder = new StringBuilder(sql.length() + 120);
- if (page.getStartRow() > 0) {
- sqlBuilder.append("SELECT * FROM ( ");
- }
- if (page.getEndRow() > 0) {
- sqlBuilder.append(" SELECT TMP_PAGE.*, ROWNUM ROW_ID FROM ( ");
- }
- sqlBuilder.append(sql);
- if (page.getEndRow() > 0) {
- sqlBuilder.append(" ) TMP_PAGE WHERE ROWNUM <= ? ");
- }
- if (page.getStartRow() > 0) {
- sqlBuilder.append(" ) WHERE ROW_ID > ? ");
- }
- return sqlBuilder.toString();
- }
其他數(shù)據(jù)庫分頁操作類似。關于具體原理分析,這里就沒必要贅述了,因為分頁插件源代碼里注釋基本上全是中文。
Mybatis插件應用場景
- 水平分表
- 權限控制
- 數(shù)據(jù)的加解密
總結
Spring-Boot+Mybatis繼承了分頁插件,以及使用案例、插件的原理分析、源碼分析、如何自定義插件。
涉及到技術點:JDK動態(tài)代理、責任鏈設計模式、模板方法模式。
Mybatis插件關鍵對象總結:
- Inteceptor接口:自定義攔截必須實現(xiàn)的類。
- InterceptorChain:存放插件的容器。
- Plugin:h對象,提供創(chuàng)建代理類的方法。
- Invocation:對被代理對象的封裝。
本文轉載自微信公眾號「Java后端技術全?!梗梢酝ㄟ^以下二維碼關注。轉載本文請聯(lián)系Java后端技術全棧公眾號。