vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

java.lang.reflect.Proxy に触れてみる

はじめに

Spring Data JPA プログラミング入門」を読み始めたのですが (n 回目)、その中にこのような話がでてきます。

DB へのアクセスにはリポジトリが必要 -> そのリポジトリは class として実装を直接書くのではなくのではなく、 interface を用意する -> Spring Framework 側で proxy を使ってその interface を実装したクラスを生成する

あーそーゆーことね。完全に理解した(わかってない)。

ということで、 java.lang.reflect.Proxy について調べてみることにしました。

なおタイトルなどで 「java.lang.reflect.Proxy」 とわざわざ言っているのは、会社で悩まされがちなプロキシサーバーとの区別をつけるためです。

ただし面倒なのでこれより下は Proxy と書くことにします。

Proxy について

まずは Proxy について、ドキュメントを見てみましょう。

あーry

気を取り直して、 Proxy の動きを一言にまとめると、対象の class が持つメソッドが実行されたことを、その class が継承している interface を通して取得できるようにする、といったものです。
(私の理解では)

これによって、メソッドが実行される前後に別の処理を追加することが可能になります。

Sample

まずは動作を確認するため、 改訂2版 パーフェクトJava のサンプルを参考にコードを書いてみます。

まずは元の interface と class から。

IProxySample.java

package jp.masanori;

public interface IProxySample {
    void say();
    String getName();
    void setName(String name);
    private void sayInPrivate(){
        System.out.println("private");
    }
    default void callName(){
        System.out.println("default");
        sayInPrivate();
    }
}

ImplementProxySample.java

package jp.masanori;

public class ImplementProxySample implements IProxySample {
    @Override
    public void say(){
        System.out.println("hello world");
    }
    @Override
    public String getName(){
        return "masanori";
    }
    @Override
    public void setName(String name){
        this.name = name;
    }
}

で、これが今回の話の中心となる Proxy を取得するクラスです。

ProxySampleClass.java

package jp.masanori;

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

public class ProxySampleClass implements InvocationHandler {
    private final Object targetClass;
    public ProxySampleClass(Object targetClass) {
        this.targetClass = targetClass;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 対象のメソッドを実行する前に処理を追加.
        System.out.println("before invoking method.");
        // 対象のメソッド.
        Object o = method.invoke(targetClass, args);
        // 対象のメソッドを実行した後に処理を追加.
        System.out.println("after invoking method.");
        return o;
    }
}

Proxy を呼び出して利用するコードです。

App.java

package jp.masanori;

import java.lang.reflect.Proxy;

public class App {
    public static void main( String[] args ) {
        // Proxy を取得して IProxySample にキャストする.
        var proxy = (IProxySample)Proxy.newProxyInstance(
                App.class.getClassLoader(),
                new Class[]{ IProxySample.class },
                new ProxySampleClass(new ImplementProxySample()));

        System.out.println("start");
        proxy.say();
        System.out.println("after say");

        proxy.setName("masanori");
        System.out.println("after setName");

        System.out.println("My name is " + proxy.getName());

        proxy.callName();
        System.out.println("after callName");
    }
}

実行結果は下記の通りです。

start
before invoking method.
hello world
after invoking method.

after say

before invoking method.
after invoking method.
after setName

before invoking method.
after invoking method.
My name is masanori

before invoking method.
default
private
after invoking method.

after callName

結果からは下記のようなことがわかります。

  • 対象 interface の全 public メソッドが実行されるときに ProxySampleClass.javainvoke が呼ばれる(含 default)。
  • privateメソッドでは invoke が呼ばれない。
  • 戻り値を使って何か行う処理は、 invoke で追加された処理の実行後に実行される。

invoke について

ProxySampleClass.javainvoke で渡される引数についてです。

第二引数の method について、一応 method.getName() で Invoke されたメソッド名が取れるので、条件分岐して特定のメソッドのみ処理を追加する、といったことも可能ですが、複雑になるので特定の場合のみ実行したいなら呼び出し元で対応したほうが良いような気はします。

また method.invoke の第一引数は、Proxy で処理を追加したい interface を実装する class である必要があります。

第三引数は メソッドの引数がそのまま渡され、引数がない場合は空の配列が渡されます。

Proxy.newProxyInstance について

  • Proxy.newProxyInstance の第一引数となる getClassLoader() は、 Main でも Proxy 対象となる class でも OK のようです。
  • newProxyInstance の戻り値で getClass をすると、キャスト後でも com.sun.proxy.$Proxy0 が返ります。
  • 第二引数の class 配列には interface.class を指定する必要があり、例えば実装 class を使おうとすると IllegalArgumentException が発生します。
  • 第三引数には InvocationHandler を継承した class を渡し、そのコンストラクタには Proxy 対象となる class を渡します(メソッドが呼ばれたときに、元のメソッドを実行するのに使用)。
  • 第二引数に渡す interface は、 Proxy 対象となる class が継承していないものを渡すこともできます。
  • またキャストしないのであれば第二引数に空配列を渡しても OK です。あまり意味はないと思いますが。

Proxy が継承した interface (第二引数で渡している interface )は getInterfaces で取得できます。

for(var c : proxy.getClass().getInterfaces()){
    System.out.println(c);
}

