Tambahkan kode Java dinamis ke aplikasi Anda

JavaServer Pages (JSP) adalah teknologi yang lebih fleksibel daripada servlet karena dapat merespons perubahan dinamis saat runtime. Dapatkah Anda membayangkan kelas Java umum yang memiliki kemampuan dinamis ini juga? Akan menarik jika Anda dapat memodifikasi implementasi layanan tanpa menerapkannya kembali dan memperbarui aplikasi Anda dengan cepat.

Artikel tersebut menjelaskan cara menulis kode Java dinamis. Ini membahas kompilasi kode sumber waktu proses, pemuatan ulang kelas, dan penggunaan pola desain Proxy untuk membuat modifikasi kelas dinamis menjadi transparan bagi pemanggilnya.

Contoh kode Java dinamis

Mari kita mulai dengan contoh kode Java dinamis yang mengilustrasikan apa arti kode dinamis yang sebenarnya dan juga menyediakan beberapa konteks untuk diskusi lebih lanjut. Temukan kode sumber lengkap contoh ini di Sumber.

Contohnya adalah aplikasi Java sederhana yang bergantung pada layanan bernama Postman. Layanan Postman digambarkan sebagai antarmuka Java dan berisi hanya satu metode, deliverMessage():

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

Implementasi sederhana dari layanan ini mencetak pesan ke konsol. Kelas implementasi adalah kode dinamis. Kelas ini,, PostmanImplhanyalah kelas Java normal, kecuali ia menerapkan dengan kode sumbernya alih-alih kode biner yang dikompilasi:

public class PostmanImpl implements Postman {

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

Aplikasi yang menggunakan layanan Postman muncul di bawah. Dalam main()metode ini, loop tak terbatas membaca pesan string dari baris perintah dan mengirimkannya melalui layanan 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 } }

Jalankan aplikasi, masukkan beberapa pesan, dan Anda akan melihat output di konsol seperti berikut (Anda dapat mengunduh contoh dan menjalankannya sendiri):

[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: 

Semuanya mudah kecuali untuk baris pertama, yang menunjukkan bahwa kelas PostmanImpldikompilasi dan dimuat.

Sekarang kita siap melihat sesuatu yang dinamis. Tanpa menghentikan aplikasi, mari kita ubah PostmanImplkode sumbernya. Implementasi baru mengirimkan semua pesan ke file teks, bukan konsol:

// 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(); } }

Geser kembali ke aplikasi dan masukkan lebih banyak pesan. Apa yang akan terjadi? Ya, pesan masuk ke file teks sekarang. Lihat konsol:

[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: 

Pemberitahuan [DynaCode] Init class sample.PostmanImplmuncul lagi, menunjukkan bahwa kelas PostmanImpldikompilasi ulang dan dimuat ulang. Jika Anda memeriksa file teks msg.txt (di bawah direktori kerja), Anda akan melihat yang berikut ini:

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

Luar biasa, bukan? Kami dapat memperbarui layanan Postman saat runtime, dan perubahan tersebut sepenuhnya transparan ke aplikasi. (Perhatikan bahwa aplikasi menggunakan instance Postman yang sama untuk mengakses kedua versi implementasi.)

Empat langkah menuju kode dinamis

Izinkan saya mengungkapkan apa yang terjadi di balik layar. Pada dasarnya, ada empat langkah untuk membuat kode Java menjadi dinamis:

  • Terapkan kode sumber yang dipilih dan pantau perubahan file
  • Kompilasi kode Java saat runtime
  • Muat / muat ulang kelas Java saat runtime
  • Tautkan kelas terbaru ke pemanggilnya

Terapkan kode sumber yang dipilih dan pantau perubahan file

Untuk mulai menulis beberapa kode dinamis, pertanyaan pertama yang harus kita jawab adalah, "Bagian mana dari kode yang harus dinamis — seluruh aplikasi atau hanya beberapa kelas?" Secara teknis, ada beberapa batasan. Anda dapat memuat / memuat ulang kelas Java apa pun saat runtime. Namun dalam banyak kasus, hanya sebagian dari kode yang memerlukan tingkat fleksibilitas ini.

Contoh Postman mendemonstrasikan pola khas dalam memilih kelas dinamis. Tidak peduli bagaimana suatu sistem disusun, pada akhirnya, akan ada blok bangunan seperti layanan, subsistem, dan komponen. Blok penyusun ini relatif independen, dan mereka mengekspos fungsionalitas satu sama lain melalui antarmuka yang telah ditentukan. Di balik antarmuka, itu adalah implementasi yang bebas berubah selama itu sesuai dengan kontrak yang ditentukan oleh antarmuka. Inilah kualitas yang kita butuhkan untuk kelas dinamis. Sederhananya: Pilih kelas implementasi untuk menjadi kelas dinamis .

Untuk sisa artikel ini, kami akan membuat asumsi berikut tentang kelas dinamis yang dipilih:

  • Kelas dinamis yang dipilih mengimplementasikan beberapa antarmuka Java untuk mengekspos fungsionalitas
  • Implementasi kelas dinamis yang dipilih tidak menyimpan informasi apa pun tentang kliennya (mirip dengan kacang sesi tanpa status), sehingga instance dari kelas dinamis dapat saling menggantikan.

Harap dicatat bahwa asumsi ini bukan prasyarat. Mereka ada hanya untuk membuat realisasi kode dinamis sedikit lebih mudah sehingga kami dapat lebih fokus pada ide dan mekanisme.

Dengan mempertimbangkan kelas dinamis yang dipilih, menerapkan kode sumber adalah tugas yang mudah. Gambar 1 menunjukkan struktur file dari contoh Postman.

Kita tahu "src" adalah sumber dan "bin" adalah biner. Satu hal yang perlu diperhatikan adalah direktori dynacode, yang menyimpan file sumber kelas dinamis. Di sini, di contoh, hanya ada satu file—PostmanImpl.java. Direktori bin dan dynacode diperlukan untuk menjalankan aplikasi, sedangkan src tidak diperlukan untuk penerapan.

Mendeteksi perubahan file dapat dilakukan dengan membandingkan stempel waktu modifikasi dan ukuran file. Sebagai contoh, pemeriksaan PostmanImpl.java dilakukan setiap kali metode dipanggil pada Postmanantarmuka. Alternatifnya, Anda dapat memunculkan untaian daemon di latar belakang untuk memeriksa perubahan file secara teratur. Itu dapat menghasilkan kinerja yang lebih baik untuk aplikasi skala besar.

Kompilasi kode Java saat runtime

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); 

Yang dikembalikan proxyadalah objek dari kelas anonim yang berbagi pemuat kelas yang sama dengan Postmanantarmuka ( newProxyInstance()parameter pertama metode) dan mengimplementasikan Postmanantarmuka (parameter kedua). Sebuah metode doa pada proxycontoh yang dikirim ke handler's invoke()metode (parameter ketiga). Dan handlerimplementasinya mungkin terlihat seperti berikut: