Hatena::ブログ(Diary)

プログラマーの脳みそ このページをアンテナに追加 RSSフィード

2011-12-18

ProxyパターンとProxyクラスと黒魔術

| 13:21 |  ProxyパターンとProxyクラスと黒魔術を含むブックマーク

Java Advent Calendar 2011 の18日目です。

17日目の記事は JavaEE使ってウェブアプリケーションつくろうよ - 水まんじゅう

19日目はJavaエバンジェリスト寺田さんですよ。乞うご期待。

プロローグ

後:「先輩、いまさらなんですけど上からSQLの遅いところを調査してくれって依頼がきてて、全クエリの実行時間を実データで集計とれと言ってるんですけど。これ、SQL発行前後で時間計測するしかないですかねー。このプロジェクトどんだけクエリ発行してるところあるんだろ…。簡単にやれないですかね。とりあえず調査に1週間かかるって返答しちゃいましょうか」

先:「まぁまて。全部のクエリにもれなく時間計測のコードを挿し込むとかやってられんし、手作業で漏れも発生するだろ。こういうのはオブジェクト指向で解決するのがスマートだ。あ、とりあえず調査に1週間かかるとは返答しておけ」

後:「え。どうやるんですか。とりあえず調査に1週間かかるってメール書きますから教えて下さい」

代理人

先:「このシステムはDB接続をするjava.sql.Connectionをすべてコネクションプールから取得している。ここに細工をしてProxyクラスを返すようにするんだ」

後:Proxyクラスというのは?」

先:Proxyというのは代理人という意味合いだが、処理を中継して左から右に受け流すような実装になる。イメージで言うとそうだな。このUSBマウスがPCにつながってるだろ?マウスからの信号をPCのUSBポートに入力している。これを直接指すんじゃなくて間にUSBハブを噛ませる。そうするとPCと直接通信しているのはこのUSBハブなわけだが、中継しているだけなのでこのマウスは普段通り動かすことができる。」

後:「間で中継しているイメージですね。でもマウスとPCの通信プロトコル考えたら例としては微妙じゃないですか?まぁ眼の前にあったからってのは分かるんですが」

先:「お前はいやらしいツッコミするよな。まぁいい。で、こう中継してやることでその通信に手を加えることができるようになるってわけだ。セキュリティの話題で例えるとマン・イン・ザ・ミドル攻撃(中間者攻撃)みたいな話だが」

後:「その例えってわかりやすいのかなあ…」

Connectionの実装

先:「とりあえずjava.sql.Connectionを実装するぞ」

public class ProxyConnection implements Connection {
	Connection origin;
	public ProxyConnection(Connection origin) {
		this.origin = origin;
	}
}

先:「こんな感じで元のConnection実装をコンストラクタ引数に受け取ってだな。次に各メソッドをこんな感じで中継する」

public PreparedStatement prepareStatement(String arg0) throws SQLException {
	return origin.prepareStatement(arg0);
}

後:「なるほど。まさに中継ですね。ところで先輩、java.sql.Connectionのメソッドが54個あるんですが…」

先:「とりあえず正規表現でも書いて実装しておけ*1

代理PreparedStatementを返す

後:「で、先輩、目的のクエリを投げるのは java.sql.PreparedStatementのメソッドですよね。java.sql.Connectionをプロキシ*2しても駄目じゃないですか」

先:「これは下準備だ。ここからConnection#preparedStatement()に手を入れて同じく java.sql.PreparedStatement をプロクシしたクラスを返すようにしてやる」

後:「あー。なるほど。Connectionを代理してるからメソッドから返すオブジェクトもさらに代理したものを返せるってわけですね」

public PreparedStatement prepareStatement(String arg0) throws SQLException {
	return new ProxyPreparedStatement(origin.prepareStatement(arg0));
}

先:「で、このProxyPreparedStatementのexecuteQuery()で計測コードを差し込むとかするわけだ」

