アプリケーションに動的Javaコードを追加する

JavaServer Pages(JSP)は、実行時に動的な変更に応答できるため、サーブレットよりも柔軟なテクノロジです。この動的機能も備えた一般的なJavaクラスを想像できますか?サービスを再デプロイせずにサービスの実装を変更し、その場でアプリケーションを更新できれば興味深いでしょう。

この記事では、動的Javaコードの記述方法について説明しています。ランタイムソースコードのコンパイル、クラスの再読み込み、およびプロキシデザインパターンを使用して、呼び出し元に対して透過的な動的クラスに変更を加える方法について説明します。

動的Javaコードの例

真の動的コードの意味を示し、さらに議論するためのコンテキストを提供する動的Javaコードの例から始めましょう。この例の完全なソースコードは、「リソース」にあります。

この例は、Postmanと呼ばれるサービスに依存する単純なJavaアプリケーションです。PostmanサービスはJavaインターフェースとして記述されており、メソッドは1つだけ含まれていますdeliverMessage()

public interface Postman { void deliverMessage(String msg); } 

このサービスの簡単な実装は、メッセージをコンソールに出力します。実装クラスは動的コードです。このクラスはPostmanImpl、コンパイルされたバイナリコードの代わりにソースコードを使用してデプロイされることを除いて、通常のJavaクラスです。

public class PostmanImpl implements Postman {

private PrintStream output; public PostmanImpl() { output = System.out; } public void deliverMessage(String msg) { output.println("[Postman] " + msg); output.flush(); } }

Postmanサービスを使用するアプリケーションを以下に示します。このmain()メソッドでは、無限ループがコマンドラインから文字列メッセージを読み取り、Postmanサービスを介してそれらを配信します。

public class PostmanApp {

public static void main(String[] args) throws Exception { BufferedReader sysin = new BufferedReader(new InputStreamReader(System.in));

// Obtain a Postman instance Postman postman = getPostman();

while (true) { System.out.print("Enter a message: "); String msg = sysin.readLine(); postman.deliverMessage(msg); } }

private static Postman getPostman() { // Omit for now, will come back later } }

アプリケーションを実行し、いくつかのメッセージを入力すると、次のような出力がコンソールに表示されます(例をダウンロードして自分で実行できます)。

[DynaCode] Init class sample.PostmanImpl Enter a message: hello world [Postman] hello world Enter a message: what a nice day! [Postman] what a nice day! Enter a message: 

クラスPostmanImplがコンパイルされてロードされることを示す最初の行を除いて、すべてが簡単です。

これで、動的なものを見る準備ができました。アプリケーションを停止せずに、PostmanImplのソースコードを変更しましょう。新しい実装では、すべてのメッセージがコンソールではなくテキストファイルに配信されます。

// MODIFIED VERSION public class PostmanImpl implements Postman {

private PrintStream output; // Start of modification public PostmanImpl() throws IOException { output = new PrintStream(new FileOutputStream("msg.txt")); } // End of modification

public void deliverMessage(String msg) { output.println("[Postman] " + msg);

output.flush(); } }

アプリケーションに戻り、さらにメッセージを入力します。何が起こるか?はい、メッセージはテキストファイルに送られます。コンソールを見てください:

[DynaCode] Init class sample.PostmanImpl Enter a message: hello world [Postman] hello world Enter a message: what a nice day! [Postman] what a nice day! Enter a message: I wanna go to the text file. [DynaCode] Init class sample.PostmanImpl Enter a message: me too! Enter a message: 

[DynaCode] Init class sample.PostmanImplクラスPostmanImplが再コンパイルされて再ロードされたことを示す通知が再び表示されます。テキストファイルmsg.txt(作業ディレクトリの下)を確認すると、次のように表示されます。

[Postman] I wanna go to the text file. [Postman] me too! 

すごいですよね?実行時にPostmanサービスを更新することができ、変更はアプリケーションに対して完全に透過的です。(アプリケーションが同じPostmanインスタンスを使用して両方のバージョンの実装にアクセスしていることに注意してください。)

動的コードに向けた4つのステップ

舞台裏で何が起こっているのかを明らかにしましょう。基本的に、Javaコードを動的にするための4つのステップがあります。

  • 選択したソースコードをデプロイし、ファイルの変更を監視します
  • 実行時にJavaコードをコンパイルする
  • 実行時にJavaクラスをロード/リロードする
  • 最新のクラスをその呼び出し元にリンクします

選択したソースコードをデプロイし、ファイルの変更を監視します

動的コードの記述を開始するには、最初に答える必要がある質問は、「コードのどの部分を動的にする必要があるか、つまりアプリケーション全体か、クラスの一部だけか」です。技術的には、いくつかの制限があります。実行時に任意のJavaクラスをロード/リロードできます。ただし、ほとんどの場合、このレベルの柔軟性が必要なのはコードの一部だけです。

Postmanの例は、動的クラスを選択する際の典型的なパターンを示しています。システムがどのように構成されていても、最終的には、サービス、サブシステム、コンポーネントなどの構成要素が存在します。これらのビルディングブロックは比較的独立しており、事前定義されたインターフェイスを介して機能を相互に公開します。インターフェイスの背後にあるのは、インターフェイスで定義されたコントラクトに準拠している限り、自由に変更できる実装です。これはまさに動的クラスに必要な品質です。つまり、簡単に言えば、動的クラスとなる実装クラスを選択します

この記事の残りの部分では、選択した動的クラスについて次の仮定を行います。

  • 選択した動的クラスは、機能を公開するためにいくつかのJavaインターフェースを実装します
  • 選択した動的クラスの実装は、クライアントに関するステートフル情報を保持しないため(ステートレスセッションBeanと同様)、動的クラスのインスタンスを相互に置き換えることができます。

これらの仮定は前提条件ではないことに注意してください。これらは、動的コードの実現を少し簡単にするためだけに存在するため、アイデアやメカニズムにさらに集中できます。

選択した動的クラスを念頭に置いて、ソースコードのデプロイは簡単な作業です。図1は、Postmanの例のファイル構造を示しています。

「src」がソースで「bin」がバイナリであることがわかっています。注目に値するのは、動的クラスのソースファイルを保持するdynacodeディレクトリです。この例では、PostmanImpl.javaという1つのファイルしかありません。アプリケーションを実行するにはbinディレクトリとdynacodeディレクトリが必要ですが、デプロイにはsrcは必要ありません。

ファイルの変更を検出するには、変更のタイムスタンプとファイルサイズを比較します。この例では、メソッドがPostmanインターフェースで呼び出されるたびに、PostmanImpl.javaへのチェックが実行されます。または、バックグラウンドでデーモンスレッドを生成して、ファイルの変更を定期的にチェックすることもできます。これにより、大規模なアプリケーションのパフォーマンスが向上する可能性があります。

実行時にJavaコードをコンパイルする

After a source code change is detected, we come to the compilation issue. By delegating the real job to an existing Java compiler, runtime compilation can be a piece of cake. Many Java compilers are available for use, but in this article, we use the Javac compiler included in Sun's Java Platform, Standard Edition (Java SE is Sun's new name for J2SE).

At the minimum, you can compile a Java file with just one statement, providing that the tools.jar, which contains the Javac compiler, is on the classpath (you can find the tools.jar under /lib/):

 int errorCode = com.sun.tools.javac.Main.compile(new String[] { "-classpath", "bin", "-d", "/temp/dynacode_classes", "dynacode/sample/PostmanImpl.java" }); 

The class com.sun.tools.javac.Main is the programming interface of the Javac compiler. It provides static methods to compile Java source files. Executing the above statement has the same effect as running javac from the command line with the same arguments. It compiles the source file dynacode/sample/PostmanImpl.java using the specified classpath bin and outputs its class file to the destination directory /temp/dynacode_classes. An integer returns as the error code. Zero means success; any other number indicates something has gone wrong.

