外部提供のInterfaceをインスタンス化する

目次


参考にするリンク

むいみこむ
http://muimi.com/j/reflection/#DynamicProxy
リフレクション#DynamicProxy
Proxy パターン - Wikipedia
http://ja.wikipedia.org/wiki/Proxy_%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3

存在するかどうか分からない外部提供クラス

個人で作成しているときは気にならないようなことが、企業では起こる。
その1つとして、外部提供の例を考えてみる。

システムを構築する環境を絞るということ

通常、システムを組んでいるときは環境を絞ることができる。
ここで言う環境とは、自分のシステムが動作する場所ということ。
たとえばWindowsアプリならWindows上で動作するのは確実である。
それはもちろんコンフィデンシャルな側面もある。
しかし一方で、環境を制限すると良いこともある。
その中でも「あるかどうか分からない」を回避することができることは大きい。

Javaに限らず、オブジェクト指向の概念ではsuperクラスが存在する。
システムの中で汎化・部品化する際にはとても有益なものだ。
だが他の環境との接続部分に使われると、話がややこしくなる。
そう、Javaで言えば「クラスをimportする」ことができるのか、できないのか、だ。

外部提供されたクラスが「見えない」ということ

Javaで言う「クラスをimportする」とは、そのクラスが確実に存在することを意味する。
import文はパッケージ名指定でクラスを確定させるので、
たとえ同名のクラスが別パッケージにあっても見つけることができる。
外部提供されるクラス郡は当然、「こちらに見える」ように提供されるのだから、
もちろん通常はimport文が書けるし、それで問題ない。

しかし、絶対にimport文が書けるとは限らない。

たとえば、こちらのシステムと外部提供システムがあったとする。
  • こちらのシステムはどのような環境でも動作する。
  • 外部提供のシステムは一部のハードウェアがある場合に動作する。
このとき
  • 双方が使用するリソースは2つ同時に使用できない。
という条件が付くと排他をする必要があるのだが、
  • 排他処理そのものが、外部提供のシステムが乗ってないと使わない。
    • ゆえに「排他のシステムが乗ってない」ことがある。
ということが、無視できない程度の頻度で起こる。

いや、というか実際に起こった。

クラスはClass.newInstance()。でもInterfaceのインスタンス化は?

外部提供のクラスが存在するか分からない場合、import文は書けない。
それどころか、以下のようにクラス名を書くことすらできない。
  1. public static void main(String[] args) {
  2. 外部提供クラス external = new 外部提供クラス();
  3. }
こういう時は、Javaのリフレクションを使って、
  1. public static void main(String[] args) {
  2. try {
  3. Class<?> cls = Class.forName("test.外部提供クラス");
  4. Object instance = cls.newInstance();
  5. } catch (ClassNotFoundException e) {
  6. throw new NoClassDefFoundError();
  7. } catch (InstantiationException e) {
  8. throw new InstantiationError();
  9. } catch (IllegalAccessException e) {
  10. throw new IllegalAccessError();
  11. }
  12. }
などとしながらinstanceを拾い上げる。
java.lang.reflect.Constructor<T>を使うこともあるだろう。
これで、こちらからメソッドを呼び出すのは、ほぼ、困らなくなる。


だがここで問題が起きた。
  • 外部提供のシステムが排他を解除する際に、少し時間が掛かる。
  • 排他システムでは、排他解除の通知がきたらこちらにコールバックを返す仕様。
  • このコールバックは排他システム側でInterfaceが用意されている。

さぁ、排他システムが乗っているかどうか分からない状況だ。
このコールバック用のInterfaceを実装したインスタンスを、どう渡す?

外部提供のInterfaceをインスタンス化する

というか、正確にはこちらからInterfaceを Proxy にする。
標準のJavaに用意されていて、
  • java.lang.reflect.Proxy
  • java.lang.reflect.InvocationHandler
を使って、相手が呼び出したメソッドをこちらにコールバックさせる。

Proxyそのものは・・・「デザインパターン Proxy」あたりで検索するといいかも。

提供元と呼び出しの構成

提供元さんには敢えて、漢字使ったクラスとインターフェースを準備いただいた。
理由はね、ほら、実際に提供してきたとあるとこを尊重してさ。

