インタビュアー:Mybatisプラグインの実装原理について教えてください。

ここに画像の説明を挿入

前書き

以前の記事でmybatisプラグインの実装原理について書きました。

インタビュアー:MyBatis、なぜインターフェースを書くだけでSQLを実行できるのですか?

Mybaitsプラグインの実装は、主に責任チェーンモデルと動的プロキシを使用します

動的エージェントは、SQLステートメントの実行中に特定のポイントをインターセプトすることができる。複数のプラグインが設定されている場合、責任の連鎖モードが複数回傍受することができる。責任の連鎖モードのUML図を
ここに画像の説明を挿入
見ることができる次の通りである。責任でチェーン、各ハンドラーオブジェクトにはすべて次のハンドラーオブジェクトへの参照が含まれます。ハンドラーオブジェクトがメッセージを処理した後、責任のチェーン全体が終了するまで、処理を続行するために次のハンドラーオブジェクトにリクエストを渡します。このとき、開閉の原則に従って、ハンドラーの実行順序を変更したり、ハンドラーを追加または削除したりできます。

Mybatisプラグインを書く

mybatisは次のメソッド呼び出しを傍受できます

  1. エグゼキュータ(update、query、flushStatements、commit、rollback、getTransaction、close、isClosed)
  2. ParameterHandler(getParameterObject、setParameters)
  3. ResultSetHandler(handleResultSets、handleOutputParameters)
  4. StatementHandler(準備、パラメーター化、バッチ、更新、クエリ)

これらのオブジェクトがなぜあるのかについては、後で説明します

SQL実行時間を出力するプラグインを作成する

@Intercepts({
    
    @Signature(type = StatementHandler.class, method = "query", args = {
    
     Statement.class, ResultHandler.class }),
        @Signature(type = StatementHandler.class, method = "update", args = {
    
     Statement.class }),
        @Signature(type = StatementHandler.class, method = "batch", args = {
    
     Statement.class })})
public class SqlCostTimeInterceptor implements Interceptor {
    
    

    public static final Logger logger = LoggerFactory.getLogger(SqlCostTimeInterceptor.class);

    public Object intercept(Invocation invocation) throws Throwable {
    
    
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        long start = System.currentTimeMillis();
        try {
    
    
            // 执行被拦截的方法
            return invocation.proceed();
        } finally {
    
    
            BoundSql boundSql = statementHandler.getBoundSql();
            String sql = boundSql.getSql();
            long end = System.currentTimeMillis();
            long cost = end - start;
            logger.info("{}, cost is {}", sql, cost);
        }
    }

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

    public void setProperties(Properties properties) {
    
    

    }
}

mybatis構成ファイルでプラグインを構成します

<plugins>
	<plugin interceptor="com.javashitang.part1.plugins.SqlCostTimeInterceptor"></plugin>
</plugins>

この時点で、実行されたSQLと費やされた時間を印刷できます。効果は次のとおりです。

select id, role_name as roleName, note from role where id = ?, cost is 35

主成分分析

前述のように、Mybatisは動的プロキシを介して関数を追加するため、ターゲットオブジェクトのメソッドを呼び出した後、元のメソッドの代わりにプロキシオブジェクトのメソッドが使用されます。

これについて言えば、インターセプターはInvocationHandlerインターフェースを実装し、最初に指定されたオブジェクトのプロキシクラスを生成し、次にInvocationHandlerのinvokeメソッドで指定されたメソッドを拡張するだけでよいことに気付くかもしれません。

しかし、InvocationHandlerを継承した後、プロキシクラスを生成し、指定されたメソッドを拡張することは面倒な仕事ではありません。フレームワークはそれをカプセル化するのに役立ちます。

したがって、主に複数の@Signatureアノテーションを配置する@Interceptsアノテーションがあり、@ Signatureアノテーションはインターセプトされるクラスとメソッドを定義します。

また、動的プロキシの実装を容易にするためのインターセプターインターフェイスとプラグインクラスを提供します。これらがどのように連携するかを見てみましょう。

プラグインはInterceptorインターフェースを実装する必要があるため、最初にInterceptorインターフェースを分析しましょう。

public interface Interceptor {
    
    

  /** 执行拦截逻辑的方法,Invocation只是将动态代理中获取到的一些参数封装成一个对象 */
  Object intercept(Invocation invocation) throws Throwable;

  /**
   * target是被拦截的对象,它的作用是给被拦截对象生成一个代理对象,并返回它。
   * 为了方便,可以直接使用Mybatis中org.apache.ibatis.plugin.Plugin类的wrap方法(是静态方法)生成代理对象
   */
  Object plugin(Object target);

  /** 根据配置初始化Interceptor对象 */
  void setProperties(Properties properties);

}

プラグインメソッドはプロキシオブジェクトを生成することです。一般的なアプローチは、Plugin.wrap(target、this);メソッドを直接呼び出してプロキシオブジェクトを生成することです。プラグインクラスの内部を調べます。主なメソッドは次のとおりです。

public class Plugin implements InvocationHandler {
    
    

