Javaプロキシデザインパターン(プロキシ)の特定の実装:静的プロキシと動的プロキシ
- 実装方法1:静的プロキシ
- 静的プロキシ方式の利点
- 静的プロキシ方式のデメリット
- Java動的プロキシの実装方法1:InvocationHandler
- Java動的プロキシの実装方法2:CGLIB
- CGLIBを使用したJava動的プロキシの実装の制限
インタビューの質問:Javaでプロキシデザインパターンを実装する方法はいくつありますか?このトピックは、KongYijiが「フェンネルビーンのフェンネルという言葉を書く方法は何ですか?」と尋ねるのと非常によく似ています。
いわゆるプロキシモードとは、クライアント(Client)が実際のオブジェクト(下図の右下隅にあるRealSubject)を直接呼び出すのではなく、プロキシ(Proxy)を呼び出すことによって間接的に実際のオブジェクトを呼び出すことを意味します。
プロキシモードの使用は、一般に、クライアントが実際のオブジェクトに直接アクセスすることを望まないか、実際のオブジェクトにアクセスするための技術的な障害があるため、間接アクセスはプロキシオブジェクトを介してブリッジとして完了します。
実装方法1:静的プロキシ
インターフェイスIDeveloperを開発します。インターフェイスには、メソッドwriteCode、writecodeが含まれています。
public interface IDeveloper {
public void writeCode();
}
このインターフェースを実装するDeveloperクラスを作成します。
public class Developer implements IDeveloper{
private String name;
public Developer(String name){
this.name = name;
}
@Override
public void writeCode() {
System.out.println("Developer " + name + " writes code");
}
}
コードをテストします。Jerryという名前のDeveloperインスタンスを作成し、コードを記述します。
public class DeveloperTest {
public static void main(String[] args) {
IDeveloper jerry = new Developer("Jerry");
jerry.writeCode();
}
}
さて、質問が来ます。ジェリーのプロジェクトマネージャーは、ドキュメントを維持せずにジェリーのコードを書くことに不満を持っていました。ある日、ジェリーが休暇を取り、他のプログラマーがジェリーの仕事を引き継いで、疑問符の付いた見慣れないコードを見たとします。グループ全体で話し合った後、各開発者はコードを書くときにドキュメントを同期的に更新する必要があることが決定されました。
すべてのプログラマーに、コード自体を書くアクションに影響を与えずに開発中にドキュメントを書くことを忘れないようにするために、元のDeveloperクラスを変更せずに、IDeveloperインターフェイスも実装する新しいクラスを作成しました。この新しいクラスDeveloperProxyは、元のIDeveloperインスタンスを指すメンバー変数を内部的に維持します。
public class DeveloperProxy implements IDeveloper{
private IDeveloper developer;
public DeveloperProxy(IDeveloper developer){
this.developer = developer;
}
@Override
public void writeCode() {
System.out.println("Write documentation...");
this.developer.writeCode();
}
}
このプロキシクラスによって実装されるwriteCodeメソッドでは、実際のプログラマーのwriteCodeメソッドを呼び出す前に、ドキュメントを書き込むための呼び出しが追加されます。これにより、プログラマーはコードを書き込むときにドキュメントの更新を確実に行うことができます。
テストコード:
静的プロキシ方式の利点
-
理解と実装が簡単
-
プロキシクラスと実クラスの関係は、コンパイル時に静的に決定されます。以下で説明する動的プロキシと比較すると、実行に追加のオーバーヘッドはありません。
静的プロキシ方式のデメリット
すべての実際のクラスでは、新しいプロキシクラスを作成する必要があります。上記のドキュメントの更新を例にとると、上司もテストエンジニアに新しい要件を提示し、テストエンジニアがバグを検出するたびに、対応するテストドキュメントも時間内に更新する必要があるとします。次に、静的プロキシメソッドを使用して、テストエンジニアの実装クラスITesterも対応するITesterProxyクラスを作成する必要があります。
public interface ITester {
public void doTesting();
}
Original tester implementation class:
public class Tester implements ITester {
private String name;
public Tester(String name){
this.name = name;
}
@Override
public void doTesting() {
System.out.println("Tester " + name + " is testing code");
}
}
public class TesterProxy implements ITester{
private ITester tester;
public TesterProxy(ITester tester){
this.tester = tester;
}
@Override
public void doTesting() {
System.out.println("Tester is preparing test documentation...");
tester.doTesting();
}
}
Javaの動的プロキシ実装メソッドが生まれたのは、まさにこの静的コードメソッドの欠点のためです。
Java動的プロキシの実装方法1:InvocationHandler
InvocationHandlerを使用すると、EngineProxyプロキシクラスを使用して、DeveloperとTesterの両方の動作をプロキシできます。
public class EnginnerProxy implements InvocationHandler {
Object obj;
public Object bind(Object obj)
{
this.obj = obj;
return Proxy.newProxyInstance(obj.getClass().getClassLoader(), obj
.getClass().getInterfaces(), this);
}
@Override
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable
{
System.out.println("Enginner writes document");
Object res = method.invoke(obj, args);
return res;
}
}
実クラスのwriteCodeメソッドとdoTestingメソッドは、動的プロキシクラスでのリフレクションによって実行されます。
テスト出力:
InvocationHandlerによる動的プロキシの制限
インターフェイスを実装していない製品マネージャークラス(ProductOwner)があるとします。
public class ProductOwner {
private String name;
public ProductOwner(String name){
this.name = name;
}
public void defineBackLog(){
System.out.println("PO: " + name + " defines Backlog.");
}
}
引き続きEngineProxyプロキシクラスを使用してプロキシし、エラーなしでコンパイルします。実行時に何が起こりますか?
ProductOwner po = new ProductOwner("Ross");
ProductOwner poProxy = (ProductOwner) new EnginnerProxy().bind(po);
poProxy.defineBackLog();
実行中のエラー。したがって、制限は次のとおりです。プロキシされるクラスがインターフェイスを実装していない場合、InvocationHandlerを介した動的プロキシによってその動作をプロキシすることはできません。
Java動的プロキシの実装方法2:CGLIB
CGLIBは、Javaバイトコードを作成および変更するための使いやすいAPIを提供するJavaバイトコード生成ライブラリです。このオープンソースライブラリの詳細については、githubのCGLIBリポジトリにアクセスしてください:https://github.com/cglib/cglib
ここで、CGLIBを使用して、以前にInvocationHandlerでプロキシされなかったProductOwnerクラス(インターフェイスを実装していない)をプロキシしようとします。
ここで、代わりにCGLIBAPIを使用してプロキシクラスを作成します。
public class EnginnerCGLibProxy {
Object obj;
public Object bind(final Object target)
{
this.obj = target;
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(obj.getClass());
enhancer.setCallback(new MethodInterceptor() {
@Override
public Object intercept(Object obj, Method method, Object[] args,
MethodProxy proxy) throws Throwable
{
System.out.println("Enginner 2 writes document");
Object res = method.invoke(target, args);
return res;
}
}
);
return enhancer.create();
}
}
テストコード:
ProductOwner ross = new ProductOwner("Ross");
ProductOwner rossProxy = (ProductOwner) new EnginnerCGLibProxy().bind(ross);
rossProxy.defineBackLog();
ProductOwnerはコードを実装していませんが、プロキシも正常に実行されます。
CGLIBを使用したJava動的プロキシの実装の制限
プロキシクラスを作成するためのCGLIBの原則を理解すれば、その制限が一目でわかります。ここで実験を行い、最後の修飾子をProductOwnerクラスに追加して、継承できないようにします。
テストコードを再度実行すると、今回はエラーが報告されます。最終クラスXXXXをサブクラス化できません。
したがって、CGLIBによって正常に作成された動的プロキシは、実際にはプロキシクラスのサブクラスです。次に、プロキシクラスがfinalとしてマークされている場合、CGLIBを介して動的プロキシを作成することはできません。
Java動的プロキシ実装モード3:コンパイル時に提供されるAPIを介してプロキシクラスを動的に作成します
最終的であり、インターフェイスを実装しないProductOwnerクラスの動的コードを作成する必要があるとします。InvocationHandlerとCGLIBに加えて、最後の手段があります。
プロキシクラスのソースコードを文字列で直接記述し、この文字列に基づいてJDKのコンパイラ(コンパイル時)APIを呼び出し、新しい.javaファイルを動的に作成してから、.javaファイルを動的にコンパイルします。また、新しいプロキシクラスを取得できます。
テストは成功しました:
コードクラスのソースコードをアセンブルし、プロキシクラスの.javaファイルを動的に作成し、そのコードで作成された.javaファイルをEclipseで開くことができます。
次の図は、ProductPwnerSCProxy.javaファイルを動的に作成する方法です。
次の図は、JavaCompiler APIを使用して、前の手順で動的に作成された.javaファイルを動的にコンパイルして.classファイルを生成する方法を示しています。
次の図は、クラスローダーを使用してコンパイル済みの.classファイルをメモリにロードする方法です。