メインコンテンツへ移動

デザインパターン之代理パターン(Proxy Pattern)_リモートプロキシの解析

無料2015-03-07#Design_Pattern#代理模式#Proxy Pattern

プロキシパターンは第三者(プロキシオブジェクト)を挿入することで呼び出し者と呼び出される対象(実行者とは異なる)を分離します。リモートプロキシは最も古典的なプロキシの 1 つで、呼び出される対象がローカルにない(別の JVM 内にある)ため直接呼び出せない場合、リモートプロキシが必要です。呼び出し者は呼び出しリクエストをリモートプロキシに送信し、プロキシオブジェクトが呼び出される対象と通信し、その後呼び出し結果を呼び出し者に伝達します

no_mkd

一。プロキシパターンとは何か?

名前の通り、プロキシとは第三者のことです。例えば芸能人のマネージャーのように、芸能人の仕事はすべてマネージャーに処理させ、芸能人はマネージャーに何をするか伝えれば、マネージャーが自然に方法を考え実行し、完了後に結果を芸能人に報告すればよいのです

元々は呼び出し者と呼び出される対象との間の直接インタラクションでしたが、現在は呼び出し者と呼び出される対象を分離し、プロキシが情報を伝達して呼び出しを完了します

二。プロキシパターンの用途は何か?

プロキシパターンは大きなパターンなので、応用範囲が広いです。プロキシの種類から見ることができます:

リモートプロキシ:最も古典的なプロキシパターンの 1 つで、リモートプロキシはリモート JVM と通信し、ローカル呼び出し者とリモート呼び出される対象との間の正常なインタラクションを実現します

仮想プロキシ:巨大なオブジェクトの代わりに使用し、必要なときにのみ作成されることを保証します

保護プロキシ:呼び出される対象にアクセス制御を提供し、呼び出し者の権限を確認します

この他にもファイアウォールプロキシ、スマート参照プロキシ、キャッシュプロキシ、同期プロキシ、複雑隠蔽プロキシ、コピーオンライトプロキシなどがあり、それぞれ特殊な用途があります

P.S.リモートプロキシは最も基礎的なプロキシパターンなので、单独で取り上げて説明する必要があり、そのため本稿で詳しく解説し、その他のプロキシは補足のブログ記事で詳述します

三。リモートプロキシ

有些事情不用代理也能轻松解决,但有些事情必须得依靠代理来完成,比如要调用另一台机器上的一个方法,我们可能就不得不用代理

リモートプロキシの内部メカニズムは以下の通りです:

説明すると、Stub は「スタブ」で、Server オブジェクトを表します

Skeleton は「スケルトン」(なぜ「スタブ」や「スケルトン」と呼ばれるのかはわかりませんが、もちろん、知る必要もありません)で、Client を表します

Stub は明らかにクライアント側にあるのに、なぜクライアントのプロキシではなくサービスのプロキシなのか?クライアントはサーバーとインタ��クションする必要があり、現在サービスはリモート JVM 内にあるためインタラクションできないので、Stub を使って Server を表し、Stub を呼び出すことは Server を呼び出すことと同等です(内部通信メカニズムは Client に対して透過的で、Client にとって、Stub を呼び出すことと直接 Server を呼び出すことに違いはありません。这正是プロキシパターンの利点の 1 つです)

具体的なフローは以下の通りです:

  1. Client が Stub にメソッド呼び出しリクエストを送信(Client は Stub が Server だと思っている)
  2. Stub がリクエストを受け取り、Socket を通じてサービス端の Skeleton と通信し、呼び出しリクエストを Skeleton に伝達
  3. Skeleton がリクエストを受け取り、ローカル Server を呼び出し(少し奇妙に聞こえますが、ここで Server は Service に相当します)
  4. Server が対応するアクションを実行し、結果を呼び出し元 Skeleton に返却
  5. Skeleton が結果を受け取った後、Socket を通じて Stub に送信
  6. Stub が結果を Client に伝達

P.S.第 2 歩と第 5 歩はどちらも Socket 通信を通じて行う必要があり、相互に伝達するものはすべて送信前にシリアライズし、受信後にデシリアライズする必要があります。这也解释了为什么 Server 中的 public 方法返回值都必须是可序列化的

四。リモートプロキシの実装

リモートプロキシを実装するには 2 つの方法があります:

  • Stub と Skeleton をカスタマイズ(内部通信を実装)
  • Java がサポートする RMI(Remote Method Invocation)を利用して実装。多くの手間を省けますが、内部原理を理解しにくいです

まずカスタマイズ方式の例を示します。これに関する優れたブログ記事があるので、勝手にリンクを記録しました。ここをクリックしてジャンプ>>

原文は完全な例を示しているので、ここでは繰り返さず、原文に偽クラス図を追加して理解を容易にします:

(なぜクラス図がこう描かれているのか聞かないでください。「偽」クラス図と言ったでしょう。。)

原文を注意深く見ればリモートプロキシを理解するのは難しくありません。Stub と Skeleton は通信を担当し、Socket で作成したチャットプログラムに似ており、それ以外に特別なことはありません

次に Java がサポートする RMI を利用してプロキシパターンを実装する例を示します。多くの詳細が隠蔽されていることが明らかにわかります

まずリモートインターフェースを定義する必要があります:

package ProxyPattern;

import java.rmi.RemoteException;

/**

  • サービスインターフェースの定義(java.rmi.Remote インターフェースを拡張)

  • @author ayqy / public interface Service extends java.rmi.Remote{ / 1.メソッドの戻り値の型はシリアライズ可能な Serializable でなければならない

    • 2.すべてのメソッドは例外 throws RemoteException を宣言する必要がある(RMI 方式のため)
    • */

    /**

    • @return 完全な挨拶文
    • @throws RemoteException */ public String greet(String name) throws RemoteException; }

注意:サービスインターフェース内の public メソッドの戻り値の型はシリアライズ可能でなければなりません(言い換えれば、カスタマイズされた戻り値の型は Serializable インターフェースを実装する必要があります)。String 型はすでに Serializable インターフェースを実装しています

なぜこのような java.rmi.Remote を拡張するインターフェースを定義する必要があるのか?API ドキュメントに明確な説明があります:

The Remote interface serves to identify interfaces whose methods may be invoked from a non-local virtual machine. Any object that is a remote object must directly or indirectly implement this interface. Only those methods specified in a "remote interface", an interface that extends java.rmi.Remote are available remotely. 

簡単に言えば、コンパイラに伝えるためです:私たちの Service オブジェクトはリモート呼び出し可能です。それだけです

リモートインターフェースを定義したので、もちろん具体的な実装も必要です:

package ProxyPattern;

import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;

/**

  • リモートサービスの実装(UnicastRemoteObject を拡張し、カスタムリモートインターフェースを実装)

  • @author ayqy */ public class MyService extends UnicastRemoteObject implements Service{

    /**

    • プログラムバージョンの検証に使用(受信側はデシリアライズ時に UID を検証し、一致しないと例外を発生) */ private static final long serialVersionUID = 1L;

    /**

    • 空のコンストラクタ、例外を宣言するためだけ(デフォルトのコンストラクタは例外を宣言しない)
    • @throws RemoteException */ protected MyService() throws RemoteException { }

    @Override public String greet(String name) throws RemoteException { return "Hey, " + name; } }

P.S.サービスが UnicastRemoteObject クラスを継承するのは、Stub クラスを自動生成するためです(UnicastRemoteObject は具体的な生成詳細をカプセル化し、私たちは 1 つのクラスの仕事量を省けます)

サービス端にサービスがあるだけでは不十分で、RMI 登録サービスを開始し、リモートオブジェクトを登録してクライアントから呼び出せるようにする Server が必要です:

package ProxyPattern;

import java.net.MalformedURLException; import java.rmi.Naming; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry;

