第12章 简化发布处理:自动JAR文件创建
在这一章里,我们将从servlet的编程中先停下来,看一看怎样能够使你的applet的分布处理更加容易。对一个applet进行发布处理的最艰难的一步,不仅仅是正确的封装成为一个压缩的ZIP文件或者JAR文件,而是开发一个类文件从属关系的检验程序和把任何从属关系加入到一个ZIP或者JAR文件之中的问题。
12.1 找到类文件从属关系
要找到一个给定类文件的所有从属关系,我们确实需要检查由Java虚拟机所定义的内部类结构。Java虚拟机规范描述了如下的一个类文件:一个8位的字节流。所有的16位,32位和64位数分别由读入两个,四个和八个连接的8位字节来构造。多字节的数据条目总是按照尾部先存的顺序来存储,也就是说后面的字节先存储。正如我们所要看到的,所有的类引用都保存在类文件之中,而我们所要做的全部事情就是找到它们。表12.1显示了我们所要考察的最基本的类文件结构。 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 ─────────────────────────────────
幻数(Magic) 4 次版本(Minor
version) 2 主版本(Majorversion) 2 常量池计数(Constant pool
count) 2 常量池(Constant pool) 可变长 访问标志(Access
flags) 2 当前类(This class) 2 父类(Super
class) 2 接口计数(Interfaces
count) 2 接口(Interfaces) 2*Interface count 域计数(Field
count) 2 域(Fields) 可变长 方法计数(Method
count) 2 方法(Methods) 可变长 属性计数(Attribute
count) 2 属性(Attributes) 可变长 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12.1.1 进一步考察类文件结构
现在让我们来进一步考察类文件结构中的每一个条目。一旦我们理解了这些结构是怎样配合起来的,就会很容易地遍历该结构并且找出有价值的信息。
幻数(Magic) 这个条目包含了一个所有的Java类文件都通用的幻数。这个幻数的值总是0xCAFEBASE(十六进制)
次版本和主版本(Minor Version 和 Major
Version) 次版本和主版本条目的取值是创建类文件的编译器的次版本号和主版本号。例如Sun的JDK的1.0.2版本和1.1版本,次版本号就是3,主版本号就是45。唯有Sun公司才能定义新版本号的意义。
常量池计数(Constant Pool
Count) 常量池计数的计数值必须要大于零,它定义了常量池表的表项的数目。请注意常量池计数包括了常量池表项在0值的索引,但是表项并不包括在类文件中并且被保留下来为Java虚拟机内部使用。
常量池(Constant
Pool) 常量池是一个表项数变长的表。从索引值1直到常量池计数的每一个表项都是可变长的变量。每个表项的格式由一个打头的标记字节所定义,正如表12.2所示。
表12.2
常量池标记数值 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 常量类型 值
───────────────────────────────── CONSTANT_Utf8
1 CONSTANT_Integer 3 CONSTANT_Float 4 CONSTANT_Long
5 CONSTANT_Double
6 CONSTANT_Class 7 CONSTANT_String
8 CONSTANT_Fieldref 9 CONSTANT_Methodref
10 CONSTANT_InterfaceMethodref 11 CONSTANT_NameAndType 12
─────────────────────────────────
CONSTANT_Utf8 CONSTANT_Utf8表项代表了一个常量字符串值。Uft8字符串是被编码的,这样一来,只包含非空的ASCII字符的字符序列可以只用每个字符一个字节来表示。16位的字符也可以表示。表12.3显示了CONSTANT_Utf8表项的结构。
表12.3
CONSTANT_Utf8表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Utf8,值等于1 Length
2 随后的字节数组的字节数。字符串是非空结 束的 Bytes 由Length来定长 字符串的字节数 ─────────────────────────────────
CONSTANT_Integer CONSTANT_Integer表项代表一个四字节的整数常量,表12.4显示了CONSTANT_Integer表项的结构。
表12.4
CONSTANT_Integer表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释 ───────────────────────────────── Tag 1 CONSTANT_Integer,值等于3 Bytes 4 Int常量的值。字节是按照尾部先存的顺序存储的 ─────────────────────────────────
CONSTANT_Float CONSTANT_Float表项代表一个四字节的浮点数常量。表12.5显示了CONSTANT_Float表项的结构。
表12.5
CONSTANT_Float表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Float,值等于4 Bytes 4 浮点数类型常量的值。按照IEEE
754单精度浮点数位格式 标准存储 ─────────────────────────────────
CONSTANT_Long CONSTANT_Long表项代表一个八字节的长类型常量。表12.6显示了CONSTANT_Long表项的结构
表12.6
CONSTANT_Long表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Long,值等于5 Bytes 8 长类型常量的值。字节是按照尾部先存的顺序存储的 ─────────────────────────────────
CONTANT_Long表项和CONSTANT_Double表项实际上占用了两个常量池表项。下面的常量池表项被视为非法,一定不能使用。
CONSTANT_Double CONSTANT_Double表项代表一个八字节的双精度类型常量。表12.7显示了CONSTANT_Double表项的结构。
表12.7
CONSTANT_Double表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Double,值等于6 Bytes 8 双精度类型常量的值。按照IEEE
754双精度浮点数位格式 标准存储 ─────────────────────────────────
CONSTANT_Double表项占用了两个常量池表项;要了解更多的信息,请参看CONSTANT_Long。 CONSTANT_Class CONSTANT_Class表项代表一个类或者接口。CONSTANT_Class表项包含一个指回常量池中CONSTANT_Utf8表项的索引指针。在索引表项找到的字符串是一个类或者接口的名字。表12.8显示了CONSTANT_Class表项的结构。
表12.8
CONSTANT_Class表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Class,值等于7 Name
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Utf8类型并且代表一个类或者接口的名字 ─────────────────────────────────
CONSTANT_String CONSTANT_String表项代表一个字符串常量。CONSTANT_String表项包含一个指回常量池中CONSTANT_Utf8表项的索引指针。表12.9显示了CONSTANT_String表项的结构。
表12.9
CONSTANT_Class表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_String,值等于8 String
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Utf8类型并且代表一个字符串常量 ─────────────────────────────────
CONSTANT_Fieldref CONSTANT_Fieldref表项代表类中的一个域。CONSTANT_Fieldref表项包含一个指回常量池中声明该域的CONSTANT_Class表项的索引指针和一个定义该域的名字和描述符的CONSTANT_NameAndType表项的索引指针。表
12.10显示了CONSTANT_Fieldref表项的结构。
表12.10
CONSTANT_Fieldref表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Fieldref,值等于9 Class
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Class类型并且代表一个类或者接口的声 明类型 Name
and
Type 2 合法的常量池的索引。索引中的表项必须是 Index CONSTANT_NameAndType类型,并且代表一个域的 名字和域的描述符 ─────────────────────────────────
CONSTANT_Methodref CONSTANT_Methodref表项代表一个类的方法。CONSTANT_Methodref表项包含一个指回常量池中声明该方法的CONSTANT_Class表项的索引指针和一个定义该方法的名字和描述符的CONSTANT_NameAndType表项的索引指针。表12.11显示了CONSTANT_Methodref表项的结构。
表12.11
CONSTANT_Methodref表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_Methodref,值等于10 Class
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Class类型,并且代表一个类或者接口 的声明类型 Name
and
Type 2 合法的常量池的索引。索引中的表项必须是 Index CONSTANT_NameAndType类型,并且代表一个域的 名字和域的描述符 ─────────────────────────────────
CONSTANT_InterfaceMethodref CONSTANT_InterfaceMethodref表项代表一个定义在接口中的方法。CONSTANT_InterfaceMethodref表项包含一个指回常量池中声明该方法的CONSTANT_Class表项索引指针和一个定义该方法的名字和描述符的CONSTANT_NameAndType表项的索引指针。表12.12显示了CONSTANT_InterfaceMethodref表项的结构。
表12.12
CONSTANT_InterfaceMethodref表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_InterfaceMethodref,值等于11 Class
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Class类型,并且代表一个类或者接口 的声明类型 Name
and
Type 2 合法的常量池的索引。索引中的表项必须是 Index CONSTANT_NameAndType类型,并且代表一个域的 名字和域的描述符 ─────────────────────────────────
CONSTANT_NameAndType CONSTANT_NameAndType表项代表一个域或者方法名字和类型。请注意该域或者方法所属的类或者接口并没有被指出。CONSTANT_Fieldref,CONSTANT_Methodref和CONSTANT_InterfaceMethodref表项用来将类或者接口绑定到方法名字和类型上。表12.13显示了CONSTANT_NameAndType表项的结构。
表12.13
CONSTANT_NameAndType表项 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 条目 长度 注 释
───────────────────────────────── Tag 1 CONSTANT_NameAndType,值等于12 Class
Index 2 合法常量池的索引。索引中的表项必须是 CONSTANT_Utf8类型,并且代表一个合法的 Java方法或者域的名字 Descript
or
Index 2 合法的常量池的索引。索引中的表项必须是 CONSTANT_Utf8类型,并且代表一个域的 Java方法或者域的描述符 ─────────────────────────────────
访问标志(Access Flags) 访问标志的值指出了类或者接口声明的修改者。表12.14显示了访问标志修改者的取值。
表12.14
类和接口的修改者标志 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 标志 值 注释 ───────────────────────────────── ACC_PUBLIC
0x0001 公有类或者接口 ACC_FINAL 0x0010 常量类;不允许子类
ACC_SUPER 0x0020 指针特别对父类的方法 ACC_INTERFACE
0x0200 接口 ACC_ABSTRACT
0x0400 抽象类或者接口,不可以被示例 ─────────────────────────────────
当前类(This Class) This
Class表项必须是常量池中的一个合法的索引。索引中的表项必须是CONSTANT_Class类型,并且代表当前类所定义的类或者接口。
父类(Superclass) Superclass表项必须是常量池中的一个合法的索引。索引中的表项必须是CONSTANT_Class类型,并且代表 当前类的父类。唯一的例外就是java.lang.Object,它的父类索引是0。
接口计数(Interface Count) Interface
Count定义了接口表的表项数目,接口表定义了当前类或者接口的直接父接口。
接口表(Interface Table) Interface
Table包含一个合法的常量池索引指针的数组。每一个接口表的表项必须引用一个CONSTANT_Class表项并且代表一个当前类或者接口的直接父接口。
域计数(Field Count) Field Count定义了域表(Field
Table)的表项数目,域表定义当前类或者接口的每一个域。
域表(Field
Table) 域表包含一个表项数可变长的数组,该数组表示当前类或者接口的每一个域。域表并不包括从父类或者父接口继承的那些域;它只包含定义在当前类或者接口的那些域。由于我们还用不到域表,我将把它留到Java虚拟机规范中去解释每一个域表表项的内容。
方法计数(Method Count) Method Count定义了方法表(Method
Table)的表项数,方法表定义了当前类或者接口的每一个方法。
方法表(Method
Table) 方法表包含一个表项数可变长的数组,该数组表示当前类或者接口的每一个方法。方法表并不包括从父类或者父接口继承的那些方法;它只包含定义在当前类或者接口的那些方法。和域表一样,我们还用不到方法表,我同样将把它留到Java虚拟机规范中去解释每一个表项的内容。
属性计数(Attribute Count) Attribute Count定义了属性表(Attribute
Table)的表项数,属性表定义了当前类或者接口的每一个属性。
属性表(Attribute
Table) 属性表包含一个表项数可变长的数组,该数组表示当前类或者接口的每一个属性。这些属性给出了有关类文件的附加信息,比如源文件(SourceFile),例外(Exceptions),和线数表(LineNumberTable)。同样,我们还用不到属性表,我也将把它留到Java虚拟机规范中去解释细节。
12.1.2 一个找从属关系的算法
现在我们已经对类文件的内容有了牢固的掌握,我们可以容易地找出从属关系。我们认为一个从属关系就是在常量池中找到的任何一个类引用。下面的表就是一个找到类引用的基本算法: 1.打开读到类文件。 2.取得常量池中的表项数。 3.读常量池,维护一个类文件和字符串引用的列表。请注意类文件引用实际上是一个指回常量池的索引。索引所指向的常量池表项是一个给出类或者接口名字的字符串。 4.对于每一个类文件引用,找到包含类文件名的相应的字符串常量。 5.对于每一个找到的类文件,重复第1步到第5步。 在下面的几节里,我们将走完上述步骤的每一步,并且看一看在我们用到的从属关系检查程序中相应的Java代码。
12.2 打开和读取一个类文件
我们的从属关系检查程序的第一个挑战看上去好像是一个非常简单的进程。我们所要做的就是打开一个类文件;那能有多难呢?你有可能试图把一个文件当作一个资源并且用ClassLoader.getSystemResourceAsStream()方法来返回一个我们易读的输入流。不幸的是,作为一种安全尺度,类装入器(Class
Loader)禁止类文件按照这种方式读取;你可能也不想人们能够直接从Internet上读取你的类文件,是吗? 好的,如果你不想把类文件当作一个系统资源来对待,那么怎样来打开一个类文件呢?其实这一点很好做到,只要你总能够保证类文件在你的本地文件结构中被找到。但是,那些从CLASSPATH从一个压缩的ZIP或者JAR文件载入的类应该怎么办呢?这是一个很通用的方法,对于包和发布的类来说——毕竟,这就是我们想要做的事情!为了可靠地打开一个类文件,而不管它的物理定位是何处,我们都通过CLASSPATH来寻找一个类,或者在目录中,或者在每个CLASSPATH元素的存档中(ZIP或者JAR)。
/** * Given a class name, open it and return a buffer with * the
contents. The class is loaded from * the current CLASSPATH setting *
源文件名:RollCall.java */
protected byte[] openResource(String name) throws
Exception { byte buf[] = null;
// Get the defined
classpath
String classPath = System.getProperty("java.class.path"); int
beginIndex = 0; int endIndex = classPath.indexOf(";");
// Walk through the classpath
while (true) { String
element = "";
if (endIndex == -1) { // No ending
semicolon element =
classPath.substring(beginIndex); } else { element =
classPath.substring(beginIndex, endIndex); }
// We've got an element from the classpath. Look for // the
resource here
buf = openResource(name, element);
// Got it! Exit the loop
if (buf != null)
{ break; }
if (endIndex == -1)
{ break; } beginIndex = endIndex + 1; endIndex =
classPath.indexOf(";", beginIndex); }
return buf; }
/** * Given a resource name and path, open the resource and *
return a buffer with the contents. Returns null if * not found */
protected byte[] openResource(String name, String path) throws
Exception { byte buf[] = null;
// If the path is a zip or
jar file, look inside for the // resource
String lPath = path.toLowerCase(); if (lPath.endsWith(".zip")
|| lPath.endsWith(".jar")) {
buf = openResourceFromJar(name,
path); } else {
// Not a zip or jar file. Look for the resource as // a file
String fullName = path;
// Put in the directory separator if necessary if
(!path.endsWith("\") && !path.endsWith("/")) { fullName
+= "/"; } fullName += name;
java.io.File f = new java.io.File(fullName);
// Check to make sure the file exists and it truely // is a
file
if (f.exists() && f.isFile()) {
// Create an input stream and read the
file java.io.FileInputStream fi = new
java.io.FileInputStream(f); long length = f.length(); buf = new
byte[(int)
length]; fi.read(buf); fi.close(); } }
return buf; }
/** * Given a resource name and jar file name, open the jar
file * and return a buffer containing the contents. Returns null * if
the jar file could not be found or the resource could * not be
found */
protected byte[] openResourceFromJar(String name, String
jarFile) throws Exception { byte buf[] =
null;
java.io.File f = new
java.io.File(jarFile); java.util.zip.ZipFile zip = null;
// Make sure the file exists before opening it
if (f.exists() && f.isFile()) {
// Open the zip file
zip = new java.util.zip.ZipFile(f);
// Is the entry in the zip file?
java.util.zip.ZipEntry entry = zip.getEntry(name);
// If found, read the corresponding buffer for the entry
if (entry != null) { java.io.InputStream in =
zip.getInputStream(entry);
// Get the number of bytes available
int len = (int) entry.getSize();
// Read the contents of the class buf = new
byte[len]; in.read(buf, 0,
len); in.close(); } }
if (zip != null) { zip.close(); } return
buf; }
12.3 读取常量池中的表项数
既然我们已经打开并且读取了类文件中的内容,我们就可以开始处理这些未加工的字节流了。第一步是读取类文件的头部并且判断常量池中的表项数。因为类文件的头部是一个固定长度的结构(见表12.1),我们就可以对头部做一些基本的验证并且直接读取数值。
// Create a DataInputStream using the buffer. This will // make
reading the buffer very easy // 源文件名:RollCall.java
java.io.ByteArrayInputStream bais = new
java.io.ByteArrayInputStream(buf);
java.io.DataInputStream in = new
java.io.DataInputStream(bais);
// Read the magic number. It should be 0xCAFEBABE
int magic = in.readInt(); if (magic != 0xCAFEBABE) { throw new
Exception("Invalid magic number in " + className); }
// Validate the version numbers
short minor = in.readShort(); short major = in.readShort(); if
((minor != 3) && (major != 45)) { // The VM specification
defines 3 as the minor version // and 45 as the major version for
1.1 throw new Exception("Invalid version number in " +
className); }
// Get the number of items in the constant pool
short count = in.readShort();
12.3.1 处理常量池
下面一步,我们可以处理常量池中的每一个表项。我们将跳过常量池中的大多数信息;我们只对CONSTANT_Class(表12.8)和CONSTANT_Utf8(表12.3)表项感兴趣。
// 源文件名:RollCall.java // We'll keep a vector containing an entry for
each // CONSTANT_Class tag in the constant pool. The value // in the
vector will be an Integer object containing // the name index of the class
name
java.util.Vector classInfo = new java.util.Vector();
// We'll also keep a HashTable containing an entry for // each
CONSTANT_String. The key will be the index // of the entry (relative to 1),
while the element // will be the String value.
java.util.Hashtable utf8 = new java.util.Hashtable();
// Now walk
through the constant pool looking for class // constants. All other
constants are ignored, but we // still need to understand the format so
that they // can be skipped.
for (int i = 1; i < count; i++) { // Read the tag byte tag =
in.readByte();
switch (tag) { case 7: // CONSTANT_Class // Save the constant
pool index for the class name short nameIndex =
in.readShort(); classInfo.addElement(new
Integer(nameIndex)); break; case 9: // CONSTANT_Fieldref case
10: // CONSTANT_Methodref case 11: //
CONSTANT_InterfaceMethodref // Skip past the
structure in.skipBytes(4); break; case 8: //
CONSTANT_String // Skip past the string
index in.skipBytes(2); break; case 3: //
CONSTANT_Integer case 4: // CONSTANT_Float // Skip past the
data in.skipBytes(4); break; case 5: //
CONSTANT_Long case 6: // CONSTANT_Double // Skip past the
data in.skipBytes(8);
// As dictated by the Java Virtual Machine specification, //
CONSTANT_Long and CONSTANT_Double consume two // constant pool
entries. i++;
break; case 12: //
CONSTANT_NameAndType // Skip past the
structure in.skipBytes(4); break; case 1: //
CONSTANT_Utf8 String s = in.readUTF(); utf8.put(new Integer(i),
s); break; default: System.out.println("WARNING: Unknown
constant tag (" + tag + "@" + i + " of " + count + ") in " +
className); } }
请注意,即使我们只使用CONSTANT_Class和CONSTANT_Utf8表项,我们仍然需要理解其他表项的格式才能正确地跳过它们。特别需要注意CONSTANT_Long和CONSTANT_Double表项,它们每个都要占用两个表项空间;我们必须确保正确地将池计数进行了前跳。
12.3.2 找到所有的类名字
在前面一节里,我们已经读出了常量池表,并且维护了一个所有CONSTANT_Class和CONSTANT_Utf8表项的列表。记住CONSTANT_Class表项包含一个合法的常量池索引,它指向类或者索引的名字。由于常量池表项的顺序并没有在Java虚拟机规范中规定,我们必须在进行任何更进一步的处理之前,读出所有的表项。既然整个常量池都被读出了,我们就可以再回到我们的CONSTANT_Class表项的列表,找到相对应的CONSTANT_Utf8表项。
// 原文件是:RollCall.java // Now we can walk through our vector of class
name // index values and get the actual class name
for (int i = 0; i < classInfo.size(); i++) { Integer index =
(Integer) classInfo.elementAt(i); String s = (String) utf8.get(index);
// Look for arrays. Only process arrays of objects
if (s.startsWith("[")) { // Strip off all of the array
indicators while (s.startsWith("[")) { s =
s.substring(1); } // Only use the array if it is an object. If it
is, // the next character will be an 'L'
if (!s.startsWith("L")) { continue; }
// Strip
off the leading 'L' and trailing ';' s = s.substring(1, s.length() -
1); }
// Append the .class s += ".class"; //Now we
have the full class or interface name in
's' } 请注意,类名字的数组包含一个Java数组类型的描述符,需要特别对待。例如,类名字代表一个一维的类对象数组。 Object[] 按照Java数组类型的描述符来表示: [Ljava.lang.Object; 现在我们已经对我们找到的每一个类名字执行了附加的处理。根据我们的意图,我们还需要把每一个文件做成一个新的存档(压缩的ZIP或者JAR文件)。并且,对我们找到的每一个新类名字,还要对它递归检查类文件从属关系。这样做过以后,我们就可以从我们的原始类找到所有的从属关系。让我们稍微退回一点,看看怎样创建一个新的ZIP或者JAR文件。这两种存档文件都有相同的格式,除了JAR(Java
ARchive的缩写)文件可以有一个明晰文件,正如JavaBeans规范中所定义的,明晰文件列出了JAR中所有合法的beans。我们将假定JAR中没有beans,这样我们将忽略创建一个明晰。
// Attempt to create the archive if one was given //
原文件:RollCall.java
if (m_archive != null) { System.out.println("Creating archive " +
m_archive); java.io.File f = new
java.io.File(m_archive); java.io.FileOutputStream fo = new
java.io.FileOutputStream(f);
// A new file was created. Create our zip
output stream
m_archiveStream = new java.util.zip.ZipOutputStream(fo); }
类java.util.zip.ZipOutputStream使得创建一个压缩的存档文件变得非常简单。我们所要做的事情就是创建一个新的java.util.zip.ZipEntry对象,代表一个存档的头部,然后写入数据。
/* * 原文件:RollCall.java * Adds the given buffer to the archive
with the given * name */
private void addToArchive(String name, byte buf[]) throws
Exception { // Create a zip entry
java.util.zip.ZipEntry entry = new
java.util.zip.ZipEntry(name); entry.setSize(buf.length);
// Add the next entry
m_archiveStream.putNextEntry(entry);
// Write the contents out as well
m_archiveStream.write(buf, 0,
buf.length); m_archiveStream.closeEntry(); }
12.4 合而为一:CreateArchive应用程序
我们已经得到了所有的必要例程来找到一个给定的类的所有从属关系,让我们将它们合而为一,通过编写一个CreateArchive——一个简单的应用程序,将接收一个要被检查的类文件的列表,也接收一个要创建的存档的名字,后者是可选择的。CreateArchive将使用RollCall类,该类将读出类文件并且为我们创建存档;我们在这一章里一直都在看RollCall.java的各部分。与别处一样,你可以在本书配套CD-ROM中找到完整的源代码。 CreateArchive应用程序把将要检查的一个或者多个类文件作为它的参数(去掉.class扩展名),并且可以增加“-a”选项来指定将创建的存档的名字。如果没有指定存档,从属关系只是简单地显示出来。
package javaservlets.rollcall;
/** * 文件名:CreateArchive.java * This simple application
will use the RollCall class to * find all of the class file dependency for
a given set of * classes. If an archive file is specified it will be
created * and all of the dependent files will be added. */
public class CreateArchive { public static void main(String
args[]) { // Create a new object and
process
CreateArchive ca = new
CreateArchive(); ca.create(args); }
public void create(String args[]) { // Get a list of all of
the class files to check. Any // arguments given without a switch '-'
will be considered // a class file
String files[] = getFiles(args);
if (files == null) { System.out.println("No class files
specified"); showHelp(); return; }
// Get the archive to create, if given
String archive = getArg(args, "-a");
// Create a new RollCall object
RollCall rollCall = new RollCall(); try {
// Set the class files to
check rollCall.setClasses(files);
// Set the archive to
create rollCall.setArchive(archive);
// Check all strings, if
necessary rollCall.setCheckStrings(isArg(args, "-c"));
//
Perform the check and create the archive, if
necessary.
rollCall.start(); } catch (Exception ex)
{ ex.printStackTrace(); } } ... }
值得一提的是,当我们运行CreateArchive的时候,所有的java.*类都会被过滤出来。你没有必要去分配这些类,因此它们被明确地排除掉(不要说你的存档将会非常的大,如果你在即使最简单的类里面也包括了所有的java.*类的话)。 让我们来试验一下。作为一个简单的测试,我们运行CreateArchive并以它自己作为输入类文件:
java
javaservlets.rollcall.CreateArchive javaservlets.rollcall.CreateArchive 你应该得到下面的输出: javaservlets.rollcall.CreateArchive.class javaservlets.rollcall.RollCall.class 你也可以试着用一个-a选项来运行CreateArchive,指定要创建的存档文件: java
javaservlets.rollcall.CreateArchive javaservlets.rollcall.CreateArchive
-atemp.jar 你应该得到下面的输出: Creating
temp.jar javaservlets.rollcall.CreateArchive.class javaservlets.rollcall.RollCall.class temp.jar
created.
12.5 发布一个Applet
我们已经有了自己的CreateArchive实用工具,它可以为一系列给定的类文件找到所有的从属关系,让我们来试试。我们将创建一个非常简单的applet,它使用另外一个实现一个接口的基本类。连锁效果就是我们的applet将有三个从属关系:一个类,一个接口和applet自身。我们将从写一个简单的类开始,这个类实现了一个接口,接口带有一个能返回一个字符串值的方法。
package javaservlets.rollcall.test;
/** * This is a simple interface used for testing
CreateArchive * 来自SimpleInterface.java */
public interface SimpleInterface { String
getString(); }
package javaservlets.rollcall.test;
/** * This is a simple class used for testing
CreateArchive * 来自SimpleClass.java */
public class SimpleClass implements SimpleInterface { public
String getString() { return "I loaded all of my classes from an
archive!"; } }
我们所要做的就是创建一个简单的TextField来保存对我们的简单类的方法调用的结果。
package javaservlets.rollcall.test;
/** * This is a simple applet for testing CreateArchive.
A * distribution archive will be created and used to load this *
applet. */
public class SimpleApplet extends java.applet.Applet { // Define
our fields
java.awt.TextField output = new java.awt.TextField();
/* * init is called when the applet is
loaded */
public void init() { // Add
components
add(output);
// Use our simple class to get some data. Set the // results in
the output text field.
SimpleClass sc = new
SimpleClass(); output.setText(sc.getString()); } }
让我们继续往下做,为简单的applet创建一个存档,这样它就可以容易的被发布了。 java
javaservlets.rollcall.CreateArchive javaservlets.rollcall.test.SimpleApplet -aSimpleApplet.zip 你应该得到下面的输出: Creating
archive
SimpleApplet.zip javaservlets.rollcall.test.SimpleApplet.class javaservlets.rollcall.test.SimpleClass.class javaservlets.rollcall.test.SimpleInterface.class SimpleApplet.zip
created.
SimpleApplet.zip现在包含了执行我们简单的applet的所有必要的类文件。记住没有java.*文件被包含,但是这些将成为浏览器的虚拟机的一部分。在我们可以用我们的applet之前,需要创建一个HTML文件来正确装载这个applet。
SimpleApplet - Simple
applet for testing
archives SimpleApplet
This
is a simple applet to demonstrate the loading of class files from an
archive. The archive was generated by the
CreateArchive utility.
width=400 height=100 archive=SimpleApplet.zip>
请注意在applet标记上的“archive=”选项。它指定了用来搜索applet的存档,在我们这种情况下,该存档就是由CreateArchive创建的SimpleApplet.zip文件。让我们继续往下试一试。我们将使用Netscape
Navigator来执行applet,当然你也可以使用任何支持Java1.1的浏览器。为了使之成为一个合法的测试,一定要确保这个简单的类文件不在你的CLASSPATH目录中,这样才能保证从存档中加载这个applet。将SimpleApplet.zip和SimpleApplet.html放在你的Web服务器的WWW根目录中。
12.6 一些缺陷
我可不希望你看完了这一章却不知道我们的这个程序检验程序的那些缺陷: ·如果你试图检查的在显示地通过Class.forName("")加载了其他类,那么这个从属检验程序的算法就不能得到被调用的类的名称。这是因为我们仅仅检查了CONSTANT_Class常量;而Class.frName()方法中的命名类创建了一个CONSTANT_Utf8常量(为这个命名类)。你可以扩展这个从属检查算法,使之将每一个CONSTANT_Utf8常量都当作类名来处理并且试图将它们加载。 ·如果你所谋略检查的类使用了其他系统资源——如语音或者图像文件,那么这个从属检验程序也不能发现它们。你可以扩展这个从属检验程序,使之检查每一个CONSTANT_Utf8常量是否是书籍的文件扩展名(如.wav,.giv和.jpg等),并且将它们也加入到从属关系中。
12.7 小结
在本章中,我们看到了如何开发一个Java应用来发现某个指定的类的所有的从属关系并且创建一个存档文件(压缩过后ZIP或是JAR),这个存档可以用来简单而快速地发布applet应用程序。为了发现类文件的从属关系,我们不但要加载这个类文件,而且还要读入它并处理这个原始的字节流。为了处理这个字节流,我们需要检查Java虚拟机规范所定义类文件的格式。初看起来这一切好像很复杂,不过我希望看到如何使用类结构之后,你可以认识到实际上这十分简单。 在下一章中,我们将要回到servlet的开发。我们将要编写一个JDBC驱动程序,这个驱动程序可以在Internet上使用。这个叫做SQLServlet的JDBC驱动程序将使用我们在第10章和第11章所看到的HTTP遂道技术。 |