The com.sun.tools.javac.Main class also provides another compile() method that accepts an additional PrintWriter parameter, as shown in the code below. Detailed error messages will be written to the PrintWriter if compilation fails.

 // Defined in com.sun.tools.javac.Main public static int compile(String[] args); public static int compile(String[] args, PrintWriter out); 

I assume most developers are familiar with the Javac compiler, so I'll stop here. For more information about how to use the compiler, please refer to Resources.

Load/reload Java class at runtime

The compiled class must be loaded before it takes effect. Java is flexible about class loading. It defines a comprehensive class-loading mechanism and provides several implementations of classloaders. (For more information on class loading, see Resources.)

The sample code below shows how to load and reload a class. The basic idea is to load the dynamic class using our own URLClassLoader. Whenever the source file is changed and recompiled, we discard the old class (for garbage collection later) and create a new URLClassLoader to load the class again.

// The dir contains the compiled classes. File classesDir = new File("/temp/dynacode_classes/");

// The parent classloader ClassLoader parentLoader = Postman.class.getClassLoader();

// Load class "sample.PostmanImpl" with our own classloader. URLClassLoader loader1 = new URLClassLoader( new URL[] { classesDir.toURL() }, parentLoader); Class cls1 = loader1.loadClass("sample.PostmanImpl"); Postman postman1 = (Postman) cls1.newInstance();

/* * Invoke on postman1 ... * Then PostmanImpl.java is modified and recompiled. */

// Reload class "sample.PostmanImpl" with a new classloader. URLClassLoader loader2 = new URLClassLoader( new URL[] { classesDir.toURL() }, parentLoader); Class cls2 = loader2.loadClass("sample.PostmanImpl"); Postman postman2 = (Postman) cls2.newInstance();

/* * Work with postman2 from now on ... * Don't worry about loader1, cls1, and postman1 * they will be garbage collected automatically. */

Pay attention to the parentLoader when creating your own classloader. Basically, the rule is that the parent classloader must provide all the dependencies the child classloader requires. So in the sample code, the dynamic class PostmanImpl depends on the interface Postman; that's why we use Postman's classloader as the parent classloader.

We are still one step away to completing the dynamic code. Recall the example introduced earlier. There, dynamic class reload is transparent to its caller. But in the above sample code, we still have to change the service instance from postman1 to postman2 when the code changes. The fourth and final step will remove the need for this manual change.

Link the up-to-date class to its caller

How do you access the up-to-date dynamic class with a static reference? Apparently, a direct (normal) reference to a dynamic class's object will not do the trick. We need something between the client and the dynamic class—a proxy. (See the famous book Design Patterns for more on the Proxy pattern.)

Here, a proxy is a class functioning as a dynamic class's access interface. A client does not invoke the dynamic class directly; the proxy does instead. The proxy then forwards the invocations to the backend dynamic class. Figure 2 shows the collaboration.

When the dynamic class reloads, we just need to update the link between the proxy and the dynamic class, and the client continues to use the same proxy instance to access the reloaded class. Figure 3 shows the collaboration.

In this way, changes to the dynamic class become transparent to its caller.

The Java reflection API includes a handy utility for creating proxies. The class java.lang.reflect.Proxy provides static methods that let you create proxy instances for any Java interface.

The sample code below creates a proxy for the interface Postman. (If you aren't familiar with java.lang.reflect.Proxy, please take a look at the Javadoc before continuing.)

 InvocationHandler handler = new DynaCodeInvocationHandler(...); Postman proxy = (Postman) Proxy.newProxyInstance( Postman.class.getClassLoader(), new Class[] { Postman.class }, handler); 

返されるのproxyは、Postmanインターフェイス(newProxyInstance()メソッドの最初のパラメーター)と同じクラスローダーを共有し、インターフェイス(Postman2番目のパラメーター)を実装する匿名クラスのオブジェクトです。proxyインスタンスでのメソッド呼び出しは、handlerinvoke()メソッド(3番目のパラメーター)にディスパッチされます。そして、handlerの実装は次のようになります。