/**

  • サーバークラスの実装、サービスの開始とサービスオブジェクトの登録を担当
  • @author ayqy */ public class Server { public static void main(String[] args){ try { //RMI 登録サービスの開始、ポートを 1099 に指定(1099 はデフォルトポート) LocateRegistry.createRegistry(1099); //サービスオブジェクトの作成 MyService service = new MyService(); //service を RMI 登録サーバーに登録し、MyService という名前にする Naming.rebind("MyService", service); } catch (RemoteException e) { // TODO Auto-generated catch block e.printStackTrace(); } catch (MalformedURLException e) { // TODO Auto-generated catch block e.printStackTrace(); } } }

LocateRegistry.createRegistry() 方式でサービスを開始する以外にも、コマンドライン方式で開始できます(rmiregistry コマンド)。効果は同じです

サービス端のコードはこれで完了です。Socket チャットプログラムと同じように、2 つの部分のコードを書く必要があります。Server と Client です。次にクライアントの実装を行います。非常に簡単で、テストクラスのようです:

package ProxyPattern;

/* 参考資料:

/**

  • クライアントクラスの実装
  • @author ayqy / public class Client { /*
    • リモートオブジェクトを検索してリモートメソッドを呼び出す */ public static void main(String[] argv) { try { //別の RMI 登録サービスを実行しているマシンから MyService オブジェクトを検索する場合は、IP アドレスを変更するだけです Service service = (Service) Naming.lookup("//127.0.0.1:1099/MyService");

       //リモートメソッドの呼び出し
       System.out.println(service.greet("SmileStone"));
      

      } catch (Exception e) { System.out.println("Client exception: " + e); } } }

P.S.私たちは直接 Naming.lookup() を使用して Stub オブジェクトを取得します(そうです、Stub です。本当のオブジェクトはまだ別のマシン上にあるので、もちろん取得できません。ここで取得できるのは Service の Stub プロキシだけです)。その後プロキシのメソッドを呼び出して結果を取得します

この詳細に注意してください:

//別の RMI 登録サービスを実行しているマシンから MyService オブジェクトを検索する場合は、IP アドレスを変更するだけです
Service service = (Service) Naming.lookup("//127.0.0.1:1099/MyService");

たった 1 行ですが、2 つの詳細が隠蔽されています:

  1. クライアントはサービスインターフェース Service を知っている必要があります。ここではローカルで同じ package 下にあるので気にする必要はありませんが、実際のアプリケーションでは Client と Server は分離されているため、Client は Service インターフェースのコピーを入手する必要があります。否则无法调用
  2. クライアントはサーバーの IP とポート番号を知る必要があります(通信ですから、これがなければなりません)

半天忙しくしましたが、実行結果を見てみましょう(まず Server を実行し、次に Client を実行):

P.S.Java がサポートする RMI を利用してリモートプロキシを実装する部分は、他人のブログ記事を参考にしました。里面解释的更详细一些

P.S.コマンドライン方式で RMI 登録サービスを開始するのは、あまりにも面倒で、しかもまず Stub クラスを生成する必要があるため、この方式はお勧めしません。具体的な操作詳細は、上記のリンクブログ記事にも詳しく紹介されているので、ここでは繰り返しません

注意:テスト結果を目にするまで、我们始终没有自定义 Stub 与 Skeleton 类,不是吗?对,没错,它们确实存在,只是被 RMI 隐藏起来了(据说 Java1.5 之后 RMI 中没有了 Skeleton,甚至更高的版本中连 Stub 都没有了。。不过这都没关系,我们已经清楚了最原始的远程代理)

五。まとめ

振り返ってリモートプロキシが何をしたのか考えてみましょう:

  1. メソッド呼び出しをインターセプトし制御する(这也是プロキシパターンの最大の特徴。最も典型的なのは、ファイアウォールプロキシ。。)
  2. リモートオブジェクトの存在はクライアントに対して透過的(クライアントは Stub プロキシオブジェクトを完全にリモートオブジェクトだと思っています。虽然客户有点好奇为什么可能会出现异常。。)
  3. リモートプロキシは通信詳細を隠蔽します

別のマシン(JVM)上の指定されたオブジェクトのメソッドを呼び出す必要がある場合、リモートプロキシを使用するのは良い選択です。。

コメント

コメントはまだありません

コメントを書く