首頁 > 軟體

從 PageHelper 到 MyBatis Plugin執行概要及實現原理

2022-09-24 14:00:04

一、背景

在很多業務場景下我們需要去攔截 SQL,達到不入侵原有程式碼業務處理一些東西,比如:歷史記錄、分頁操作、資料許可權過濾操作、SQL 執行時間效能監控等等,這裡我們就可以用到 MyBatis 的外掛 Plugin。下面我們來了解一下 Plugin 到底是如何工作的。

使用過 MyBatis 框架的朋友們肯定都聽說過 PageHelper 這個分頁神器吧,其實 PageHelper 的底層實現就是依靠 plugin。下面我們來看一下 PageHelper 是如何利用 plugin 實現分頁的。

二、MyBatis 執行概要圖

首先我們先看一下 MyBatis 的執行流程圖,對其執行流程有一個大體的認識。

三、MyBatis 核心物件介紹

MyBatis 程式碼實現的角度來看,MyBatis 的主要的核心部件有以下幾個:

  • Configuration:初始化基礎設定,比如 MyBatis 的別名等,一些重要的型別物件,如,外掛,對映器,ObjectFactorytypeHandler 物件,MyBatis 所有的設定資訊都維持在 Configuration 物件之中。
  • SqlSessionFactorySqlSession 工廠,用於生產 SqlSession
  • SqlSession: 作為 MyBatis 工作的主要頂層 API,表示和資料庫互動的對談,完成必要資料庫增刪改查功能
  • ExecutorMyBatis 執行器,是 MyBatis 排程的核心,負責 SQL 語句的生成和查詢快取的維護
  • StatementHandler:封裝了 JDBC Statement 操作,負責對 JDBC Statement 的操作,如設定引數、將 Statement 結果集轉換成List集合。
  • ParameterHandler:負責對使用者傳遞的引數轉換成 JDBC Statement 所需要的引數,
  • ResultSetHandler:負責將 JDBC 返回的 ResultSet 結果集物件轉換成 List 型別的集合;
  • TypeHandler:負責 java 資料型別和 jdbc 資料型別之間的對映和轉換
  • MappedStatementMappedStatement 維護了一條 <select|update|delete|insert> 節點的封裝,
  • SqlSource:負責根據使用者傳遞的 parameterObject,動態地生成 SQL 語句,將資訊封裝到 BoundSql 物件中,並返回
  • BoundSql:表示動態生成的 SQL 語句以及相應的引數資訊

說了這麼多,怎麼還沒進入正題啊,別急,下面就開始講解 Plugin 的實現原理。

四、Plugin 實現原理

MyBatis 支援對 Executor、StatementHandler、PameterHandler和ResultSetHandler 介面進行攔截,也就是說會對這4種物件進行代理。

下面我們結合 PageHelper 來講解 Plugin 是怎樣實現的。

1、定義 Plugin

要使用自定義 Plugin 首先要實現 Interceptor 介面。可以通俗的理解為一個 Plugin 就是一個攔截器。

public interface Interceptor {
  // 實現攔截邏輯   
  Object intercept(Invocation invocation) throws Throwable;
  // 獲取代理類
  Object plugin(Object target);
  // 初始化設定
  void setProperties(Properties properties);
}

現在我們來看一下 PageHelper 是如何通過 Plugin 實現分頁的。

