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 时告诉用户应该做些什么。有许多商业的和开源的产品, 例如, Launch4jIzPack

资源

在 applet 和应用程序中使用的类通常需要使用一些相关的数据文件, 例如:

  • 图像和声音文件
  • 带有消息字符串和按钮标签的文本文件
  • 二进制数据文件, 例如, 描述地图布局的文件

在 Java 中,这些关联的文件被称为 资源(resource)

注释: 在 Windows 中, 术语“ 资源” 有着更加特殊的含义。Windows 资源也是由图像、按钮标签等组成,但是它们都附属于可执行文件, 并通过标准的程序设计访问。相比之下,Java 资源作为单独的文件存储, 并不是作为类文件的一部分存储。对资源的访问和解释由每个程序自己完成

类加载器知道如何搜索类文件,直到在类路径、 存档文件或 web 服务器上找到为止。利用资源机制, 对于非类
文件也可以同样方便地进行操作。下面是必要的步骤:

  1. 获得具有资源的 Class 对象,例如, AboutPanel.class
  2. 如果资源是一个图像或声音文件, 那么就需要调用 getResource(filename) 获得作为URL 的资源位置,然后利用 getlmagegetAudioClip 方法进行读取
  3. 与图像或声音文件不同,其他资源可以使用 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 类名为 PropertiesProperties 是线程安全的。

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 接口的 getput 方法。 不过,get 方法会返回类型 Object , 而 put 方法允许插入任何对象。 最好坚持使用 getPropertysetProperty 方法,这些方法会处理字符串, 而不是对象。

属性映射是没有层次结构的简单表。 通常会用类似 window.main.colorwindow.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() 生成一个以 “懒” 方式加载服务类的迭代器。也就是说,迭代器推进时类才会加载
posted @ 2022-04-24 21:16  流星<。)#)))≦  阅读(43)  评论(0编辑  收藏  举报