Proxyパターン
特定のオブジェクトに対するアクセスや操作を、Proxy(代理)オブジェクトを介して行う方法。
Decorator パターンに似ている。
Decorator パターンが、既存のオブジェクトに対して透過的に新たな振る舞い、責務を追加することを目的としているのに対して、Proxy パターンはアクセス制御を主な目的としている。
また、Decorartor パターンではオブジェクトが何重にもラップされることがあるのに対して、Proxy パターンでは通常オブジェクトが何重にも重ねてラップされることはない。
ただし、Proxy パターンは、アクセス制御以外にも様々な目的で利用できる。
利用場面
アクセス制御・保護プロキシ
アクセス制御や権限管理に利用される。
Proxy を介入させることで、特定のユーザーに対してのみアクセスを許可し、他のユーザーからのアクセスに制限をかけることができる。
リモートアクセス制御・リモートプロキシ
Proxy が(リモート)サーバー上に存在するオブジェクトにアクセスするための手段として利用される。
クライアント側からは、クライアントと同じローカル環境に存在するオブジェクトを操作しているように見えるが、実際にはネットワーク越しのリモート環境のオブジェクトに対する操作が実行される。
遅延初期化・仮想プロキシ
リソースの遅延初期化に利用される。
特定のオブジェクトの生成や初期化に時間がかかる場合、それらの処理をプロキシに行わせることでプログラムの効率化を図ることができる。
キャッシング
仮想プロキシの特殊な形式。
計算コストの高い操作結果を一時的に保存し、同じ操作が要求された時に結果をキャッシュから提供することで、ネットワークによる処置の遅延を軽減させることができる。
また、複数のクライアントが結果を共有することもできる。
また、複数のスレッドからのアクセスを安全に行うためのアクセス制御にも利用できる。
モック
Proxy がテスト時に本番用のオブジェクトの代わりに使用できるモックオブジェクトとなる。
Proxyパターンの構成要素
Subject
主体、主題。
Proxy と RealSubject 共通のインターフェース(または抽象クラス)。
Proxy
RealSubject の代理。
RealSubject をラップすることで、クライアントからの要求を中継、または制御する。
RealSubject への参照を保持する(コンポジション)。
Real Subject
実際の処理を実装したオブジェクト。
Proxy によってラップされるため、クライアントから直接利用されなくなり、Proxy を介して利用されるようになる。
保護プロキシの例
RealSubject
が持つ request1()
、request2()
を使用不可にしたい場合、Proxy
を利用することでアクセスを制御することができる。
public interface Subject {
public void request1();
public void request2();
public void request3();
}
public class RealSubject implements Subject {
@Override
public void request1() {
System.out.println("request1 を実行します。");
}
@Override
public void request2() {
System.out.println("request2 を実行します。");
}
@Override
public void request3() {
System.out.println("request3 を実行します。");
}
}
public class Proxy implements Subject {
// Real Subject を保持
private Subject realSubject = new RealSubject();
@Override
public void request1() {
// Proxy によって、使用不可のメソッドにできる(例外を発生させる方法もある)
System.out.println("権限がありません。");
}
@Override
public void request2() {
// Real Subject に処理を転送する
realSubject.request2();
}
@Override
public void request3() {
// Proxy によって、使用不可のメソッドにできる(例外を発生させる方法もある)
System.out.println("権限がありません。");
}
}
public class Main {
public static void main(String[] args) {
// Real Subject を使用した時
Subject realSubject = new RealSubject();
realSubject.request1();
realSubject.request2();
realSubject.request3();
// >> request1 を実行します。
// >> request2 を実行します。
// >> request3 を実行します。
// Proxy を利用した時
Subject proxy = new Proxy();
proxy.request1();
proxy.request2();
proxy.request3();
// >> 権限がありません。
// >> request2 を実行します。
// >> 権限がありません。
}
}
遅延初期化の例
遅延初期化は、Proxy
の生成時に RealSubject
を生成するのではなく、request1()
、request2()
、request3()
のいずれかのリクエストが要求されたタイミングで生成を行う。
RealSubject
の生成が非常に負荷のかかる処理であるために、アプリケーション起動時などに初期化処理を実行させたくない場合や、RealSubject
が非常にサイズの大きなオブジェクトであるために、メモリを節約したい場合などに有効。
また、循環参照・循環依存(あるオブジェクトAが別のオブジェクトBに依存し、一方で、オブジェクトBもオブジェクトAに依存しているような状況)が発生している状況で、オブジェクトの初期化の順番の管理が複雑になってしまった場合にも有効。
public class Proxy implements Subject {
private Subject realSubject;
private Subject getRealSubject() {
if (realSubject == null) {
// Real Subject は初回リクエストを受けつけたタイミングで初めて生成される
realSubject = new RealSubject();
}
return realSubject;
}
@Override
public void request1() {
getRealSubject().request1();
}
@Override
public void request2() {
getRealSubject().request2();
}
@Override
public void request3() {
getRealSubject().request3();
}
}
キャッシングの例
RealSubject
が、通信を利用した重たい処理によって何らかのデータを取得しているような場合に、同じデータに対して複数回取得リクエストが行われたときに、2度目以降のリクエストでは通信を使用せず、Proxy
が保持するキャッシュを利用させることができる。
public interface Subject {
public String fetchData(String key);
}
public class RealSubject implements Subject {
@Override
public String fetchData(String key) {
// 実際のデータを取得する処理(通信を利用した重たい処理)
System.out.println("通信中...");
System.out.println(key + "を使用して、データを取得します。。。");
return key + "をもとに取得したデータ";
}
}
import java.util.HashMap;
import java.util.Map;
public class Proxy implements Subject {
private Subject realSubject;
// データをキャッシュしながら保持するためのマップ
private Map<String, String> cache = new HashMap<>();
// コンストラクタが Real Subject を受け取る
public Proxy(Subject realSubject) {
this.realSubject = realSubject;
}
@Override
public String fetchData(String key) {
// キャッシュにデータが存在する場合、キャッシュからデータを取得
if (cache.containsKey(key)) {
System.out.println("キャッシュから" + key + "に対応するデータを取得します。");
return cache.get(key);
} else {
// キャッシュにデータが存在しない場合、RealSubject からデータを取得
String data = realSubject.fetchData(key);
// 取得したデータをキャッシュに保存
cache.put(key, data);
return data;
}
}
}
public class Main {
public static void main(String[] args) {
Subject realSubject = new RealSubject();
Subject proxy = new Proxy(realSubject);
System.out.println(proxy.fetchData("key1"));
System.out.println(proxy.fetchData("key2"));
System.out.println(proxy.fetchData("key1"));
System.out.println(proxy.fetchData("key3"));
System.out.println(proxy.fetchData("key2"));
// >> 通信中...
// >> key1を使用して、データを取得します。。。
// >> key1をもとに取得したデータ
// >> 通信中...
// >> key2を使用して、データを取得します。。。
// >> key2をもとに取得したデータ
// >> キャッシュからkey1に対応するデータを取得します。
// >> key1をもとに取得したデータ
// >> 通信中...
// >> key3を使用して、データを取得します。。。
// >> key3をもとに取得したデータ
// >> キャッシュからkey2に対応するデータを取得します。
// >> key2をもとに取得したデータ
}
}
動的Proxy
Proxy クラスが実行時に作成されるProxy パターン。
通常の静的な Proxy よりも、さらに柔軟性、拡張性が求められる場面で利用される。
Java では java.lang.reflect
パッケージで動的プロキシがサポートされている。Proxy クラスはjava.lang.reflect
クラスによってプログラム実行時に作成されるため、開発者は Proxy クラスを実装する必要がない。
リフレクション
リフレクションはプログラムの実行時にクラス(型情報)やメソッドの情報にアクセスするための仕組みであり、動的Proxy を実現する上で重要な役割を果たしている。
Java はコンパイル型言語であるため、通常、型情報の解析はコンパイル時に行われる。リフレクションを使用した場合、型情報の解析が実行時に行われるため、通常よりもコストがかかる。
さらに、リフレクションを使用すると、非公開(プライベート)メソッドへのアクセスも可能なため、セキュリティ上のリスクがあることも理解しながら使用する必要がある。
動的 Proxy パターンの構成要素
Subject
Proxy と RealSubject 共通のインターフェース(または抽象クラス)。
Real Subject
実際の処理を実装したオブジェクト。
Proxy Class
Dynamic Proxy を生成するためのクラス。
Java ではjava.lang.reflect.Proxy
クラスが Proxy Class の機能を持つ。
Dynamic Proxy オブジェクトを、静的メソッド Proxy.newProxyInstance()
によってプログラム実行時に生成する。
public static Object newProxyInstance(
ClassLoader loader, → クラスローダー
Class>[] interfaces, → インターフェース(Subject
)
InvocationHandler h → 呼び出しハンドラ(InvocationHandler
)
) throws IllegalArgumentException
Dynamic Proxy
開発者は実装しないクラス。
実行時に動的に生成される Real Subject の代理オブジェクト。
Dynamic Proxy は、メソッド実行のリクエストを受け取ると、Invocation Handler にそのリクエストを転送する。
Invocation Handler
呼び出しハンドラ。メソッドの呼び出しに対応する役割を持つ。
Real Subject への参照を保持している(コンポジション)。
Proxy と同様に、java.lang.reflect
パッケージに含まれる。
Invocation Handler は、Dynamic Proxy から転送されたリクエストを処理する。
処理、と言うのは、メソッド名、引数などの情報をもとに、実行すべき処理を判断する。
メソッド名で判断する場合は「get」から始まるメソッドが呼ばれたとか、特定の文字列を含むメソッド名が呼ばれた、など。
引数で判断する場合は、引数の数や、引数の型など。
Invocation Handler が実質的に Proxy の役割を果たす
一連の流れ
動的Proxy の例
public interface Subject {
public void request1();
public void request2(String arg);
}
public class RealSubject implements Subject {
@Override
public void request1() {
System.out.println("request1 を実行します。");
}
@Override
public void request2(String arg) {
System.out.println("request2 を実行します。 引数:" + arg);
}
}
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
public class MyInvocationHandler implements InvocationHandler {
private Subject realSubject;
public MyInvocationHandler(Subject realSubject) {
this.realSubject = realSubject;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
try {
// メソッド名で判断する
if (method.getName().contains("1")) {
System.out.println("ここで代理処理を実行する");
return method.invoke(realSubject, args);
// 引数の型で判断する
} else if (args[0] instanceof String) {
System.out.println("ここで代理処理を実行する");
return method.invoke(realSubject, args);
}
} catch (Exception e) {
// Real Subject が例外を発生させた時の処理
}
// Real Subject の処理は実行されず、クライアントには null が返却される
return null;
}
}
import java.lang.reflect.Proxy;
public class Main {
public static void main(String[] args) {
// Real Subject
Subject realSubject = new RealSubject();
// 動的Proxy を生成する
Subject proxy = (Subject) Proxy.newProxyInstance(
realSubject.getClass().getClassLoader(),
realSubject.getClass().getInterfaces(),
new MyInvocationHandler(realSubject)
);
proxy.request1();
// > ここで代理処理を実行する
// > request1 を実行します。
proxy.request2("val2");
// > ここで代理処理を実行する
// > request2 を実行します。 引数:val2
}
}