Spring Data JPA の repository のコードを見てみる

最初の話に戻って、Spring Data JPA の repository のコードを見てみることにします。

Spring Framework の GUIDES のサンプルを見て、 Proxy がどのように使われているのか追ってみることにします。

https://spring.io/guides/gs/accessing-data-jpa/

Customer という Entity クラス(ドメインオブジェクト)に対して CustomerRepository という Repository (interface) が用意されています。

で、メインクラスである Application.java で CustomerRepository を継承した Proxy が渡される、という内容になっています。

Application.java

package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
    ~省略~
    @Bean
    public CommandLineRunner demo(CustomerRepository repository) {
    return (args) -> {
    ~省略~
    }
  }
}

この Proxy 、デバッガのブレークポイントを止めて見てみると、 org.springframework.data.jpa.repository.support.SimpleJpaRepository という class に対して生成されているようです。

https://github.com/spring-projects/spring-data-jpa/blob/master/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

このクラスから EntityManager にアクセスしているわけですが、 Spring徹底入門 によると、処理の流れは下記のようなものとのことです。

  1. Spring Data JPA が CustomerRepository を継承した Proxy を生成する。
  2. SimpleJpaRepository に 1. の処理を委譲する。

で、実際の動きを見てみると、 SimpleJpaRepository に CustomerRepository のメソッドが追加されたような状態になっていて、これまで触れてきた Proxy の動きとは少し違っているようですね。

Mixin

Proxy を使って実現可能なこととして、 InvocationHandler の対象クラスが実装していないメソッドを追加する (Mixin) 、というものがあるそうです。

  • Javaにおける動的Mixin - Qiita

  • 追加したいメソッドを、 interface でデフォルトメソッドとして書いておく。

    1. を Proxy に継承させる。
  • そのまま呼び出すと実装クラスに該当メソッドがなくエラーになるため、 (InvocationHandler の) invoke でバインドする。

※下記のコードはあくまで特定の条件においてうまく動作することだけを確認したもので、エラー処理などが抜けています。
正しくは下記リンクをご覧ください。

https://qiita.com/kawasima/items/f735ef0c0a9fa96f6eb4

https://github.com/kawasima/enkan/blob/master/enkan-core%2Fsrc%2Fmain%2Fjava%2Fenkan%2Futil%2FMixinUtils.java

App.java

package jp.masanori;

import java.lang.reflect.Proxy;

public class App { public static void main( String args ) { // ImplementProxySample で継承していない IOtherSample を持った Proxyを生成する. var proxy = (IOtherSample)Proxy.newProxyInstance( App.class.getClassLoader(), new Class<?>{ IProxySample.class, IOtherSample.class}, new ProxySampleClass(new ImplementProxySample())); // IOtherSample のデフォルト実装を呼び出す. proxy.sayMessage(); } }

IOtherSample.java

package jp.masanori;

public interface IOtherSample {
    default void sayMessage(){
        System.out.println("hello everyone");
    }
}

ProxySampleClass.java

package jp.masanori;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class ProxySampleClass implements InvocationHandler {
    ~省略~
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // method実行の後に処理を挟まないなら直接ReturnしてもOK.
        Object result;
        if (method.getDeclaringClass().isAssignableFrom(targetClass.getClass())) {
            result = method.invoke(targetClass, args);
        }
        else{
            // methodを定義しているクラス(IOtherSample)を取得.
            final Class declaringClass = method.getDeclaringClass();
            // methodの参照(MethodHandle)のファクトリであるLookupの取得.
            MethodHandles.Lookup lookup = MethodHandles.publicLookup()
                    .in(declaringClass);

            // そのままlookup.unreflectSpecialで対象methodのMethodHandleを取ろうとすると,
            // IllegalAccessExceptionが発生するのでallowedModesの値を変更.
            if (Modifier.isFinal(modifiers)) {
                  final Field modifiersField = Field.class.getDeclaredField("modifiers");
                  modifiersField.setAccessible(true);
                  modifiersField.setInt(f, modifiers & ~Modifier.FINAL);
                  f.setAccessible(true);
                  f.set(lookup, MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PRIVATE);
            }
            // アクセス可能になったらProxyにMethodHandleをバインドしてmethodを実行する.
            MethodHandle methodhandle = lookup.unreflectSpecial(method, declaringClass);
            result = methodhandle.bindTo(proxy)
                    .invokeWithArguments(args);
        }
        return result;
    }
}

  • Proxy 対象の class が該当の method を定義している場合、 Mixin の処理を実行するとエラーになるため、isAssignableFrom で確認しています。
  • method にデフォルト実装がない場合は java.lang.AbstractMethodError が発生します。
  • ProxySampleClass のコメントにも書きましたが、 MethodHandle を取得して Proxy にバインドするためには Private アクセス特権が必要で、そのままだとエラーが発生します。
  • 上記の通りエラー処理などは含まれていないため、実際にはリンク先を確認するなどして正しくエラーが処理されるようにしてください。

正直ほとんどよくわからないままではあるのですが、 SimpleJpaRepository でもこれと同じような処理が行われているものと考えられます。

Java に限らず Reflection や Proxy を触りまくる、ということは普段はないかもしれませんが、また新たな世界が垣間見れたような気がします。

参照

Proxy

Mixin