你所不知道的五件事情--JAR文件
这是Ted Neward在IBM developerWorks中5 things系列文章中的一篇,讲述了关于JAR的一些应用窍门,值得大家学习。(2010.06.27最后更新)
摘要:许多Java开发者从没有深入思考过JAR--他们只是在将类传到产品服务器之前使用JAR打包这些文件罢了。但JAR并不仅仅是一个被重命名的 ZIP文件。学习如何使用Java归档文件的全部能力,包括打包Spring依赖和配置文件的小窍门。
对大多数Java开发者而言,JAR与它的兄弟们,WAR和EAR,都是一长串Ant或Maven处理后的最终结果。一个标准的过程是将JAR复制到服务器的适应位置(或者,更少见地,复制到用户的机器上),然后就把它遗忘了。
准确地说,JAR能做的远不止存储源代码,但你必须要知道它能做的其它事情,以及怎样去使用它。在本"5 things"系列的分期文章中所介绍的窍门将展示如何制作大部分的Java归档文件(在有些例子中,也会涉及WAR和EAR),特别是在开发时期。
因为有众多的Java开发者在使用Spring(也因为Spring框架展示了一些相对于我们对JAR传统应用的挑战),其中若干窍门是特别针对Spring应用中的JAR文件。
我将以一个标准的Java归档文件处理的例子开始,该例是下面各窍门的基础。
置于JAR中
一般地,在编译源代码之后你会制作一个JAR文件,通过jar命令工具,或更为通用的Ant的jar工作,去把Java代码(已经被包分隔开)归集到单个文件中。这种处理很明了,在此处我就不作展示了,但在本文的后面我将回到JAR文件是如何被构造的这个主题中来。现在,我们只需打包Hello类,这是一个独立运行的控制台工具应用,该应用会向控制台打印一条信息,这无疑是很有用的任务,如清单1所示:
清单1. 用于打包的控制台工具
package com.tedneward.jars;
public class Hello
{
public static void main(String[] args)
{
System.out.println("Howdy!");
}
}
Hello工具并不复杂,但以可执行程序开始,它是探索JAR文件的一个有用的辅助手段。
1. JAR是可执行的
像.NET和C++这样的编程语言,在历史上有操作系统友好方面优势,只需在命令行中引入它们的名字(helloWorld.exe)或在GUI Shell中双击它们的图标就会启动这些应用。然而在Java编程中,启动器程序--java--引导JVM运行,而后我们必须传入一个命令行参数 (com.tedneward.Hello)用于指定将要启动的含有main()方法的类。
这些额外的步骤使得很难为Java创建用户友好的应用。不仅仅是由于最终用户必须在命令行中键入所有的这些元素(很多用户都想避免这种情况),而且他或她会由于某种原因打错字并得到一个晦涩的错误返回。
解决方案就是使JAR文件"可执行",以便在执行JAR文件时,能让Java启动器自动地知道启动哪个类。我们所需要做的只是在JAR文件的manifest(JAR文件META-INF中的MENIFEST.MF文件)中引入一个属性,例如:
清单2. 显示入口点
Main-Class: com.tedneward.jars.Hello
manifest就是一组名值对。因为mainfest有时候对回车和空格比较敏感,所以在制作JAR时使用Ant去生成该文件要方便些。在清单3中,我就在Ant的jar任务中使用了manifest元素去指定要生成的manifest:
清单3. 构建入口点
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
</manifest>
</jar>
</target>
现在为了执行JAR文件,用户所需要做的只是在命令行中指定文件名,通过命令java -jar outapp.jar。对于这种情况,在有些GUI Shell中双击JAR文件也是可以的。
2. JAR能够包含依赖信息
Hello工具类的文字似乎已经扩展了,这样对不同的实现的需求就变得很紧急了。像Spring或Guice这样的依赖注入(DI)容器为我们处理了许多细节,但仍有一点儿障碍:
清单4. Hello, Spring world!
package com.tedneward.jars;
import org.springframework.context.*;
import org.springframework.context.support.*;
public class Hello
{
public static void main(String[] args)
{
ApplicationContext appContext =
new FileSystemXmlApplicationContext("./app.xml");
ISpeak speaker = (ISpeak) appContext.getBean("speaker");
System.out.println(speaker.sayHello());
}
}
因为启动器的-jar选项会被命令行中的-classpath选项所覆盖,那么当你运行上述程序时,Spring需要出现在CLASSPATH中,并且要在环境变量中。幸运地是,JAR允许针对其它JAR依赖的声明出现在manfiest中,这就隐式地创建了CLASSPATH而不需要你去声明,如清单5 所示:
清单5. Hello, Spring CLASSPATH!
<target name="jar" depends="build">
<jar destfile="outapp.jar" basedir="classes">
<manifest>
<attribute name="Main-Class" value="com.tedneward.jars.Hello" />
<attribute name="Class-Path"
value="./lib/org.springframework.context-3.0.1.RELEASE-A.jar
./lib/org.springframework.core-3.0.1.RELEASE-A.jar
./lib/org.springframework.asm-3.0.1.RELEASE-A.jar
./lib/org.springframework.beans-3.0.1.RELEASE-A.jar
./lib/org.springframework.expression-3.0.1.RELEASE-A.jar
./lib/commons-logging-1.0.4.jar" />
</manifest>
</jar>
</target>
注意到Class-Path属性包含有该应用的依赖相对于JAR的引用路径。你也可以写绝对引用路径,或者完全不需要前缀,在这种情况下就要假设这些依赖 JAR文件与应用程序的JAR文件在同一目录下。
不幸地是,Ant的Class-Path属性对应的value属性必须出现在一行中,因为JAR manfiest无法应对多个Class-Path属性,所以,所有的依赖必须出现在manifest文件的同一行中。可以肯定的是,这种做法很丑陋,但为了能使用命令java -jar outapp.jar,这是值得的。
3. 可隐式地引用JAR
如果你有多个不同的命令行工具(或其它的应用)需要使用Spring框架的JAR文件,那么将这些Spring JAR文件置于公共路径中,以便所有的工具类都能被引用到。这样做就能避免文件系统中满是JAR文件的多份拷贝。Java运行时环境的公共JAR文件路径,即大家所知的"扩展目录",默认是位于JRE安装路径下的lib/ext子目录中。
JRE是一个可定制的路径,但仍然很少在一个给定的Java环境中定制该路径使我们能够安全放心地假设lib/ext是一个存放JAR文件的安全地方,该目录中的JAR文件将默认出现在Java运行时环境的CLASSPATH中。
4. Java 6允许类路通配符
作为一种避免庞大CLASSPATH环境变量(Java开发者在多年前就已经抛弃它了)和/或命令行-classpath参数的努力,Java 6引入了类路径通配符选项。与在启动时必须在一个参数中显示地列出每个JAR文件不同,类路径通配符允许你通过lib/*来指定该目录下的所有JAR文件 (但不允许递归其子目录中的JAR文件)设置到类路径中。
不幸地是,类路径通配符并不能支持之前讨论过的Class-Path属性manifest条目。为了某些开发者任务,例如代码生成或分析工具,使用类路径通配符可以更方便地启动Java应用程序(包括服务器)。
5. JAR不只是包含代码
就像Java生态系统中的许多组成部分那样,Spring依赖一个配置文件,该文件描述了如何去构建运行环境。如前所述,Spring依赖app.xml 文件,该文件与JAR文件存在于同一个目录下--但经常地,开发者们会忘记复制JAR文件边上的配置文件。
sysadmin会编辑某些配置文件,但也有大量的配置文件(如Hibernate映射文件)在sysadmin的域之外,这将导致发布错误。一种明智的解决方案就是将代码与配置文件打包在一起--这是可行的,因为JAR本质上就是改头换面的ZIP。只需将配置文件包含在Ant任务中,或使用jar命令去构建JAR文件。
不仅仅是配置文件,JAR还可以包含其它类型的文件。例如,如果我的SpeakEnglish组件想到访问一个属性文件,那么我会像清单6那样进行设置:
清单6. 随机响应
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
Random random = new Random();
public String sayHello()
{
// Pick a response at random
int which = random.nextInt(5);
return responses.getProperty("response." + which);
}
}
将responses.properties置入JAR文件就意味着,不需要操心有太多文件要随JAR文件一同部署了。要做到这些,只需在制作JAR的过程中包含上responses.properties文件。
一旦你在JAR中存放了配置文件,你就可能就想着如何得到它。如果你所想要的数据位于同一JAR文件中,可以让类的ClassLoader将该文件作为 JAR文件中的"资源"进行查找,使用ClassLoader的getResourceAsStream()方法,如清单7所示:
清单7. ClassLoader定位资源
package com.tedneward.jars;
import java.util.*;
public class SpeakEnglish
implements ISpeak
{
Properties responses = new Properties();
//
public SpeakEnglish()
{
try
{
ClassLoader myCL = SpeakEnglish.class.getClassLoader();
responses.load(
myCL.getResourceAsStream(
"com/tedneward/jars/responses.properties"));
}
catch (Exception x)
{
x.printStackTrace();
}
}
//
}
你能使用这种操作找到任何类型的资源:配置文件,音频文件,图形文件,以及你所命名的其它文件。事实上,任何文件类型都可以绑定到JAR文件中,并通过 InputStream可再获得该文件(使用ClassLoader类),然后就可以任何符合你喜好的方式来使用它们了。
结论
本文涵盖了关于JAR的多数Java开发者最不知道的5件事情--至少基于历史和轶事证据可以这么认为。注意,所有这些与JAR相关的窍门也同样适用于 WAR。有些窍门(特别是Class-Path和Main-Class属性)对于WAR不完全正确,因为Servlet环境会获取目录中的全部内容并有一个预定义的入口点。但综合来看,这些窍门还是让我们超越了这样一种范式:"好吧,让我们开始复制目录下的所有文件吧..."。除了这些,他们还使得部署 Java应用变得非常容易。
本系列的下一往篇文章是:你所不知道的五件事情--Java应用的性能监控。