本文介绍了一个利用类库加载器ClassLoader 实现在运行时刻更新部分功能模块的Java程序,并将其与C/C++中实现同样功能的动态链接库方案进行了简单比较。
介绍
在嵌入式系统的设计中,经常涉及到在运行时刻更新部分功能模块的设计。例如一个用于数据采集与处理的设备,包括数据采集,数据发送,命令接收等功能模块,有可能被要求在继续进行数据采集的同时采用新的数据格式向一个新的数据处理系统发送数据。在这种情况下,就必须在运行时刻动态的更新数据发送的功能模块。
在C/C++中,这样的功能可以很轻易的利用动态链接库来实现。Win32 API函数LoadLibrary和FreeLibrary提供了在运行时刻加载新的功能模块和释放内存空间的功能。需要被更新的功能模块被封装在动态连接库里,主程序利用LoadLibrary 函数装载该动态链接库,然后调用其中的功能模块。需要更新某功能模块的时候,首先终止运行该功能模块,利用FreeLibrary 函数卸载现有的动态链接库,通过网络或者是其他通讯端口将新的动态链接库文件发送到指定目录下,然后利用再次利用LoadLibrary 函数装载新的动态链接库并调用其中的新功能模块。(假如需要进一步了解动态链接库程序设计的内容,请参阅参考文献1中的相关部分。)
在Java中,有一个被称为类库加载器的抽象类ClassLoader 能够用来实现类似于LoadLibrary的功能。本文下面的部分着重介绍ClassLoader的一般结构以及利用ClassLoader实现在运行时刻更新部分功能模块的方法。
类库加载器
类库加载器ClassLoader 是一个负责加载类库的抽象类。它接受一个类库的名称并试图定位和生成包含有改类库定义的数据。通常的实现方法是将该类库的名称转化成一个文件名,然后从文件系统中找到该文件并读取其中的内容。(关于类库加载器的定义,请参阅参考文献2。)
所有的Java虚拟机都包括一个内置的类库加载器。这个内置的类库加载器被称为主类库加载器。主类库加载器的非凡之处是它只能够加载在设计时刻已知的类,因此虚拟机假定由主类库加载器所加载的类都是可信任的,可以不经过安全认证而直接运行。当应用程序需要加载在设计时刻未知的类库时,就必须使用用户自定义的类库加载器。
一个用户自定义的类库加载器是抽象类java.lang.ClassLoader 的派生类,其中唯一必须实现的抽象方法是loadClass()。通常来说,loadClass()方法需要实现如下操作:
确认类库名称
检查请求加载的类库是否已经被加载
检查请求加载的类库是否是系统类库
尝试从类库加载器的存储区获取所清求的类库
在虚拟机中定义所请求的类库
解析所请求的类库
返回所请求的类库
一个用户自定义类库加载器几乎可以从任何存储设备上加载类库。装载本地硬盘上的类库当然不在话下,通过超级连接装载网络上的类库也很轻易。由于类库加载器的存在,Java虚拟机并不需要事先知道关于将要运行的类库的任何细节。由于类库加载器的功能是如此的强大,出于安全考虑某些Java类库如applets 等不答应启用自定义的类库加载器。
参考文献3 给出了关于用户自定义类库的更具体描述,同时提供了一个示例程序SimpleClassLoader。在本文下面的例子中,使用该文献中的SimpleClassLoader作为用户自定义类库加载器。
在运行时刻更新功能模块
在动态链接库技术中,LoadLibrary函数负责加载功能模块,FreeLibrary函数负责卸载功能模块。新的功能模块与旧的功能模块同名,新的动态链接库文件也与旧的动态链接库文件同名。当需要更新某个功能模块的时候,使用新的动态链接库文件替换旧的动态链接库文件,被旧的功能模块所占用的内存空间也同时被释放。
但是Java并不提供一个类似于类库卸载器(ClassUnloader) 的功能,能够把已经装载的功能模块从内存里面清除掉。目前的虚拟机,大都使用了及时编译(JIT) 技术,也就是说一个功能模块只有在它第一次被使用的时候才被编译。经过编译的可执行代码被放到内存里面,用一个HashTable 做索引,其要害字为与之相对应的类库名。虚拟机需要用到某个功能模块的时候,它先到这个HashTable 里面查找相应的要害字。假如该功能模块已经存在,虚拟机直接从内存里调用经过编译的可执行代码,反之则调用类库加载器装载新的功能模块并进行编译。由于没有模块卸载功能,在运行时刻已经被装载的功能模块是一直存在的。当某功能模块实际上已经被更新(即.class文件被替换为同名的新文件)并需要被重新加载的时候,虚拟机并不会试图装载新的功能模块而直接调用旧的功能模块。假如试图利用用户自定义的类库加载器强行装载新的功能模块,则会因为新的功能模块与旧的功能模块同名而导致虚拟机抛出链接错误: Linkage Error: duplicate class definition。
脑子快的朋友也许已经想出了以下的方法:
SimpleClassLoader scl = new SimpleClassLoader();
Object o;
Class c;
c = scl.loadClass("SomeNewClass");
o = c.newInstance();
((SomeNewClass) o).SomeClassMethod(SomeParam);
但是,这样的方法实际上是不能够实现的。首先,SomeNewClass在程序设计的时候尚未存在,这样的程序是无法通过编译的。其次,在运行时刻只有用户自定义的类库加载器SimpleClassLoader能够获取有关SomeNewClass 的定义,虚拟机的主类库加载器是无法创建一个SomeNewClass对象的,因此以上程序的最后一行也会出错。
参考文献3 指出有两个方法可以解决这个问题。一是被装载的模块是虚拟机的主类库加载器已经加载的某个类库的派生类库(subclass),一是被加载的模块实现某个已经被系统虚拟机的主类库加载器加载的接口(interface)。 在浏览器中通常都使用了第一种方法,譬如说所有的applet都是java.applet.Applet的派生类库,因此在所有的applet源代码中都有类似于public class MyClass extends Applet 的声明。在这里我们采用参考文献3 中介绍的第二种方法,也就是被加载的新模块实现某个预先设计好的接口。
声明接口UpdatableModule如下:
public interface UpdatableModule
{
void start(String RunTimeParam)
}
由于这个接口在设计时刻已经存在,它可以被虚拟机的主类库加载器和将要被加载的新功能模块所调用。新功能模块所需要做的,只是实现这个接口中的方法,例如:
public class NewModule_1 implements UpdatableModule
{
void start(String RunTimeParam)
{
System.out.println("This is new module 1.");
}
}
public class NewModule_2 implements UpdatableModule
{
void start(String RunTimeParam)
{
System.out.println("This is new module 2.");
}
}
在运行时刻,主程序需要从外部获得新的功能模块名,利用用户自定义的类库加载器加载新的功能模块,生成一个新的功能模块对象,然后通过事先定义好的接口调用新的功能模块中的方法。例如:
public class Test
{
public static void main(String[] args)
{
SimpleClassLoader scl = new SimpleClassLoader();
String RunTimeModule;
Object o;
Class c;
RunTimeModule = args[0];
c = scl.loadClass(RunTimeModule);
o = c.newInstance();
((UpdatableModule) o).start("No parameter needed.");
}
}
示范程序
下面我们介绍一个简单的数据采集与处理程序。该程序采集当前的系统时间并按照一定的格式输出到标准输出设备,其中的数据处理模块(即数据输出模块)可以在运行时刻被更新。该程序包括如下功能模块:
DataBuffer---------数据缓冲区
DataCollector------数据采集模块
DataProcessor------数据处理模块接口
PrintData_1--------数据输出模块,实现数据处理模块DataProcessor的接口
PrintData_2--------数据输出模块,实现数据处理模块DataProcessor的接口
TestGUI------------测试图形界面
数据缓冲区DataBuffer存放数据采集模块DataCollector 所采集的数据,它提供了更新数据和查询数据的方法。
public class DataBuffer
{
private String data;
// 更新数据的方法
public synchronized void UpdateData(String s)
{
data = s;
notifyAll();
}
// 查询数据的方法
public String GetData()
{
return data;
}
}
数据采集模块DataCollector是一个线程,它每隔5秒钟采集一次系统时间并更新数据缓冲区DataBuffer。
import java.util.Calendar;
public class DataCollector extends thread
{
private DataBuffer DB;
// 构造函数
public D