後:「ところで先輩、java.sql.PreparedStatementのメソッドが99個あるんですが…」

Proxy

先:「まぁそんなわけでだな、これはデザインパターンProxyパターンってやつの応用なわけだ」

後:「なるほど。こういう使い方ができるんですね」

先:「で、ここから本題に入ろうと思う」

後:「今までのは何だったんですか」

先:「前座と言うか前フリだな」

後:「長いですよ。普通に感心してへー、Proxyパターンはそういうふうに使えるんだーとか思って満足して帰っちゃいますよ」

先:「しかし、こういうのはシチュエーションが大事なんだ。デザインパターンなんて本でそれだけ覚えてても使いもんにならん。こういうシチュエーションでこのパターンが使えるのかってのは実体験しないと身につかないからな」

後:「で、何が出てくるんです?」

先:java.lang.reflect.Proxyってクラスがあるんだが、知ってるか?」

後:「なんかまんまの名前ですね」

先:「とりあえずjavadoc見てみろ」

後:「どれどれ。えーっと。…。…。…。」

Proxy は、動的プロキシのクラスおよびインスタンスを生成する静的メソッドを提供し、また、それらのメソッドによって生成された動的プロキシクラスすべてのスーパークラスでもあります。

動的プロキシクラス (以下単にプロキシクラスと呼ぶ) は、クラス生成の実行時に指定されたインタフェースのリストを実装するクラスで、

以下に述べる動作をします。 プロキシインタフェースは、プロキシクラスによって実装されるインタフェースです。

プロキシインスタンスは、プロキシクラスのインスタンスです。 各プロキシインスタンスには関連した「呼び出しハンドラ」オブジェクトがあり、

これはインタフェース InvocationHandler を実装しています。プロキシインタフェースの 1 つを使ったプロキシインスタンスでのメソッド呼び出しは、

インスタンスの呼び出しハンドラの invoke メソッドにディスパッチされ、呼び出されたメソッドを識別する java.lang.reflect.Method オブジェクト

および引数を格納する Object 型の配列プロキシインスタンスに引き渡します。呼び出しハンドラは符号化されたメソッド呼び出しを適切に処理し、

呼び出しハンドラが返す結果が、プロキシインスタンスでのメソッド呼び出しの結果として返されます。

Oracle Technology Network for Java Developers | Oracle Technology Network | Oracle

後:「先輩、訳がわかりません」

先:「まぁそうだろうな。どの辺が分からない?」

後:プロクシってのは要はさっき作ったみたいなやつですよね?動的ってのがよく分からないんですが」

先:「リフレクションは知ってるか?」

後:java.lang.Classクラスとかjava.lang.reflect.Methodとかですよね。あまり触ったことはないですけど」

先:「リフレクションを用いることでクラスやメソッドなどを動的に扱うことができるってのはなんとなく分かるか?*3

後:「なんとなくですが」

先:「さっき作ったProxyConnectionとかProxyPreparedStatementは、それぞれのメソッドでそれぞれoriginのインスタンスのそれぞれのメソッド呼び出しをすることを書いた。これが静的な」

後:「はあ」

先:Proxyクラスを使うことでこんな手書きコード書かなくても機械で右から左へ受け流すモノを作れる」

ClassLoader cl = Connection.class.getClassLoader();
Class<?>[] interfaces = new Class<?>[]{Connection.class};
InvocationHandler handler = new InvocationHandler() {
	public Object invoke(Object arg0, Method arg1, Object[] arg2)
			throws Throwable {
		// TODO Auto-generated method stub
		return null;
	}
};

Connection c = (Connection) Proxy.newProxyInstance(cl, interfaces, handler);

先:「とりあえず、Proxyクラスからjava.sql.Connectionの実装を作るサンプルだ」

後:「このConnection cってなにもんですか?動くんですか?」

