建議收藏,mybatis插件原理詳解

上次發文說到了如何集成分頁插件MyBatis插件原理分析,看完感受本身better了,今天咱們接着來聊mybatis插件的原理。sql

插件原理分析

mybatis插件涉及到的幾個類:數據庫

圖片

我將以 Executor 爲例,分析 MyBatis 是如何爲 Executor 實例植入插件的。Executor 實例是在開啓 SqlSession 時被建立的,所以,咱們從源頭進行分析。先來看一下 SqlSession 開啓的過程。設計模式

public SqlSession openSession() {
    return openSessionFromDataSource(configuration.getDefaultExecutorType(), nullfalse);
}

private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
    Transaction tx = null;
    try {
        // 省略部分邏輯
        
        // 建立 Executor
        final Executor executor = configuration.newExecutor(tx, execType);
        return new DefaultSqlSession(configuration, executor, autoCommit);
    } 
    catch (Exception e) {...} 
    finally {...}
}

Executor 的建立過程封裝在 Configuration 中,咱們跟進去看看看。mybatis

// Configuration類中
public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
    executorType = executorType == null ? defaultExecutorType : executorType;
    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
    Executor executor;
    
    // 根據 executorType 建立相應的 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 方法在建立好 Executor 實例後,緊接着經過攔截器鏈 interceptorChain 爲 Executor 實例植入代理邏輯。那下面咱們看一下 InterceptorChain 的代碼是怎樣的。app

public class InterceptorChain {
    private final List<Interceptor> interceptors = new ArrayList<Interceptor>();
    public Object pluginAll(Object target) {
        // 遍歷攔截器集合
        for (Interceptor interceptor : interceptors) {
            // 調用攔截器的 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循環表明了只要是插件,都會以責任鏈的方式逐一執行(別期望它能跳過某個節點),所謂插件,其實就相似於攔截器。ide

這裏就用到了責任鏈設計模式,責任鏈設計模式就至關於咱們在OA系統裏發起審批,領導們一層一層進行審批。源碼分析

以上是 InterceptorChain 的所有代碼,比較簡單。它的 pluginAll 方法會調用具體插件的 plugin 方法植入相應的插件邏輯。若是有多個插件,則會屢次調用 plugin 方法,最終生成一個層層嵌套的代理類。形以下面:ui

圖片

當 Executor 的某個方法被調用的時候,插件邏輯會先行執行。執行順序由外而內,好比上圖的執行順序爲 plugin3 → plugin2 → Plugin1 → Executorthis

plugin 方法是由具體的插件類實現,不過該方法代碼通常比較固定,因此下面找個示例分析一下。spa

// TianPlugin類
public Object plugin(Object target) {
    return Plugin.wrap(target, this);
}

//Plugin
public static Object wrap(Object target, Interceptor interceptor) {
    /*
     * 獲取插件類 @Signature 註解內容,並生成相應的映射結構。形以下面:
     * {
     *     Executor.class : [query, update, commit],
     *     ParameterHandler.class : [getParameterObject, setParameters]
     * }
     */

    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;
}

如上,plugin 方法在內部調用了 Plugin 類的 wrap 方法,用於爲目標對象生成代理。Plugin 類實現了 InvocationHandler 接口,所以它能夠做爲參數傳給 Proxy 的 newProxyInstance 方法。

到這裏,關於插件植入的邏輯就分析完了。接下來,咱們來看看插件邏輯是怎樣執行的。

執行插件邏輯

Plugin 實現了 InvocationHandler 接口,所以它的 invoke 方法會攔截全部的方法調用。invoke 方法會對所攔截的方法進行檢測,以決定是否執行插件邏輯。該方法的邏輯以下:

//在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)) {
            // 執行插件邏輯
            return interceptor.intercept(new Invocation(target, method, args));
        }
        // 執行被攔截的方法
        return method.invoke(target, args);
    } catch (Exception e) {
        throw ExceptionUtil.unwrapThrowable(e);
    }
}

invoke 方法的代碼比較少,邏輯不難理解。首先,invoke 方法會檢測被攔截方法是否配置在插件的 @Signature 註解中,如果,則執行插件邏輯,不然執行被攔截方法。插件邏輯封裝在 intercept 中,該方法的參數類型爲 Invocation。Invocation 主要用於存儲目標類,方法以及方法參數列表。下面簡單看一下該類的定義。

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 {
        //反射調用被攔截的方法
        return method.invoke(target, args);
    }
}

關於插件的執行邏輯就分析到這,整個過程不難理解,你們簡單看看便可。

自定義插件

下面爲了讓你們更好的理解Mybatis的插件機制,咱們來模擬一個慢sql監控的插件。

/**
 * 慢查詢sql 插件
 */

@Intercepts({@Signature(type = StatementHandler.classmethod "prepare", args = {Connection.classInteger.class})})
public class SlowSqlPlugin implements Interceptor 
{

    private long slowTime;

    //攔截後須要處理的業務
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        //經過StatementHandler獲取執行的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("本次數據庫操做是慢查詢,sql是:");
            System.out.println(sql);
        }
        return proceed;
    }

    //獲取到攔截的對象,底層也是經過代理實現的,其實是拿到一個目標代理對象
    @Override
    public Object plugin(Object target) {
        //觸發intercept方法
        return Plugin.wrap(target, this);
    }

    //設置屬性
    @Override
    public void setProperties(Properties properties) {
        //獲取咱們定義的慢sql的時間閾值slowTime
        this.slowTime = Long.parseLong(properties.getProperty("slowTime"));
    }
}

而後把這個插件類注入到容器中。

圖片

而後咱們來執行查詢的方法。

圖片

耗時28秒的,大於咱們定義的10毫秒,那這條SQL就是咱們認爲的慢SQL。

經過這個插件,咱們就能很輕鬆的理解setProperties()方法是作什麼的了。

回顧分頁插件

也是實現mybatis接口Interceptor。

@SuppressWarnings({"rawtypes""unchecked"})
@Intercepts(
    {
        @Signature(type = Executor.classmethod "query", args = {MappedStatement.classObject.classRowBounds.classResultHandler.class}),
        @Signature(type 
= Executor.classmethod "query", args = {MappedStatement.classObject.classRowBounds.classResultHandler.classCacheKey.classBoundSql.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類的實現類有以下(也就是此分頁插件支持的數據庫就如下幾種):

圖片

咱們用的是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();
    }

到這裏咱們就知道了,它無非就是在咱們執行的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();
    }

其餘數據庫分頁操做相似。關於具體原理分析,這裏就不必贅述了,由於分頁插件源代碼裏註釋基本上全是中文。

Mybatis插件應用場景

  • 水平分表
  • 權限控制
  • 數據的加解密

總結

Spring-Boot+Mybatis繼承了分頁插件,以及使用案例、插件的原理分析、源碼分析、如何自定義插件。

涉及到技術點:JDK動態代理、責任鏈設計模式、模板方法模式。

Mybatis插件關鍵對象總結:

  • Inteceptor接口:自定義攔截必須實現的類。
  • InterceptorChain:存放插件的容器。
  • Plugin:h對象,提供建立代理類的方法。
  • Invocation:對被代理對象的封裝。
相關文章
相關標籤/搜索