MyBatis(四)MyBatis插件原理

MyBatis插件原理

MyBatis對開發者非常友好,它通過提供插件機制,讓我們可以根據自己的需要去增強MyBatis的功能。其底層是使用了代理模式+責任鏈模式

MyBatis官方https://mybatis.org/mybatis-3/zh/configuration.html#plugins可以看到MyBatis允許使用插件來攔截的方法調用

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed) //攔截執行器的方法
  • ParameterHandler (getParameterObject, setParameters) //攔截參數的處理
  • ResultSetHandler (handleResultSets, handleOutputParameters) //攔截結果集的處理
  • StatementHandler (prepare, parameterize, batch, update, query) //攔截SQL語法構建的處理

我們可以先看一下官網的例子,要想使用插件,有三步:
1.實現 Interceptor 接口
2.指定想要攔截的方法簽名
3.在mybatis-config.xml的<plugins>標籤裏進行配置

前面分析源碼的時候我們就看到了使用插件來代理Executor,最後調用了Interceptor接口的plugin方法來創建代理對象

 public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
        executorType = executorType == null ? this.defaultExecutorType : executorType;
        executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
        Object executor;
        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 (this.cacheEnabled) {
            executor = new CachingExecutor((Executor)executor);
        }

        Executor executor = (Executor)this.interceptorChain.pluginAll(executor);
        return executor;
    }

public Object pluginAll(Object target) {
        Interceptor interceptor;
        for(Iterator var2 = this.interceptors.iterator(); var2.hasNext(); target = interceptor.plugin(target)) {
            interceptor = (Interceptor)var2.next();
        }

        return target;
    }

至於其他幾個對象則是在具體的Executor執行doUpdate/doQuery方法的時候在創建對象之後創建了代理對象

public int doUpdate(MappedStatement ms, Object parameter) throws SQLException {
        Statement stmt = null;

        int var6;
        try {
            Configuration configuration = ms.getConfiguration();
            StatementHandler handler = configuration.newStatementHandler(this, ms, parameter, RowBounds.DEFAULT, (ResultHandler)null, (BoundSql)null);
            stmt = this.prepareStatement(handler, ms.getStatementLog());
            var6 = handler.update(stmt);
        } finally {
            this.closeStatement(stmt);
        }

        return var6;
    }

 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 = (StatementHandler)this.interceptorChain.pluginAll(statementHandler);
        return statementHandler;
    }



//configuration.newStatementHandler最後調用了BaseStatementHandler的構造方法,這裏創建了ParameterHandler和ResultSetHandler,並在創建後使用責任鏈模式創建代理對象
 protected BaseStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) {
        this.configuration = mappedStatement.getConfiguration();
        this.executor = executor;
        this.mappedStatement = mappedStatement;
        this.rowBounds = rowBounds;
        this.typeHandlerRegistry = this.configuration.getTypeHandlerRegistry();
        this.objectFactory = this.configuration.getObjectFactory();
        if (boundSql == null) {
            this.generateKeys(parameterObject);
            boundSql = mappedStatement.getBoundSql(parameterObject);
        }

        this.boundSql = boundSql;
        this.parameterHandler = this.configuration.newParameterHandler(mappedStatement, parameterObject, boundSql);
        this.resultSetHandler = this.configuration.newResultSetHandler(executor, mappedStatement, rowBounds, this.parameterHandler, resultHandler, boundSql);
    }


  public ParameterHandler newParameterHandler(MappedStatement mappedStatement, Object parameterObject, BoundSql boundSql) {
        ParameterHandler parameterHandler = mappedStatement.getLang().createParameterHandler(mappedStatement, parameterObject, boundSql);
        parameterHandler = (ParameterHandler)this.interceptorChain.pluginAll(parameterHandler);
        return parameterHandler;
    }

分析下MyBatis關於插件的幾個核心類

Plugin

MyBatis裏提供了一個Plugin 類,可以通過Plugin.wrap(target,inteceptor)方法來直接返回一個代理對象,mybatis的分頁插件PageInterceptor也是直接調用了Plugin的wrap方法通過jdk動態代理創建代理對象,其用來增強的InvocationHandler就是Plugin
所以代理對象在執行方法的時候會執行下面的invoke代碼,實際會調用Interceptor的intercept方法,因爲這裏沒有目標對象方法的調用,所以我們在實現Interceptor的intercept方法時,需要自己調用目標方法

public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor)
        //獲取目標對象的Class
        Class<?> type = target.getClass();
        //獲取目標對象的接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        //創建代理對象,滿足註解的簽名的實現類纔會創建代理對象
        return interfaces.length > 0 ? Proxy.newProxyInstance(type.getClassLoader(), interfaces, new Plugin(target, interceptor, signatureMap)) : target;
    }

    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            //獲取需要攔截的方法
            Set<Method> methods = (Set)this.signatureMap.get(method.getDeclaringClass());
            return methods != null && methods.contains(method) ? this.interceptor.intercept(new Invocation(this.target, method, args)) : method.invoke(this.target, args);
        } catch (Exception var5) {
            throw ExceptionUtil.unwrapThrowable(var5);
        }
    }

Invocation

invocation裏保存了目標對象和代理的方法,可以通過調用proceed()方法來調用目標對象的方法

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 this.method.invoke(this.target, this.args);
    }
}

總結一下:MyBatis插件相關的類

對象  作用 
Interceptor 自定義插件需要實現的接口
InterceptChain 配置文件中配置的插件會解析後會保存在 Configuration 的 InterceptChain 中 
Plugin 用來創建代理對象,包裝四大對象 
Invocation 對被代理對象進行包裝,可以調用 proceed()調用到被攔截的方法

 

 

 

 

 

 

public interface Interceptor {
    //覆蓋被攔截對象的原有方法(需要在實現裏調用目標對象的方法) 
    Object intercept(Invocation var1) throws Throwable;
    //創建代理對象
    Object plugin(Object var1);
    //用於設置在mybatis-config.xml裏配置的property屬性
    void setProperties(Properties var1);
}

PageHelper原理

用法

 PageHelper.startPage(pageNumber, pageSize); //pageNumber, pageSize,第幾頁,每頁幾條
 List<?> list= service.getAll();
 PageInfo page = new PageInfo(list, 10);

分頁插件的核心類PageInterceptor,從實現類的註解可以看到,攔截的方法是Executor的兩個重載的query方法