先:「まぁ動かないな。InvocationHandlerの実装が空だからNullPointerExceptionが関の山だろう。実態が何かはc.getClass()でもしてみれば分かるんじゃないか」

後:「やってみます」

Connection c = (Connection) Proxy.newProxyInstance(cl, interfaces, handler);
System.out.println(c.getClass());

class $Proxy0

後:「なんですか?このクラス。こんなの書いてないですよ」

先:「そうだ。書いてないが実行時に動的に作られたクラスだ。この手のクラスは先頭が$から始まる名前になる。内部クラスとかもそうだな。」

後:「えー。なんか不思議ですね。え、動かないんでしたっけ?実装書いてないのにこのクラスは存在するんですよね?実装どうなってるんですか?」

先:「実装はInvocationHandlerに一任される。例えばそのcに対してConnection#commit() とか呼んでみろ。InvocationHandlerのinvokeの中、return null;ってところにブレークポイントを張ってだな」

後:「あ、止まりました」

先:「invokeの引数をSystem.out.printlnしてみろ。なんとなく分かるんじゃないか」

InvocationHandler handler = new InvocationHandler() {
	public Object invoke(Object arg0, Method arg1, Object[] arg2)
			throws Throwable {
		System.out.println("arg0 : "+arg0);
		System.out.println("arg1 : "+arg1);
		System.out.println("arg2 : "+arg2);
		return null;
	}
};

後:「こうですかね。あれ?StackOverflowError?なんで?」

先:「あー。arg0はマズイな。arg0はとりあえずコメントアウトしておけ」

arg1 : public abstract void java.sql.Connection.commit() throws java.sql.SQLException

arg2 : null

後:「って出てきました」

先:「このarg1が呼ばれたメソッドだ。でこのarg2がその引数。commit()じゃ引数がなくてつまらんな。prepareStatement(String sql)でも呼んでみるか。arg2配列なんだよなー。面倒くせー。」

ClassLoader cl = Connection.class.getClassLoader();
Class<?>[] interfaces = new Class<?>[]{Connection.class};
InvocationHandler handler = new InvocationHandler() {
	public Object invoke(Object arg0, Method arg1, Object[] arg2)
			throws Throwable {
//		System.out.println("arg0 : "+arg0);
		System.out.println("arg1 : "+arg1);
		System.out.print("arg2 : ");
		if (arg2 == null) {
			System.out.println(arg2);
		} else {
			for (Object o : arg2) {
				System.out.println(o);
			}
		}
		return null;
	}
};

Connection c = (Connection) Proxy.newProxyInstance(cl, interfaces, handler);
c.prepareStatement("dummy SQL");

arg1 : public abstract java.sql.PreparedStatement java.sql.Connection.prepareStatement(java.lang.String) throws java.sql.SQLException

arg2 : dummy SQL

先:「ほら、どうだ?動きが見えてきたか?」

後:「あ、はい。cのメソッドを叩くとInvocationHandler#invokeの引数にたたいたメソッドと、その引数が渡ってくるんですね」

先:「ああ。で、invokeメソッドでreturnすると、その値がcのメソッド呼び出し側に返される」

後:「このarg0は?」

先:「これはcの参照だな。インスタンスメソッド呼び出しならcそのものが渡される。staticメソッド呼び出しならnullだ。さっきStackOverflowErrorが起きたのはtoString()メソッド呼び出しがされてinvokeに処理が来てtoString()メソッド呼び出しがされてinvokeに処理が来てを繰り返したからだ」

後:「うへ」

Proxyクラスでの実装

先:「で、ProxyパターンをこのProxyクラスを使って実装してみるぞ」

public static Connection getProxy(final Connection origin) {
	ClassLoader cl = Connection.class.getClassLoader();
	Class<?>[] interfaces = new Class<?>[]{Connection.class};
	InvocationHandler handler = new InvocationHandler() {
		public Object invoke(Object proxy, Method method, Object[] param)
				throws Throwable {
			return method.invoke(proxy, param);
		}
	};

	Connection c = (Connection) Proxy.newProxyInstance(cl, interfaces, handler);
	return c;
}