Main.java

使ってみる。

Listenerが提供されているということは、
普通、Listenerとして登録するクラスがあると思う。
ただ動的にInterfaceをインスタンス化する必要があるときは
これも見えないと思うので、Reflectionを使って登録することになるだろう。

Proxy.newProxyInstance(ClassLoader, Class<?>[], InvocationHandler)
で取得したインスタンスが、実際に上手くやってくれるインスタンス。
これをListenerとして登録する。

  1. // コールバックをもらうためのHandler
  2. InvocationHandler handler = new InterfaceHandler();
  3. // 提供元のインタフェースのクラス型を拾ってくる。
  4. Class<?> clsImpl = Class.forName("test.外部提供インタフェース");
  5. // その他、必要なもの
  6. ClassLoader loader = clsImpl.getClassLoader();
  7. Class<?>[] interfaces = new Class<?>[] { clsImpl };
  8. // Proxy用のインスタンス生成。
  9. // 実際に提供元に渡すのはこのインスタンス。
  10. Object listener = Proxy.newProxyInstance(loader, interfaces, handler);
  11.  
  12. // 提供元のListener登録するクラス型を拾ってくる。
  13. Class<?> clsExtn = Class.forName("test.外部提供クラス");
  14. // 適当にインスタンスにしてListener登録
  15. Method SETLISTENER = clsExtn.getMethod("setListener", interfaces);
  16. Object instExtn = clsExtn.newInstance();
  17. SETLISTENER.invoke(instExtn, new Object[] { listener });
  18.  

で、このInvocationHandlerというのが実際にinvokeを受ける。
  • result = getClass().getMethod(name, method.getParameterTypes()).invoke(this, args);
というのはちょっと強引な気もするけど、
そもそも強引にProxyにしているのだから今更感満載でいいかなーって。

  1. private static final class InterfaceHandler implements InvocationHandler {
  2. @Override
  3. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
  4. // proxyをここでログ出すと
  5. // 「toString() の invoke が呼ばれる。」
  6. // とか意味不明なことになってStackOverflowで落ちるので気をつけるように。
  7. L.d("method=[%s]", method);
  8. if (args != null) {
  9. L.d("args=%s", Arrays.asList(args));
  10. }
  11.  
  12. Object result = null;
  13. String name = method.getName();
  14. // 面倒なときの呼び出し
  15. result = getClass().getMethod(name, method.getParameterTypes()).invoke(this, args);
  16. /* こっちはこっちで面倒である
  17.   if ("call".equals(name)) {
  18.   call();
  19.   } else if ("callcall".equals(name)) {
  20.   result = callcall((Integer) args[0]);
  21.   }
  22.   //*/
  23. return result;
  24. }
  25.  
  26. public void call() {
  27. L.std();
  28. }
  29.  
  30. public int callcall(int a) {
  31. L.std("a = %d", a);
  32. return a;
  33. }
  34. }

実行結果

添付のファイルを実行すると以下のようになる。

Main.java
Main                 : main() Start.
Main                 : main() clsImpl = interface test.外部提供インタフェース
Main                 : main() handler = test.Main$InterfaceHandler@201f9
外部提供クラス       : setListener() Start.
Main$InterfaceHandler: invoke() method=[public abstract int test.外部提供インタフェース.callcall(int)]
Main$InterfaceHandler: invoke() args=[10]
Main$InterfaceHandler: callcall() Start : a = 10
外部提供クラス       : setListener() End.
Main                 : main() End.

まとめ

Interfaceをインスタンス化する方法があることを示した。
これには以下のクラスを使用してProxyを提供元に登録するものだ。
  • java.lang.reflect.Proxy
  • java.lang.reflect.InvocationHandler

確実に相手が存在する環境では、当然ながらimplementsを利用すべき。
しかし存在が不確定な相手とのInterfaceを張る場合は注意が必要になる。
それでも、環境毎に分けることが可能なら、やはりimplementsを利用すべき。

この手法が当てはまるのは、主に相手のせいではない。
こちら側が何らかの理由で環境を分けられない立場にあるためである。

関連リンク

&trackback()



最終更新:2012年02月26日 22:42
添付ファイル