CodeGym /Java Blog /ランダム /Java の動的プロキシ
John Squirrels
レベル 41
San Francisco

Java の動的プロキシ

ランダム グループに公開済み
やあ!今日は、Java での動的プロキシ クラスの作成という、かなり重要で興味深いトピックについて考えます。これはそれほど単純ではないので、例を使用して理解していきます :) それで、最も重要な質問は、動的プロキシとは何ですか、またその目的は何ですか? プロキシ クラスは、元のクラスに追加される一種の「アドオン」であり、必要に応じて元のクラスの動作を変更できるようになります。「行動を変える」とは何を意味し、それはどのように機能するのでしょうか? 簡単な例を考えてみましょう。Personインターフェイスと、このインターフェイスを実装する 単純なManクラスがあるとします。

public interface Person {

   public void introduce(String name);
  
   public void sayAge(int age);
  
   public void sayWhereFrom(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("My name is " + this.name);
   }

   @Override
   public void sayAge(int age) {
       System.out.println("I am " + this.age + " years old");
   }

   @Override
   public void sayWhereFrom(String city, String country) {

       System.out.println("I'm from " + this.city + ", " + this.country);
   }

   // ...getters, setters, etc.
}
Manクラスに、introduction、sayAge、sayWhereFrom という 3 つのメソッドがあります。このクラスを既製の JAR ライブラリの一部として取得し、そのコードを単純に書き直すことができないと想像してください。しかし、その動作を変える必要もあります。たとえば、オブジェクトでどのメソッドが呼び出されるかはわかりませんが、相手に「こんにちは!」と言わせたいとします。(失礼な人を好む人はいません) いずれかのメソッドが呼び出されたとき。 動的プロキシ - 2この状況では何をすべきでしょうか? いくつか必要なものがあります:
  1. 呼び出しハンドラ

これは何ですか? 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("Hi!");
       return null;
   }
}
実装する必要があるインターフェイス メソッドは 1 つだけです: invoke()。ちなみに、これは必要なことを実行します。オブジェクトへのすべてのメソッド呼び出しをインターセプトし、必要な動作を追加します (invoke ()メソッド内で、コンソールに「Hi!」を出力します)。
  1. 元のオブジェクトとそのプロキシ。
オリジナルのManオブジェクトとその「アドオン」(プロキシ) を作成します。

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());

   }
}
これはとても単純なことではありません。具体的には、コードの各行にコメントを追加しました。何が起こっているのかを詳しく見てみましょう。最初の行では、プロキシを作成する元のオブジェクトを作成するだけです。次の 2 行は問題を引き起こす可能性があります。

 // Get the class loader from the original object
ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();
実際、ここでは特別なことは何も起こっていません :) 4 行目では、特別なProxyクラスとその静的newProxyInstance()メソッドを使用します。

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
このメソッドはプロキシ オブジェクトを作成するだけです。最後のステップで受け取った元のクラスに関する情報 (そのClassLoaderとそのインターフェイスのリスト)、および以前に作成したInvocationHandlerオブジェクトをメソッドに渡します。重要なことは、元のarnoldオブジェクトを呼び出しハンドラーに渡すことを忘れないことです。そうしないと、「処理」するものが何もなくなります :) 結局、何ができたのでしょうか? これで、プロキシ オブジェクトproxyArnoldができました。Personインターフェイスの任意のメソッドを呼び出すことができます。なぜ?ここですべてのインターフェイスのリストを指定したためです。

// Get all the interfaces that the original object implements
Class[] interfaces = arnold.getClass().getInterfaces();

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
これで、 Personインターフェイス のすべてのメソッドが認識されました。さらに、arnoldオブジェクトと連携するように設定されたPersonInvocationHandlerオブジェクトをプロキシに渡しました。

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
ここで、プロキシ オブジェクトのPersonインターフェイス のメソッドを呼び出すと、ハンドラーが呼び出しをインターセプトし、代わりに独自のinvoke()メソッドを実行します。main()メソッドを実行してみましょう。コンソール出力:

Hi!
素晴らしい!元のPerson.introduce()メソッドの代わりに、PersonInvocationHandler()invoke()メソッドが呼び出されていることがわかります。

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

   System.out.println("Hi!");
   return null;
}
"やあ!" とコンソールに表示されますが、これは私たちが望んでいた動作とはまったく異なります:/ 私たちが達成しようとしていたのは、最初に「Hi!」を表示することです。そして、元のメソッド自体を呼び出します。

proxyArnold.introduce(arnold.getName());
単に「こんにちは!」ではなく、「こんにちは! 私の名前はアーノルドです」と表示する必要があります。どうすればこれを達成できるでしょうか? それは複雑ではありません: ハンドラーとinvoke()メソッドをある程度自由にする必要があるだけです:) このメソッドにどの引数が渡されるかに注意してください。

public Object invoke(Object proxy, Method method, Object[] args)
invoke ()メソッドは、最初に呼び出されたメソッドとそのすべての引数 (Method メソッド、Object[] args) にアクセスできます。つまり、proxyArnold.introduce(arnold.getName())メソッドを呼び出して、 introduced()メソッドの代わりにinvoke()メソッドが呼び出される場合、このメソッド内で元のinclude()メソッドにアクセスできるようになります。そしてその主張!その結果、次のことが可能になります。

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("Hi!");
       return method.invoke(person, args);
   }
}
invoke()メソッド に、元のメソッドへの呼び出しを追加しました。前の例のコードを実行してみると、次のようになります。

import java.lang.reflect.Proxy;

public class Main {

   public static void main(String[] args) {

       // Create the original object
       Man arnold = new Man("Arnold", 30, "Thal", "Austria");

       // Get the class loader from the original object
       ClassLoader arnoldClassLoader = arnold.getClass().getClassLoader();

       // Get all the interfaces that the original object implements
       Class[] interfaces = arnold.getClass().getInterfaces();

       // Create a proxy for our arnold object
       Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

       // Call one of our original object's methods on the proxy object
       proxyArnold.introduce(arnold.getName());
   }
}
そうすれば、すべてが正常に動作することがわかります:) コンソール出力:

Hi! My name is Arnold
これが必要になるのはいつですか? 実際、かなり頻繁にあります。「ダイナミック プロキシ」デザイン パターンは、一般的なテクノロジで積極的に使用されています... あ、ちなみに、ダイナミック プロキシはデザイン パターンであることを忘れていました。おめでとうございます。また 1 つ学びました! :) 動的プロキシ - 3たとえば、セキュリティに関連する一般的なテクノロジやフレームワークで積極的に使用されています。プログラムにサインインしているユーザーのみが実行すべきメソッドが 20 個あると想像してください。学習したテクニックを使用すると、各メソッドで確認コードを複製することなく、ユーザーが有効な資格情報を入力したかどうかを確認するチェックをこれら 20 のメソッドに簡単に追加できます。 あるいは、すべてのユーザーのアクションが記録されるログを作成するとします。これもプロキシを使用すると簡単に実行できます。今でも、上記の例にコードを追加するだけで、 invoke()を呼び出したときにメソッド名が表示されるようになります。これにより、プログラムの非常に単純なログが生成されます :) 結論として、1 つの重要な制限に注意してください。プロキシ オブジェクトは、クラスではなくインターフェイスで動作します。インターフェイス用にプロキシが作成されます。このコードを見てください。

// Create a proxy for our arnold object
Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));
ここでは、特にPersonインターフェイス 用のプロキシを作成します。クラスのプロキシを作成しようとした場合、つまり参照のタイプを変更してManクラスにキャストしようとした場合、機能しません。

Person proxyArnold = (Person) Proxy.newProxyInstance(arnoldClassLoader, interfaces, new PersonInvocationHandler(arnold));

proxyArnold.introduce(arnold.getName());
スレッド "main" java.lang.ClassCastException での例外: com.sun.proxy.$Proxy0 を Man にキャストすることはできません インターフェイスを持つことは絶対的な要件です。プロキシはインターフェイスと連携して動作します。今日はここまでです:) さて、いくつかのタスクを解決するとよいでしょう。:) 次回まで!
コメント
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION