`
JAVA天地
  • 浏览: 674243 次
  • 性别: Icon_minigender_1
  • 来自: 太原
文章分类
社区版块
存档分类
最新评论

为你的应用程序添加动态Java代码

阅读更多

原来一直以为,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
}
}


执行这个应用程序,输入一些信息,你将看到控制台输出如下(你可以下载该例子并自行运行):
DynaCodeInit class sample.PostmanImpl
Enter a message: hello world
Postmanhello world
Enter a message: what a nice day!
Postmanwhat 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();
}
}


回到应用程序,输入更多信息,将会发生什么呢?是的,信息都到文本文件里去了。看控制台:
DynaCodeInit class sample.PostmanImpl
Enter a message: hello world
Postmanhello world
Enter a message: what a nice day!
Postmanwhat a nice day!
Enter a message: I wanna go to the text file.
DynaCodeInit class sample.PostmanImpl
Enter a message: me too!
Enter a message:


注意 DynaCode 初始类sample.PostmanImpl再次出现了,说明类PostmanImpl被再次编译和装载了。如果你检查文本文件msg.txt(在working目录下),你会看到如下内容:
PostmanI wanna go to the text file.
Postmanme too!


令人惊讶,对吧?我们可以在运行时更新Postman服务,这种改变对应用程序是完全透明的。(注意应用程序使用了相同的Postman实例来访问各种执行程序的版本。)

实现动态代码的四个步骤

让我来揭示在这种表象后面到底发生了什么。基本上,构成动态代码分四个步骤:
+部署选择的部分源代码并监控源代码文件的变化
+在运行时编译java代码
+在运行时装载/重装载java类
+将最新的类链接给它的调用者

部署选择的部分源代码并监控源代码文件的变化

在开始写一些动态的代码之前,我们必须回答的第一个问题是,“哪部分代码应该是动态的——整个应用程序还是仅仅某些类?”从技术上来讲,这部分是没什么约束的。你可以在运行时装载/重装载任何java类。但是在更多的情况下,只有部分代码需要这种灵活性。

Postman的例子示范了一种典型的选择动态类的模式。不管系统是如何构成的,最终,总有诸如服务,子系统,组件这样的组装块。这些组装块相对独立,通过预先定义的接口,互相暴露出了自己的功能。在接口后面,是自由变化的执行程序,只要它符合接口定义的限制。这是我们所需要的动态类的明确的性质。简单说来就是:选择实现类作为动态类。
文章的其余部分,我们做如下关于选择动态类的假设:

+被选择的实现了的java接口从而暴露出自己的功能。
+被选择的动态类的执行程序不保留任何关于其客户的状态信息(类似无状态的会话bean),这样动态类的实例可以互相替换。

请注意这种假设不是必要的。这样做只是为了让动态代码的实现稍简单一些,以便我们可以集中更多的精力到概念和机制上去。
利用心中选定好的动态类,配置源码是很简单的任务。图1指出了Postman例子的文件结构。

image
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
Figure 2. Collaboration of the proxy

当动态类重新装载时,我们仅需更新代理和动态类之间的链接即可,客户继续用同样的代理实例来访问被重新转载的类。图3显示了这种协作。

image
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
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
分享到:
评论

相关推荐

    java简单实例程序源代码

    "java简单实例程序源代码"这个压缩包包含了一系列章节相关的Java实例源代码,适合初学者和有经验的开发者用来加深对Java语言的理解。以下是这些章节可能涉及的重要知识点的详细解释: 1. **CH11**: 这个章节可能...

    java程序40例 源代码

    Java编程语言以其强大的跨平台能力和丰富的库资源深受程序员的喜爱,尤其在GUI(图形用户界面)开发方面,Swing组件提供...通过分析和运行这些代码,你将深入理解Java Swing的工作原理,为以后的项目开发打下坚实基础。

    java代码封装为应用程序exe4j

    Java代码封装为应用程序exe4j是一种技术,它允许开发者将Java程序转换为Windows平台下的可执行文件(.exe)。exe4j是一个强大的工具,专为此目的设计,它简化了Java应用在非Java环境中运行的过程,使得用户无需安装...

    java代码启动tomcat

    在 Java 中,使用 Tomcat 服务器来发布 Web 应用程序是一种常见的做法。下面,我们将详细介绍如何使用 Java 代码来启动 Tomcat 服务器,并实现远程控制 Tomcat。 标题: Java 代码启动 Tomcat 描述: Java 实现 ...

    Java源代码(添加、删除、查询)

    在这个项目中,MyEclipse被用作编写和运行Java代码的平台,可能还用于设计和测试数据库连接。 3. **数据操作:添加记录** 添加记录通常涉及创建一个`PreparedStatement`实例,设置参数(如果SQL包含占位符),然后...

    java记事本小程序源代码

    在这个项目中,开发者Koma不仅创建了一个基本的记事本应用程序,还包含了色盲检测程序的源码,这为学习者提供了额外的实践机会。下面将详细探讨这两个知识点。 首先,我们来关注Java记事本小程序。这个程序通常会...

    java日历小程序(源代码)

    Java日历小程序是一款基于Swing库开发的桌面应用程序,它为用户提供了一个直观的方式来查看和管理日期。Swing是Java Standard Edition (Java SE)的一部分,是一个用于构建用户界面的图形工具包,提供了丰富的组件和...

    Java常用源程序代码

    源程序代码是Java开发者用来创建应用程序、服务和游戏等的文本文件,通常以`.java`为扩展名。在这些代码中,我们可能会看到类(class)定义,它们是Java程序的基本构建块,包含了数据(字段,fields)和操作数据的...

    简单的JAVA日记本程序源代码

    这个开源项目是一个非常适合初学者学习的JAVA应用程序,它提供了一个简洁而可爱的日记本界面,让编程新手可以快速理解JAVA编程的基本概念和GUI设计。项目的核心亮点在于其清晰易懂的代码注释,使得学习过程更加直观...

    Java中的WHOIS应用程序及其源代码

    项目:Java中的WHOIS应用程序及其源代码 关于项目 WHOIS是一个用Java构建的非常基础的应用程序。这个简单应用程序的功能包括搜索域名详情和IP工具。该应用程序使用Java编程语言并借助Netbeans IDE构建。为了检查...

    Java时钟程序源代码

    Java时钟程序是一种基于Java编程语言实现的模拟时钟应用,它可以实时显示当前时间,并通常在GUI(图形用户界面)上以数字或指针形式展示。这个程序利用了Java的多线程特性,特别是`Thread`类或者`...

    java代码生成应用软件工具

    Java代码生成应用软件工具是一种将Java程序转换为可独立运行的应用程序的解决方案。这使得开发者无需依赖Java开发环境(如Eclipse或IntelliJ IDEA)就能执行他们编写的Java程序。这种工具通常会将Java字节码(.class...

    java写的购物车小程序java代码

    购物车系统是电商网站或应用程序的核心部分,它允许用户选择商品并管理他们的购买意向。让我们深入了解一下这个项目可能涉及的关键技术和知识点。 首先,`Java`是用于开发此程序的语言。Java是一种面向对象的、跨...

    一些比较有意思的Java小程序

    "一些比较有意思的Java小程序"这个标题暗示了我们将会探讨一系列趣味性强、易于理解的Java代码示例,这些示例通常适合Java初学者用来学习和实践编程概念。 在描述中提到的“不错的Java小程序”可能包括各种实用的小...

    Java版简单程序集合源代码.zip

    该项目包含六个不同的Java应用程序或Java程序:记事本、拼图、井字棋、单词计数器、测验和IP查找器。这是一个单帧程序,您可以选择并运行您想要运行的程序。要运行此项目,您必须有Eclipse IDE。 关于项目 这个不同...

    防止Java程序被反编译

    反编译工具能够解析Class文件中的方法和变量名,甚至重构出接近源代码的代码,这为保护Java代码的知识产权带来了挑战。以下是一些常见的防止Java程序被反编译的技术: 1. **隔离Java程序**:最直接的方式是不让用户...

    经典java小程序源代码合集

    这个“经典java小程序源代码合集”是为初学者提供的一份宝贵资源,它包含了一系列精心挑选的小程序示例,旨在帮助新手理解Java编程的基础概念,并逐步提升编程技能。 在学习Java时,首先会接触到的是基础语法,包括...

    Java图形界面程序代码

    Java图形界面程序设计是Java编程领域的一个重要组成部分,主要用于创建具有可视化组件的应用程序,...通过学习这些代码,开发者可以了解到如何在Java中创建自定义的图形界面,从而开发出更丰富、更具交互性的应用程序。

    Java语言-动态编译代码并热加载类

    在Java编程中,动态编译代码并热加载类是一项重要的技术,它允许程序在运行时修改或添加新的类,而无需重启应用。这种能力对于快速迭代开发、调试和性能优化非常有用。本主题将深入探讨Java中的动态编译与热加载机制...

    java 在线更新源码 自动更新工具

    Java在线更新源码及自动更新工具是为了解决软件应用程序在部署后需要频繁更新的问题而设计...选择合适的自动更新库或框架,结合良好的设计实践,可以确保Java应用程序始终保持最新状态,同时为用户提供无缝的更新体验。

Global site tag (gtag.js) - Google Analytics