本文介绍了一个利用类库加载器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 DataCollector(DataBuffer db)
{
DB = db;
}
// 每5秒钟采集一次系统时间并更新数据缓冲区
public void run()
{
while (true)
{
try
{
DB.UpdateData("" + Calendar.getInstance().getTime());
sleep(5000);
} catch (InterruptedException e) {}
}
}
}
数据处理模块接口DataProcessor定义了数据处理模块所需要实现的方法。
public interface DataProcessor
{
// 数据处理模块所需要实现的方法, 开始处理数据
void start(DataBuffer db);
// 数据处理模块所需要实现的方法, 停止处理数据
void stop();
}
数据输出模块PrintData_1实现数据处理模块DataProcessor的接口。该模块每3秒钟查询一次数据缓冲区DataBuffer 中的数据并将其加上标志"Data 1: "输出到标准输出设备。
public class PrintData_1 implements DataProcessor
{
private PrintData PD;
private boolean run;
// 数据处理模块所需要实现的方法, 开始处理数据
public void start(DataBuffer db)
{
run = true;
PD = new PrintData(db);
PD.start();
}
// 数据处理模块所需要实现的方法, 停止处理数据
public synchronized void stop()
{
run = false;
notifyAll();
}
// 进行数据处理的线程
class PrintData extends Thread
{
private DataBuffer DB;
private String data;
// 构造函数
public PrintData(DataBuffer db)
{
DB = db;
}
// 数据处理
public void run()
{
while (run)
{
try
{
data = "Data 1: " + DB.GetData();
System.out.println(data);
sleep (3000);
} catch (InterruptedException e) {}
}
}
}
}
数据输出模块PrintData_2实现数据处理模块DataProcessor的接口。该模块每2秒钟查询一次数据缓冲区DataBuffer 中的数据并将其加上标志"Data 2: "输出到标准输出设备。
public class PrintData_2 implements DataProcessor
{
private PrintData PD;
private boolean run;
// 数据处理模块所需要实现的方法, 开始处理数据
public void start(DataBuffer db)
{
run = true;
PD = new PrintData(db);
PD.start();
}
// 数据处理模块所需要实现的方法, 停止处理数据
public synchronized void stop()
{
run = false;
notifyAll();
}
// 进行数据处理的线程
class PrintData extends Thread
{
private DataBuffer DB;
private String data;
// 构造函数
public PrintData(DataBuffer db)
{
DB = db;
}
// 数据处理
public void run()
{
while (run)
{
try
{
data = "Data 2: " + DB.GetData();
System.out.println(data);
sleep (2000);
} catch (InterruptedException e) {}
}
}
}
}
测试图形界面TestGUI 创建一个简单的用户图形界面。该程序声明一个数据缓冲区对象并启动数据采集线程。用户可以通过该图形界面输入运行时刻的数据处理模块名称,启动或者是终止数据处理模块,以及退出应用程序。
import java.awt.*;
import java.awt.event.*;
import com.agnc.loader.*;
public class TestGUI extends Frame implements ActionListener
{
DataBuffer DB;
DataCollector DC;
TextField ClassNameText;
Object o;
// 构造函数
public TestGUI()
{
// 构造图形界面
add(CreateGui());
addWindowListener(new WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
// 启动测试模块
InitTest();
}
// 构造图形界面
public Panel CreateGui()
{
// 创建按钮以及文本框
Button b1, b2, b3;
b1 = new Button("启动数据处理模块");
b1.setActionCommand("1");
b1.addActionListener(this);
b2 = new Button("终止数据处理模块");
b2.setActionCommand("2");
b2.addActionListener(this);
b3 = new Button("退出示例程序");
b3.setActionCommand("3");
b3.addActionListener(this);
ClassNameText = new TextField("请输入数据处理模块名称");
// 创建控制面板
Panel GuiPanel = new Panel();
GuiPanel.setLayout(new GridLayout(2,2));
GuiPanel.add(b1);
GuiPanel.add(b2);
GuiPanel.add(b3);
GuiPanel.add(ClassNameText);
return GuiPanel;
}
// 启动测试模块
public void InitTest()
{
// 构造数据缓冲区
DB = new DataBuffer();
// 启动数据采集线程
DC = new DataCollector(DB);
DC.start();
}
// 事件处理模块
public void actionPerformed(ActionEvent e)
{
String command = e.getActionCommand();
String name;
Class c;
if (command == "1")
{
// 调用新的数据处理模块
try
{
name = ClassNameText.getText();
SimpleClassLoader scl = new SimpleClassLoader();
c = scl.loadClass(name);
o = c.newInstance();
((DataProcessor) o).start(DB);
} catch (Exception ex) {}
}
else if (command == "2")
{
// 终止当前的数据处理模块
((DataProcessor) o).stop();
o = null;
}
else
{
// 退出示例程序
System.exit(0);
}
}
// 测试程序
public static void main(String[] args)
{
TestGUI window = new TestGUI();
window.setTitle("OPS Commander");
window.pack();
window.setVisible(true);
}
}
将以上源代码编译以后运行TestGUI ,在测试图形界面的文本框里面输入需要运行的数据处理模块的名称,然后点击“启动数据处理模块”按钮,即可运行指定的数据处理模块,点击“终止数据处理模块”即可终止当前的数据处理模块。在运行时刻,用户可以根据数据处理接口DataProcessor 编制新的数据处理模块并提交该示例程序运行。因此,本示例程序能够实现在运行时刻更新功能模块的功能。
本示例程序没有实现复杂的出错处理。在运行时刻,用户需要首先终止当前的数据处理模块,才能够启动新的数据处理模块。
讨论与比较
到此为止我们已经利用类库加载器实现了在运行时刻更新程序模块的功能。下面我们将这种方法与C/C++中的动态链接库做一个简单的比较。
首先,C/C++ 所使用的动态链接库是经过编译的可执行代码,可以直接被主程序所调用执行。Java的类库是字节码,在第一次被调用之前必须经过虚拟机的及时编译才能够被调用执行。因此,Java程序的启动时间要比C/C++ 程序的启动事件更长。另外,Java程序的执行效率普遍来说要比C/C++ 程序低20% ~ 30%。
其次,C/C++能够利用FreeLibrary函数释放旧的功能模块所占用的内存空间,从而保持了功能模块名和动态链接库文件名的一致性,同时节省了内存空间和磁盘空间。而Java并不提供类似的类库卸载功能,被旧的功能模块所占用的内存空间已经不再被使用却无法被释放,因此会有相当数量的内存被浪费。
由于动态链接库文件在其中的功能模块被使用的过程中是一直打开的,更新动态链接库文件的操作只能够在其中的功能模块已经被终止以后才能够进行。而Java的新功能模块和旧功能模块使用不同的文件名,可以在运行旧的功能模块的同时下载和传输新的功能模块到指定的位置。通常来说,下载和传输文件要涉及到效率低下的磁盘操作,因此,在实际应用中动态链接库实现方案下载和传输文件所需要的时间可能比虚拟机编译新的功能模块所需要的时间更长。但是如果在下载和传输过程中新的动态链接库文件采用一个不同的文件名,在需要加载新的功能模块之前删除旧的动态链接库文件并且将新的动态链接库文件改名,动态链接库实现方案所需要的时间就要大大的缩短。
综上所述,利用C/C++ 和Java都能够实现在运行时刻更新功能模块的功能。相对来说,基于动态链接库技术的C/C++ 方案在执行效率,内存利用以及操作效率等方面都比基于类库加载器的Java方案更有优势,应该作为首选的实现方案。
参考文献