原来一直以为,JAVA程序都必须在执行前被编译,而不是在运行时可以动态加载,看了这篇文章后,才知道这么一回事,看来JAVA里面的东西没有熟悉还有的是,不要以人好像什么都懂都了。
作者:Amydeng;… 文章来源:matrix
摘要
你曾经希望你的java代码能够像JSP一样是动态的吗?它可以在运行时被修改和重新编译,同时你的应用程序自动更新。本文阐述了如何让你的代码动态化。同样的,你的一些源代码将会被直接部署,而不是编译好的字节码。这些源代码的任何改变都将引起这些源代码的再编译和类的重新装载。然后你的应用程序就会运行在新的类上,用户将立即看到这种改变。本文不仅讲述了运行时源码编辑和类装载,而且还提出一个将动态代码与其调用者分离的设计方案。调用者保存对动态代码的一个静态引用,而不管动态代码运行时如何再次装载,调用者总能访问最新的类且不用更新引用。这样,动态代码改变对客户是透明的。
JSP是一种比servlets更有弹性的技术,因为它可以响应运行时的动态改变。你可以想象一个普通的java类也有这种动态的能力吗?如果你能修改服务的执行而不用重新部署和更新应用程序,将会是很有趣的。
文章说明了如何编写动态的代码。它讨论运行时源码编辑,类的再装载,和让动态类的修改对它的调用者透明的代理设计模式。
版权声明:任何获得Matrix授权的网站,转载时请务必保留以下作者信息和链接
作者:Li Yang;Amydeng
原文:http://www.javaworld.com/javaworld/jw-06-2006/jw-0612-dynamic.html
Matrix:http://www.matrix.org.cn/resource/article/44/44615_Java+Dynamic+Code.html
关键字:Java;动态代码
一个动态java代码的例子
让我们以一个动态java代码的例子开始来阐释真正的动态代码意味着什么,为下文的讨论做铺垫。请在源码中找到这个例子完整的源代码。
这个例子是一个简单的依靠名叫Postman的服务的java应用程序。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的源码。新的执行程序将会把所有的信息输出到一个文本文件,而不是控制台。
// 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 初始类sample.PostmanImpl再次出现了,说明类PostmanImpl被再次编译和装载了。如果你检查文本文件msg.txt(在working目录下),你会看到如下内容:
Postman I wanna go to the text file.
Postman me too!
令人惊讶,对吧?我们可以在运行时更新Postman服务,这种改变对应用程序是完全透明的。(注意应用程序使用了相同的Postman实例来访问各种执行程序的版本。)
实现动态代码的四个步骤
让我来揭示在这种表象后面到底发生了什么。基本上,构成动态代码分四个步骤:
+部署选择的部分源代码并监控源代码文件的变化
+在运行时编译java代码
+在运行时装载/重装载java类
+将最新的类链接给它的调用者
部署选择的部分源代码并监控源代码文件的变化
在开始写一些动态的代码之前,我们必须回答的第一个问题是,“哪部分代码应该是动态的——整个应用程序还是仅仅某些类?”从技术上来讲,这部分是没什么约束的。你可以在运行时装载/重装载任何java类。但是在更多的情况下,只有部分代码需要这种灵活性。
Postman的例子示范了一种典型的选择动态类的模式。不管系统是如何构成的,最终,总有诸如服务,子系统,组件这样的组装块。这些组装块相对独立,通过预先定义的接口,互相暴露出了自己的功能。在接口后面,是自由变化的执行程序,只要它符合接口定义的限制。这是我们所需要的动态类的明确的性质。简单说来就是:选择实现类作为动态类。
文章的其余部分,我们做如下关于选择动态类的假设:
+被选择的实现了的java接口从而暴露出自己的功能。
+被选择的动态类的执行程序不保留任何关于其客户的状态信息(类似无状态的会话bean),这样动态类的实例可以互相替换。
请注意这种假设不是必要的。这样做只是为了让动态代码的实现稍简单一些,以便我们可以集中更多的精力到概念和机制上去。
利用心中选定好的动态类,配置源码是很简单的任务。图1指出了Postman例子的文件结构。
![image](http://www.j2medev.com/Article/UploadFiles/200609/20060904191410728.gif)
Figure 1. The file structure of the Postman example
我们知道“src”是源码,“bin”是二进制码。一件值得注意的事情是动态代码目录,它包括了动态类的源文件。这儿的例子中,只有一个文件——PostmanImpl.java。bin和动态代码的目录是需要用来运行应用程序的,src在配置时是不需要的。
检查文件改变可以通过比较修改时间和文件大小实现。我们的例子中,对PostmanImpl.java的检查是每次调用一个基于Postman接口的方法。要不,你也可以产生一个后台线程来有规则地检查文件改变。这可能会为大规模的应用程序带来更好的性能。
运行时编译java代码
探测到源码被改变后,我们要面临编译的问题。将实际的编译工作委派给某个已经存在的java编译器的话,运行时编辑就不成问题了。许多java编译器都是可用的,但在本文中,我们使用包含在Sun的 java SE平台的javac编译器(java SE是Sun为J2SE的新命名)。
最简单的方式,你可以仅用一条语句编译java文件,这需要系统在类路径上包含javac编译器的tools.jar(你可以在<JDK_HOME>/lib/下找到tools.jar):
int errorCode = com.sun.tools.javac.Main.compile(new String[] {
"-classpath", "bin",
"-d", "/temp/dynacode_classes",
"dynacode/sample/PostmanImpl.java" });
类com.sun.tools.javac.Main是javac编译器的程序接口。它为编译java源文件提供静态的方法。如上所述的执行,与运行从命令行运行javac有相同的效果。它利用指定的类路径bin编译了源文件dynacode/sample/PostmanImpl.java,并输出其类文件到目的文件目录/temp/dynacode_classes。如果编译出错, 则会返回一个整型值。0意味着成功;其他的数字说明某些地方编译出错了。
com.sun.tools.javac.Main类还提供了另外一个compile()方法,接受附加的PrintWriter参数,如下代码所示。如果编译失败的话,详细的出错信息将会被输出到PrintWriter。
// Defined in com.sun.tools.javac.Main
public static int compile(String[] args);
public static int compile(String[] args, PrintWriter out);
我想大部分的开发者应该都熟悉javac编译器,所以这里我就讲这么多了。要看更多的如何使用编译器的信息,请参考Resources。
运行时装载/再装载java类
编译好的类在起作用之前必须被装载进来。Java在类装载方面是灵活的。它定义了一个全面的类装载机制,提供了几个类装载器的实现。(关于类装载的更多信息,看Resources。)
下面的例子代码展示了如何装载和再装载类。基本的想法是利用我们自己的URLClassLoader装载动态的类。不论何时源码改变和重新编译了,我们抛弃掉旧的类(因为稍后会有垃圾收集)并创造一个新的URLClassLoader来再次装载该类。
// 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.
*/
创造你自己的类装载器时注意parentLoader。基本上,父类装载器必须提供所有的子类装载器所需要的(依赖的)相关内容。在例子代码中,动态类PostmanImpl依赖接口Postman;这就是我们利用Postman的类装载器作父类装载器的原因。
我们离完成动态代码还差一步。回想早先介绍的例子。那里,动态类再装载对它的调用者是透明的。但在上述例子代码中,当代码改变时,我们仍必须改变从postman1到postman2的服务实例。第四步即最后一步将移除这种手动改变的需要。
连接最新的类到它的调用者
对于一个静态引用,我们如何访问最新的动态类呢?显然,一个对动态类的对象直接(通常都是这样)的引用是不会成功的。我们需要介于客户和动态类之间的东西——代理。(关于代理模式请参阅著名的《设计模式》一书。)
这里,代理是一个作为动态类的访问接口的功能类。客户不能直接调用动态类;要用代理代替。然后代理指向后端动态类的调用。图2显示了这种协作。
![image](http://www.j2medev.com/Article/UploadFiles/200609/20060904191412862.jpg)
Figure 2. Collaboration of the proxy
当动态类重新装载时,我们仅需更新代理和动态类之间的链接即可,客户继续用同样的代理实例来访问被重新转载的类。图3显示了这种协作。
![image](http://www.j2medev.com/Article/UploadFiles/200609/20060904191412298.jpg)
Figure 3. Collaboration of the proxy with reloaded dynamic class
这样,动态类的改变对它的调用者就是透明的了。
Java反射API包括了一个创建代理的方便的工具。类java.lang.reflect.Proxy提供了静态的方法,让你为任何java接口创建代理实例。
下面的例子为接口Postman创建了一个代理。(如果你对java.lang.reflect.Proxy不熟悉,请在继续往下看之前看看javadoc。)
InvocationHandler handler = new DynaCodeInvocationHandler(...);
Postman proxy = (Postman) Proxy.newProxyInstance(
Postman.class.getClassLoader(),
new Class[] { Postman.class },
handler);
返回的proxy是匿名类的一个对象,它与Postman接口共享了同一个类装载器(newProxyInstance()方法的第一个参数)并执行Postman接口(第二个参数)。Proxy实例的方法调用被分配给handler的invoke()方法(第三个参数)。Handler的执行程序可能看起来如下:
public class DynaCodeInvocationHandler implements InvocationHandler {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// Get an instance of the up-to-date dynamic class
Object dynacode = getUpToDateInstance();
// Forward the invocation
return method.invoke(dynacode, args);
}
}
invoke()方法获得最新的动态类实例并且调用它。如果动态类的源码文件被修改了的话,这可能导致源代码的编辑和类的重新装载。
现在我们有了完整的Postman服务的动态代码。客户创建一个服务的代理并通过该代理调用deliverMessage()方法。代理的每次调用被分配到DynaCodeInvocationHandler类的invoke()方法。在那个方法里,将会得到可能需要源码编译和类的重新装载的最新的服务实现,然后,调用服务实现进行实际的处理。
把它们放到一起
我们讲述了动态java代码需要的所有窍门。是时候把它们放到一起来建立一些可重用的东西了。
我们可以通过建立一个工具, 从而压缩以上四个步骤,使得采用动态代码变得更简单。 Postman例子会依赖于这个名叫DynaCode的工具。记得PostmanApp应用程序和它的省略方法getPostman()吧?是时候展示它了:
public class PostmanApp {
public static void main(String[] args) throws Exception {
// ......
}
private static Postman getPostman() {
DynaCode dynacode = new DynaCode();
dynacode.addSourceDir(new File("dynacode"));
return (Postman) dynacode.newProxyInstance(Postman.class,
"sample.PostmanImpl");
}
}
看看getPostman()方法是如何创建一个动态的Postman服务、创建一个DynaCode实例、指定一个源目录、返回一个某些动态执行程序的代理的。为了使用你自己的动态java代码,你只是需要写三行代码。其它事情都由内部的DynaCode负责。自己试试吧(DynaCode的源码包含在例子中)。
我不会更进一步的讲解DynaCode的细节了,但是作为我们所讲的技术回顾,看看图4的序列图,以理解DynaCode是如何高水平工作的。
![image](http://www.j2medev.com/Article/UploadFiles/200609/20060904191414386.jpg)
Figure 4. Sequence diagram of DynaCode. Click on thumbnail to view full-sized image.
结论
在这篇文章中,我介绍了动态java代码的思想和实现它的步骤。我包含了诸如运行时源码编辑,类的重装载和代理设计模式等主题。虽然这些话题一个都不新鲜,但把它们放在一起,我们为普通的java类创建了一个有趣的动态的未来,它们能够像JSP一样在运行时被修改和更新。
在Resources中提供了一个例子作为示范。它包括一个可以重用的叫DynaCode的实用程序,可以让你的动态代码更容易编写。
我想以讨论动态代码的价值和应用作结:动态代码可以快速响安全变化的要求。它可以被用来实现真实的动态服务和时时改变的业务规则,代替工作流的任务节点中使用的嵌入式脚本。动态代码也减轻了应用程序维护和大大减少了由应用软件重新部署引起的故障。
关于作者
Li Yang在2004年4月加入了IBM,获得Rational Unified Process认证 (译者注:RUP,统一软件过程。是一个完善的软件开发过程框架,具有若干种即装即用的实例,将项目管理、商业建模、分析与设计等,统一到一致的、贯穿整个开发周期的处理过程。),是一个需求管理,和面向对象分析与设计的IBM Rational顾问。他在服务器端技术,web架构,java EE项目和java SE系统库开发方面有丰富的经验。
资源
+Matrix: http://www.matrix.org.cn
+源代码: http://www.javaworld.com/javaworld/jw-06-2006/dynamic/jw-0612-dynamic.zip
+Javac compiler documentation: http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/javac.html