  /** 目标对象 */
  private final Object target;
  /** Interceptor对象 */
  private final Interceptor interceptor;
  /** 记录了@Signature注解中的信息 */
  /** 被拦截的type->被拦截的方法 */
  private final Map<Class<?>, Set<Method>> signatureMap;

  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);
    // 取得要改变行为的类 (ParameterHandler | ResultSetHandler | StatementHandler | Executor)
    Class<?> type = target.getClass();
    // 拿到被代理对象的拦截方法,所实现的接口
    Class<?>[] interfaces = getAllInterfaces(type, signatureMap);
    // 如果当前传入的Target的接口中有@Intercepts注解中定义的接口,那么为之生成代理,否则原Target返回
    if (interfaces.length > 0) {
    
    
      return Proxy.newProxyInstance(
          type.getClassLoader(),
          interfaces,
          new Plugin(target, interceptor, signatureMap));
    }
    return target;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    
    
    try {
    
    
      // 获取当前方法所在类或接口中,可被当前 Interceptor 拦截的方法
      Set<Method> methods = signatureMap.get(method.getDeclaringClass());
      // 如果当前调用的方法需要被拦截,则调用interceptor.intercept()方法进行拦截处理
      if (methods != null && methods.contains(method)) {
    
    
        return interceptor.intercept(new Invocation(target, method, args));
      }
      // 如果当前调用的方法不能被拦截,则调用target对象的相应方法
      return method.invoke(target, args);
    } catch (Exception e) {
    
    
      throw ExceptionUtil.unwrapThrowable(e);
    }
  }
}

これまで、プロキシクラスの実装と特定のメソッドのインターセプトは、泥棒にとって便利なPlugin.wrap()メソッドを使用して行われてきました。

Plugin.invoke()メソッドでは、Interceptorインターフェイスのinterceptメソッドが最終的に呼び出され、ターゲットクラス、ターゲットメソッド、およびパラメーターがInvocationオブジェクトにカプセル化されます。

return interceptor.intercept(new Invocation(target, method, args));

次に、呼び出しの定義を見てください

/**
 * @author Clinton Begin
 * 将要调用的类,方法,参数封装成一个对象,方便传递给拦截器
 */
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 getTarget() {
    
    
    return target;
  }

  public Method getMethod() {
    
    
    return method;
  }

  public Object[] getArgs() {
    
    
    return args;
  }

  /** 这个方法是给拦截器调用的,拦截器最后会调用这个方法来执行本来要执行的方法,这样就可以在方法前后加上拦截的逻辑了 */
  public Object proceed() throws InvocationTargetException, IllegalAccessException {
    
    
    return method.invoke(target, args);
  }

}

メソッドproceed()メソッドは1つだけで、proceed()はインターセプトされたメソッドのみを実行します。現時点では、Interceptorオブジェクトのインターセプトメソッドで実行する必要がある操作は明確です。拡張ロジックを記述するだけで、最後に、invocationオブジェクトのproceed()メソッドを呼び出します。

これまでのところ、プラグインの動作原理を大まかに理解しました。最後のステップは、ターゲットオブジェクトのプロキシオブジェクトを生成することだけです。Mybatisの初期化から答えを見つけます。

構成ファイルでプラグインを構成する形式は次のとおりです。インターセプターは完全なクラス名を入力し、複数のキーと値の値を以下に書き込むことができます。インターセプターインターフェイスが実装された後、setPropertiesがあります。これらのプロパティ値をPropertiesオブジェクトにカプセル化するメソッド、Comeinを設定します

<plugin interceptor="">
	<property name="" value=""/>
	<property name="" value=""/>
</plugin>

mybatis構成ファイルの解析は、XMLConfigBuilderのparseConfigurationメソッドにあります。ここでは、プラグインの解析プロセスのみを確認します。

pluginElement(root.evalNode("plugins"));
  private void pluginElement(XNode parent) throws Exception {
    
    
    if (parent != null) {
    
    
      for (XNode child : parent.getChildren()) {
    
    
        String interceptor = child.getStringAttribute("interceptor");
        // 解析拦截器中配置的属性,并封装成一个Properties对象
        Properties properties = child.getChildrenAsProperties();
        // 通过类名示例化一个Interceptor对象
        Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance();
        // 可以给拦截器的Properties属性赋值
        interceptorInstance.setProperties(properties);
        configuration.addInterceptor(interceptorInstance);
      }
    }
  }

インスタンス化されたInterceptorオブジェクトは、InterceptorChainオブジェクトのinterceptorsプロパティに配置されます。

public class InterceptorChain {
    
    

  private final List<Interceptor> interceptors = new ArrayList<Interceptor>();

  /** 这里有个特别有意思的地方,先添加的拦截器最后才会执行,因为代理是一层一层套上去的,就像这个函数f(f(f(x))) */
  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);
  }

}

プロキシオブジェクトの生成にInterceptorChainオブジェクトのpluginAllメソッドが使用されていませんか?それが呼ばれる場所を参照してください

parameterHandler = (ParameterHandler) interceptorChain.pluginAll(parameterHandler);
resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler);
executor = (Executor) interceptorChain.pluginAll(executor);

前述のmybatisが特定のクラスしかインターセプトできないのはこのためではありませんか?これらのクラスのみがプロキシであるためです。

これまで、Mybatisプラグインの原理を分析してきました。非常にシンプルですが、ParameterHandlerやその他のメソッドの機能と拡張方法を理解する必要があるため、実用的なMybatisプラグインを作成するのは簡単ではありません。

最後に、要約すると、@ Signatureアノテーションは主に、インターセプトされるクラスとそのメソッドを定義するために使用され、Interceptorインターフェイスとプラグインは連携して指定されたオブジェクトのプロキシオブジェクトを生成し、指定されたメソッドをインターセプトします。その前後に実行されます。

フォローへようこそ

ここに画像の説明を挿入

リファレンスブログ

Mybatisプラグインの作成
[1] https://www.cnblogs.com/xrq730/p/6972268.htmlMybatis
プラグインの実装原則
[2] https://www.cnblogs.com/xrq730/p/6984982。 html
[3] https://juejin.im/post/5abe12f5f265da237411177f

おすすめ

転載: blog.csdn.net/zzti_erlie/article/details/109562703