@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 Object intercept(Invocation invocation) throws Throwable {
        try {
            //獲取目標對象的參數,根據攔截的方法簽名獲取各個參數
            Object[] args = invocation.getArgs();
            //獲得MappedStatement參數,裏面封裝了跟本次statement id相關的各種參數
            MappedStatement ms = (MappedStatement)args[0];
            Object parameter = args[1];
            RowBounds rowBounds = (RowBounds)args[2];
            ResultHandler resultHandler = (ResultHandler)args[3];
            //通過invocation的getTarget方法獲取被代理的Executor對象,後面直接調用而不是調用
            //invocation.proceed()來執行目標對象的方法
            Executor executor = (Executor)invocation.getTarget();
            CacheKey cacheKey;
            BoundSql boundSql;
            if (args.length == 4) {
                boundSql = ms.getBoundSql(parameter);
                cacheKey = executor.createCacheKey(ms, parameter, rowBounds, boundSql);
            } else {
                cacheKey = (CacheKey)args[4];
                boundSql = (BoundSql)args[5];
            }

            List resultList;
            if (this.dialect.skip(ms, parameter, rowBounds)) {
                //執行目標對象的方法
                resultList = executor.query(ms, parameter, rowBounds, resultHandler, cacheKey, boundSql);
            } else {
                Map<String, Object> additionalParameters = (Map)this.additionalParametersField.get(boundSql);
                if (this.dialect.beforeCount(ms, parameter, rowBounds)) {
                    CacheKey countKey = executor.createCacheKey(ms, parameter, RowBounds.DEFAULT, boundSql);
                    countKey.update("_Count");
                    MappedStatement countMs = (MappedStatement)this.msCountMap.get(countKey);
                    if (countMs == null) {
                        countMs = MSUtils.newCountMappedStatement(ms);
                        this.msCountMap.put(countKey, countMs);
                    }

                    String countSql = this.dialect.getCountSql(ms, boundSql, parameter, rowBounds, countKey);
                    BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameter);
                    Iterator var16 = additionalParameters.keySet().iterator();

                    while(var16.hasNext()) {
                        String key = (String)var16.next();
                        countBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }

                    Object countResultList = executor.query(countMs, parameter, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql);
                    Long count = (Long)((List)countResultList).get(0);
                    if (!this.dialect.afterCount(count, parameter, rowBounds)) {
                        Object var18 = this.dialect.afterPage(new ArrayList(), parameter, rowBounds);
                        return var18;
                    }
                }

                if (!this.dialect.beforePage(ms, parameter, rowBounds)) {
                    resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, boundSql);
                } else {
                    parameter = this.dialect.processParameterObject(ms, parameter, boundSql, cacheKey);
                    //調用dialect的getPageSql方法獲取分頁SQL
                    String pageSql = this.dialect.getPageSql(ms, boundSql, parameter, rowBounds, cacheKey);
                    BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameter);
                    Iterator var25 = additionalParameters.keySet().iterator();

                    while(true) {
                        if (!var25.hasNext()) {
                            resultList = executor.query(ms, parameter, RowBounds.DEFAULT, resultHandler, cacheKey, pageBoundSql);
                            break;
                        }

                        String key = (String)var25.next();
                        pageBoundSql.setAdditionalParameter(key, additionalParameters.get(key));
                    }
                }
            }

            Object var22 = this.dialect.afterPage(resultList, parameter, rowBounds);
            return var22;
        } finally {
            this.dialect.afterAll();
        }
    }

    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }
//AbstractHelperDialect
public String getPageSql(MappedStatement ms, BoundSql boundSql, Object parameterObject, RowBounds rowBounds, CacheKey pageKey) {
        String sql = boundSql.getSql();
        Page page = this.getLocalPage();
        return this.getPageSql(sql, page, pageKey);
    }

最後會調用AbstractHelperDialect的getPageSql抽象方法,根據不同的數據庫選擇不同的實現類

public class MySqlDialect extends AbstractHelperDialect {
    public MySqlDialect() {
    }
    
    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 ");
            sqlBuilder.append(page.getPageSize());
        } else {
            //從Page對象獲取對應的分頁信息  起始下標和獲取個數拼接SQL
            sqlBuilder.append(" LIMIT ");
            sqlBuilder.append(page.getStartRow());
            sqlBuilder.append(",");
            sqlBuilder.append(page.getPageSize());
            pageKey.update(page.getStartRow());
        }

        pageKey.update(page.getPageSize());
        return sqlBuilder.toString();
    }
}

這裏的Page是通過getLocalPage方法獲取的,實際是從PageMethod裏的ThreadLocal裏獲取的,所以我們在使用的時候直接設置page信息就可以了,因爲ThreadLocal幫我們保證了線程安全

 protected static final ThreadLocal<Page> LOCAL_PAGE = new ThreadLocal();

    public PageMethod() {
    }
    
    //PageHelper.startPage最終調用的方法
    protected static void setLocalPage(Page page) {
        LOCAL_PAGE.set(page);
    }

    public static <T> Page<T> getLocalPage() {
        return (Page)LOCAL_PAGE.get();
    }

應用場景

1.分表
在接口上添加註解,然後使用Interceptor對 query,update 方法進行攔截 ,根據註解上配置的參數進行分表操作

2.數據加解密
可以攔截獲得入參和返回值,在update的時候加密;查詢的時候解密

3. 菜單權限控制
對 query 方法進行攔截,在方法上添加不同的權限註解,這樣就可以註解的權限信息,在 SQL 上加上權限過濾條件

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章