AndroidアプリケーションのSSL通信をプロキシで解析する(1)


0x00. はじめに


Androidアプリケーションの解析の際に、それがどのようなSSL通信を行っているかが重要となる場面がある。そのようなとき、Doormanのようなローカルプロキシでその通信をフックすることができれば目的が達成できる。

しかし通常の(PC上の)ウェブブラウザのSSL通信と同じように、Androidにも元々「信頼できるもの」として扱われるルート証明書群がインストールされており、これらの証明書を元にSSL通信が実施されてしまう。ただ単にローカルプロキシでSSL通信をフックしようとしても、当然「偽物の証明書である」としてエラーとなってしまうため、プロキシでのフックを実施するためには少し工夫が必要となる。いくつかの方法が考えられるが、このエントリではまず筆者が一番はじめに試した方法を紹介する。

0x01. 対象


まず、今回はURLConnectionクラスを使ったSSLのアクセスを対象とする。なぜならば、ふつうAndroidアプリケーションの開発者がインターネット上のウェブサイトと通信を行いたい場合には、このクラスを使うと想定されるからだ(実は筆者自身は殆どこのクラスを使ったことがないのでよく知らないのだが…)。

URLConnectionクラスはAndroid上では以下のようなクラス階層となっており、URLがHTTPSの場合には自動的にSSL通信を行ってくれるようになっている。

