読者です 読者をやめる 読者になる 読者になる

新人SEの学習記録

14年度入社SEの学習記録用に始めたブログです。気づけば社会人3年目に突入。

Javaの学習:プロキシ

[Java] Javaの学習

内容:10章 プロキシ

プロキシとは
  • 他のオブジェクトの代役を務め、そのオブジェクトの機能を実行しているかのようにみえるオブジェクト
  • プロキシが代役を務めるオブジェクトを実装オブジェクトと呼ぶ。
// 実装クラス
public class SomeClassImpl {
	private String userName;

	public SomeClassImpl(final String userName) {
		this.userName = userName;
	}
	
	public void someMethod() {
		System.out.println(this.userName);
	}
	
	public void someOtherMethod(final String text) {
		System.out.println(text);
	}
}

// プロキシ
public class SomeClassProxy {
	private final SomeClassImpl impl;
	
	public SomeClassProxy(final SomeClassImpl impl) {
		this.impl = impl;
	}
	
	public void someMethod() {
		this.impl.someMethod();
	}
	
	public void someOtherMethod(String text) {
		this.impl.someOtherMethod(text);
	}
}
  • 上記のコードは、SomeClassImplと全く同じに見えるプロキシを作成する
    • しかし、SomeClassProxyではimplメンバ変数に格納されている実装オブジェクトに要求を転送
  • どんな場合にプロキシが有効なのか?
    • 実装クラスのソースコードにアクセスできない場合(サードパーティのライブラリ、別の部署から提供)
    • 実装オブジェクトが、ユーザから隠蔽すべきさまざまな情報を公開してしまう場合
    • セキュリティ上の理由から、実装オブジェクトのユーザに実装メソッドの名前を公開したくない場合
    • 機能がオブジェクトの使用される状況に依存する場合
    • 機能が開発段階に依存する場合(オブジェクトが呼び出される回数をカウントしてトレース>リリース時には不要な機能)
    • 実装オブジェクトの位置が変化する場合
ファクトリ
  • ファクトリオブジェクト:プロキシと実装オブジェクトを一つの単位に結合
  • プロキスが初期化ルーチンを実行して使用の準備をできるようにしたりする
  • また、許可されていない操作を防ぐためのセキュリティポリシーも実装する
// ダメなコード
// ユーザが適当な名前を渡せてしまう
SomeClassProxy proxy = new SomeClassProcy(new SomeClassImpl("Fred"));

// ファクトリ
public class SomeClassFactory {
	public static final SomeClassProxy getProxy() {
		SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name"));
		return new SomeClassProxy(impl);
	}
}

// ファクトリを介したプロキシの取得
public class DemoProxyFactory {
	public static void main(String[] args) {
		SomeClassProxy proxy = SomeClassFactory.getProxy();
		proxy.someMethod();
		proxy.someOtherMethod("hoge");
	}
}
/* 
 * 実行結果:
 * (PCのusername)
 * hoge
 */
2種類のプロキシ:静的プロキシ
  • 手動で記述されるプロキシ
  • 規則が2つ
    1. プロキシは実装オブジェクトを拡張できない:拡張するとキャストしてプロキシを迂回できるため
    2. プロキシのユーザが実装オブジェクトを作成できるようにしてはいけない:同様にプロキシを迂回してしまう
  • プロキシは必ずファクトリから取得するか、他のプロキシの呼び出しを介して取得するようにすべき
    • ユーザがプロキシを無視して実装オブジェクトを直接使用しないようにする方法はない
  • デコレータパターン
    • オブジェクトの実装とオブジェクト自身との間にコードを追加し、機能を拡張する
    • サードパーティのライブラリのようにオブジェクトの変更ができない場合
    • ある特定の状況下でのみ使う機能を追加したい場合
  • ex) 前述のプロキシ例にメソッドの呼び出された回数を管理する機能を追加
public class SomeClassProxy {
	private final SomeClassImpl impl;

	// 呼び出された回数
	private int invocationCount = 0;
	
	public SomeClassProxy(final SomeClassImpl impl) {
		this.impl = impl;
	}
	
	public int getInvocationCount() {
		return this.invocationCount;
	}
	
	public void someMethod() {
		this.invocationCount++;
		this.impl.someMethod();
	}
	
	public void someOtherMethod(String text) {
		this.invocationCount++;
		this.impl.someOtherMethod(text);
	}
}
インタフェースによるプロキシ
  • 前述の例では、ユーザは必要なプロキシの種類を知っている必要がある
    • オブジェクトのメソッドが呼び出された回数を数えるプロキシは、デバッグ中にしか必要ない
    • これを実現するためには、デバッグ中にはユーザにカウントするプロキシを与え、そうでない場合にはカウントしないプロキシを与える
    • ユーザはどちらのプロキシを受け取るのか実行時までわからないため、プロキシを参照する別の方法が必要
  • インタフェースを使用
// 実装に対するインタフェース
public interface SomeClass {
	public void someMethod();
	public void someOtherMethod(final String text);
}

// 各クラスでインタフェースを実装
public class SomeClassImple implements SomeClass { /* 前と同じ */ }
public class SomeClassProxy implements SomeClass { /* 前と同じ(カウントしない版) */ }
public class SomeClassCountingProxy implements SomeClass { /* 前と同じ */ }
<||

- プロキシは<b>実装を継承しているわけではない</b>ので、前述の規則に反していない
-- このプロキシをインタフェースにキャストしても問題ない
-- インタフェースを実装にキャストしようとすると、ClassCastExceptionが発生する
- デバッグ実行中か否かでファクトリに返すプロキシを返させ、mainではインタフェースで受け取る

