天天看點

Mybatis插件原理(攔截器)

    • 一、MyBatis攔截器原理探究
      • 1.1 MyBatis攔截器介紹
      • 1.2 攔截器的使用
    • 二、Plugin原理
      • 2.1 Plugin原理
        • 2.1.1 InterceptorChain
        • 2.1.2 插件鍊的建立
        • 2.1.3 插件攔截
        • 2.1.3 Plugin
        • 2.1.4 插件配置
        • 2.1.5 插件實作

一、MyBatis攔截器原理探究

1.1 MyBatis攔截器介紹

MyBatis提供了一種插件(plugin)的功能,雖然叫做插件,但其實這是攔截器功能。那麼攔截器攔截MyBatis中的哪些内容呢?

MyBatis 允許你在已映射語句執行過程中的某一點進行攔截調用。

預設情況下,MyBatis 允許使用插件來攔截的方法調用包括:

  • Executor (update, query, flushStatements, commit, rollback, getTransaction, close, isClosed)
  • ParameterHandler (getParameterObject, setParameters)
  • ResultSetHandler (handleResultSets, handleOutputParameters)
  • StatementHandler (prepare, parameterize, batch, update, query)

我們看到了可以攔截Executor接口的部分方法,比如update,query,commit,rollback等方法,還有其他接口的一些方法等。

總體概括為:

  • 攔截執行器的方法
  • 攔截參數的處理
  • 攔截結果集的處理
  • 攔截Sql文法建構的處理

1.2 攔截器的使用

首先我們看下MyBatis攔截器的接口定義:

public interface Interceptor {

  Object intercept(Invocation invocation) throws Throwable;

  Object plugin(Object target);

  void setProperties(Properties properties);

}
           

比較簡單,隻有3個方法。 MyBatis預設沒有一個攔截器接口的實作類,開發者們可以實作符合自己需求的攔截器。

下面的MyBatis官網的一個攔截器執行個體:

@Intercepts({@Signature(
  type= Executor.class,
  method = "update",
  args = {MappedStatement.class,Object.class})})
public class ExamplePlugin implements Interceptor {
  public Object intercept(Invocation invocation) throws Throwable {
    return invocation.proceed();
  }
  public Object plugin(Object target) {
    return Plugin.wrap(target, this);
  }
  public void setProperties(Properties properties) {
  }
}
           

全局xml配置:

<plugins>
    <plugin interceptor="org.format.mybatis.cache.interceptor.ExamplePlugin"></plugin>
</plugins>
           

這個攔截器攔截Executor接口的update方法(其實也就是SqlSession的新增,删除,修改操作),所有執行executor的update方法都會被該攔截器攔截到。

二、Plugin原理

2.1 Plugin原理

Plugin的實作采用了Java的動态代理,應用了責任鍊設計模式

2.1.1 InterceptorChain

攔截器鍊,用于儲存從配置檔案解析後的所有攔截器

2.1.2 插件鍊的建立

在Configuration解析配置檔案的時候,XMLConfigBuilder.parseConfiguration中會調用pluginElement解析插件資訊并執行個體化後,儲存到插件鍊中

// /configuration/plugins節點
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);
        // 儲存到插件鍊中
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }
// Configuration.addInterceptor
public void addInterceptor(Interceptor interceptor) {
    interceptorChain.addInterceptor(interceptor);
}

public class InterceptorChain {
  // 所有攔截器執行個體
  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  public Object pluginAll(Object target) {
    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);
  }

}
           

2.1.3 插件攔截

在MyBatis中,隻能攔截四種接口的實作類:

  • Executor
  • ParameterHandler
  • ResultSetHandler
  • StatementHandler

    每種類型的攔截方式都是一樣的,這裡取executor為例:

    在建立SqlSession的時候,會需要建立Executor實作類,在建立時,會調用插件鍊的加載插件功能:executor = (Executor) interceptorChain.pluginAll(executor);,該方法會形成一個調用鍊。

// 依次調用每個插件的plugin方法,如果該插件無需攔截target,則直接傳回target
  public Object pluginAll(Object target) {
    for (Interceptor interceptor : interceptors) {
      target = interceptor.plugin(target);
    }
    return target;
  }
           

2.1.3 Plugin

插件代理的實作,這裡應用了Java Dynamic Proxy

public class Plugin implements InvocationHandler {
    // 需要被代理的執行個體
    private Object target;
    // 攔截器執行個體
    private Interceptor interceptor;
    // 攔截器需要攔截的方法摘要,這裡Class鍵為Executor等上述的四個
    // 值為需要被攔截的方法
    private Map<Class<?>, Set<Method>> signatureMap;

    // 此類不能直接建立,需要通過靜态方法wrap來建立代理類
    private Plugin(Object target, Interceptor interceptor,
            Map<Class<?>, Set<Method>> signatureMap) {
        this.target = target;
        this.interceptor = interceptor;
        this.signatureMap = signatureMap;
    }

    public static Object wrap(Object target, Interceptor interceptor) {
        Map<Class<?>, Set<Method>> signatureMap = getSignatureMap(interceptor);
        Class<?> type = target.getClass();
        // 擷取需要被代理類的所有待攔截的接口
        Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
        if (interfaces.length > ) {
            // 建立代理類
            return Proxy.newProxyInstance(type.getClassLoader(), interfaces,
                    new Plugin(target, interceptor, signatureMap));
        }
        // 沒有需要攔截的方法,直接傳回原執行個體
        return target;
    }

