Proxyの生成手法を素振りしてみる。

最初に

さて、タイトルどおりなのですが
素振りするだけの記事だったりします。

素振りしたライブラリはこちら


Proxyと言えばjava.lang.reflect.Proxyですね。

今回は次の2つのクラスについて、Proxyを作ってみます。

  public static class Some implements SomeInterface {
    @Override
    public void apply() {
      System.out.println("hello world");
    }

  }
  interface SomeInterface {
    public void apply();
  }

まず前提として、今回書くサンプルコードは全て次の出力文が出力されます。

apply:start
hello world
apply:end

標準API

まずはJDKについてるProxyクラスを使ってProxyを作ってみましょう。

  @Test
  public void testCreateDynamicProxy() {
    SomeInterface impl = new Some();
    SomeInterface proxy = (SomeInterface) Proxy.newProxyInstance(
      this.getClass().getClassLoader(),
      new Class[] {SomeInterface.class},
      (Object _proxy, Method method, Object[] args) -> {
        System.out.println(method.getName() + ":start");
        method.invoke(impl, args);
        System.out.println(method.getName() + ":end");
        return null;
    });
    proxy.apply();
  }

implを生成しています。今回は引数も戻り値も特に使われないので記述は基本的に簡単になっていますね。

ところで、余談ですがラムダに型つけられるんですね。
最近全然使ってなかったので知りませんでした。

ではここからはライブラリを使っていきます。

Javassist

Javassistを使ってみましょう。

  @Test
  public void testCreateProxyWithJavassist() throws NoSuchMethodException, IllegalArgumentException, InstantiationException, IllegalAccessException,
    InvocationTargetException {
    ProxyFactory factory = new ProxyFactory();

    factory.setSuperclass(Some.class);
    Some some = (Some) factory.createClass()
      .newInstance();

    ProxyObject proxyObject = (ProxyObject) some;
    proxyObject.setHandler((Object self, Method thisMethod, Method proceed, Object[] args) -> {
      System.out.println(thisMethod.getName() + ":start");
      proceed.invoke(self, args);
      System.out.println(thisMethod.getName() + ":end");
      return null;
    });
    some.apply();
  }

Proxy作るためのクラスが用意されていますね。
ByteCode操作をするライブラリに入ってるクラスなので、setSuperClassといった形で、クラスを渡す形になってますね。
Bytecode操作してサブクラスを動的に生成して、メソッドにhandlerの処理とか追加するようですね。

ちなみにProxyFactoryクラスにはwriteDirecotryフィールドがあって
そこにパスを渡してやるとcreateClassした際に.classファイルが生成されます。
生成されたクラスをデコンパイルしてみると面白いかもしれません。

cglib

今度はcglibで書いてみます。
こちらもProxyを作るためのクラスが用意されています。

  @Test
  public void testCreateProxyWithCglib() {
    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(Some.class);
    MethodInterceptor interceptor = (Object obj, Method method, Object[] args, MethodProxy proxy) -> {
      System.out.println(method.getName() + ":start");
      proxy.invokeSuper(obj, args);
      System.out.println(method.getName() + ":end");
      return null;
    };
    enhancer.setCallback(interceptor);
    Some some = (Some) enhancer.create();
    some.apply();
  }

こちらもByteCode操作をするライブラリに入ってるクラスなのでなんだか似てますね。
特に難しいことはないかと思われるのですが
Callbackというマーカインターフェースが存在するのですが
その子クラスに、InvocationHandlerとMethodInterceptorというのがあります。

ぱっと見た感じだと引数がすごい似てるので、どっち使えばいいか分かりませんでしたが
superのメソッドを呼びたい、ただただInterceptしたいだけならMethodInterceptorで良さそうですね。

ちなみにCallbackインターフェースのJavadocを見ると分かるのですが
以下のインターフェースがCallbackインターフェースを継承してます。
色々できそうですね。

  • MethodInterceptor
  • NoOp
  • LazyLoader
  • Dispatcher
  • InvocationHandler
  • FixedValue

Byte Buddy

最後はByte Buddyを使ってみます。

少しこいつは毛並みが違います。

まずコードを晒してみます。

  @Test
  public void testCreateProxyWithByteBuddy() throws InstantiationException, IllegalAccessException {
    Some some = (Some) new ByteBuddy().subclass(Some.class)
      .method(ElementMatchers.any())
      .intercept(MethodDelegation.to(TestInterceptor.class))
      .make()
      .load(this.getClass()
        .getClassLoader())
      .getLoaded()
      .newInstance();
    some.apply();
  }

  public static class TestInterceptor {
    @RuntimeType
    public static Object intercept(@SuperCall Callable<?> zuper, @Origin Method method) throws Exception {
      System.out.println(method.getName() + ":start");
      zuper.call();
      System.out.println(method.getName() + ":end");
      return null;
    }
  }

chain methodによる流れるようなインターフェースが特徴的です。

また、いくつかのstaticメソッドを提供するクラスがあるみたいですね。
また、@RuntimeType、や@SuperCall、@Originなどのアノテーションも出現しています。

リフレクションとバイトコード操作を組み合わせた感じのゴリゴリの黒魔術感が出ています。
個人的には今回始めて触ったので、はっきり言ってキモイのですが
すごく発想としては面白いのではないでしょうか。

まとめ

今まで、Javaの標準機能ではインターフェースを定義しないと
Proxy的な形でMethodのinterceptionが行えませんが
ライブラリを使って、ByteCodeを操作することによって、サブクラスを動的に生成し
インターフェースの定義をしなくても、Methodのinterceptionが行えるようになりました。

ちなみに、Spring 4.3.4ではcglibを使ってclass-based proxyを作っているようですね。
ScopedProxyMode (Spring Framework 4.3.5.RELEASE API)

旅行の準備しなきゃ。

とりあえず今日はここまで~

リポジトリはここに置いてます。
github.com

おまけ

Bytecode操作についてはひしだまさんがJavassistについて書いてくださってます。
Javassistメモ(Hishidama's Javassist Memo)

jyukutyoさんがJJUG 2016 Fallで話していたスライドも見てみると面白いです。
JJUG CCC 2016 fall バイトコードが君のトモダチになりたがっている

Bytecode操作を使った、DIコンテナについてのきしださんの記事を読むと非常に面白い上に為になります。
作って理解するDIコンテナ - きしだのはてな