>|java|
// ファクトリ
public class SomeClassFactory {
	public static final SomeClassProxy getProxy() {
		SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name"));
		return new SomeClassProxy(impl);
	}

	public static final SomeClass getSomeClassProxy() {
		SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name"));
		// デバッグ中ならカウンタプロキシを返す
		if (LOGGER.isDebugEnabled()) {
			return new SomeClassCountingProxy(impl);
		} else {
			return new SomeClassProxy(impl);
		}
  	}
}

// main
public class DemoInterfaceProxy {
	public static void main(String[] args) {
		SomeClass proxy = SomeClassFactory.getSomeClassProxy();
		proxy.someMethod();
		proxy.someOtherMethod("hogehoge");
		
		// CountingProxyのインスタンスなら、呼び出し回数を表示
		if (proxy instanceof SomeClassCountingProxy) {
			System.out.println(((SomeClassCountingProxy)proxy).getInvocationCount());
		}
	}
}

/*
 * 実行例
 * (PCのusername)
 * hogehoge
 * 2
 */

*** 動的プロキシ
- プロキシクラスでは同じ作業を何度もしていることに気づくかもしれない
-- カウントプロキシを必要とする新しい実装オブジェクトを作る度に、コードを全て複製する必要がある
-- <b>動的プロキシ</b>は、リフレクションを利用してこれを実行する
-- 実行時にJDKによって生成され、ユーザが利用できるようになる

- 呼び出しハンドラ
-- 動的プロキシの記述する際には、プログラマの仕事は呼び出しハンドラと呼ばれるオブジェクトを記述すること
-- java.lang.refrectパッケージのInvocationHandlerを実装

>|java|
public class MethodCountingHandler implements InvocationHandler{

	public final Object impl;
	private int invocationCount = 0;
	
	public MethodCountingHandler(final Object impl) {
		this.impl = impl;
	}
	
	public int getInvocationCount() {
		return invocationCount;
	}
	
	@Override
	public Object invoke(Object proxy, Method method, Object[] args)
			throws Throwable {
		try {
			this.invocationCount++;
			Object result = method.invoke(impl, args);
			return result;
		} catch (InvocationTargetException ex) {
			throw ex.getTargetException();
		}
	}
}
  • ユーザがプロキシのメソッドを実行すると、実装オブジェクトの代わりに呼び出しハンドラが呼び出される
    • 呼び出しハンドラの内部では、コードを挿入してinvocationCountをインクリメントした後、
    • Methodオブジェクトのinvoke()を使用してこの呼び出しを実装オブジェクトに転送
    • 呼び出しが完了すると、実装オブジェクトはハンドラに値を返す
    • その後、その値を呼び出し側に戻す
  • ユーザ向けのプロキシの生成
    • 呼び出しハンドラの記述は、動的プロキシを生成するための第一段階にすぎない
    • 続いて、ユーザ向けのプロキシを生成
    • さらに、プロキシのデザパタによると、プロキシが実装オブジェクトのように見えるようにする必要がある
    • プロキシファクトリと一緒にjava.lang.reflect.Proxyクラスを使用することで実現
public class SomeClassFactory {
	public static final SomeClass getDynamicSomeClassProxy() {
		SomeClassImpl impl = new SomeClassImpl(System.getProperty("user.name"));
		InvocationHandler handler = new MethodCountingHandler(impl);
		Class[] interfaces = new Class[] { SomeClass.class } ;
		ClassLoader loader = SomeClassFactory.class.getClassLoader();
		SomeClass proxy = (SomeClass)Proxy.newProxyInstance(loader, interfaces, handler);
		return proxy;
	}
}
  • このファクトリメソッドでは、SomeClassはSomeClassImplという実装オブジェクトが実装しているインタフェースとなっている
  • Proxyクラスは、4つのメソッドにより莫大な力を発揮する
    • getInvocationHanlder():パラメータとして指定されたプロキシで使用される呼び出しハンドラへの参照を返す
    • getProxyClass():インタフェースの配列とクラスローダを引数として取り、プロキシクラス用のバイトコードを生成する
    • isProxyClass():プロキシクラスが動的に生成されたかを通知
    • newProxyInstance():getProxyClass()を呼び出し、その結果得られるクラスに大してnewInstance()を呼び出すためのショートカット
  • 動的プロキシの使用
public class DemoDynamicProxy {
	public static void main(String[] args) {
		// 使い方はだいたい静的プロキシと同じ
		SomeClass proxy = SomeClassFactory.getDynamicSomeClassProxy();
		proxy.someMethod();
		proxy.someOtherMethod("hogeee");

		// ただし、呼び出し回数を取得するとは、
		// 呼び出しハンドラを取得し、カウント用の呼び出しハンドラに問い合わせる
		InvocationHandler handler = Proxy.getInvocationHandler(proxy);
		if (handler instanceof MethodCountingHandler) {
			System.out.println(((MethodCountingHandler)handler).getInvocationCount());
		}
	}
}
プロキシに関する注意点
  • プロキシは極めて協力なツールだが、注意を払わないと問題が発生する可能性がある
    • アプリケーションの性能劣化
      • 実装オブジェクトを呼び出すたびによけいなメソッド呼び出しが最低1回は発生する
      • 複雑なプロキシでは、実装オブジェクトとのやり取りの度に大量のコードを実行する可能性がある
    • 動的プロキシの問題としては、動的プロキシを考慮してクラスを設計する必要がある
      • クラスがインタフェース(上の例ではSomeClass)を実装していないと使えない
      • インタフェースを使用してクラスに対するプロキシを生成するため
      • 自分の管理するコードならまだしも、サードパーティのライブラリでは面倒になる可能性がある