パッケージ名も含めたクラス名は以下のようになっている。

  • java.lang.Object
  • java.net.URLConnection
  • java.net.HttpURLConnection
  • javax.net.ssl.HttpsURLConnection
  • org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl
  • org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl
  • org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl.HttpsEngine
  • 上記パッケージ名を見てわかるとおり、AndroidのJava実装はApacheのHarmonyが使われている。わざわざこんなマニアックな実装(失礼)を使っている理由はどうやらライセンスにあるらしい。うーむ…。

    OracleのJREではSSLの実装はすべてJavaで完結しているのに対し、AndroidではJNIを通じて下層のOpenSSLのネイティブコードを呼び出す形になっているようだ(つまり、OpenSSLの脆弱性の影響をもろに受けるということである)。

    0x02. エミュレータを選択する


    筆者ははじめに簡単なアプリケーションを作成し、プロキシサーバを経由せずにURLConnectionクラスを使ってSSLのサイトにアクセスできるようにした。次にプロキシサーバを経由してアクセスするようにしようとしたのだが、ここでなんと衝撃の事実に直面した。AndroidではWiFi使用時にプロキシサーバを使用できないのだ。この問題はずいぶん前から大騒ぎになっていたようなのだが(当然だ)、未だに対応されておらず、また対応されない理由も不明のようだ。普通に考えて、オフィスのWiFiからは必ずプロキシを使用しないとインターネットに出ることができないような環境はたくさんあると思うのだが、いったい何がどうなっているのだろうか。そもそもオープンソースなのになぜこの点が改善できないのか?なかなか謎が深い。該当スレッドのコメントはすでに1000を超えており、「Fuck Google」などの建設的な意見で埋め尽くされている。

    さて今回の目的はあくまでも対象となるAndroidアプリケーションの通信内容の解析であるため、実機でのプロキシ設定についてはひとまずあきらめ、エミュレータで動作させるという妥協をすることにした。エミュレータでは起動の際のコマンドライン引数に、以下のように-http-proxyを渡すことでプロキシが使用されるようになる。

    -http-proxy http://192.168.1.20:8080/
    

    先述したように普通にMITMしようとしても証明書のエラーとなる。少し長いが、このときのスタックトレースを以下に示す。

    02-23 12:13:57.144: INFO/HA(274): https://www.verisign.co.jp/
    02-23 12:13:57.315: INFO/HA(274): javax.net.ssl.SSLException: Not trusted server certificate
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:371)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.http.HttpConnection.getSecureSocket(HttpConnection.java:168)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:399)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:1152)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:253)
    02-23 12:13:57.315: INFO/HA(274):     at net.jumperz.app.android.test1.HelloActivity.http(HelloActivity.java:113)
    02-23 12:13:57.315: INFO/HA(274):     at net.jumperz.app.android.test1.HelloActivity.onKey(HelloActivity.java:89)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.View.dispatchKeyEvent(View.java:3735)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewGroup.dispatchKeyEvent(ViewGroup.java:788)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow$DecorView.superDispatchKeyEvent(PhoneWindow.java:1667)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow.superDispatchKeyEvent(PhoneWindow.java:1102)
    02-23 12:13:57.315: INFO/HA(274):     at android.app.Activity.dispatchKeyEvent(Activity.java:2063)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.policy.impl.PhoneWindow$DecorView.dispatchKeyEvent(PhoneWindow.java:1643)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.deliverKeyEventToViewHierarchy(ViewRoot.java:2471)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.handleFinishedEvent(ViewRoot.java:2441)
    02-23 12:13:57.315: INFO/HA(274):     at android.view.ViewRoot.handleMessage(ViewRoot.java:1735)
    02-23 12:13:57.315: INFO/HA(274):     at android.os.Handler.dispatchMessage(Handler.java:99)
    02-23 12:13:57.315: INFO/HA(274):     at android.os.Looper.loop(Looper.java:123)
    02-23 12:13:57.315: INFO/HA(274):     at android.app.ActivityThread.main(ActivityThread.java:4627)
    02-23 12:13:57.315: INFO/HA(274):     at java.lang.reflect.Method.invokeNative(Native Method)
    02-23 12:13:57.315: INFO/HA(274):     at java.lang.reflect.Method.invoke(Method.java:521)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:868)
    02-23 12:13:57.315: INFO/HA(274):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:626)
    02-23 12:13:57.315: INFO/HA(274):     at dalvik.system.NativeStart.main(Native Method)
    02-23 12:13:57.315: INFO/HA(274): Caused by: java.security.cert.CertificateException: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:168)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:366)
    02-23 12:13:57.315: INFO/HA(274):     ... 26 more
    02-23 12:13:57.315: INFO/HA(274): Caused by: java.security.cert.CertPathValidatorException: TrustAnchor for CertPath not found.
    02-23 12:13:57.315: INFO/HA(274):     at org.bouncycastle.jce.provider.PKIXCertPathValidatorSpi.engineValidate(PKIXCertPathValidatorSpi.java:149)
    02-23 12:13:57.315: INFO/HA(274):     at java.security.cert.CertPathValidator.validate(CertPathValidator.java:202)
    02-23 12:13:57.315: INFO/HA(274):     at org.apache.harmony.xnet.provider.jsse.TrustManagerImpl.checkServerTrusted(TrustManagerImpl.java:164)
    02-23 12:13:57.315: INFO/HA(274):     ... 27 more
    

    余談だがスタックトレースの下の方を見ると、bouncycastleのライブラリも使用されているらしいこともわかる。

    さてこのスタックトレースの中で、どこかで例外が発生しないようにしてしまえばいい。つまり実行されるJavaのコードのどこかをあらかじめ書き換えておくというアプローチだ。androidアプリケーションのリバースエンジニアリングで示したように、アプリケーション自身の書き換えを行ってもよいのだが(この方法については別エントリで書くかもしれない)、せっかくのオープンソースのOSなので、OS側を書き換えてしまうことにする。なぜなら、OS側でSSLのエラーが出ないようにしてしまえば、その上で動作するあらゆるアプリケーションの通信をフックできるようになるからだ。そこで、今回はAndroidをソースコードからビルドする環境を整えた上で、OS側(Dalvik側)のコードを書き換えることにした。

    0x03. 関数の追加


    まず、先述のスタックトレースの5行目である

    org.apache.harmony.luni.internal.net.www.protocol.https.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:399)
    

    に注目する。ソースコードは以下のようになっている。

    sslSocket = connection.getSecureSocket(getSSLSocketFactory(), getHostnameVerifier());
    

    getSecureSocket関数を呼び出す際の2つの引数は、それぞれSSLのソケットファクトリのインスタンスと、ホスト名が証明書のCommonNameとマッチしているかどうかを判定するHostnameVerifierのインスタンスとなっている。この2つのインスタンスがきちんと仕事をしてくれるおかげで正しいSSL通信が実現される。そこで、今回はこの2つのインスタンスとして、仕事をしないだめだめなソケットファクトリとHostnameVerifierを渡すように書き換えてみる。

    HttpsURLConnectionImpl.javaに、以下の2つの関数を追加する。都合がよいことにこれらの関数はsuperクラスで定義されているものなので、このクラスに追加することで自動的にこの実装が呼び出されるようになる。

    public javax.net.ssl.SSLSocketFactory getSSLSocketFactory()
    {
    try
    {
    javax.net.ssl.SSLContext ctx = javax.net.ssl.SSLContext.getInstance( "TLS" );
    javax.net.ssl.X509TrustManager bogusTm = new javax.net.ssl.X509TrustManager(){
    public java.security.cert.X509Certificate[] getAcceptedIssuers(){return null;}
    public void checkClientTrusted( java.security.cert.X509Certificate[] arg0, String arg1 )
    throws java.security.cert.CertificateException{}
    public void checkServerTrusted( java.security.cert.X509Certificate[] certs, String arg1 )
    throws java.security.cert.CertificateException{}
    };
    ctx.init( null, new javax.net.ssl.TrustManager[]{ bogusTm }, null );
    return ctx.getSocketFactory();
    }
    catch( Exception e )
    {
    return getDefaultSSLSocketFactory();
    }
    }

    public javax.net.ssl.HostnameVerifier getHostnameVerifier()
    {
    javax.net.ssl.HostnameVerifier hv = new javax.net.ssl.HostnameVerifier()
    {
    public boolean verify( String hostname, javax.net.ssl.SSLSession session )
    {
    return true;
    }
    };
    return hv;
    }

    内容の詳しい解説は省略するが、とにかく一切証明書やホスト名等を評価しない、やる気なしのインスタンスを返す実装となっている。実に無駄のない、美しいソースコードだと言える。

    上記の2つの関数を追加したら、Androidをビルドする。するとURLConnectionクラスを使っている部分については、SSLの例外が発生しなくなるため、プロキシを使ってフックできるようになる。

    筆者はFroyoでテストした。Froyoの場合、書き換える対象のファイルは

    dalvik/libcore/luni/src/main/java/org/apache/harmony/luni/internal/net/www/protocol/https/HttpsURLConnectionImpl.java
    

    となる。

    0x04. Kindle for Androidでテスト


    実際にこのように書き換えたAndroidエミュレータ上でKindle for Androidを動作させてみたところ、見事にAmazonのサーバとの間の通信がフックできるようになった(すべての通信がSSLなところはさすがといったところである)。

    0x05. まとめ


    今回はAndroid上で動作するアプリケーションのSSL通信の内容を、あらかじめ書き換えたDalvik上で動作させることにより、プロキシサーバを使って解析する方法を1つ紹介した。この方法は下記のような特徴がある。

  • エミュレータ上で動作するアプリケーションのみ対象となる
  • URLConnectionクラスを使った操作のみが対象となる
  • エミュレータさえ準備できれば、すべてのアプリケーションをインストールして動作させるだけで解析することができる
  • 筆者の個人的な目的はKindle for Androidの通信を見てみたいというものだったので、既に目的は達成されてしまったのだが、別の方法についても探ってみるつもりである。

    0x06. 余談


    HTTPプロキシを経由させるためにはAndroidエミュレータ(QEMU)の起動時にコマンドラインオプションを渡すわけだが、この実装には下記2点のクセがあることがわかったのでここに記しておく。

  • chunkedエンコーディングされたレスポンスをまともに扱えないバグがある(新しいバージョンでは修正済みらしいが、筆者が落としてきたFroyoのソースコードでは直っていなかった)。そのため、プロキシ側で可能であればchunkedエンコーディングを扱わないようにしておくのがよい。Doormanであれば(.chunkToNormal response)というHookを定義すればOKである。
  • CONNECTリクエストの引数で、普通はホスト名を渡してくるものだが、QEMUは自身で名前解決を行った後のIPアドレスを渡してくる。MITMしたいプロキシの立場からすると接続したいホスト名を教えて欲しいところだろう。
  • Advertisements

    HTTPサーバにJava NIOは必要か

    0x00. はじめに


    筆者はJava製のWAF(Web Application Firewall)、Guardian@JUMPERZ.NETの開発とメンテナンスを行っている。元は自社のシステムを守るために(そして半分趣味で)作ったものだが、数年前にこれをコアのエンジンとしてさらに拡張し、SaaS型の商用サービス「Scutum(スキュータム)」を立ち上げた。

    その後順調に顧客を獲得することができ、システムリソース的にも増強が必要となる段階などを経験した。Google、mixiやはてな等、さまざまな大規模サイトのインフラエンジニアの方々がインフラ設計に関する考え方などをインターネット上で公開してくれているおかげで、初期のシステム設計時に「将来的にスケールアウト可能なシステム構成にしておくこと」が重要であるということがわかっていた。その教えに従っていたおかげで、リソースの逼迫(ちなみに今回はCPUリソースだった)に遭遇した際にも、単にサーバを追加することでこれを乗り切ることができた。先人の知恵に感謝したい。

    WAFというのはわかりやすく言えばSQLインジェクションのようなウェブアプリケーションに対する攻撃を防ぐものである。技術的にはそれはHTTP/HTTPSのプロキシサーバであり、一度リクエストをパースして中身をチェックする。チェックの際には(大量の)既知の攻撃パターンとの比較を行うため、文字列マッチングによって多くのCPUサイクルを消費する。

    Guardian@JUMPERZ.NETのアーキテクチャはシンプルなスレッドプール+ブロッキングI/Oである(キューも使っているのでスレッドプールの数以上のクライアントが押し寄せた場合にはそれらのタスクはキューに溜まる)。昔のApacheのようなマルチプロセス+ブロッキングI/Oほどではないが、やや古き良きアーキテクチャと言ったところである。1台のハードウェアで同時に処理できるコネクション数はスレッド数と同等となる(キューにタスクとして溜めておくことができるので、一時的に受け付けることができるクライアント数はスレッド数より多くすることが可能ではある)。Linux上でOracleのJRE6系列を使う場合、64bitでメモリをたっぷり積んだマシンであれば1〜2万スレッドほど生成して動作させることも可能である。この場合ボトルネックはスレッド数の増加に伴うメモリ消費量となる(Linuxのスレッドは非常に優秀であるため、コンテキストスイッチのオーバーヘッドは筆者がベンチマークしてみた感覚では全く問題にならない)。

    Javaでは一般的にこのようなアーキテクチャ(スレッドプール+ブロッキングI/O)ではC10K問題をクリアできないとされており、C10K問題の向こう側に行くためにはNIOパッケージを使用した非同期I/Oによるアーキテクチャに切り替える必要があるとされている。筆者としてもこの問題が非常に気になっており、「JavaのNIOパッケージは速い」「NIOでスケーラブルなソケット」などの文句を目にするたびに自分が使っているアーキテクチャは過去の遺物なのかと心配になっていた。もしやGuardian@JUMPERZ.NETをNIOで書き直せば爆速になるのでは…?という期待と、非同期プログラミング(その難しさには定評がある)に手を出す怖さの間で悶々とした日々を過ごしていたのである。

    0x01. C10K問題


    2011年の2月に、Infoq.comに掲載されていたDeftと呼ばれるソフトウェアの記事などを目にしたことがきっかけで、これまで敬遠していたJavaNIOに少し手を出してみることにした。積ん読となっていたJavaネットワークプログラミングの真髄とKindle版のJava Network Programming(20$以下で買える!)を参考にNIOを使った簡単なコードを書いてみた。Selectorを中心にループするという、いわゆるselect()を使ったソケットプログラミングのJava版という感じである。筆者は王道的な(UNIX系OS上における)C言語のソケットプログラミングについてもブロッキングI/Oを使ったものしか経験がないため、JavaNIOを使ったプログラミングはかなりハードルが高かった。

    NIOを使ったアプリケーションのコードは通常のIOを使ったコードとはまったく異なるものになるため、Guardian@JUMPERZ.NETをNIOで書き換えるのはかなり困難だということがわかった。そこで本当にNIOで書き直す価値があるのかを再検討することにした。

    NIOがIOに比べて明らかに優れているのは、スレッド数の上限(=メモリの上限)を超える数のコネクションを同時に処理できるということである。そのため、Guardian@JUMPERZ.NETにおいて、同時に処理したいクライアントの数が現在のボトルネックであれば、NIOにすることでこれを解消できる可能性がある。しかし現在のシステムにおいて実際にボトルネックとなっているのはCPUリソースであり、その主な原因は先述した文字列マッチングによるものである。同時接続数としては数百〜数千程度であり、従来のIOで(そしてスレッドモデルで)処理できる数だ。そもそも同時に処理するクライアントの数を増やしてしまうと、より多くの文字列マッチングが行われることでCPUリソースがさらに不足する。そのため、一時的にアクセスが集中する際などには、処理しきれないクライアント(ソケット)はacceptした後にタスクとしてキューに待機させておき、実際に同時処理を行うスレッドプールのスレッド数は一定の数にしておく方がよいといえる。このとき重要なのは、Guardian@JUMPERZ.NETはごく一般的なウェブサーバの前にリバースプロキシとして位置するHTTPプロキシサーバであるため、クライアントはFIFOで先に接続してきたものから順番に処理すればそれでよいということだ。この点がNIOを検討する際の非常に大きなポイントとなる。(もちろんウェブサーバ側のアプリケーションがCometなどを使っている場合にはまずいことになるが、ここではそのようなケースは想定しない)

    Cometなどを使用するような、数万オーダのクライアントが同時接続するチャットアプリケーションなどの場合、実際にその数万のソケットを絶え間なく監視する必要がある。いわゆる、多くのソケットが待機時間が長い(ロングポーリングなどと呼ばれる)状態で使用され、アプリケーション自体のCPU使用率としては低いというシステムである。このようなケースでは、先述したGuardian@JUMPERZ.NETのように、クライアントをFIFOで処理すればよい、というわけにはいかない。そのためNIOが必須となり、旧来のIOでは対応できないということになる。これがC10K問題である(この議論は既に数年前にインターネット上では完結していたもので、知っている人には今さら、という感じだろう。筆者は不勉強だったため、ここにきてようやくこの問題の切り分けができた)。

    つまり結論としては、必ずしもGuardian@JUMPERZ.NETはNIOにする必要はないということだ。Guardian@JUMPERZ.NETのアプリケーションの処理ではCPUサイクルを多く消費する。そのためより多くのクライアントからのアクセスを同時に処理するためにはCPUリソースを追加する必要があり、それはサーバ数の増加によって行うのが適切である。

    0x02. NIO vs IO


    次に気になるのは、比較的少なめの、数百〜数千のクライアントを処理する場合に、NIOにすることでパフォーマンスが上がるのかということだ。この程度の数であればスレッド+ブロッキングIOで十分に処理できる。しかしNIOにすることでパフォーマンスが大きく異なるのであれば、それはNIOで書き直すモチベーションになる可能性がある。

    個人的な実感を伴って本当に「NIOが速い!」と感じたのは、TomcatのNIOコネクタのベンチマークテストを行ったときである。Tomcatについては、デフォルトのコネクタのパフォーマンスはひどいもので、そもそも大量の並列度でベンチマークを取ろうとしてもエラーが多発してしまいまともにレスポンスを返せない(もしかしたらチューニングすればマシになるのかもしれない。要調査)。しかしNIOコネクタはまったく異なり、CPUの使用率を低めに抑えながら素晴らしいパフォーマンスを見せる。また、クライアントの同時接続数とスレッド数が独立しているため、クライアントの並列度を1000としてベンチマークを行った際にもスレッドの数は220前後となっており、メモリ使用量が抑制されている。

    そこで、「NIOは爆速なのか?」を念頭に、ギガビットLAN環境でベンチマークテストを行った。対象はTomcatなどのJavaNIOサーバと、スレッド+ブロッキングIOであるGuardian@JUMPERZ.NETである。ベンチマークテストの方法についてはここでは省略するが、基本的にはGuardian@JUMPERZ.NETも十分なパフォーマンスを見せるという結論に達した。スレッドの数が数千から1万程度でもコンテキストスイッチのオーバーヘッドは気にならないレベルであり、スレッドの生成も問題にならないコストであった。

    その後ウェブ上で情報を漁ってみた。JavaのNIOはどのくらい速いのか?本当に速いのか?というのが気になっていたエンジニアは当然ながら大勢いたようで、下記のような興味深い記事が見つかった。

    http://stackoverflow.com/questions/4057853/java-i-o-vs-java-new-i-o-nio-with-linux-nptl
    http://www.thebuzzmedia.com/java-io-faster-than-nio-old-is-new-again/
    http://blog.uncommons.org/2008/09/03/avoid-nio-get-better-throughput/
    http://paultyma.blogspot.com/2008/03/writing-java-multithreaded-servers.html
    http://monkeypatched.blogspot.com/2008/09/java-multi-threaded-blocking-io-wins.html

    これらの記事からわかったのは、Linux2.6ではスレッド実装が非常に優秀であり、従来のモデル(スレッドプール+IO)でもパフォーマンスが問題ない、あるいはNIOよりも速いということだ。これは筆者自身がテストしたベンチマーク結果とも一致する。この段階で筆者としては調査の目的を達成し、ひとまず満足した。

    0x03. Java NIOのバグ?


    ここからは少々本題から外れる。今回の調査で気になったのが、TomcatのNIOコネクタのベンチマークを取っていると、たまにTCPコネクションが張れないというエラーが発生することである。負荷をかける(大量のリクエストを生成する)クライアント側アプリケーションが筆者自作のJavaアプリケーションの場合も、Apache Bench(abコマンド)の場合も同様であった。おそらくこれはTomcat独自のバグだろうと思い、JavaNIOを使っている別のHTTPサーバ実装についてもいくつか同じベンチマークテストを行ってみることにした。対象は以下である(リストにはTomcatも含める)。

  • Jboss Netty
  • Apache HttpComponents
  • Deft
  • Apache Tomcat
  • JavaNIOにはエンジニアを熱狂させる何かがあるらしく、多くのアプリケーションはウェブサイト等においてJavaNIOを使っていること、非同期I/Oであることをアピールしていたのが印象的だった。さて結果だが、驚いたことに全てのアプリケーションでTCPコネクションのエラーが発生した。abはこのエラーが出るとベンチマークテストを中断してしまうため、リクエスト数をある程度多くしてしまうと毎回テストが完走できないという状態であった。一方で筆者自作のベンチマークツールではエラーがあってもログに記録するだけでテストそのものは中断しない。300万リクエストを並列度1000でテストした結果、だいたい10〜20回程度のTCPコネクションエラーが発生するようだった。また、一応書いておくと、Guardian@JUMPERZ.NETではエラーはまったく発生しない。

    コネクションエラーが発生する原因はよくわからないが、JavaNIOを使っているアプリケーションのみに特徴的に発生することを考えると、おそらくJavaNIOそのもののバグなのではないかと思う。上記リストにあるアプリケーションは、(Deftを除き)それなりに多くのユーザが使っているものだと思われるため、アプリケーションのコードに問題があるとは考えにくい。筆者以外でもこの現象に遭遇したユーザがいるようでメーリングリストに投稿があったのだが、結局原因は特定されていないようだ。また、Javaとは関係ないのだが、ついでにnode.jsについても同じ負荷をかけてみたところ、TCPコネクションエラーはまったく発生せずに300万リクエストを処理することができた。そのため原因はOS設定やネットワークではないと考えられる。

    また、Deftに対してTelnetでHTTPリクエストを送ろうとしたところ、HTTPリクエストヘッダの終わりを意味するCR LF CR LFを送る前にレスポンスが返ってくる状態だった。まともにプロトコルを解釈できておらず、アプリケーションとしては非常に未熟なものであることが伺えた。また、少し前の話だがTomcatのNIOコネクタに対してHTTPリクエストを1バイトずつ間を開けて送った際も、正しく動作しないことがあった。JavaNIOのコーディングの難しさがこのような点に現れているように思える。これらのことから筆者自身は当分の間NIOをメインで使うことはないだろうと考えている。

    0x04. スケーラブルな…?


    さてここまで調べてみてあらためて感じるのは、NIOは騒がれすぎているということだ。「スケーラブルなソケット!」「スケーラブルなI/O!」などという文句を目にするが、そもそもスケーラブルの定義は何なのか。筆者の感覚では、新たにハードウェアリソースを投入すればそれだけ処理能力が上がるように設計されたシステムが「スケーラブルなシステム」である。スケーラブルなソケットということは、つまりメモリか何かを増やせばポート番号が10万まで使えるようになるとかそういうことなのだろうか。

    結局、NIOが解決するのは同時に扱えるクライアントの数がスレッド数に引っ張られずに済む、ということでしかない。つまり今まではスレッドの数(=メモリの量)がボトルネックだったが、そのボトルネックは解消できる、ということだ。ひとつボトルネックを解消すれば次は別の部分がボトルネックになるわけであって、結局NIOを使っている場合でもそこで壁に当たるだろう。NIOはまるで魔法のように何でも解決してくれるソリューションではない。また、仮にスレッドモデルで動いているシステムがあり、そこにメモリを追加することでさらに多くのスレッドを生成できるのであれば、そのシステムもスケーラブルだと言えるだろう(「スケーラブルなスレッド」といったところだろうか?)。NIOを「スケーラブルな…」と表現することは問題をわかりにくくするだけのように感じる。

    0x05. ハードウェアはボトルネックではない?


    C10K問題を調べる際に、「ハードウェアはもはやボトルネックではない」という表現を目にしたが、これは違うのではないだろうか(C10K問題を提起した記事に書かれている記述なので突っ込むのは野暮なのかもしれないが)。ハードウェアがボトルネックでないということは、そのハードウェアのCPUをいくら速くし、メモリをいくら足しても(そしてストレージの速度をいくら上げても)システムの性能が変わらないということである。マルチスレッドモデルやマルチプロセスモデルはCPUコアが増えることで性能が上がることが考えられるため、ハードウェアはボトルネックになっていると言えるだろう。本当にハードウェアがボトルネックではないケースというのは、例えばポート番号が足りないなどのパラメータの制限によるものになるだろう。

    0x06. まとめ


    今回の調査で筆者が得たことを簡単にまとめると以下のようになる。

  • アクセスしてくるクライアントをFIFO的に処理すればよいシステム(多くのHTTPサーバがこれに当たる)では、必ずしもJavaNIOを使う必要はない
  • 特にアプリケーションの処理自体がそれなりに重いシステムではスレッドプールの数によって同時に処理するタスクの数をコントロールすることが適切である
  • 数万以上のクライアントを同時に相手にする必要があるシステムでは、NIOを使う必要がある
  • 特にアプリケーションの処理自体が軽いものの場合NIOが適切である
  • JavaNIOの実装にはバグがありそう
  • JavaNIOを使用しているHTTP実装にもまだまだバグがありそう
  • 数千から1万くらいのスレッドは十分に速く、IOもNIOより速い場合が多い

  • Eclipseプラグイン開発の感想

    2010年後半に、拙作のHTTPデバッグ用ソフトウェアであるDoorman@JUMPERZ.NETをEclipseプラグインに移植した(詳しくはこちら)。これは筆者にとって初めてのEclipseプラグイン開発であった。そのとき感じたことを簡単にこのエントリにまとめてみたい。

    Doorman@JUMPERZ.NETというソフトウェアは、2007年に筆者が拙著「ウェブアプリケーションセキュリティ」執筆の際に作成したものである。ローカルマシンで動作するGUIのHTTPプロキシサーバであり、ウェブブラウザとウェブサーバの間に位置し、そこを通り過ぎるHTTPリクエストとレスポンスをユーザが自由に見たり書き換えたりすることで、ウェブ開発の際のデバッガとして使用することができる。筆者はウェブアプリケーション開発者でもあるので、自分自身の開発の際などにも便利に使っている。

    2007年のリリース時、このソフトウェアはSWTという技術をプラットフォームとして採用した。SWTはStandard Widget Toolkitの略で、JavaでのGUIアプリケーションの際に利用できるオープンソースのライブラリの1つである。SWTはJava標準のGUIライブラリであるSwingとは異なり、クロスプラットフォーム開発(WindowsやMacOS,Linux)が可能であるにもかかわらず、動作時のルック&フィールはネイティブアプリケーションのそれとまったく同じとなる(JNIを通じて下層でネイティブコードを呼び出している)。筆者は個人的にSwingのルック&フィールがまったく好きになれず、JavaでGUIアプリケーションを開発したいがSwingはあり得ないと考えていたため、SWTはまさに福音であった。SWTは元々IBMが強力なIDEを開発するための基礎として開発したものであり、その血脈は現在もEclipseに力強く受け継がれている。SWTをオープンソース化してくれたIBMにここで感謝の意を表しておきたい。

    開発開始当初はEclipseのように柔軟なGUIアプリケーションにしたいと考えていたので、Eclipse RCP(Rich Client Platform)として開発を開始した。しかしEclipseフレームワークが想像していたよりもかなり大規模であり、またRCPの配布形式が当時の筆者にとってはわかりにくいものに見えたため、少し勉強してみたものの諦めたという経緯がある。とにかく早く動くものを開発したかったため、結局GUIのライブラリとしてのSWTのみを使用し、アプリケーションのベースとなる部分は単にシンプルなJavaのアプリケーションとして開発したのである。このような経緯があったため、開発がとりあえず完了してからも、心の中ではいつかEclipseのプラグインかRCPアプリケーションにしてみたいと考えていた。

    2010年に筆者は35回目の誕生日を迎え、いわゆるプログラマーの定年に達することとなった。ちょうどプログラマーとして独立してから10年が経過したこともあり、個人的なキャリアの振り返りを行っていたタイミングでもあった。二ヶ月ほどゆっくり時間をかけ、これから自分がどのような技術にどんな風に触れていくべきか考えをまとめた。そして出した結論は、「今まで手を出していなかった様々なプラットフォームにも、浅く広く触れていこう」というものである。それまでの筆者はどちらかというとかなり限定した狭い範囲(Javaでの開発やHTTP、セキュリティなどの分野)を深く掘り下げるのが好きであり、反面、手を出さないジャンルには徹底的に手を出していなかった。ちょうどKindleやiPadなどの新しい形のデバイスを手にすることで新しい刺激を受けていたこともあり、開発者としての自分ももっと積極的に新しいジャンルに踏み出していこうと考えたのである。

    そこで、とりあえずは自分の技術の拠点であるJavaを中心に、今まで触っていなかった方面に手を伸ばしてみることにした。10年ぶりに新しいプログラミング言語の勉強を始めることにし、その対象としてClojureを選択した。ここで勉強の対象をRubyやPythonにしなかった理由は、ClojureがJavaとはとても異なる性質を持っているからだ。RubyやPythonは所詮C言語の流れをくんだ王道的なOOPの言語であり、C++、JavaScript、Perlを少し、そしてJavaを十分に経験している筆者にとってはそれほど違和感を感じる言語ではない。せっかく新しい言語を覚えるのであれば、今までに経験した言語とはまったく性質の異なるものにしようと考えた。Clojureは言語としてJavaとはまったく異なっている反面、JVM上で動作するという非常に大きなアドバンテージがある。安定性、速度、マルチコアへの対応、クロスプラットフォーム性、そして既存のJavaライブラリ(筆者は10年間かけてそれなりに自分で使いやすいと思うライブラリを構築してしまっている)を活かせることなど、実案件で使えそうな条件も整っている。また「ハッカーと画家」を読んだ経験と、筆者がベッチーこと苫米地氏のファンであることから、LISPをいつか勉強していたいと思っていたことなどもClojureへのモチベーションを後押ししてくれた。Clojureは(かなり独自色はあるが)JVM上で動作するLISPである。

    その後Clojureの学習は順調に進めることができ、今ではデータ処理などを行う際の書き捨てツールなどはすべてClojureで書くようになった。「新しい技術に積極的に手を出していく」という目標に対する第一歩が順調に踏み出せたことでうまくモチベーションが回るようになり、次は以前からやりたいと思っていたEclipseプラグイン(あるいはRCP)に手を出すことにした。

    Eclipseプラットフォームはこの数年の間にIDE環境として不動の地位を築き上げた(既にIDE市場の製品の70%はEclipseベースであるという記述も見たことがある)。筆者は2002年頃からEclipseを使っているので、そこそこ古株の利用者である。当時はPCのパワーが現在よりかなり劣っていたため、Eclipseは起動するだけで1分かかるようなこともあり、非常に重いという評判だった。しかしEclipseでのJava開発ではエディタでの編集やコードの保存に合わせてコンパイルがさくさくと進み、C++での開発のように、アプリケーション全体のコンパイルに時間がかかるようなことがなかった。そのため、筆者がそれ以前にやっていたBorland C++ Builderでの開発よりも、よほど軽い感触で開発できるという印象を持っていた。

    Eclipseは初期はJava開発環境として知られていたが、少しずつ着実に汎用IDEプラットフォームとして成長していたようだ。数十から数百といった数のモジュールを破綻することなく扱うためにOSGiをベースとして選択し、プラグインベースで拡張可能なプラットフォームとして独自の路線を歩み始めた。その結果、筆者が出会っただけでもAdobeのFlex、AptanaのAptana Studio、(元)BorlandのJBuilderなどのIDEがEclipseをベースとして開発されるようになった。このことからEclipseプラットフォームは今後も長く安定して提供されるだろうと確信することができた。10年間ただ利用するだけだったEclipseプラットフォームだが、せっかくJavaプログラマーであるので、プラグインによって自分の好きなように機能を拡張できるようになればそれはきっと素晴らしいことになるだろう。そのような想いで、2007年に一度は挫折したEclipseプラットフォームでの開発に再びチャレンジすることにした。

    移植前のDoorman@JUMPERZ.NETはスタンドアローンのJavaアプリケーションであり、GUIライブラリとしてSWTを使用している形となる。そのためロードされるクラスは最低限アプリケーションの動作に必要なものだけとなっており、起動は2〜5秒程度で済む(それでもJavaアプリケーションなので、ネイティブのバイナリで起動するアプリケーションよりよほど遅いが)。また、必要となるファイルもjarファイルとswtのライブラリのみで、数MBだけである。しかしEclipseプラグインにしてしまうと、起動するためには当然Eclipseを起動する必要があり、これは遅いマシンでは(2011年現在でも)20秒近くかかる可能性がある。また、使うためにはEclipse本体のパッケージをインストールする必要があり、ファイルの数もサイズも膨大なものとなる。これらのポイントはEclipseプラグインにすることのデメリットだと言える。一方でメリットとしてはクロスプラットフォームでのインストールの容易さがあり、またViewやPerspectiveといった優れたインターフェースを使えるという点がある(これらの点については後述)。

    筆者はAmazonが提供するKindleのサービスが非常に気に入っており、Eclipseプラグインの開発やOSGi、RCP関連などの洋書を電子書籍として購入し、基本的に開発に必要となる知識を英語(洋書の電子書籍と英語のウェブサイト)で得ることを決めた。このようなルールを決めてしまえば、いやでも英語力が高まることになる。また、元々これらのジャンルは日本ではそれほど人気がなく、日本語での情報が不足しているという面もあった。当初は慣れない英語の電子書籍ということで疲れることもあったが、次第に順応することができた。その省スペース性や可搬性の高さなど、電子書籍、そしてKindleの魅力に徐々にハマっていくことになった。

    Eclipseプラットフォームの魅力はなんといってもその柔軟なウィンドウ(View)配置能力である。それぞれのウィンドウはViewと呼ばれ、マウスによって自由に(本当に自由に)各View間のレイアウトを変更することができる。複数のViewを同じ位置に配置すれば上部のタブで管理できるようになるため、画面が狭い場合でも工夫して作業しやすいレイアウトを作成することができる。複数のViewの配置によって決定されるアプリケーション全体のレイアウトはPerspectiveと呼ばれる概念で捉えられ、Perspectiveに名前を付けて保存しておくことができる。そのため開発時やデバッグ時などに違うレイアウトを使いたい場合でもワンタッチでアプリケーション全体のレイアウトを変更することができる。この15年の間にさまざまなGUI環境に触れてきたが、Eclipseの、このViewとPerspectiveからなる仕組みはかなり完璧に近いGUIインターフェースであると考えている。

    Doorman@JUMPERZ.NETの移植を開始する際にまず迷ったのが、これをEclipseプラグインにするか、あるいは独立したRCPアプリケーションとするかという点である。もともと独立したアプリケーションであったのでRCPアプリケーションにするつもりだったのだが、最終的にはEclipseプラグインとして開発することに決めた。これは次のような理由からである。

  • プラグインは単体のjarファイルにまとめることができるため、配布やインストール・アンインストールの方法がシンプルでわかりやすい
  • Eclipseがインストールされていれば、ファイルをコピーしてEclipseを再起動するだけでインストールができる
  • Windows/MacOS/Linux/32bit/64bit等の環境を考慮せずにjarファイルのみ配布できる(各環境に合わせたEclipseのパッケージはeclipse.orgで配布されており、誰でも利用可能である)
  • RCPの配布がやや複雑でわかりにくい
  • 元々はRCPという形態は存在せず、プラグイン開発が元祖である
  • RCPではなくプラグインとして開発することを決め、Kindleで購入したAddison-Wesleyの「Eclipse Plug-ins Third Edition」「OSGi and Equinox」などを読むところからスタートした。長らくEclipseを使っていたが、それがOSGiという基盤を持っていたことはこの段階まで知らなかった。OSGiではBundleという刺激的な概念が登場する。単なるjarファイルやパッケージ名によるモジュール化だけではエンタープライズ級の巨大なアプリケーション開発には不十分であると考え、自らが外部に提供する機能や依存する他のBundle、あるいはそのバージョンなどを明記するというアプローチである。以下に例を示す。

    Manifest-Version: 1.0
    Bundle-ManifestVersion: 2
    Bundle-Name: DoormanHook
    Bundle-SymbolicName: net.jumperz.app.MDoormanHook;singleton:=true
    Bundle-Version: 1.0.0.qualifier
    Bundle-Activator: net.jumperz.app.MDoormanHook.Activator
    Bundle-Vendor: JUMPERZ.NET
    Require-Bundle: org.eclipse.ui,
     org.eclipse.core.runtime,
     net.jumperz.app.MDoorman;bundle-version="1.0.0"
    Bundle-RequiredExecutionEnvironment: J2SE-1.5
    Bundle-ActivationPolicy: lazy
    Export-Package: net.jumperz.app.MDoormanHook,
     net.jumperz.app.MDoormanHook.view
    Bundle-ClassPath: .,
     lib/clojure.jar

    これはDoorman@JUMPERZ.NETの移植後に作成された2つのプラグインのうち、片方のMANIFEST.MFファイルの中身である。自らのBundleの名前(Bundle-Name)やバージョン(Bundle-Version)、依存する外部のBundle(Require-Bundle)、実行時に最低限必要となるJavaのバージョン(Bundle-RequiredExecutionEnvironment)などが記述されている。これらのメタ情報がコンパイルされたクラスファイル群と一緒になっていることで、単なるパッケージやjarファイルによるアーカイブよりも明確に、Javaコードの管理されたモジュール化が実現されることになる。Eclipseプラグインはどれもこのようなメタ情報を持っている。Eclipseがインストールされているディレクトリの下のplugins/の中から興味があるプラグインを探し、メタ情報を見てみるだけでもなかなか興味深いかもしれない。

    また、プラグイン(=Bundle)がどのようなViewを持っているか、等のプラグイン内部のメタ情報もXMLファイルの形で管理されており、こちらもコードとは別に明確に宣言されている。以下に例を示す。

    <?xml version="1.0" encoding="UTF-8"?>
    <?eclipse version="3.4"?>
    <plugin>
       <extension
             point="org.eclipse.ui.views">
          <view
                category="Doorman.category1"
                class="net.jumperz.app.MDoormanHook.view.MHookView"
                icon="src/net/jumperz/app/MDoormanHook/resource/doorman.gif"
                id="net.jumperz.app.MDoormanHook.view.MHookView"
                name="Hook"
                restorable="true">
          </view>
       </extension>
       <extension
             point="net.jumperz.app.MDoorman.Hook">
          <client
                class="net.jumperz.app.MDoormanHook.MHookFactoryImpl">
          </client>
       </extension>
    </plugin>

    これらのメタ情報について、開発者は直接ファイルを編集する必要はない。プラグイン開発の際に必ず使用するであろうGUIツールであるEclipseプラグインエディタを使用することで、自動的にXMLファイルの中身が編集されていく。プラグインエディタの使い方は独自の作法が多く、開発者はまずこれを覚えないと開発を進めることができない。そのため初めての場合はチュートリアル的な参考書などが必須となるだろう。基本的にはプラグインエディタはわかりやすいツールだが、たまにプラグインエディタが何をやっているのか不明な場合などもある。このような場合、プラグインエディタによって変更が自動的に反映されたXMLファイル側を直接読むことで、その意味が明確になる場合などもある。

    このようにメタ情報がXMLファイル等で管理される開発では、Javaのクラス名が要素として重要なものとして扱われることが多い。これはある意味仕方がないことではあるが、メインとなるクラスだけではなく、Viewなどについてもすべてクラス名がXMLファイル中に記述されるようになっている。クラス名そのものがアプリケーションにおいて重要な意味を持ってしまうというのは、筆者にとっては少し抵抗があることだったが、これも作法だと思って受け入れるしかないだろう。クラス名そのものが意味を持ってしまうことは、ClojureのようにJVM上で動作する別の言語を使ってEclipseプラグインを開発しようとする場合に邪魔になることが考えられる(わざわざJavaのクラスを生成しなければならないため)。筆者はDoorman@JUMPERZ.NETの移植に際してClojureでの開発(移植)にチャレンジしてみたかったのだが、Eclipseプラグイン開発がプラグインエディタというツールやJavaのクラスファイル名への依存が大きかったため、これは諦めることとなった。ただし、これは手間をかければ不可能ではないかもしれないので、興味がある人にはぜひチャレンジして欲しい。

    Eclipseプラットフォームにおける開発において、とても良い印象をうけたのは、Preference(設定)関係である。Eclipseでは設定情報をPreferenceダイアログで管理できる。設定情報にはツリー構造を持たせることが可能であり、項目の分類を適切にすることで使いやすいインターフェースを実現できる。また、項目数が多くなってもテキスト入力欄から気軽に検索して目的の設定を見つけることができるため、多数のプラグインをインストールした後でも非常に使いやすいものになっている。Eclipseプラグイン開発では、このPreferenceまわりについての作法に従うことで、非常に簡潔に、使いやすい設定ダイアログを開発することができる。データのセーブ・ロードなどをプラットフォーム任せにすることができるため、殆どコードを記述する必要がない。この部分の出来の良さにはとても感銘を受けた。

    しかし一方で、メニューに関する作法には疑問を持った。メニューというのはアプリケーションの使用状況に合わせて表示する項目を変えたり、あるいはEnable/Disableを切り替えたりすることが多い。この切り替えのルーチンは、結局の所コードを書くことで管理するのがプログラマーにとってはもっともわかりやすいと感じる。プラグインエディタにはGUIベースでこれをやろうとする仕組みが存在しているのだが、これがどうにも使いにくい。しばらく格闘したのだが、結局諦めて自分のコードで片付けてしまった。このように作法に従うと楽な場面もあり、一方で作法が難しすぎると感じる場面もあった。

    また、Eclipse自体が既に長い歴史を持っていることから、参照しているプラグイン開発用の情報が既に古くなってしまっているケースがある。特にウェブサイトを参考にする際にはそのページがいつごろ書かれたものなのかに注意が必要だ。このような意味では、(電子)書籍は記述された時期が特定でき、書籍内を通じて統一されているため使いやすい。

    先述したようにEclipseプラグインはOSGiのBundleであり、高度なモジュール化がウリの一つである。そこで移植作業が順調に進んできた頃、プラグイン間の連携機能についても学んでみることにした。Doorman@JUMPERZ.NETが以前持っていた機能はほぼすべて1つのプラグイン(いわば本体)として移植し、さらに追加で開発した新機能を別のプラグインとして実装することとした。本体の名前はMDoorman、別プラグインの名前がMDoormanHookとなる。

    MDoormanは自身のHTTPプロキシサーバにおいて、リクエストあるいはレスポンスを受信したタイミングで特別な処理(Hook)を実装するための拡張機能を持つことにする。この拡張機能はMDoormanプラグインに拡張ポイント(ExtensionPoint)として定義される。他のプラグインはこの拡張ポイントに定められた実装を行うことでMDoormanプラグインと連携した動作を行うことができるようになる。筆者自身が両方のプラグインを実装することで、Eclipseプラグインにおいて拡張ポイントを提供する側と、それを使用する側の両方の実装を学ぶことができると考えた。

    拡張ポイントの作法は筆者が予想していたよりもはるかに複雑であり、結局こちらのサイトのチュートリアルをほぼそのまま実装することで何とかやりたい動きを実現することができるようになった。拡張ポイントまわりは(あくまで予想だが)より多くのパターンの拡張をサポートするために多機能となっているのだと考えている。完全に把握するためにはかなりの学習が必要になりそうだと感じた。

    今回拡張ポイントを利用する側のプラグインであるMDoormanHookはユーザに対して「Hook」という名前のViewを提供する。ユーザは自身のコードをこのViewを使って登録することで、プロキシサーバを通過するリクエストやレスポンスを書き換えることができるようになる。MDoorman本体は(もちろん当然なのだが)MDoormanHookなしでも動作することができる。MDoormanHookプラグインをインストールすればHookのViewと連携し、アンインストールすれば何事もなかったかのように本体だけで動作する。筆者はGUIのツールにおいてこのようにきれいに機能を拡張可能な機構を使った経験がなかったため、はじめに動作を確認した際にはなかなかに感動させられた。モジュール間の区切りが鮮明になるため、エンタープライズ級のアプリケーションでは特に役に立つだろう。

    先述したように、MDoormanHookを使うことで、ユーザはプロキシサーバ上を通過するリクエストやレスポンスを変更することができる。わかりやすい例として、User-Agentの書き換えなどがある。この書き換えはClojureのコードを記述することで実現される。

    元々のアイデアではJavaとClojureのコードの両方をサポートするつもりだったのだが、Javaについてはうまく行かなかった。拡張ポイントの向こうとこちらが異なるプラグインであることから、クラスローダが異なっていることがあり、結果としてJavaのコードを動的にコンパイルして動作させることができなかった(MDoorman本体側で記述すれば可能なのだが、それでは今回の目的である「Hookの機能は別プラグインで実装する」ということから外れてしまう)。こちらのプラグインで動的に生成したクラスを向こうのプラグインに渡す、ということができないのだ。このことから、クラスローダが直接影響するような仕組みをEclipseプラグインで操る場合には注意が必要であることがわかった。またClojureそのものについても、このクラスローダがプラグインごとに異なっているという問題が影響を及ぼすケースがあり、例えばClojure開発用のEclipseプラグインであるcounterclockwiseはMDoormanHookと共存させようとした場合にエラーが出てしまうケースがあるようだ。

    拡張ポイントの実装を終えた後には、アップデート機能の検討に取り組んだ。ここではFeatureという概念が登場する。Eclipseプラットフォームが提供するアップデート機能を使うためには、アップデート用のウェブサイトや、Featureを使ったプラグインの管理が必要となる。標準的な手続きにしたがってアップデート用のウェブサイトまで構築し、実際にインストールやアップデート作業を行ってみたのだが、最終的にはこれは使わないことにした。理由は下記のようなものである。

  • いくつかの環境で、アップデートのテスト中に、ネットワークの通信中のステータスのままEclipseが固まってしまうことがあった
  • すべてがうまく行く場合でも、手順があまりシンプルでなく、ユーザにとって本当にわかりやすいかどうか疑問が残った(単にjarファイルのコピーでインストールできた方が簡単では?)
  • このように様々な経緯を経て移植作業を完了させることができた。WindowsでもMacOSでもLinuxでも何の問題もなくアプリケーションが動作する様子は圧巻である。すべての環境でネイティブアプリケーションとまったく同じLook&Feelとなり、またインストールもEclipseさえ入っていれば簡単に終了する。特に筆者にとって開発経験がまったくないMacOS上できちんと自分が開発したアプリケーションが動作したことは軽いカルチャーショックであり、クロスプラットフォーム性を持つJava、あるいはEclipseの凄さを再認識させられることとなった。

    今回開発したアプリケーションは「Doorman Eclipse Plugin」と名付けた。2011年にはSSL関連の作業に便利な新機能も実装した。また、近いうちにClojureだけでなく、JavaScriptでも拡張コードが書けるようにする予定である(実装にはJavaに含まれているrhinoを使う予定)。

    また、この後筆者はAndroidアプリケーションの開発にも触れてみることにしたのだが、その際にこのEclipseプラグインの開発経験が役に立った。Android開発はEclipseプラグイン開発と同様に、IDE(もちろんEclipseだ)にヘビーに依存する開発手法となっている。また、GUIのエディタツールでの操作がXMLファイルを更新する部分などもそっくりである。おそらくAndroidの開発環境はEclipseのプラグイン開発を参考に設計されたのではと思う。そしてAndroidの開発環境はまさにEclipseプラグインそのものとなっており、その動作の仕組みなどを把握する際にも今回のEclipseプラグイン開発経験が役に立ちそうだ。

    このように、筆者にとっては得るところの非常に多い開発経験となった。最近ではあまり「Eclipseプラグインを開発しよう!」のような記事を見かけなくなってしまったが、2011年現在でもまったく問題なくおすすめできる最新の技術であると思う。