2020/05/02

Java - Proxy でプロパティアクセスログを取る

java.lang.reflect.Proxy は、動的プロキシを生成するためのクラスです。
ここで言うプロキシとは、オブジェクトを包み込むような形で、かつ元のオブジェクトの型を維持したオブジェクトです。プロキシはメソッド呼び出しを代理で一旦受け取り、何らかの処理を追加できます。型は維持されるので、呼び出し元からはプロキシを介していることをあまり意識する必要がありません。

動的プロキシのサンプルとして、getter/setter へのアクセスをロギングするようなプロキシを生成するユーティリティクラスを作ってみました。

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class LoggingPropertyAccessProxyCreator {
  @SuppressWarnings("unchecked")
  public static <T> T create(final T target) {
    InvocationHandler handler = new LoggingPropertyAccessInvocationHandler(target);
    return (T) Proxy.newProxyInstance(target.getClass().getClassLoader(), target.getClass().getInterfaces(), handler);
  }

  private static final class LoggingPropertyAccessInvocationHandler implements InvocationHandler {
    private final Object target;

    private LoggingPropertyAccessInvocationHandler(final Object target) {
      this.target = target;
    }

    @Override
    public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
      try {
        return method.invoke(target, args);
      } finally {
        String methodName = method.getName();
        if (methodName.startsWith("get") || methodName.startsWith("set")) {
          System.out.println(methodName + " is invoked.");
        }
      }
    }
  }
}

get*(), set*() が呼ばれたときに標準出力に「<methodName> is invoked.」と出力するプロキシが生成されます。
create() は型パラメータを使って戻り値をキャストしてから返すようにしているので、TestBeanImpl implements TestBean という関係だとすると、以下のようにキャストなしで使うことができます。

TestBean bean = LoggingPropertyAccessProxyCreator.create(new TestBeanImpl());

戻り値が TestBean 型なので、コンパイラは型推論によって引数が TestBean 型であれば問題ないと判断します。
ちなみに Java の動的プロキシは、この TestBean と TestBeanImpl の関係のようにインターフェースが分離されていないと使えません。