@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 {
    //快取count查詢的ms
    protected Cache<CacheKey, MappedStatement> msCountMap = null;
    private Dialect dialect;
    private String default_dialect_class = "com.github.pagehelper.PageHelper";
    private Field additionalParametersField;
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        try {
            Object[] args = invocation.getArgs();
            MappedStatement ms = (MappedStatement) args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds) args[2];
            ResultHandler resultHandler = (ResultHandler) args[3];
            Executor executor = (Executor) invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            //由於邏輯關係,只會進入一次
            if(args.length == 4){
                //4 個引數時
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                //6 個引數時
                cacheKey = (CacheKey) args[4];
                boundSql = (BoundSql) args[5];
            }
            List resultList;
            //呼叫方法判斷是否需要進行分頁,如果不需要,直接返回結果
            if (!dialect.skip(ms, parameter, rowBounds)) {
                //反射獲取動態引數
                Map<String, Object> additionalParameters = (Map<String, Object>) additionalParametersField.get(boundSql);
                //判斷是否需要進行 count 查詢
                if (dialect.beforeCount(ms, parameter, rowBounds)) {
                    //建立 count 查詢的快取 key
                    CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
                    countKey.update(MSUtils.COUNT);
                    MappedStatement countMs = msCountMap.get(countKey);
                    if (countMs == null) {
                        //根據當前的 ms 建立一個返回值為 Long 型別的 ms
                        countMs = MSUtils.newCountMappedStatement(ms);
                        msCountMap.put(countKey, countMs);
                    }
                    //呼叫方言獲取 count sql
                    String countSql = dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
                    countKey.update(countSql);
                    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
                    //當使用動態 SQL 時,可能會產生臨時的引數,這些引數需要手動設定到新的 BoundSql 中
                    for (String key : additionalParameters.keySet()) {
                        countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //執行 count 查詢
                    Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                    Long count = (Long) ((List) countResultList).get(0);
                    //處理查詢總數
                    //返回 true 時繼續分頁查詢,false 時直接返回
                    if (!dialect.afterCount(count, parameter, rowBounds)) {
                        //當查詢總數為 0 時,直接返回空的結果
                        return dialect.afterPage(new ArrayList(), parameter, rowBounds);
                    }
                }
                //判斷是否需要進行分頁查詢
                if (dialect.beforePage(ms, parameter, rowBounds)) {
                    //生成分頁的快取 key
                    CacheKey pageKey = cacheKey;
                    //處理引數物件
                    parameter = dialect.processParameterObject(ms, parameter, boundSql, pageKey);
                    //呼叫方言獲取分頁 sql
                    String pageSql = dialect.getPageSql(ms, boundSql, parameter, rowBounds, pageKey);
                    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
                    //設定動態引數
                    for (String key : additionalParameters.keySet()) {
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                    //執行分頁查詢
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql);
                } else {
                    //不執行分頁的情況下,也不執行記憶體分頁
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                }
            } else {
                //rowBounds用引數值,不使用分頁外掛處理時,仍然支援預設的記憶體分頁
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            }
            return dialect.afterPage(resultList, parameter, rowBounds);
        } finally {
            dialect.afterAll();
        }
    }
    @Override
    public Object plugin(Object target) {
        //TODO Spring bean 方式設定時,如果沒有設定屬性就不會執行下面的 setProperties 方法,就不會初始化,因此考慮在這個方法中做一次判斷和初始化
        //TODO https://github.com/pagehelper/Mybatis-PageHelper/issues/26
        return Plugin.wrap(target, this);
    }
    @Override
    public void setProperties(Properties properties) {
        //快取 count ms
        msCountMap = CacheFactory.createCache(properties.getProperty("msCountCache"), "ms", properties);
        String dialectClass = properties.getProperty("dialect");
        if (StringUtil.isEmpty(dialectClass)) {
            dialectClass = default_dialect_class;
        }
        try {
            Class<?> aClass = Class.forName(dialectClass);
            dialect = (Dialect) aClass.newInstance();
        } catch (Exception e) {
            throw new PageException(e);
        }
        dialect.setProperties(properties);
        try {
            //反射獲取 BoundSql 中的 additionalParameters 屬性
            additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters");
            additionalParametersField.setAccessible(true);
        } catch (NoSuchFieldException e) {
            throw new PageException(e);
        }
    }
}

程式碼太長不看系列: 其實這段程式碼最主要的邏輯就是在執行 Executor 方法的時候,攔截 query 也就是查詢型別的 SQL, 首先會判斷它是否需要分頁,如果需要分頁就會根據查詢引數在 SQL 末尾加上 limit pageNum, pageSize來實現分頁。

2、註冊攔截器

  • 通過 SqlSessionFactoryBean 去構建 Configuration 新增攔截器並構建獲取 SqlSessionFactory