    // 在代理類中調用
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        try {
            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);
        }
    }
    // 擷取需要被攔截的方法摘要
    private static Map<Class<?>, Set<Method>> getSignatureMap(
            Interceptor interceptor) {
        // 先擷取攔截器實作類上的注解,提取需要被攔截的方法
        /* 注解示例:@Intercepts(value={@Signature(args={Void.class},method="query",type=Void.class)})*/
        Intercepts interceptsAnnotation = interceptor.getClass().getAnnotation(
                Intercepts.class);
        if (interceptsAnnotation == null) { // issue #251
            throw new PluginException(
                    "No @Intercepts annotation was found in interceptor "
                            + interceptor.getClass().getName());
        }
        Signature[] sigs = interceptsAnnotation.value();
        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()]);
    }
}
           

2.1.4 插件配置

在mybatis.xml配置檔案:

<plugins>
        <plugin interceptor="com.shareinfo.framework.pagination.mybatis.PageInterceptor" />
</plugins>
           

2.1.5 插件實作

// Ibatis 分頁攔截器
@Intercepts({ @Signature(type = Executor.class, method = "query", args = { MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class }) })
public class PageInterceptor implements Interceptor
{
    static int MAPPED_STATEMENT_INDEX = ;
    static int PARAMETER_INDEX = ;
    static int ROWBOUNDS_INDEX = ;
    static int RESULT_HANDLER_INDEX = ;

    public Object intercept(Invocation invocation) throws Throwable
    {
        processIntercept(invocation.getArgs());
        return invocation.proceed();
    }

    public void processIntercept(Object[] queryArgs) throws ConfigurationException
    {
        // 目前環境 MappedStatement,BoundSql,及sql取得
        MappedStatement mappedStatement = (MappedStatement)queryArgs[MAPPED_STATEMENT_INDEX];
        // 請求的對象
        Object parameter = queryArgs[PARAMETER_INDEX];
        // 分頁資訊
        RowBounds rowBounds = (RowBounds)queryArgs[ROWBOUNDS_INDEX];
        int offset = rowBounds.getOffset();
        int limit = rowBounds.getLimit();

        if (offset !=  || limit != Integer.MAX_VALUE)
        {
            Dialect dialect = getDialect(mappedStatement.getConfiguration());
            BoundSql boundSql = mappedStatement.getBoundSql(parameter);
            String sql = boundSql.getSql().trim();
            sql = dialect.getPaginationSql(sql, offset, limit);
            offset = ; // 這裡沒有增加的話導緻後面分頁查詢不出來
            limit = Integer.MAX_VALUE;
            queryArgs[ROWBOUNDS_INDEX] = new RowBounds(offset, limit);
            BoundSql newBoundSql = new BoundSql(mappedStatement.getConfiguration(), sql, boundSql.getParameterMappings(), boundSql.getParameterObject());
            MappedStatement newMs = copyFromMappedStatement(mappedStatement, new BoundSqlSqlSource(newBoundSql));
            queryArgs[MAPPED_STATEMENT_INDEX] = newMs;
        }
    }

    private Dialect getDialect(Configuration configuration) throws ConfigurationException
    {
        Dialect.Type databaseType = null;
        try
        {
            databaseType = Dialect.Type.valueOf(configuration.getVariables().getProperty("dialect").toUpperCase());
        }
        catch (Exception e)
        {
            throw new ConfigurationException("the value of the dialect property in mybatis-config.xml is not defined : " + configuration.getVariables().getProperty("dialect"));
        }

        Dialect dialect = null;
        switch (databaseType)
        {
            case MYSQL:
                dialect = new MySQL5Dialect();
            case SQLSERVER:
                dialect = new SqlServerDialect();
            case ORACLE:
                dialect = new OracleDialect();
        }

        return dialect;
    }

    private MappedStatement copyFromMappedStatement(MappedStatement ms, SqlSource newSqlSource)
    {
        Builder builder = new Builder(ms.getConfiguration(), ms.getId(), newSqlSource, ms.getSqlCommandType());

        builder.resource(ms.getResource());
        builder.fetchSize(ms.getFetchSize());
        builder.statementType(ms.getStatementType());
        builder.keyGenerator(ms.getKeyGenerator());
        // builder.keyProperty(ms.getKeyProperty());

        builder.timeout(ms.getTimeout());

        builder.parameterMap(ms.getParameterMap());

        builder.resultMaps(ms.getResultMaps());
        builder.resultSetType(ms.getResultSetType());

        builder.cache(ms.getCache());
        builder.flushCacheRequired(ms.isFlushCacheRequired());
        builder.useCache(ms.isUseCache());

        return builder.build();
    }

    public class BoundSqlSqlSource implements SqlSource
    {
        BoundSql boundSql;

        public BoundSqlSqlSource(BoundSql boundSql)
        {
            this.boundSql = boundSql;
        }

        public BoundSql getBoundSql(Object parameterObject)
        {
            return boundSql;
        }
    }

    public Object plugin(Object arg0)
    {
        return Plugin.wrap(arg0, this);
    }

    public void setProperties(Properties properties)
    {
    }
}
           

繼續閱讀