首頁>技術>

上次發文說到了如何整合分頁外掛MyBatis外掛原理分析,看完感覺自己better了,今天我們接著來聊mybatis外掛的原理。

外掛原理分析

mybatis外掛涉及到的幾個類:

我將以 Executor 為例,分析 MyBatis 是如何為 Executor 例項植入外掛的。Executor 例項是在開啟 SqlSession 是被建立的,因此,我們從源頭進行分析。先來看一下 SqlSession 開啟的過程。

public SqlSession openSession() {    return openSessionFromDataSource(configuration.getDefaultExecutorType(), null, false);}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 中,我們跟進去看看看。

// 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 的程式碼是怎樣的。

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迴圈代表了只要是外掛,都會以責任鏈的方式逐一執行(別指望它能跳過某個節點),所謂外掛,其實就類似於攔截器。

這裡就用到了責任鏈設計模式,責任鏈設計模式就相當於我們在OA系統裡發起審批,領導們一層一層進行審批。

以上是 InterceptorChain 的全部程式碼,比較簡單。它的 pluginAll 方法會呼叫具體外掛的 plugin 方法植入相應的外掛邏輯。如果有多個外掛,則會多次呼叫 plugin 方法,最終生成一個層層巢狀的代理類。形如下面:

當 Executor 的某個方法被呼叫的時候,外掛邏輯會先行執行。執行順序由外而內,比如上圖的執行順序為 plugin3 → plugin2 → Plugin1 → Executor。

plugin 方法是由具體的外掛類實現,不過該方法程式碼一般比較固定,所以下面找個示例分析一下。

// TianPlugin類public Object plugin(Object target) {    return Plugin.wrap(target, this);}//Pluginpublic 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.class, method = "prepare", args = {Connection.class, Integer.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.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類中@Overridepublic 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:對被代理物件的封裝。

20
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 太可怕了!差點因為一條SQL被拖出去祭天