1. はじめに
この記事では、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」で説明する、動的プロキシの事前知識となるプロキシパターン(Proxy Pattern)を説明します。
本記事で掲載するJavaコードは、以下の環境で動作確認をしています。(Java以外の言語の動作環境は、随時記載します)また、掲載するJavaコードはコンパイルに必要な一部のimport句を省略しています。
名称 | バージョン |
---|---|
OpenJDK | 11.0.1 |
2. 「静的」なプロキシ
「静的」なプロキシを解説します。これは、GoF(Gang of Four)のデザインパターンで紹介されている一般的なプロキシパターンに相当します。本章では静的なプロキシパターン(以下「プロキシ」と表記)の説明します。「動的」なプロキシについては、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」を参照してください。
2-1. 独自のインターフェースとクラスを定義する
プロキシを説明するための準備実装をします。MyInterface
とその具象クラスであるMyClass
を独自に定義します。これは、静的プロキシを適用するためのインターフェースとクラスです。
/** 独自のインターフェース定義 */
public interface MyInterface {
public String myMethod(String value);
}
/** MyInterfaceの実装クラス */
public class MyClass implements MyInterface {
@Override
public String myMethod(String value) {
// MyClassのMyInterface.myMethodの処理
System.out.println("Method:" + value);
return "Result";
}
}
上記のMyClass
クラスをnew
でオブジェクトを作成して、そのオブジェクトからmyMethod
メソッドを呼び出す実装を、以下に示します。
MyInterface myObj = new MyClass();
System.out.println(myObj.myMethod("Argument"));
上記を実行した結果、myMethod
のメソッド内で出力したMethod:Argument
と、myMethod
を呼び出した戻り値を出力したResult
を標準出力に出力します。
Method:Argument
Result
2-2. プロキシパターン
プロキシパターンとなる、静的なプロキシの説明します。
MyInterface
の具象クラスであるMyClass
オブジェクトの利用者は、MyClass
のコンストラクタを呼び出してオブジェクトを生成し、そのオブジェクトのメソッドを呼び出していました。これは、プロキシを仲介しないオブジェクトの利用です。
MyInterface myObj = new MyClass();
System.out.println(myObj.myMethod("Argument"));
プロキシパターンでは、利用者とオブジェクトの仲介をする代理人の役割を果たします。代理人であるプロキシは、実際に利用するオブジェクトと同じインターフェースを持ちます。利用者はあたかも直接利用するオブジェクトのメソッドを直接呼出すように、プロキシのメソッドを呼び出します。プロキシはメソッドの呼び出しを受けて、事前処理を実行した後に実際に利用するオブジェクトの同じメソッドを呼び出して戻り値を受け取ります。プロキシは事後処理を実行した後に、利用者にプロキシから戻り値を返します。
下の絵は、プロキシのイメージを擬人化して表したものです。プロキシは日本語で代理人の意味となるので、図内では「代理人(プロキシ)」と表記しています。
現実の人と人との間でも、直接本人同士がやり取りをせずに代理人を仲介するメリットがあるように、プログラムの世界でもオブジェクトの呼出しにプロキシを仲介するメリットはあります。それは後述します。
2-3. 静的なプロキシの実装
それでは、具体的に実装をしていきましょう。
ここでは、プロキシパターンを実現する実装のMyProxy
というクラスを新たに定義します。MyProxy
クラスはMyClass
と同じようにMyInterface
の具象クラスです。MyProxy
クラスは、MyInterface
のオブジェクトをtargetObj
という名前のクラスメンバを保持し、myMethod
メソッドの中ではクラスメンバのtargetObj
のmyMethod
を呼び出します。それに加えてプロキシ固有の処理となる、targetObj
のmyMethod
を呼び出しの前後では、事前処理でMyProxy:start
事後処理でMyProxy:end
を標準出力に出力します。
/** MyInterfaceを実装するクラスの仲介をするクラス */
public class MyProxy implements MyInterface {
/** 仲介先のオブジェクトを格納するクラス変数 */
private final MyInterface targetObj;
public MyProxy(MyInterface targetObj) {
this.targetObj = targetObj;
}
@Override
public String myMethod(String value) {
// MyProxyの事前の処理
System.out.println("Proxy:before");
// MyClassオブジェクトのmyMethodメソッドを呼び出す
String result = targetObj.myMethod(value);
// MyProxyの事後の処理
System.out.println("Proxy:after");
// MyClassオブジェクトのmyMethodメソッドの戻り値を返却
return result;
}
}
上記で定義したMyProxy
の使い方で最もシンプルな方法を紹介します、利用側の実装でMyProxy
のオブジェクトをコンストラクタで作成します。
MyInterface myObj = new MyProxy(new MyClass());
System.out.println(myObj.myMethod("Argument"));
Proxy:before
Method:Argument
Proxy:after
Result
MyProxy
の処理の実行を示すProxy:before
とProxy:after
の出力がMethod:Argument
の前後にあります。この実装例では、MyClass
の変更なしでMyProxy
の仲介でメソッドを呼び出すようになりました。プロキシパターンは、実装を一切変更せずに共通的あるいは付加的な処理を追加できる、という特徴があります。
2-4. プロキシの使用を強制する実装
上記の例では、オブジェクトのメソッドをプロキシを仲介して呼び出すか直接呼出すかは、利用側で任意に選択できました。しかし、あるクラスのオブジェクトは、必ずプロキシを経由して使用させたいケースがあるかもしれません。そのような場合にはプロキシの使用を強制して、不正な場合はコンパイルエラーで検出できるように実装できます。
これを実現するために、以下の方針でMyClass
を修正します。
- コンストラクタを
private
にして、外部から直接コンストラクタ呼び出しによるオブジェクトの作成をできないようにする。 -
MyClass
のオブジェクトを取得するためのstaticのgetInstance()
メソッドを定義する。MyClass
のオブジェクトの利用者はgetInstance()
メソッドからMyClass
のオブジェクトを取得する。 - 2.のメソッドは
MyClass
のオブジェクトではなく、MyProxy
のオブジェクトを返却する。
具体的なMyClass
の実装を、以下に示します。
/** MyInterfaceの実装クラス(MyProxy経由での利用を強制するパターン) */
public class MyClass implements MyInterface {
/** 外部から直接MyClassオブジェクトを作成させいないため、デフォルトコンストラクタをprivateで定義する */
private MyInterface() {}
@Override
public String myMethod(String value) {
// MyClassのMyInterface.myMethodの処理
System.out.println("Method:" + value);
return "MyMethod result";
}
/** MyClassのインスタンスを取得するメソッド */
public static MyInterface getInstance() {
// MyClassのオブジェクトを作成
MyClass myObj = new MyClass();
// 上記のMyClassオブジェクトを持つMyProxyオブジェクトを作成
MyProxy proxy = new MyProxy(myObj);
// MyProxyオブジェクトを返却
return proxy;
}
}
上記のMyClass
ではnew MyClass()
のように、直接コンストラクタを呼び出してオブジェクトを作成しようとすると、コンストラクタがprivate
であるためにコンパイルエラーとなります。
このMyClass
の実装はSingletoneパターンと似ていますが、getInstance()
が毎回新しいオブジェクトを返却する点がSingletoneパターンとは異なります。もちろん、SingletoneパターンのようにgetInstance
が同じオブジェクトを返却する実装もできます。
MyClass
の利用側の実装は以下になります。
MyInterface myObj = MyClass.getInstance();
System.out.println(myObj.myMethod("Argument"));
実行結果は同じです。
Proxy:before
Method:Argument
Proxy:after
Result
###2-5. 静的なプロキシのまとめと活用例
ここで、これまでに実装したクラスのクラス図を示します。
改めて、静的なプロキシを適用する実装方針をまとめます。
- プロキシを通して呼び出したいクラスがある場合、そのクラスのインターフェースを定義する。
- プロキシのクラスを、1.で定義したインターフェースの具象クラスで定義する。
- 2.で定義したプロキシのクラスには、インターフェースのオブジェクトを格納するメンバ(クラス変数)を用意する。
- 2.で定義したプロキシのクラスのメソッドの中では、メンバの同じメソッドを呼び出す。(呼び出す前後でプロキシの処理を実装する)
これにより、オブジェクトの利用者がプロキシオブジェクトのメソッドを呼び出すと、プロキシオブジェクトの仲介で本当に呼び出したいオブジェクトのメソッドを呼び出します。
それでは、プロキシは具体的にどのような場面で活用できるのか、具体例を次に示します。
既存のクラスに処理を追加するプロキシ
変更不可能な既存のクラスの処理を追加するのは、プロキシパターンを有効に適用するケースの一つです。
例えば、Javaの標準ライブラリのクラスで古くから提供されているjava.text.SimpleDateFormat
は、Date
型とString
型を相互に変換するクラスですが、同期化がされておらずひとつのSimpleDateFormat
オブジェクトを複数のスレッドで同時に使うと不正な値を返却する可能性があります。
この問題の解決法の一つで、SimpleDateFormat
のプロキシを定義して、プロキシ内で処理をsynchronizedブロックで同期化するか、スレッドごとに別のSimpleDateFormat
オブジェクトを持つようにThreadLocalに格納します。SimpleDateFormat
とスーパークラスのDateFormat
は日付処理に関するインターフェースを持たないため、プロキシはSimpleDateFormat
と共有するインターフェースを持ちません。
public class MySimpleDateFormat {
private SimpleDateFormat innerFormat;
public MySimpleDateFormat(String pattern) {
innerFormat = new SimpleDateFormat(pattern);
}
public StringBuffer format(Date date,
StringBuffer toAppendTo,
FieldPosition fieldPosition) {
synchronized(innerFormat) {
innerFormat.format(date, toAppendTo, fieldPosition);
}
}
/* その他のDateFormatのabstractメソッドも、上記と同様に実装する */
}
この方法は、二点問題があることを留意してください。
-
現在のJava実行環境では性能面で最適ではないとされています。実行時に
SimpleDateFormat
のオブジェクトを都度生成する方が、性能面で有利である報告があります。 - この方式は、直接SimpleDateFormatを使用する実装をコンパイルエラーで検知できません。システム開発内のコーディング規約等で禁止にするなどの運用ルールが必要です。
インターフェースに対する複数の具象クラスの共通処理を追加するプロキシ
あなたがひとつのインターフェースと、そのインターフェースで複数の具象クラスを実装したとします。
そのクラスは全て処理の前後で実行すべき共通の処理があるのであれば、プロキシを活用できます。
これまでの実装例では、インターフェースに対して一つの具象クラスを定義して、そのクラスにプロキシを使用していました。本来は、共通のインターフェースを具象するクラスであれば複数の異なるクラスであっても、同じプロキシを共有して使用できます。プロキシは各クラスの前後で実行するべき共通処理を実装するクラスの役割を担います。
3. さいごに
本記事では、「静的な」プロキシの解説をしました。
この続きで、「動的プロキシ(DynamicProxy)をできる限りわかりやすく解説してみる」では動的プロキシを解説します。