こんにちは!今日は、Java での動的プロキシ クラスの作成という、かなり重要で興味深いトピックを取り上げます。それほど単純ではないので、例を使って理解してみましょう :) それでは、最も重要な質問は、動的プロキシとは何で、何のためにあるのでしょうか? プロキシ クラスは、元のクラスの一種の「上部構造」であり、必要に応じてその動作を変更できます。行動を変えるとは何を意味し、どのように機能するのでしょうか? 簡単な例を見てみましょう。インターフェイスと、このインターフェイスを実装する
Person
単純なクラスがあるとします。Man
public interface Person {
public void introduce(String name);
public void sayAge(int age);
public void sayFrom(String city, String country);
}
public class Man implements Person {
private String name;
private int age;
private String city;
private String country;
public Man(String name, int age, String city, String country) {
this.name = name;
this.age = age;
this.city = city;
this.country = country;
}
@Override
public void introduce(String name) {
System.out.println("Меня зовут " + this.name);
}
@Override
public void sayAge(int age) {
System.out.println("Мне " + this.age + " years");
}
@Override
public void sayFrom(String city, String country) {
System.out.println("Я из города " + this.city + ", " + this.country);
}
//..геттеры, сеттеры, и т.д.
}
私たちのクラスにはMan
3 つの方法があります。自己紹介、自分の年齢、出身地を言うことです。このクラスを既製の JAR ライブラリの一部として受け取り、そのコードを単純に取得して書き直すことができないと想像してみましょう。しかし、私たちは彼の行動を変える必要があります。たとえば、オブジェクトでどのメソッドが呼び出されるのかわからないため、いずれかのメソッドを呼び出すときは、最初に「こんにちは!」と言ってもらいたいとします。(失礼な人を好む人はいません)。 このような状況ではどうすればよいでしょうか? いくつか必要なものがあります:
-
InvocationHandler
InvocationHandler
は、オブジェクトへのメソッド呼び出しをインターセプトし、必要な動作を追加できる特別なインターフェイスです。独自のインターセプターを作成する必要があります。つまり、クラスを作成してこのインターフェイスを実装します。それは非常に簡単です:
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PersonInvocationHandler implements InvocationHandler {
private Person person;
public PersonInvocationHandler(Person person) {
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Hello!");
return null;
}
}
実装する必要があるインターフェイス メソッドは 1 つだけですinvoke()
。実際、これは必要なことを行います。オブジェクトへのすべてのメソッド呼び出しをインターセプトし、必要な動作を追加します (ここでは、invoke()
メソッド内のコンソールに「Hello!」を出力します)。
- 元のオブジェクトとそのプロキシ。
Man
とその「上部構造」(プロキシ) を作成しましょう。
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
//Создаем оригинальный an object
Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
//Вызываем у прокси an object один из методов нашего оригинального an object
proxyVasia.introduce(vasia.getName());
}
}
とても単純そうには見えません!コードの各行に特にコメントを書きました。そこで何が起こっているのかを詳しく見てみましょう。
最初の行では、プロキシを作成する元のオブジェクトを作成するだけです。次の 2 行は混乱を引き起こす可能性があります。
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
しかし、ここでは実際には特別なことは何も起こっていません:) プロキシを作成するには、ClassLoader
元のオブジェクトの(クラスローダー)と、元のクラス(つまり)が実装するすべてのインターフェイスのリストがMan
必要です。それが何なのかわからない場合は、 JVM へのクラスのロードに関するこの記事、またはHabré のこのClassLoader
記事を読むことができますが、まだあまり気にしないでください。プロキシ オブジェクトを作成するために必要となる、少し追加の情報を取得していることに注意してください。4 行目では、特別なクラスとその静的メソッドを使用します。 Proxy
newProxyInstance()
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
このメソッドはプロキシ オブジェクトを作成するだけです。このメソッドには、前のステップで受け取った元のクラス (クラスClassLoader
とそのインターフェイスのリスト) に関する情報と、前に作成したインターセプターのオブジェクトを渡しますInvocationHandler
。重要なことは、元のオブジェクトをインターセプターに渡すことを忘れないことですvasia
。そうしないと、「インターセプト」するものが何もなくなります :) 結局、何ができたのでしょうか? これでプロキシ オブジェクトができましたvasiaProxy
。任意のインターフェイス メソッドPerson
を呼び出すことができます。なぜ?すべてのインターフェイスのリストを渡しているため、次のようになります。
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
今、彼はすべてのインターフェースメソッドを「認識」していますPerson
。さらに、PersonInvocationHandler
オブジェクトと連携するように設定されたオブジェクトをプロキシに渡しましたvasia
。
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
ここで、プロキシ オブジェクトのインターフェイス メソッドを呼び出すとPerson
、インターセプタがこの呼び出しを「キャッチ」し、代わりに独自のメソッドを実行しますinvoke()
。メソッドを実行してみましょうmain()
。 コンソール出力: こんにちは! 素晴らしい!実際のメソッドの代わりに、私たちのPerson.introduce()
メソッドが呼び出されていることがわかります。 invoke()
PersonInvocationHandler()
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Hello!");
return null;
}
そしてコンソールに「Hello!」と表示されました。 しかし、これはまさに私たちが望んでいた動作ではありません:/ 私たちの考えによれば、最初に「Hello!」が表示され、その後、呼び出しているメソッド自体が機能するはずです。つまり、このメソッドは以下を呼び出します。
proxyVasia.introduce(vasia.getName());
コンソールに「Hello!」と出力されるはずです。私の名前はヴァシャです」というだけではなく、「こんにちは!」どうすればこれを達成できるでしょうか? 何も複雑なことはありません: インターセプターとメソッドを少しいじるだけですinvoke()
:) このメソッドにどの引数が渡されるかに注意してください。
public Object invoke(Object proxy, Method method, Object[] args)
メソッドは、invoke()
代わりに呼び出されるメソッドとそのすべての引数(Method メソッド、Object[] args) にアクセスできます。つまり、メソッドを呼び出した場合proxyVasia.introduce(vasia.getName())
、メソッドの代わりにintroduce()
メソッドが呼び出され、そのメソッド内で元のメソッドとその引数のinvoke()
両方にアクセスできるようになります。introduce()
その結果、次のようなことができるようになります。
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class PersonInvocationHandler implements InvocationHandler {
private Person person;
public PersonInvocationHandler(Person person) {
this.person = person;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Hello!");
return method.invoke(person, args);
}
}
invoke()
これで、元のメソッドへの呼び出しがメソッドに 追加されました。前の例のコードを実行してみると、次のようになります。
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
//Создаем оригинальный an object
Man vasia = new Man("Vasya", 30, "Санкт-Петербург", "Россия");
//Получаем загрузчик класса у оригинального an object
ClassLoader vasiaClassLoader = vasia.getClass().getClassLoader();
//Получаем все интерфейсы, которые реализует оригинальный an object
Class[] interfaces = vasia.getClass().getInterfaces();
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
//Вызываем у прокси an object один из методов нашего оригинального an object
proxyVasia.introduce(vasia.getName());
}
}
そうすれば、すべてが正常に動作することがわかります:) コンソール出力: こんにちは! 私の名前はヴァシャです。 どこで必要ですか? 実際、多くの場所で。「動的プロキシ」設計パターンは、一般的なテクノロジで積極的に使用されています...ところで、Dynamic Proxy
これがパターンであることをお伝えするのを忘れていました。おめでとうございます。また一つ学びました! :) そのため、セキュリティに関連する一般的なテクノロジやフレームワークで積極的に使用されています。プログラムにログインしているユーザーのみが実行できるメソッドが 20 個あると想像してください。学んだテクニックを使用すると、各メソッドで個別に確認コードを複製することなく、ユーザーがログインとパスワードを入力したかどうかを確認するチェックをこれら 20 のメソッドに簡単に追加できます。たとえば、すべてのユーザー アクションが記録されるログを作成したい場合も、プロキシを使用すると簡単に作成できます。今でも可能です: 呼び出されたときにメソッドの名前がコンソールに表示されるようにサンプルにコードを追加するだけでinvoke()
、プログラムの簡単なログが得られます :) 講義の最後に、重要な 1 つの点に注意してください。制限がございます。プロキシ オブジェクトの作成は、クラス レベルではなくインターフェイス レベルで行われます。インターフェイス用にプロキシが作成されます。このコードを見てください。
//Создаем прокси нашего an object vasia
Person proxyVasia = (Person) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
ここでは、インターフェイス専用のプロキシを作成しますPerson
。クラスのプロキシを作成しようとした場合、つまり、リンクのタイプを変更して class にキャストしようとした場合Man
、何も機能しません。
Man proxyVasia = (Man) Proxy.newProxyInstance(vasiaClassLoader, interfaces, new PersonInvocationHandler(vasia));
proxyVasia.introduce(vasia.getName());
スレッド "main" java.lang.ClassCastException での例外: com.sun.proxy.$Proxy0 を Man にキャストできません インターフェイスの存在は必須の要件です。プロキシはインターフェイス レベルで機能します。今日はここまでです :) プロキシのトピックに関する追加資料として、優れたビデオと優れた記事をお勧めします。さて、いくつかの問題を解決できれば幸いです。:) またね!
GO TO FULL VERSION