Javaダイナミックエージェントの簡単な使用と理解

14410 ワード

  • 前言
  • JDK動的エージェント
  • エージェントクラス
  • CGLIBダイナミックエージェント
  • エージェントクラス
  • Spring @Configuration

  • まとめ
  • 結語
  • 前言
    Javaでは、ダイナミックエージェントはよく使われる機能で、一般的には自分で直接使う必要はありませんが、何が起こっているのかを知る必要があります.
    このブログの主な内容はJDKダイナミックエージェントとCGLIBダイナミックエージェントの簡単な使用と理解です.
    JDKダイナミックエージェント
    JDKダイナミックエージェントは、インタフェースに依存してエージェントを必要とするメソッドを決定します.使用する場合は、次のロールに分けることができます.
  • TargetInterfaces-エージェントを必要とするターゲットインタフェース、JDKダイナミックエージェントは、これらのインタフェースのメソッドの作成エージェント
  • を呼び出す
  • TargetObject-ターゲットインタフェースを実現したオブジェクト
  • InvocationHandler-メソッド呼び出しプロセッサ、JDKダイナミックエージェントは、InvocationHandlerオブジェクトを介してターゲットメソッドの呼び出しを内部で処理
  • java.lang.reflect.Proxy-アセンブリInvocationHandlerおよびTargetObjectは、プロキシオブジェクトを作成します.作成されたプロキシオブジェクトは、そのサブクラスのインスタンス
  • です.TargetInterfacesおよびTargetObjectは比較的容易に理解できるが、いくつかのインタフェースおよびこれらのインタフェースを実現したオブジェクト、例えば、以下のようなものである.
    interface TargetInterfaceA {
      void targetMethodA();
    }
    
    interface TargetInterfaceB {
      void targetMethodB();
    }
    
    class TargetClass implements TargetInterfaceA, TargetInterfaceB {
      @Override
      public void targetMethodA() {
        System.out.println("Target method A...");
      }
    
      @Override
      public void targetMethodB() {
        System.out.println("Target method B...");
      }
    }
    

    上記の例では、ターゲットインタフェースは[TargetInterfaceA, TargetInterfaceB]である、ターゲットオブジェクトはTargetClassの例である.
    次に、TargetClass実装インタフェースのメソッドの呼び出しをブロックするには、InvocationHandlerによってエージェントロジックを定義する必要があります.
    class SimpleInvocationHandler implements InvocationHandler {
      private Object target;
    
      public SimpleInvocationHandler(Object target) {
        this.target = target;
      }
    
      @Override
      public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println(String.format("Before invocation method %s", method.getName()));
        Object result = method.invoke(target, args);
        System.out.println(String.format("After invocation method %s", method.getName()));
        return result;
      }
    }
    
    InvocationHandlerこのインタフェースは、次のパラメータを持つメソッドinvokeのみを定義します.
  • proxy-エージェント・オブジェクト・インスタンスです.ただし、TargetObjectではなく、Proxyサブクラスのインスタンスです.したがって、InvocationHandlerインスタンスの内部にTargetObject
  • を持つ必要があります.
  • method-呼び出すメソッド
  • args-メソッド呼び出しパラメータ
  • InvocationHandlerTargetClassがあれば、TargetObjectを作成し、Proxyを組み立ててエージェントオブジェクトを作成することができます.主にnewProxyInstanceの方法で完了します.
    TargetClass targetObject = new TargetClass();
    Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), new SimpleInvocationHandler(targetObject););
    

    メソッドProxy.newProxyInstanceのパラメータは、次のとおりです.
  • ClassLoader-一つClassLoader,簡単なら直接targetObjectClassLoaderを使えばいい
  • Class>[]-エージェントするインタフェース配列も,同様に,targetObject実装されたすべてのインタフェース
  • を直接取得する.
  • InvocationHandler-メソッド呼び出し処理ロジックを定義したInvocationHandler
  • PS:エージェントオブジェクトを作成するにはTargetObjectを作成する必要があります.また、TargetObjectInvocationHandlerに手動で渡す必要があります.面倒です.
    完全なコードとテスト:
    interface TargetInterfaceA {
        void targetMethodA();
    }
    
    interface TargetInterfaceB {
        void targetMethodB();
    }
    
    class TargetClass implements TargetInterfaceA, TargetInterfaceB {
        @Override
        public void targetMethodA() {
            System.out.println("Target method A...");
        }
    
        @Override
        public void targetMethodB() {
            System.out.println("Target method B...");
        }
    }
    
    class SimpleInvocationHandler implements InvocationHandler {
        private Object target;
    
        public SimpleInvocationHandler(Object target) {
            this.target = target;
        }
    
        public static Object bind(Object targetObject) {
            SimpleInvocationHandler handler = new SimpleInvocationHandler(targetObject);
            return Proxy.newProxyInstance(targetObject.getClass().getClassLoader(), targetObject.getClass().getInterfaces(), handler);
        }
    
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            System.out.println(String.format("Before invocation method %s", method.getName()));
            Object result = method.invoke(target, args);
            System.out.println(String.format("After invocation method %s", method.getName()));
            return result;
        }
    }
    
    public class ProxyTest {
      public static void main(String[] args) {
        Object proxy = SimpleInvocationHandler.bind(new TargetClass());
        ((TargetInterfaceA) proxy).targetMethodA();
        ((TargetInterfaceB) proxy).targetMethodB();
      }
    }
    

    出力:
    Before invocation method targetMethodA
    Target method A...
    After invocation method targetMethodA
    Before invocation method targetMethodB
    Target method B...
    After invocation method targetMethodB
    

    エージェントクラス
    コードを実行するときに、次の行のコードを一番前に置いて、Proxy動的に生成されたエージェントクラスがどのようになっているかを確認できます.
    System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true");
    

    前のコードで生成されたプロキシクラスは次のとおりです.
    final class $Proxy0 extends Proxy implements TargetInterfaceA, TargetInterfaceB {
      private static Method m0;
      private static Method m1;
      private static Method m2;
      private static Method m4;
      private static Method m3;
    
      static {
        try {
          m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
          m2 = Class.forName("java.lang.Object").getMethod("toString");
    
          //           
          m4 = Class.forName("classload.TargetInterfaceB").getMethod("targetMethodB");
          m3 = Class.forName("classload.TargetInterfaceA").getMethod("targetMethodA");
    
          m0 = Class.forName("java.lang.Object").getMethod("hashCode");
        } catch (NoSuchMethodException var2) {
          throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
          throw new NoClassDefFoundError(var3.getMessage());
        }
      }
    
      public $Proxy0(InvocationHandler var1) throws  {
        super(var1);
      }
    
      //    InvocationHandler        
      public final void targetMethodA() throws  {
        try {
          super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }
    
      //    InvocationHandler        
      public final void targetMethodB() throws  {
        try {
          super.h.invoke(this, m4, (Object[])null);
        } catch (RuntimeException | Error var2) {
          throw var2;
        } catch (Throwable var3) {
          throw new UndeclaredThrowableException(var3);
        }
      }
    }
    

    エージェントクラスのコードを読むと、次のことがわかります.
  • エージェントクラスはProxyを継承し、ターゲットインタフェース
  • を実現した.
  • エージェントクラスが静的初期化ブロックで反射によりターゲットインタフェースを取得する方法
  • エージェントクラスによって実装されるインタフェースメソッドは、InvocationHandlerによってターゲットメソッド
  • を呼び出す.
  • InvocationHandler渡された最初のパラメータはエージェントオブジェクトであり、TargetObject1
  • ではありません.
    また、エージェントクラスは、ObjectのhashCode、equals、toStringメソッドを取得しました.これらの呼び出しロジックは、InvocationHandlerオブジェクトに対応するメソッドを直接呼び出すことです.たとえば、次のようになります.
    public final int hashCode() throws  {
      try {
        return (Integer)super.h.invoke(this, m0, (Object[])null);
      } catch (RuntimeException | Error var2) {
        throw var2;
      } catch (Throwable var3) {
        throw new UndeclaredThrowableException(var3);
      }
    }
    

    したがって、ターゲットオブジェクトのこれらのメソッドをエージェントすることもできます.
    CGLIBダイナミックエージェント
    CGLIBダイナミックエージェントはJDKダイナミックエージェントと似ているが、CGLIBダイナミックエージェントはクラスベースであり、インタフェースを実現する必要はなく、簡単に使用するとMethodInterceptorを定義するだけでよい、JDKダイナミックエージェントにおけるInvocationHandlerに相当する.
    class SimpleMethodInterceptor implements MethodInterceptor {
      @Override
      public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
        System.out.println(String.format("Before invocation method %s", method.getName()));
        Object result = proxy.invokeSuper(obj, args);
        System.out.println(String.format("After invocation method %s", method.getName()));
        return result;
      }
    }
    
    MethodInterceptorがあれば、エージェントオブジェクトを作成できます.
    class ProxyTest {
      public static void main(String[] args) {
        Enhancer enhancer = new Enhancer();
        //          
        enhancer.setSuperclass(TargetClass.class);
        //    MethodInterceptor
        enhancer.setCallback(new SimpleMethodInterceptor());
        //       
        TargetClass proxyObject = (TargetClass) enhancer.create();
        //     
        proxyObject.targetMethodA();
        proxyObject.targetMethodB();
      }
    }
    
    
    class TargetClass {
      public void targetMethodA() {
        System.out.println("Target method A...");
      }
    
      public void targetMethodB() {
        System.out.println("Target method B...");
      }
    }
    

    実行出力:
    Before invocation method targetMethodA
    Target method A...
    After invocation method targetMethodA
    Before invocation method targetMethodB
    Target method B...
    After invocation method targetMethodB
    

    エージェントクラスCGLIBでは、生成されたプロキシクラス2を保存するためにDebuggingClassWriter.DEBUG_LOCATION_PROPERTY属性の値を設定できます.
    public class TargetClass$$EnhancerByCGLIB$$eb42b691 extends TargetClass implements Factory {
      private MethodInterceptor CGLIB$CALLBACK_0;
    
      static void CGLIB$STATICHOOK1() {
        //         
        var10000 = ReflectUtils.findMethods(new String[]{"targetMethodA", "()V", "targetMethodB", "()V"}, (var1 = Class.forName("TargetClass")).getDeclaredMethods());
        CGLIB$targetMethodA$0$Method = var10000[0];
        CGLIB$targetMethodA$0$Proxy = MethodProxy.create(var1, var0, "()V", "targetMethodA", "CGLIB$targetMethodA$0");
        CGLIB$targetMethodB$1$Method = var10000[1];
        CGLIB$targetMethodB$1$Proxy = MethodProxy.create(var1, var0, "()V", "targetMethodB", "CGLIB$targetMethodB$1");
      }
    
      //          
      final void CGLIB$targetMethodA$0() {
        super.targetMethodA();
      }
    
      public final void targetMethodA() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
          CGLIB$BIND_CALLBACKS(this);
          var10000 = this.CGLIB$CALLBACK_0;
        }
    
        //   MethodInterceptor        MethodInterceptor       
        if (var10000 != null) {
          var10000.intercept(this, CGLIB$targetMethodA$0$Method, CGLIB$emptyArgs, CGLIB$targetMethodA$0$Proxy);
        } else {
          super.targetMethodA();
        }
      }
    
      //          
      final void CGLIB$targetMethodB$1() {
        super.targetMethodB();
      }
    
      public final void targetMethodB() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
          CGLIB$BIND_CALLBACKS(this);
          var10000 = this.CGLIB$CALLBACK_0;
        }
    
        //   MethodInterceptor        MethodInterceptor       
        if (var10000 != null) {
          var10000.intercept(this, CGLIB$targetMethodB$1$Method, CGLIB$emptyArgs, CGLIB$targetMethodB$1$Proxy);
        } else {
          super.targetMethodB();
        }
      }
    
      final int CGLIB$hashCode$5() {
        return super.hashCode();
      }
    
      //   Object      
      public final int hashCode() {
        MethodInterceptor var10000 = this.CGLIB$CALLBACK_0;
        if (var10000 == null) {
          CGLIB$BIND_CALLBACKS(this);
          var10000 = this.CGLIB$CALLBACK_0;
        }
    
        if (var10000 != null) {
          Object var1 = var10000.intercept(this, CGLIB$hashCode$5$Method, CGLIB$emptyArgs, CGLIB$hashCode$5$Proxy);
          return var1 == null ? 0 : ((Number)var1).intValue();
        } else {
          return super.hashCode();
        }
      }
    
      // ...
    }
    

    見える
  • CGLIBメソッドごとに2つのエージェントが設定され、1つは親メソッドを直接呼び出し、もう1つはMethodInterceptorが存在するかどうかを判断して呼び出し
  • を行う
  • エージェントクラスはTargetClassを継承し、JDKダイナミックエージェントでProxyを継承する方法とは異なります
  • MethodInterceptorを設定すると、CGLIBはMethodInterceptorでターゲットメソッドを呼び出すことができ、またMethodInterceptor.interceptメソッドを呼び出すときに渡される最初のパラメータはエージェントクラスインスタンスであるため、エージェントされるメソッドを実行する必要がある場合はMethodProxy.invokeSuperで完了し、Method.invokeを使用すると無限再帰呼び出しを招く.
    Spring @Configuration
    Springを使用する場合、Beanを次のように定義できます.
    @Configuration
    @ComponentScan(basePackageClasses = Company.class)
    public class Config {
      @Bean
      public Address getAddress() {
        return new Address("High Street", 1000);
      }
    
      @Bean
      public Person getPerson() {
        return new Person(getAddress());
      }
    }
    

    当初、このような方式に対する困惑は、SpringがどのようにgetAddressメソッドの呼び出しをブロックしたのかということであった.私の印象ではJDKダイナミックエージェントにはできないということだったが、SpringはCGLIBを通じてConfigエージェントオブジェクトを作成し、getAddressメソッドの呼び出しをブロックし、Beanの単例性を保証することに気づいた.
    Javaでは先のオブジェクトの実際のタイプに応じてメソッドが検索されないため、親に検索されますが、CGLIBで作成されたエージェントオブジェクトは親のメソッドを上書きしているため、エージェントクラスではMethodInterceptorブロックメソッドの呼び出しでBeanの重複作成を回避できます.
    これはSpringにおける対応するMethodInterceptorConfigurationClassEnhancer.BeanMethodInterceptorである.
    小結
    ここでは、JDKダイナミックエージェントとCGLIBダイナミックエージェントのエージェント方式をまとめます.
  • JDK動的エージェントProxyを継承し、TargetInterfacesを実現したエージェントクラスを作成することでエージェントを完了し、TargetInterfacesを呼び出すメソッドを呼び出すと、エージェントクラスはメソッド呼び出しをInvocationHandler完了
  • に渡す
  • CGLIBダイナミックエージェントTargetClassを継承したエージェントクラスを作成することによりエージェントを完了し、TargetClassのメソッドを呼び出す場合、MethodInterceptorが空でなければ、メソッド呼び出しをMethodInterceptor完了
  • に渡す
    動的エージェントを実装する方法は2つとも近いが,1つはインタフェースを介し,1つはサブクラスを介している.
    締めくくり
    最初にダイナミックエージェントという概念に触れたのは『Javaコア技術巻』という本を読んだときで、当時Javaを習い始めて間もないのに、これを見た後の考えは、こんな不便なものは、誰が大丈夫なのかということでした.→
    結局、広く使われていました(´ゝ`)
    似たようなソース注記もありますが、これらの操作は面倒だと言わざるを得ませんが、機能的で強力な特性で、いつも誰かが模様を完成しています(̄▽̄)∞
    Footnotes
    1最初にInvocationHandlerこのインタフェースを見たとき、最初のパラメータはTargetObjectだと思っていました.
    2読みやすいように他の不要な内容を省略している