はじめに
開発者のコンピュータには、将来的なビジネスルールの変更やアプリケーションの実行時環境の変化を予測する水晶玉は付いていません。しかし、JavaのリフレクションAPIを使用すれば、コンパイル時に判明している情報のみに基づいてアプリケーションの能力を制限する必要はなくなり、アプリケーションを実行時環境の変化に動的に対応させることができます。
初期設定ファイルやプロパティファイルを読み取るテクニックはよく知られていますが、リフレクションAPIを使い慣れている開発者はあまり多くありません。リフレクションを利用すると、コードの機能を実行時環境に合わせて大幅に拡張できます。つまり、ユーザー設定やサーバー設定、実行時変数などに基づいてプログラムの動作を変更することが可能です。本稿では、Java Authentication and Authorization Services(JAAS)のLoginContext
クラスがリフレクションAPIをどのように利用して柔軟性のあるセキュリティサービス層を実装しているかを解説します。このクラスは、ログインモジュールをアプリケーションの起動時にロードするのではなく、オンデマンドでロードします(JAASはJava 1.4のコアJDKの一部です。Java 1.3xをお使いの方は、JAASを個別にダウンロードできます)。
リフレクションの目的
リフレクションAPIは「java.lang.reflect」パッケージに含まれています。このパッケージのJavadocをざっと見てみると、Constructor
、Field
、Method
といったJava言語の基本要素の名前を持つクラスがたくさんあることに気付くでしょう。これはリフレクションAPIの目的を示す第一のヒントです。つまり、このAPIはクラスの内部的な動作の「リフレクション」(reflection=反射、反映)を提供するのです。しかし、このAPIで実現できるのがクラスの要素のレポートを取得することだけであるならば、興味深くはあっても、動的なコードを記述するための役には立ちません。リフレクションAPIでは、クラスの構成要素を問い合わせるだけでなく、クラスを実行時に動的にロードおよびインスタンス化することができ、さらにそのクラスのオブジェクトのメソッドを呼び出すこともできます。
リフレクションを使った設計で難しいのは、静的には実装できない(または実装しようとは思わない)サービスのうちどれを実行時に提供するかを判断することです。実行時環境に応じて異なる動作をする再使用可能なサービスを提供したい、というのがStrutsやJAASなどのフレームワークが作成される理由の1つですが、小さなアプリケーションでも、静的なソリューションより動的なソリューションの方が好ましい場面はよくあります。ただ注意しなければならないのは、リフレクションを多用しすぎると、ロードすべきクラスを判別するための負荷が大きくなってパフォーマンスが低下するおそれがあるという点です。
JAASとリフレクションを使用してセキュリティ層を作成する
セキュリティが必要とされる場面では、通常は、実行時にユーザーに対して識別情報とアクセス権限が関連付けられます。アプリケーションの開発者は、ユーザーの身元を資格情報に基づいて識別し、そのユーザーに対してどの操作を認めるかを判断する必要があります。JAASはこの問題を解決するために、プラグ可能なログインモジュールをサポートするセキュリティサービス層を提供しています。JAAS自身にはカスタムモジュールという概念がないので、JAASは実行時に状況に応じてログインモジュールを構築する必要があります。
このログインモジュールは、ログインページ、スマートカードリーダー、指紋リーダーなど、どんな入力デバイスからのユーザー入力でも処理できます。JAASは、認証と認可の層をアプリケーションから切り離して抽象化し、それぞれのアプリケーションに最適な方法でセキュリティを処理できるようにします。アプリケーション開発者がJAASに対してしなければならないのは、設定ファイル内で複数のログインモジュールの名前を指定することだけです(もちろん、アプリケーションのクラスパスのどこかにコンパイル済みクラスを配置しておく必要があります)。実行時にユーザーが認証を行おうとすると、必要なログインモジュールが動的にロードされてインスタンス化され、適切なメソッドが呼び出されます。
行番号を表示できるエディタ(Eclipseなど)を使用している場合は、そのエディタでSun提供のJavaソースコードを開いてみると、これから見ていくinvoke(String methodName)
メソッドが619行目から始まっているはずです。先ほど述べたとおり、JAASはこのメソッドの中で、設定ファイルに指定されているモジュールの一覧を反復処理します。
1. for (int i = 0; i < moduleStack.length; i++) { 2. try { 3. int mIndex = 0; 4. Method[ ] methods = null; 5. if (moduleStack[i].module != null) { 6. methods = moduleStack[i].module.getClass().getMethods(); 7. } else { // instantiate the LoginModule 8. Class c = Class.forName (moduleStack[i].entry.getLoginModuleName(), true, contextClassLoader);
4行目でMethod
型の配列を宣言し、これを6行目で初期化します。この配列に格納したメソッドを、後で状況に応じて呼び出します。8行目では、現在のmoduleStack
エントリの名前で表されるクラスを探し、それを汎用のClass
型オブジェクトとしてロードします。2つ目のブール型パラメータは、このクラスオブジェクトを初期化する必要があることを意味しています(初期化とインスタンス化はイコールでないことに注意してください。この時点では、静的変数とブロックが初期化されていますが、オブジェクトはまだ作成されていません)。3つ目のパラメータは、クラスをロードするために使用するjava.lang.Classloader
オブジェクトです。
次のコードは、Constructor
オブジェクトをインスタンス化する方法を示しています。LoginContext
クラスの先頭で定義したPARAMS
変数(1行目)と3行目で宣言したargs
変数は、どちらも同じ目的に使用されます。このオブジェクト配列は、いずれもオブジェクトを作成するための引数として使用されます。2行目ではConstructor
オブジェクトを作成するため、4行目ではLoginModule
の実際のインスタンスを作成するために使われます。今回の例では、どちらの場合も引数の配列が空ですが、必ずしもそうなるとは限りません。他のシナリオでは、リフレクションを使用しない場合のコンストラクタのシグネチャと同様に、コンストラクタ引数が配列内の引数に一致することもあります。
1. private static final Class[ ] PARAMS = { }; //Defined at the start of the class 2. Constructor constructor = c.getConstructor(PARAMS); 3. Object[] args = { }; // allow any object to be a LoginModule // as long as it conforms to the interface 4. moduleStack[i].module = constructor.newInstance(args); 5. methods = moduleStack[i].module.getClass().getMethods();
引数をClass
型またはObject
型の配列として扱うという考え方は、最初は奇妙に見えるかもしれません。しかし、どのクラスがインスタンス化されるかは実行時にならないとわからないので、このような「汎用化」の手法は不可欠です。Class
型またはObject
型を使用すれば、どんな型の引数でも効果的に表現することができます。Boolean.TYPE
やInteger.TYPE
などを使用すれば、プリミティブ型ですらこの方法で表現できます。4行目では、2行目で作成したConstructor
オブジェクトを使用してLoginModule
のインスタンスを作成し、5行目では、このクラスのメソッドをMethod
型の配列に格納しています。
次はモジュールを初期化します。INIT_METHOD
変数は、実行される実際のメソッド名を格納する静的変数です。したがって、Methods
配列の中に目的のメソッドが含まれているかどうかをgetName()
メソッドを呼び出して確認し(2行目)、含まれている場合はループを抜けて、引数を表すObject
型の配列を初期化し(3行目)、その後で目的のメソッドを呼び出します(4行目)。
// call the LoginModule's initialize method 1. for (mIndex = 0; mIndex < methods.length; mIndex++) { 2. if (methods[mIndex].getName().equals(INIT_METHOD)) break; } 3. Object[] initArgs = {subject, callbackHandler, state, moduleStack[i].entry.getOptions() }; // invoke the LoginModule initialize method 4. methods[mIndex].invoke(moduleStack[i].module, initArgs);
この時点で、実行中のコードが、JAASの設定ファイルで指定されているどのLoginModule
クラスの初期化処理でも呼び出せるようになります。簡単に言えば、これがリフレクションの威力です。操作対象のクラスやオブジェクトをコード内で指定しなくても、実行時環境が自動的に判別してくれるのです。
完全なソースコードリストを見ると、invoke()
メソッドがtry-catch
ブロック内に書かれていることに気付くでしょう。リフレクションでは、作成されて動的にメソッド呼び出しが行われるクラスとそのオブジェクトが既に存在していることを前提にしています。そのようなクラスやメソッドが存在しないことが判明した場合、または要求されたアクションを実行するためのセキュリティアクセス権がない場合は、適切な例外がスローされます。
JAASやStrutsのような堅牢なサービスフレームワークではリフレクションがよく使用されますが、大規模なアプリケーションやフレームワークをコーディングしなくても、リフレクションの長所を利用することができます。さまざまな種類のクラスやインターフェイスがあり、アプリケーションでどのサービスを提供すべきかが実行時にならないとわからない場合には、リフレクションを試してみる価値があります。