public class SqlSessionFactoryBean implements FactoryBean<SqlSessionFactory>, InitializingBean, ApplicationListener<ApplicationEvent> {
    // ... 此處省略部分原始碼
    protected SqlSessionFactory buildSqlSessionFactory() throws IOException {
        // ... 此處省略部分原始碼
        // 檢視是否注入攔截器,有的話新增到Interceptor集合裡面
        if (!isEmpty(this.plugins)) {
            for (Interceptor plugin : this.plugins) {
                configuration.addInterceptor(plugin);
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Registered plugin: '" + plugin + "'");
                }
            }
        }
        // ... 此處省略部分原始碼
        return this.sqlSessionFactoryBuilder.build(configuration);
    }
    // ... 此處省略部分原始碼
}
  • 通過原始的 XMLConfigBuilder 構建 configuration 新增攔截器
public class XMLConfigBuilder extends BaseBuilder {
    //解析設定
    private void parseConfiguration(XNode root) {
        try {
            //省略部分程式碼
            pluginElement(root.evalNode("plugins"));
        } catch (Exception e) {
            throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e);
        }
    }
    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 interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
                interceptorInstance.setProperties(properties);
                //呼叫InterceptorChain.addInterceptor
                configuration.addInterceptor(interceptorInstance);
            }
        }
    }
}

上面是兩種不同的形式構建 configuration 並新增攔截器 interceptor,上面第二種一般是以前 XML 設定的情況,這裡主要是解析組態檔的 plugin 節點,根據設定的 interceptor 屬性範例化 Interceptor 物件,然後新增到 Configuration 物件中的 InterceptorChain 屬性中。

如果定義多個攔截器就會它們鏈起來形成一個攔截器鏈,初始化組態檔的時候就把所有的攔截器新增到攔截器鏈中。

public class InterceptorChain {
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
  public Object pluginAll(Object target) {
    //迴圈呼叫每個Interceptor.plugin方法
    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);
  }
}

3、執行攔截器

從以下程式碼可以看出 MyBatis 在範例化 Executor、ParameterHandler、ResultSetHandler、StatementHandler 四大介面物件的時候呼叫 interceptorChain.pluginAll() 方法插入進去的。

其實就是迴圈執行攔截器鏈所有的攔截器的 plugin() 方法, MyBatis 官方推薦的 plugin 方法是 Plugin.wrap() 方法,這個就會生成代理類。

public class Configuration {
    protected final InterceptorChain interceptorChain = new InterceptorChain();
    //建立引數處理器
    public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        //建立ParameterHandler
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        //外掛在這裡插入
        parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }
    //建立結果集處理器
    public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
                                                ResultHandler resultHandler, BoundSql boundSql) {
        //建立DefaultResultSetHandler
        ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
        //外掛在這裡插入
        resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
        return resultSetHandler;
    }
    //建立語句處理器
    public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        //建立路由選擇語句處理器
        StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql);
        //外掛在這裡插入
        statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }
    public Executor newExecutor(Transaction transaction) {
        return newExecutor(transaction, defaultExecutorType);
    }
    //產生執行器
    public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? defaultExecutorType : executorType;
        //這句再做一下保護,囧,防止粗心大意的人將defaultExecutorType設成null?
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Executor executor;
        //然後就是簡單的3個分支,產生3種執行器BatchExecutor/ReuseExecutor/SimpleExecutor
        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);
        }
        //如果要求快取,生成另一種CachingExecutor(預設就是有快取),裝飾者模式,所以預設都是返回CachingExecutor
        if (cacheEnabled) {
            executor = new CachingExecutor(executor);
        }
        //此處呼叫外掛,通過外掛可以改變Executor行為
        executor = (Executor) interceptorChain.pluginAll(executor);
        return executor;
    }
}

4、Plugin 的動態代理