後:「おー。54個のメソッドを実装した僕が馬鹿みたいじゃないですか」

先:「馬鹿だったろ。さっきまで」

後:ぐぬぬ

先:「まぁ、置換用の正規表現をちゃんと書けたのは褒めてやるよ」

後:「で、PreparedStatement返すところとかどうするんですか?」

先:「ここからダサくなる」

後:「ダサいんですか」

先:「ダサいというか泥臭いというか。とりあえずコピペでPreparedStatementのProxy返すメソッドを作っておくだろ」

public static PreparedStatement getProxy(PreparedStatement origin) {
	ClassLoader cl = PreparedStatement.class.getClassLoader();
	Class<?>[] interfaces = new Class<?>[]{Connection.class};
	InvocationHandler handler = new InvocationHandler() {
		public Object invoke(Object proxy, Method method, Object[] param)
				throws Throwable {
			return method.invoke(proxy, param);
		}
	};

	PreparedStatement c = (PreparedStatement) Proxy.newProxyInstance(cl, interfaces, handler);
	return c;
}

先:「でだな、Connection側のInvocationHandler#invokeの中で分岐処理を書く」

後:「分岐ってメソッドでですか?」

先:「そうだ。ダサいだろう?」

InvocationHandler handler = new InvocationHandler() {
	public Object invoke(Object proxy, Method method, Object[] param)
			throws Throwable {
		if ("prepareStatement".equals(method.getName())){
			return getProxy((PreparedStatement) method.invoke(proxy, param));
		} else {
			return method.invoke(proxy, param);
		}
	}
};

先:「ここではprepareStatementのオーバーロードは全部getProxyに流し込んでるからこんな感じだが、オーバーロードで処理を分岐したければMethod#getParameterTypes()でとった引数の型を比較して分岐する必要がある」

後:「嫌すぎるー」

先:「まぁとりあえずこんな感じで処理してinvokeの前後で時間計測とログ出力をしてやればとりあえず目的は達成できるって寸法だな」

エピローグ

後:「先輩、このProxyクラスの応用例って他にないですか」

先:interfaceならなんでも使えるんだがな。ある種のフレームワークinterfaceメソッドProxyすると動きを追うのが捗るぞ。んー。例えばawtやswingとかのGUIだとListenerのメソッド呼び出しを追いかけるのに使ったりできる」

後:「今時awtはないでしょー。Androidとかでもいけます?」

先:「もちろんAndroidGUIのListenerの追跡もできる。Android上でHttpClientのorg.apache.http.client.CookieStoreをProxyしてCookieの書き換えタイミングを確認とかしてたな」

後:「へー。リフレクションってなんか凄いですねー」

先:「まあこれは黒魔術だからな。プロダクトコード(製品のコード)でProxyを使うようなことは滅多に無い。フレームワークでも作らん限りはリフレクション周りはがつがつ使うことはないな」

後:「あ、先輩、メールの返事来てますよ。一週間待つって言ってますね」

先:「よし。じゃぁ明日から溜まってた有給休暇消化するかー」

後:「あー。ずるいですよ」

先:「お前も休めるうちに休んどけよ。納期前に休みなんてとれんからな」

*1正規表現グループを利用して置換処理を使えばこの手の単純作業は一気に片付けれる

*2Proxyは日本語表記するとプロクシプロキシと表記がぶれる。「ぷろきし」は「プロ棋士」と誤変換されることがあるのでプロクシがオススメ。本稿では後輩はプロキシ、先輩はプロクシと読んでいる。このあたりの呼び方で喧嘩してもしょうがない

*3:リフレクション関連のクラスはjava.lang.reflectパッケージにまとまっている。パッケージ解説を眺めておくとよいかもしれない

リンク元