20220424 Java核心技术 卷1 基础知识 13
部署 Java 应用程序
JAR 文件
在将应用程序进行打包时,使用者一定希望仅提供给其一个单独的文件, 而不是一个含有大量类文件的目录,Java 归档( JAR ) 文件就是为此目的而设计的。一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音这些其他类型的文件。此外, JAR 文件是压缩的,
它使用了大家熟悉的 ZIP 压缩格式 。
pack200 是一种较通常的 ZIP 压缩算法更加有效的压缩类文件的方式。Oracle 声称, 对类文件的压缩率接近 90%。
创建 JAR 文件
可以使用 jar 工具制作 JAR 文件(在默认的 JDK 安装中, 位于 jdk/bin 目录下)。
jar 命令的格式如下:
jar options File File2 . . .
可以将应用程序、 程序组件 (有时称为 “beans” ) 以及代码库打包在 JAR 文件中。 例如, JDK 的运行时库包含在一个非常庞大的文件 rt.jar 中。
清 单 文 件
除了类文件、 图像和其他资源外, 每个 JAR 文件还包含一个用于描述归档特征的 清单文件(manifest)
清单文件被命名为 MANIFEST.MF , 它位于 JAR 文件的一个特殊 META-INF 子目录中
最小的符合标准的清单文件是很简单的:
Manifest-Version: 1.0
复杂的清单文件可能包含更多条目。 这些清单条目被分成多个节。 第一节被称为 主节( main section ) 。它作用于整个 JAR 文件。随后的条目用来指定已命名条目的属性,这些已命名的条目可以是某个文件、 包或者 URL。它们都必须起始于名为 Name 的条目。 节与节之间用空行分开。例如:
Manifest-Version: 1.0
描述这个归档文件的行
Name: Woozle.class
描述这个文件的行
Name: cora/mycompany/mypkg/
描述这个包的行
可执行 JAR 文件
可以使用 jar 命令中的 e 选项指定程序的人口点, 即通常需要在调用 java 程序加载器时指定的类:
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass [files to add]
或者, 可以在清单中指定应用程序的主类, 包括以下形式的语句:
Main-Class: com.mycompany.mypkg.MainAppClass
警告: 清单文件的最后一行必须以换行符结束。 否则, 清单文件将无法被正确地读取。常见的错误是创建了一个只包含 Main-Class 而没有行结束符的文本文件。
不论哪一种方法,用户可以简单地通过下面命令来启动应用程序:
java -jar MyProgram.jar
在 Windows 平台中, 可以使用第三方的包装器工具将 JAR 文件转换成 Windows 可执行文件。包装器是一个大家熟知的扩展名为 .exe 的 Windows 程序,它可以查找和加载 Java 虚拟机(JVM) 或者在没有找到 JVM 时告诉用户应该做些什么。有许多商业的和开源的产品, 例如, Launch4j 和 IzPack 。
资源
在 applet 和应用程序中使用的类通常需要使用一些相关的数据文件, 例如:
- 图像和声音文件
- 带有消息字符串和按钮标签的文本文件
- 二进制数据文件, 例如, 描述地图布局的文件
在 Java 中,这些关联的文件被称为 资源(resource)
注释: 在 Windows 中, 术语“ 资源” 有着更加特殊的含义。Windows 资源也是由图像、按钮标签等组成,但是它们都附属于可执行文件, 并通过标准的程序设计访问。相比之下,Java 资源作为单独的文件存储, 并不是作为类文件的一部分存储。对资源的访问和解释由每个程序自己完成
类加载器知道如何搜索类文件,直到在类路径、 存档文件或 web 服务器上找到为止。利用资源机制, 对于非类
文件也可以同样方便地进行操作。下面是必要的步骤:
- 获得具有资源的
Class对象,例如,AboutPanel.class - 如果资源是一个图像或声音文件, 那么就需要调用
getResource(filename)获得作为URL 的资源位置,然后利用getlmage或getAudioClip方法进行读取 - 与图像或声音文件不同,其他资源可以使用
getResourceAsStream方法读取文件中的数据
重点在于类加载器可以记住如何定位类,然后在同一位置査找关联的资源。例如,要想利用 about.gif 图像文件制作图标,可以使用下列代码:
URL url = ResourceTest.class.getResource("about.gif");
Image img = new ImageIcon(url).getImage();
这段代码的含义是 “在找到 ResourceTest 类的地方查找 about.gif 文件”。
要想读取 about.txt 文件,可以使用下列命令:
InputStream stream = ResourceTest.class.getResourceAsStream("about.txt");
Scanner in = new Scanner(stream, "UTF-8");
除了可以将资源文件与类文件放在同一个目录中外,还可以将它放在子目录中。可以使用下面所示的层级资源名:
data/text/about.txt
这是一个相对的资源名,它会被解释为相对于加载这个资源的类所在的包。注意, 必须使用 / 作为分隔符,而不要理睬存储资源文件的系统实际使用哪种目录分隔符。例如,在 Windows 文件系统中, 资源加载器会自动地将 / 转换成 \
一个以 / 开头的资源名被称为绝对资源名。它的定位方式与类在包中的定位方式一样。例如,资源
/corejava/title.txt
定位于 corejava 目录下(它可能是类路径的一个子目录,也可能位于 JAR 文件中, 对 applet 来说在 web 服务器上。
文件的自动装载是利用资源加载特性完成的。没有标准的方法来解释资源文件的内容。每个程序必须拥有解释资源文件的方法。
另一个经常使用资源的地方是程序的国际化。 与语言相关的字符串, 如消息和用户界面标签都存放在资源文件中, 每种语言对应一个文件。
java.lang.Class<T> 方法名称 |
方法声明 | 描述 |
|---|---|---|
getResource getResourceAsStream |
java.net.URL getResource(String name)InputStream getResourceAsStream(String name) |
找到与类位于同一位置的资源, 返回一个可以加载资源的 URL 或者输入流。 如果没有找到资源, 则返回 null , 而且不会抛出异常或者发生 I/O 错误 |
密封
可以将 Java 包 密封 ( seal ) 以保证不会有其他的类加入到其中,如果在代码中使用了包可见的类、方法和域,就可能希望密封包。如果不密封, 其他类就有可能放在这个包中,进而访问包可见的特性。
例如, 如果密封了 com.mycompany.util 包, 就不能用下面的语句顶替密封包之外的类:
package com.mycompany.util;
要想密封一个包,需要将包中的所有类放到一个 JAR 文件中。在默认情况下,JAR 文件中的包是没有密封的。可以在清单文件的主节中加入下面一行:
Sealed: true
来改变全局的默认设定。对于每个单独的包,可以通过在 JAR 文件的清单中增加一节, 来指定是否想要密封这个包。例如:
Name: com/mycoinpany/util/
Sealed: true
Name: com/myconpany/misc/
Sealed: false
要想密封一个包,需要创建一个包含清单指令的文本文件。然后用常规的方式运行 jar 命令:
jar cvfw MyArchive.jar manifest.mf [files to add]
应用首选项的存储
属性映射( Properties )
属性映射(property map) 是一种存储键 / 值对的数据结构。属性映射通常用来存储配置信息,它有 3 个特性:
- 键和值是字符串
- 映射可以很容易地存人文件以及从文件加载
- 有一个二级表保存默认值
实现属性映射的 Java 类名为 Properties 。 Properties 是线程安全的。
Properties settings = new Properties();
settings.setProperty("width", "200");
settings.setProperty("title", "Hello, World!");
可以使用 store 方法将属性映射列表保存到一个文件中。在这里, 我们将属性映射保存在文件 program.properties 中。第二个参数是包含在这个文件中的注释。
OutputStream out = new FileOutputStream("program.properties") ;
settings.store(out, "Program Properties");
要从文件加载属性,可以使用以下调用:
Properties settings = new Properties();
settings.setProperty("k1", "v1");
settings.setProperty("width", "xxx");
InputStream in= new FileInputStream("program.properties");
settings.load(in); // 已有相同key被覆盖,没有的仍然保留
System.out.println(settings); // {width=200, title=Hello, World!, k1=v1}
要找出用户的主目录,可以调用 System.getProperties 方法,它恰好也使用一个 Properties 对象描述系统信息。主目录包含键 usen.home 。还有一个便利方法可以读取单个键:
String userDir = System.getProperty("user.home");
可以为程序属性提供默认值, 这是一个很好的想法, 因为用户有可能手动编辑这个文件。Properties 类有两种提供默认值的机制。第一种方法是, 查找一个字符串的值时可以指定一个默认值,这样当键不存在时就会自动使用这个默认值。
String title = settings.getProperty("title", "Default title");
如果觉得在每个 getProperty 调用中指定默认值太过麻烦, 可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射
Properties defaultSettings = new Properties();
defaultSettings.setProperty("width", "300");
defaultSettings.setProperty("height", "200");
defaultSettings.setProperty("titie", "Default title");
Properties settings = new Properties(defaultSettings);
System.out.println(settings.getProperty("width")); // 300
System.out.println(settings.getProperty("width", "1234")); // 300
没错, 如果为 defaultSettings 构造器提供另一个属性映射参数, 甚至可以为默认值指定默认值, 不过一般不会这么做。
警告: 出于历史上的原因, Properties 类实现了 Map<Object,Object>。 因此, 可以使用 Map 接口的 get 和 put 方法。 不过,get 方法会返回类型 Object , 而 put 方法允许插入任何对象。 最好坚持使用 getProperty 和 setProperty 方法,这些方法会处理字符串, 而不是对象。
属性映射是没有层次结构的简单表。 通常会用类似 window.main.color 、window.main.title 等键名引入一个伪层次结构。不过 Properties 类没有提供方法来组织这样一个层次结构。 如果存储复杂的配置信息, 就应当使用 Preferences 类。
java.util.Properties 方法名称 |
方法声明 | 描述 |
|---|---|---|
| 构造器 | Properties() |
创建一个空属性映射 |
| 构造器 | Properties(Properties defaults) |
用一组默认值创建一个空属性映射 |
getProperty |
String getProperty(String key) |
获得一个属性。返回与键(key) 关联的值, 或者如果这个键未在表中出现, 则返回默认值表中与这个键关联的值, 或者如果键在默认值表中也未出现, 则返回 null |
getProperty |
String getProperty(String key, String defaultValue) |
如果键未找到, 获得有默认值的属性。返回与键关联的字符串, 或者如果键在表中未出现, 则返回默认字符串 |
setProperty |
synchronized Object setProperty(String key, String value) |
设置一个属性。返回给定键之前设置的值 |
load |
synchronized void load(InputStream inStream) throws IOException |
从一个输入流加载一个属性映射 |
store |
void store(OutputStream out, String comments) throws IOException |
将一个属性映射保存到一个输出流 |
java.lang.System 方法名称 |
方法声明 | 描述 |
|---|---|---|
getProperties |
static Properties getProperties() |
获取所有系统属性。应用必须有权限获取所有属性, 否则会拋出一个安全异常 |
getProperty |
static String getProperty(String key) |
获取给定键名对应的系统属性。应用必须有权限获取这个属性, 否则会抛出一个安全异常 |
可以在 Java 运行时目录的 security/java.policy 文件中找到可以自由访问的系统属性名
首选项 API ( Preferences )
使用属性文件有以下缺点:
- 有些操作系统没有主目录的概念, 所以很难找到一个统一的配置文件位置
- 关于配置文件的命名没有标准约定, 用户安装多个 Java 应用时,就更容易发生命名冲突
有些操作系统有一个存储配置信息的中心存储库。最著名的例子就是 Microsoft Windows 中的注册表。Preferences 类以一种平台无关的方式提供了这样一个中心存储库。在 Windows 中, Preferences 类使用注册表来存储信息;在 Linux 上, 信息则存储在本地文件系统中。当然,存储库实现对使用 Preferences 类的程序员是透明的。
Preferences 存储库有一个树状结构, 节点路径名类似于 /com/mycompany/myapp 。类似于包名, 只要程序员用逆置的域名作为路径的开头, 就可以避免命名冲突。实际上, API 的设计者就建议配置节点路径要与程序中的包名一致。
存储库的各个节点分别有一个单独的键 / 值对表, 可以用来存储数值、字符串或字节数组,但不能存储可串行化的对象。 API 设计者认为对于长期存储来说, 串行化格式过于脆弱,并不合适。当然, 如果你不同意这种看法,也可以用字节数组保存串行化对象。
为了增加灵活性,可以有多个并行的树。每个程序用户分别有一棵树;另外还有一棵系统树, 可以用于存放所有用户的公共信息。 Preferences 类使用操作系统的 “当前用户” 概念来访问适当的用户树。
若要访问树中的一个节点,需要从用户或系统根开始:
Preferences root = Preferences.userRoot();
// 或
Preferences root = Preferences.systemRoot();
然后访问节点。可以直接提供一个节点路径名:
Preferences node = root.node("/com/mycompany/myapp");
如果节点的路径名等于类的包名,还有一种便捷方式来获得这个节点。 只需要得到这个类的一个对象,然后调用:
Preferences userNode = Preferences.userNodeForPackage(this.getClass());
Preferences systemNode = Preferences.systemNodeForPackage(this.getClass());
读取信息时必须指定一个默认值, 以防止没有可用的存储库数据。 之所以必须有默认值, 有很多原因。可能由于用户从未指定过首选项, 所以没有相应的数据。某些资源受限的平台可能没有存储库, 移动设备有可能与存储库暂时断开了连接。
类似 Windows 注册表这样的中心存储库通常都存在两个问题:
- 它们会变成充斥着过期信息的 “垃圾场”
- 配置数据与存储库纠缠在一起, 以至于很难把首选项迁移到新平台
Preferences 类为第二个问题提供了一个解决方案。可以通过调用方法导出一个子树(或者比较少见的,也可以是一个节点)的首选项,数据用 XML 格式保存。也可以通过调用方法将数据导入到另一个存储库:
void exportNode(OutputStream os)
void exportSubtree(OutputStream os)
void importPreferences(InputStream is)
如果你的程序使用首选项, 要让用户有机会导出和导人首选项, 从而可以很容易地将设置从一台计算机迁移到另一台计算机。
java.util.prefs.Preferences 方法名称 |
方法声明 | 描述 |
|---|---|---|
userRoot |
static Preferences userRoot() |
返回调用程序的用户的首选项根节点 |
systemRoot |
static Preferences systemRoot() |
返回系统范围的首选项根节点 |
node |
Preferences node(String pathName) |
返回从当前节点由给定路径可以到达的节点。 如果 path 是绝对路径 (也就是说, 以一个 / 开头,) 则从包含这个首选项节点的树的根节点开始查找。 如果给定路径不存在相应的节点, 则创建这样一个节点 |
userNodeForPackage systemNodeForPackage |
static Preferences userNodeForPackage(Class<?> c) static Preferences systemNodeForPackage(Class<?> c) |
返回当前用户树或系统树中的一个节点, 其绝对节点路径对应类 cl 的包名 |
keys |
String[] keys() throws BackingStoreException |
返冋属于这个节点的所有键 |
get getInt getLong getFloat getDouble getBoolean getByteArray |
String get(String key, String def)int getInt(String key, int def)long getLong(String key, long def)float getFloat(String key, float def)double getDouble(String key, double def)byte[] getByteArray(String key, byte[] def) |
返回与给定键关联的值, 或者如果没有值与这个键关联、 关联的值类型不正确或首选项存储库不可用, 则返回所提供的默认值 |
put putInt putLong putFloat putDouble putBoolean putByteArray |
void put(String key, String value)void putInt(String key, int value)void putLong(String key, long value)void putFloat(String key, float value)void putDouble(String key, double value)void putBoolean(String key, boolean value)void putByteArray(String key, byte[] value) |
在这个节点存储一个键 / 值对 |
exportSubtree |
void exportSubtree(OutputStream os) throws IOException, BackingStoreException |
将这个节点及其子节点的首选项写至指定的流 |
exportNode |
void exportNode(OutputStream os) throws IOException, BackingStoreException |
将这个节点 (但不包括其子节点) 的首选项写至指定的流 |
importPreferences |
void importPreferences(InputStream is) throws IOException, InvalidPreferencesFormatException |
导入指定流中包含的首选项 |
服务加载器 ( ServiceLoader )
JDK 还提供了一个加载插件的简单机制
通常, 提供一个插件时, 程序希望插件设计者能有一些自由来确定如何实现插件的特性。另外还可以有多个实现以供选择。 利用 ServiceLoader 类可以很容易地加载符合一个公共接口的插件。
实现类可以放在任意包中, 而不一定是服务接口所在的包。每个实现类必须有一个无参数构造器。
现在把这些类的类名增加到 META-INF/services 目录下的一个 UTF-8 编码文本文件中,文件名必须与完全限定类名一致。
package v1ch13.serviceLoader;
public interface Cipher {
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}
META-INF/services/v1ch13.serviceLoader.Cipher
v1ch13.serviceLoader.impl.CaesarCipher
v1ch13.serviceLoader.impl.CaesarCipher2
v1ch13.serviceLoader.impl.CaesarCipher3
package v1ch13.serviceLoader;
import java.io.UnsupportedEncodingException;
import java.util.ServiceLoader;
public class MyServiceLoaderTest {
public static ServiceLoader<Cipher> cipherLoader = ServiceLoader
.load(Cipher.class);
public static void main(String[] args) throws UnsupportedEncodingException {
Cipher ciph = getCipher();
String message = "Meet me at the toga party.";
byte[] bytes = ciph.encrypt(message.getBytes(), new byte[]{3});
String encrypted = new String(bytes, "UTF-8");
System.out.println(encrypted);
}
public static Cipher getCipher() {
Cipher result = null;
for (Cipher cipher : cipherLoader) {
if (result == null || cipher.strength() >= result.strength()) {
result = cipher;
}
}
return result;
}
}
这里加载了三个实现类,通过 Cipher.strength 方法,选择返回值最大的一个实现
java.util.ServiceLoader<S> 方法名称 |
方法声明 | 描述 |
|---|---|---|
load |
static <S> ServiceLoader<S> load(Class<S> service) |
创建一个服务加载器来加载实现给定服务接口的类 |
iterator |
Iterator<S> iterator() |
生成一个以 “懒” 方式加载服务类的迭代器。也就是说,迭代器推进时类才会加载 |
浙公网安备 33010602011771号