我們首先看一下Plugin.wrap() 方法,這個方法的作用是為實現Interceptor註解的介面實現類生成代理物件的。

    // 如果是Interceptor註解的介面的實現類會產生代理類 
    public static Object wrap(Object target, Interceptor interceptor) {
    //從攔截器的註解中獲取攔截的類名和方法資訊
    Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
    //取得要改變行為的類(ParameterHandler|ResultSetHandler|StatementHandler|Executor)
    Class<?> type = target.getClass();
    //取得介面
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    //產生代理,是Interceptor註解的介面的實現類才會產生代理
    if (interfaces.length > 0) {
      return Proxy.newProxyInstance(type.getClassLoader(),interfaces,new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

Plugin 中的 getSignatureMap、 getAllInterfaces 兩個輔助方法,來幫助判斷是否為是否Interceptor註解的介面實現類。

  //取得簽名Map,就是獲取Interceptor實現類上面的註解,要攔截的是那個類(Executor 
  //,ParameterHandler, ResultSetHandler,StatementHandler)的那個方法 
private static Map<Class<?>, Set<Method>> getSignatureMap(Interceptor interceptor) {
    //取Intercepts註解
    Intercepts interceptsAnnotation =interceptor.getClass().getAnnotation(Intercepts.class);
    //必須得有Intercepts註解,沒有報錯
    if (interceptsAnnotation == null) {
      throw new PluginException("No @Intercepts annotation was found in interceptor " + interceptor.getClass().getName());      
    }
    //value是陣列型,Signature的陣列
      Signature[] sigs = interceptsAnnotation.value();
    //每個class裡有多個Method需要被攔截,所以這麼定義
      Map<Class<?>, Set<Method>> signatureMap = new HashMap<Class<?>, Set<Method>>();
     for (Signature sig : sigs) {
      Set<Method> methods = signatureMap.get(sig.type());
        if (methods == null) {
          methods = new HashSet<Method>();
          signatureMap.put(sig.type(), methods);
      }
      try {
         Method method = sig.type().getMethod(sig.method(), sig.args());
         methods.add(method);
      } catch (NoSuchMethodException e) {
         throw new PluginException("Could not find method on " + sig.type() + " named " + sig.method() + ". Cause: " + e, e);
      }
    }
    return signatureMap;
  }
 //取得介面
 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()]);
  }
}

我們來看一下代理類的 query 方法,其實就是呼叫了 Plugin.invoke() 方法。代理類遮蔽了 intercept 方法的呼叫。

    public final List query(MappedStatement mappedStatement, Object object, RowBounds rowBounds, ResultHandler resultHandler, CacheKey cacheKey, BoundSql boundSql) throws SQLException {
        try {
            // 這裡的 h 就是一個 Plugin
            return (List)this.h.invoke(this, m5, new Object[]{mappedStatement, object, rowBounds, resultHandler, cacheKey, boundSql});
        }
        catch (Error | RuntimeException | SQLException throwable) {
            throw throwable;
        }
        catch (Throwable throwable) {
            throw new UndeclaredThrowableException(throwable);
        }
    }

最後 Plugin.invoke() 就是判斷當前方法是否攔截,如果需要攔截則會呼叫 Interceptor.intercept() 對當前方法執行攔截邏輯。

public class Plugin implements InvocationHandler {
    ...
  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    try {
      //獲取需要攔截的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      //是Interceptor實現類註解的方法才會攔截處理
      if (methods != null && methods.contains(method)) {
        //呼叫Interceptor.intercept,即呼叫自己寫的邏輯
        return interceptor.intercept(new Invocation(target, method, args));
      }
      //最後執行原來邏輯
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
  }
    ...

總結

我們以 PageHelper 為切入點講解了 MyBatis Plugin 的實現原理,其中 MyBatis 攔截器用到責任鏈模式+動態代理+反射機制。 通過上面的分析可以知道,所有可能被攔截的處理類都會生成一個代理類,如果有 N 個攔截器,就會有 N 個代理,層層生成動態代理是比較耗效能的。而且雖然能指定外掛攔截的位置,但這個是在執行方法時利用反射動態判斷的,初始化的時候就是簡單的把攔截器插入到了所有可以攔截的地方。所以儘量不要編寫不必要的攔截器,並且攔截器儘量不要寫複雜的邏輯。

以上就是從 PageHelper 到 MyBatis Plugin執行概要及實現原理的詳細內容,更多關於PageHelper MyBatis Plugin的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com