Добавете динамичен Java код към вашето приложение

JavaServer Pages (JSP) е по-гъвкава технология от сървлетите, защото може да реагира на динамични промени по време на изпълнение. Можете ли да си представите общ Java клас, който също има тази динамична способност? Би било интересно, ако можете да промените внедряването на услуга, без да я преразпределяте и да актуализирате приложението си в движение.

Статията обяснява как се пише динамичен Java код. Той обсъжда компилация на изходния код по време на изпълнение, презареждане на клас и използването на шаблона за проектиране на прокси, за да направи промени в динамичен клас прозрачен за повикващия.

Пример за динамичен Java код

Нека да започнем с пример за динамичен Java код, който илюстрира какво означава истински динамичен код и също така предоставя известен контекст за допълнителни дискусии. Моля, намерете пълния изходен код на този пример в Ресурси.

Примерът е просто Java приложение, което зависи от услуга, наречена Postman. Услугата Postman е описана като Java интерфейс и съдържа само един метод 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 за достъп до двете версии на реализациите.)

Четири стъпки към динамичен код

Позволете ми да разкрия какво се случва зад кулисите. По принцип има четири стъпки, за да направите Java кода динамичен:

  • Внедрете избрания изходен код и наблюдавайте промените във файловете
  • Компилирайте Java код по време на изпълнение
  • Заредете / презаредете Java клас по време на изпълнение
  • Свържете актуалния клас с повикващия

Внедрете избрания изходен код и наблюдавайте промените във файловете

За да започнем да пишем някакъв динамичен код, първият въпрос, на който трябва да отговорим, е: "Коя част от кода трябва да бъде динамична - цялото приложение или само някои от класовете?" Технически има малко ограничения. Можете да заредите / презаредите всеки клас Java по време на изпълнение. Но в повечето случаи само част от кода се нуждае от това ниво на гъвкавост.

Примерът на пощальона демонстрира типичен модел за избор на динамични класове. Без значение как е съставена система, в крайна сметка ще има градивни елементи като услуги, подсистеми и компоненти. Тези градивни елементи са относително независими и те излагат функционалности един на друг чрез предварително дефинирани интерфейси. Зад интерфейса реализацията е свободна да се променя, стига да отговаря на договора, дефиниран от интерфейса. Точно това е качеството, от което се нуждаем за динамични класове. Така просто казано: Изберете класа на внедряване, който да бъде динамичният клас .

В останалата част на статията ще направим следните предположения относно избраните динамични класове:

  • Избраният динамичен клас реализира някакъв Java интерфейс за излагане на функционалност
  • Изпълнението на избрания динамичен клас не съдържа никаква информация за състоянието на своя клиент (подобно на сеанса без гражданство), така че екземплярите на динамичния клас могат да се заменят взаимно

Моля, имайте предвид, че тези предположения не са предпоставки. Те съществуват само за да направят реализацията на динамичен код малко по-лесна, за да можем да се съсредоточим повече върху идеите и механизмите.

Имайки предвид избраните динамични класове, внедряването на изходния код е лесна задача. Фигура 1 показва файловата структура на примера на пощальона.

Знаем, че "src" е източник, а "bin" е двоичен. Едно нещо, което си струва да се отбележи, е директорията dynacode, която съдържа изходните файлове на динамични класове. Тук в примера има само един файл - PostmanImpl.java. Директориите bin и dynacode са необходими за стартиране на приложението, докато src не е необходимо за разполагане.

Откриването на промени във файла може да се постигне чрез сравняване на времеви марки и размери на модификациите. За нашия пример се извършва проверка на PostmanImpl.java при всяко извикване на метод в Postmanинтерфейса. Като алтернатива можете да създадете демонова нишка във фонов режим, за да проверявате редовно промените във файла. Това може да доведе до по-добра производителност за мащабни приложения.

Компилирайте 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е обект на анонимен клас, който има същия ClassLoader с Postmanинтерфейс (на newProxyInstance()първия параметър метода) и изпълнява Postmanинтерфейс (втори параметър). Извикване на метод в proxyекземпляра се изпраща до метода на handler' invoke()(третият параметър). И handlerвнедряването може да изглежда по следния начин: