Java核心技术1-部署

  |  

摘要: 本文是《Java核心技术 10th》中关于部署的要点总结。

【对算法,数学,计算机感兴趣的同学,欢迎关注我哈,阅读更多原创文章】
我的网站:潮汐朝夕的生活实验室
我的公众号:算法题刷刷
我的知乎:潮汐朝夕
我的github:FennelDumplings
我的leetcode:FennelDumplings


本文是《Java核心技术1》第10版 Chap13 中关于打包、部署的要点总结。主要内容如下:

  • JAR
    • 创建 JAR 文件
    • 清单文件
    • 可执行 JAR 文件
    • 资源
    • 密封
  • 应用首选项 (Application Preferences)
    • 属性映射
    • 首选项 API
  • 服务加载器

JAR

在将应用程序进行打包时,使用者一定希望仅提供给其一个单独的文件,而不是一个含有大量类文件的目录,Java 归档(JAR)文件就是为此目的而设计的。一个 JAR 文件既可以包含类文件,也可以包含诸如图像和声音这些其他类型的文件。此外,JAR 文件是压缩的,使用的 ZIP 压缩格式。

创建 JAR 文件

jar 工具,位于 jdk/bin 目录下,命令为:

1
jar cvf JARFileName File1 File2 ...

例如:

1
jar cvf CalculatorClasses.jar *.class icon.gif

jar 的可选项如下:

选项 说明
c 创建一个新的或者空的存档文件并加入文件。如果指定的文件是目录,jar 会对它们递归处理。
C 暂时改变目录,例如 jar cvf JARFileName.jar -C classes *.class
e 在清单文件中创建一个条目。
f 将 JAR 文件名指定为第二个命令行参数。
i 建立索引文件,加快对大型归档的查找。
m 将一个清单文件(manifest)添加到 JAR 中。
M 不为条目创建清单文件。
t 显示内容表。
u 更新一个已有的 JAR 文件。
v 生成详细的输出结果。
x 解压文件。
0 不进行 zip 压缩

可以将应用程序、程序组件(也称为 beans,参考第 2 卷第 11 章),以及代码库打包在 JAR 文件中。

清单文件

除了类文件、图像和其他资源外,每个 JAR 文件还包含一个用于描述归档特征的清单文件(manifest)

清单文件被命名为 MANIFEST.MF,它位于 JAR 文件的一个特殊 META-INF 子目录中。最小的符合标准的清单文件如下:

1
Manifest-Version: 1.0

清单条目被分成多个节。第一节被称为主节(main section)。它作用于整个 JAR 文件。随后的条目用来指定已命名条目的属性,这些已命名的条目可以是某个文件、包或者 URL。它们都必须起始于名为 Name 的条目。节与节之间用空行分开。

例如:

1
2
3
4
5
6
7
Manifest-Version: 1.0

Name: Woozle.class
描述这个文件的行

Name: com/mycompany/mypkg/
描述这个包的行
  • 编辑清单文件的方法:将希望添加到清单文件中的行放到文本文件中,然后运行:
1
jar cfm JARFileName ManifestFileName ...
  • 创建一个包含清单的 JAR 文件:
1
jar cfm MyArchive.jar manifest.mf com/mycompany/mypkg/*.class
  • 更新一个已有的 jar 文件的清单的方法:将需要增加的部分放到文本文件中,然后执行:
1
jar ufm MyArchive.jar manifest-additions.mf

关于 JAR 文件以及清单文件格式等更详细的信息参考 这个网页

可执行 JAR 文件

可以用 jar 的 e 选项指定程序的入口点,即调用 java 程序加载器时指定的类。例如:

1
jar cvfe MyProgram.jar com.mycompany.mypkg.MainAppClass files_to_add

可以在清单文件中指定应用程序的主类

1
Main-Class: com.mycompany.mypkg.MainAppClass

清单文件的最后一行必须以换行符结束。否则,清单文件将无法被正确地读取。

不论那种方法,用户均可以通过下面命令启动应用程序:

1
java -jar MyProgram.jar

在 Windows 平台中,可以使用第三方的包装器工具将 JAR 文件转换成 Windows 可执行文件。包装器是一个大家熟知的扩展名为 .exe 的 Windows 程序,它可以查找和加载 Java 虚拟机(JVM),或者在没有找到 JVM 时告诉用户应该做些什么。

资源

应用程序中使用的类通常需要一些相关的数据文件,称为资源,例如:

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

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

(1) 获得具有资源的 Class 对象。
(2) 如果资源是一个图像或声音文件,就需要调用 getresource(filename) 获得作为 URL 的资源位置,然后利用 getImagegetAudioClip 方法读取。
(3) 与图像或声音文件不同,其它资源可以用 getResourceAsStream 方法读取文件中的数据。

类加载器可以记住如何定位类,然后在同一位置查找关联的资源。除了可以将资源文件与类文件放在同一个目录中外,还可以将它放在子目录中。

文件的自动装载是利用资源加载特性完成的。没有标准的方法来解释资源文件的内容。每个程序必须拥有解释资源文件的方法。

例子: 资源加载、编译、创建 JAR 文件、执行 JAR 文件

(1) 代码中处理资源的加载

在代码中加载资源,主要涉及 java.lang.Class 类的下面两个方法:

1
2
3
4
// 找到与类位于同一位置的资源,返回一个可以加载资源的 URL 或者输入流。
// 如果没有找到资源,则返回 null,而且不会抛出异常或者发生 I/O 错误。
URL getResource(String name)
InputStream getResourceAsStream(String name)

下面的注释中是资源加载的代码的位置,重点关注:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import java.awt.*;
import java.io.*;
import java.net.*;
import java.util.*;
import javax.swing.*;

public class ResourceTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
JFrame frame = new ResourceTestFrame();
frame.setTitle("ResourceTest");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}

/**
* 加载图像和文本资源的 frame
*/
class ResourceTestFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 300;

public ResourceTestFrame() {
setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
URL aboutURL = getClass().getResource("about.gif");
Image img = new ImageIcon(aboutURL).getImage();
setIconImage(img);

JTextArea textArea = new JTextArea();
InputStream stream = getClass().getResourceAsStream("about.txt");
try(Scanner in = new Scanner(stream, "UTF-8")) {
while(in.hasNext()) {
textArea.append(in.nextLine() + "\n");
}
}
add(textArea);
}
}

(2) 编译

1
javac resource/ResourceTest.java

(3) 清单文件

1
2
3
4
5
6
Manifest-Version: 1.0
Main-Class: resource.ResourceTest

Name: ResourceTest.class

Name: ResourceTestFrame.class

(4) 创建 JAR 文件

1
jar cvfm ResourceTest.jar resource/ResourceTest.mf resource/*class resource/*.gif resource/*.txt

命令行输出如下:

1
2
3
4
5
added manifest
adding: resource/ResourceTest.class(in = 1147) (out= 614)(deflated 46%)
adding: resource/ResourceTestFrame.class(in = 1920) (out= 1041)(deflated 45%)
adding: resource/about.gif(in = 28969) (out= 28777)(deflated 0%)
adding: resource/about.txt(in = 11) (out= 13)(deflated -18%)

(5) 执行 JAR 文件

1
java -jar ResourceTest.jar

密封

可以将 Java 包密封(seal)以保证不会有其他的类加入到其中。如果在代码中使用了包可见的类、方法和域,就可能希望密封包。如果不密封,其他类就有可能放在这个包中,进而访问包可见的特性。

要想密封一个包,需要将包中的所有类放到一个 JAR 文件中。在默认情况下,JAR 文件中的包是没有密封的。可以在清单文件的主节中加入下面一行来改变全局的默认设定:

1
Sealed: true

对于每个单独的包,可以通过在 JAR 文件的清单中增加一节,来指定是否想要密封这个包,例如:

1
2
3
4
5
Name: com/mycompany/util/
Sealed: true

Name: com/mycompany/misc/
Sealed: false

之后按常规方式运行 jar 命令即可:

1
jar cvfm MyArchive.jar manifest.mf files_to_add

应用首选项的存储

应用用户通常希望能保存他们的首选项和定制信息,以后再次启动应用时再恢复这些配置。

有两种做法:第一种是将配置信息保存在属性文件中;第二种是首选项 API。

属性映射

属性映射(property map)是一种存储键/值对的数据结构,通常存储配置信息,有 3 个特性:

  1. 键和值是字符串
  2. 映射可以很容易地存入文件以及从文件加载
  3. 有一个二级表保存默认值

实现属性映射的类为 java.util.Properties

属性映射是没有层次结构的简单表,通常会用类似 window.main.color、window.main.title 等键名引入一个伪层次结构。

不过 Properties 类没有提供方法来组织这样一个层次结构。如果存储复杂的配置信息,就应当使用 Preferences 类。

java.util.Properties API 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//  创建一个空属性映射。
Properties()

// 用一组默认值创建一个空属性映射。参数:defaults 用于查找的默认值。
Properties(Properties defaults)

// 获得一个属性。返回与键(key)关联的值
// 如果这个键未在表中出现,则返回默认值表中与这个键关联的值
// 如果键在默认值表中也未出现,则返回 null。
// 参数:
// key 要获得相关字符串的键。
// defaultValue: 键未找到时返回的字符串
String getProperty(String key)
String getProperty(String key, String defaultValue)

// 设置一个属性。返回给定键之前设置的值。参数:
// key: 要设置相关字符串的键
// value: 与键关联的值
Object setProperty(String key, String value)

// 从一个输入流加载一个属性映射。参数:in 输入流
void load(InputStream in) throws IOException

// 将一个属性映射保存到一个输出流。参数:
// out: 输出流
// header: 存储文件第一行的标题
void store(OutputStream out, String header)

java.lang.System API 总结

1
2
3
4
5
6
7
8
9
10
11
// 获取所有系统属性。应用必须有权限获取所有属性,否则会抛出一个安全异常。
Properties getProperties()

// 获取给定键名对应的系统属性。应用必须有权限获取这个属性,否则会抛出一个安全异常。以下属性总是允许获取:
// java.version, java.vendor, java.vendor.url
// java.class.version, os.name, os.version, os.arch
// file.separator, path.separator, line.separator
// java.specification.version, java.vm.specification.version
// java.vm.specification.vendor, java.vm.specification.name
// java.vm.version, java.vm.vendor, java.vm.name
String getProperty(String key)

例子: 属性映射的使用

使用属性来存储和加载程序状态。程序会记住框架位置、大小和标题。也可以手动编辑相应的 .properties 文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
import java.awt.EventQueue;
import java.awt.event.*;
import java.io.*;
import java.util.Properties;

import javax.swing.*;

public class PropertiesTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
PropertiesFrame frame = new PropertiesFrame();
frame.setVisible(true);
});
}
}

class PropertiesFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;

private File propertiesFile;
private Properties settings;

public PropertiesFrame() {
// 从 Properties 文件中获取位置,大小和 title
String userDir = System.getProperty("user.home");
File propertiesDir = new File(userDir, ".corejava");
if(!propertiesDir.exists()) {
propertiesDir.mkdir();
}
propertiesFile = new File(propertiesDir, "program.properties");

Properties defaultSettings = new Properties();
defaultSettings.setProperty("left", "0");
defaultSettings.setProperty("top", "0");
defaultSettings.setProperty("width", "" + DEFAULT_WIDTH);
defaultSettings.setProperty("height", "" + DEFAULT_HEIGHT);
defaultSettings.setProperty("title", "");

settings = new Properties(defaultSettings);

if(propertiesFile.exists()) {
try(InputStream in = new FileInputStream(propertiesFile)) {
settings.load(in);
} catch (IOException ex) {
ex.printStackTrace();
}
}

int left = Integer.parseInt(settings.getProperty("left"));
int top = Integer.parseInt(settings.getProperty("top"));
int width = Integer.parseInt(settings.getProperty("width"));
int height = Integer.parseInt(settings.getProperty("height"));
setBounds(left, top, width, height);

// 如果没有给定 title,则对用户提出询问
String title = settings.getProperty("title");
if(title.equals("")) {
title = JOptionPane.showInputDialog("Please supply a frame title: ");
}
if(title == null) {
title = "";
}
setTitle(title);

addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent event) {
settings.setProperty("left", "" + getX());
settings.setProperty("top", "" + getY());
settings.setProperty("width", "" + getWidth());
settings.setProperty("height", "" + getHeight());
settings.setProperty("title", getTitle());

try (OutputStream out = new FileOutputStream(propertiesFile)) {
settings.store(out, "Program Properties");
} catch (IOException ex) {
ex.printStackTrace();
}
System.exit(0);
}
});
}
}

首选项 API

利用 Properties 类可以很容易地加载和保存配置信息。不过,使用属性文件有以下缺点:

  • 有些操作系统没有主目录的概念,所以很难找到一个统一的配置文件位置。
  • 关于配置文件的命名没有标准约定,用户安装多个 Java 应用时,就更容易发生命名冲突。

有些操作系统有一个存储配置信息的中心存储库。最著名的例子就是 Microsoft Windows 中的注册表。Preferences 类以一种平台无关的方式提供了这样一个中心存储库。在 Windows 中,Preferences 类使用注册表来存储信息;在 Linux 上,信息则存储在本地文件系统中。

Preferences 存储库有一个树状结构,节点路径名类似于 /com/mycompany/myapp。类似于包名,只要程序员用逆置的域名作为路径的开头,就可以避免命名冲突。

为了增加灵活性,可以有多个并行的树。每个程序用户分别有一棵树;另外还有一棵系统树,可以用于存放所有用户的公共信息。Preferences 类使用操作系统的“当前用户”概念来访问适当的用户树。

若要访问树中的一个节点,需要从用户或系统根开始:Preferences root = Preferences.userRoot();Preferences root = Preferences.userRoot();

然后访问节点。可以直接提供一个节点路径名:Preferences node = root.node("/com/mycompany/myapp");

一旦得到了节点,可以用一系列 get 方法访问键/值表。读取信息时必须指定一个默认值,以防止没有可用的存储库数据。相对应地,可以用 put 方法向存储库写数据。

可以用 key() 方法枚举一个节点中存储的所有键。

可以通过调用 exportSubtree(), exportNode() 方法导出一个子树的首选项,数据用 XML 格式保存。实例文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE preferences SYSTEM "http://java.sun.com/dtd/preferences.dtd">
<preferences EXTERNAL_XML_VERSION="1.0">
<root type="user">
<map/>
<node name="com">
<map/>
<node name="horstmann">
<map/>
<node name="corejava">
<map>
<entry key="left" value="11"/>
<entry key="top" value="9"/>
<entry key="width" value="453"/>
<entry key="height" value="365"/>
<entry key="title" value="Hello, World!"/>
</map>
</node>
</node>
</node>
</root>
</preferences>

相对应地,可以通过调用 importPreferences() 方法将数据导入到另一个存储库。

让用户有机会导出和导入首选项,可以很容易地将设置从一台计算机迁移到另一台计算机。

java.util.prefs.Preferences API 总结

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 返回调用程序的用户的首选项根节点。
Preferences userRoot()

// 返回系统范围的首选项根节点。
Preferences systemRoot()

// 返回从当前节点由给定路径可以到达的节点。
// 如果 path 是绝对路径(也就是说,以一个/开头),则从包含这个首选项节点的树的根节点开始查找。
// 如果给定路径不存在相应的节点,则创建这样一个节点。
Preferences node(String path)

// 返回当前用户树或系统树中的一个节点,其绝对节点路径对应类 cl 的包名。
Preferences userNodeForPackage(Class cl)
Preferences systemNodeForPackage(Class cl)

// 返回属于这个节点的所有键。
String[] keys()

// 返回与给定键关联的值,或者如果没有值与这个键关联、关联的值类型不正确或首选项存储库不可用,则返回所提供的默认值。
String get(String key, String defval)
int getInt(String key, int defval)
long getLong(String key, long defval)
float getFloat(String key, float defval)
double getDouble(String key, double defval)
boolean getBoolean(String key, boolean defval)
byte[] getByteArray(String key, byte[] defval)

// 在这个节点存储一个键/值对。
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)

// 将这个节点及其子节点的首选项写至指定的流。
void exportSubtree(OutputStream out)

// 将这个节点(但不包括其子节点)的首选项写至指定的流。
void exportNode(OutputStream out)

// 导入指定流中包含的首选项。
void importPreferences(InputStream in)

例子: 首选项API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
import java.awt.*;
import java.io.*;
import java.util.prefs.*;

import javax.swing.*;
import javax.swing.filechooser.*;

public class PreferencesTest {
public static void main(String[] args) {
EventQueue.invokeLater(() -> {
PreferencesFrame frame = new PreferencesFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
});
}
}

class PreferencesFrame extends JFrame {
private static final int DEFAULT_WIDTH = 300;
private static final int DEFAULT_HEIGHT = 200;
private Preferences root = Preferences.userRoot();
private Preferences node = root.node("/com/horstmann/corejava");

public PreferencesFrame() {
// 从 Preferences 中获取位置、大小、title
int left = node.getInt("left", 0);
int top = node.getInt("top", 0);
int width = node.getInt("width", DEFAULT_WIDTH);
int height = node.getInt("height", DEFAULT_HEIGHT);
setBounds(left, top, width, height);

String title = node.get("title", "");
if(title.equals("")) {
title = JOptionPane.showInputDialog("Please supply a frame title: ");
}
if(title == null) {
title = "";
}
setTitle(title);

final JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
chooser.setFileFilter(new FileNameExtensionFilter("XML files", "xml"));

JMenuBar menuBar = new JMenuBar();
setJMenuBar(menuBar);
JMenu menu = new JMenu("File");
menuBar.add(menu);

JMenuItem exportItem = new JMenuItem("Export preferences");
menu.add(exportItem);
exportItem.addActionListener(event -> {
//
if(chooser.showSaveDialog(PreferencesFrame.this) == JFileChooser.APPROVE_OPTION) {
try {
savePreferences();
OutputStream out = new FileOutputStream(chooser.getSelectedFile());
node.exportSubtree(out);
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});

JMenuItem importItem = new JMenuItem("Import preferences");
menu.add(importItem);
importItem.addActionListener(event -> {
if(chooser.showOpenDialog(PreferencesFrame.this) == JFileChooser.APPROVE_OPTION) {
try {
InputStream in = new FileInputStream(chooser.getSelectedFile());
Preferences.importPreferences(in);
in.close();
} catch (Exception e) {
e.printStackTrace();
}
}
});

JMenuItem exitItem = new JMenuItem("Exit");
menu.add(exitItem);
exitItem.addActionListener(event -> {
savePreferences();
System.exit(0);
});
}

public void savePreferences() {
node.putInt("left", getX());
node.putInt("top", getY());
node.putInt("width", getWidth());
node.putInt("height", getHeight());
node.put("title", getTitle());
}
}

服务加载器

服务加载器是 JDK 提供的一个加载插件的简单机制。ServiceLoader 类可以加载一个符合公共接口的插件。

定义一个接口(也可以定义超类),其中包含服务的各个实例需要提供的方法。

1
2
3
4
5
public interface Cipher {
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}

服务提供者可以提供一个或多个实现这个服务的类,例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class CaesarCipher implements Cipher {
public byte[] encrypt(byte[] source, byte[] key) {
byte[] result = new byte[source.length];
for(int i = 0; i < source.length; i++) {;
result[i] = (byte)(source[i] + key[0])
}
return result;
}

public byte[] decrypt(byte[] source, byte[] key) {
return encrypt(source, new byte[] { (byte) -key[0] });
}

public int strength() {
return 1;
}
}

现在把这些类的类名增加到 META-INF/services 目录下的一个 UTF-8 编码文本文件中,文件名必须与接口的完全限定类名一致。

在上面的例子中,文件名为 META-INF/services/serviceLoader.Cipher,内容如下:

1
serviceLoader.impl.CaesarCipher

然后就可以在程序中初始化一个服务加载器:

1
public static ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher.class);

服务加载器的 iterator 方法会对服务提供的所有实现返回一个迭代器。

下面的代码中在 for 循环中选择一个适当对象完成服务。

1
2
3
4
5
6
7
8
public static Cipher getCipher(int minStrength) {
for(Cipher cipher: cipherLoader) {
if(cipher.strength() >= minStrength) {
return cipher;
}
}
return null;
}

完整例子

源文件

我们介绍了类加载器的使用流程,这里把前面的例子整理一下,此时项目的文件结构如下:

1
2
3
4
5
6
7
8
├── serviceLoader/
│ ├── impl/
│ │ └── CaesarCipher.java
│ ├── Cipher.java
│ └── Main.java
└── META-INF/
└── services/
└── serviceLoader.Cipher
  • serviceLoader/Cipher.java
1
2
3
4
5
6
7
package serviceLoader;

public interface Cipher {
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}
  • serviceLoader/CaesarCipher.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package serviceLoader.impl;

public class CaesarCipher implements Cipher {
public byte[] encrypt(byte[] source, byte[] key) {
byte[] result = new byte[source.length];
for(int i = 0; i < source.length; i++) {;
result[i] = (byte)(source[i] + key[0]);
}
return result;
}

public byte[] decrypt(byte[] source, byte[] key) {
return encrypt(source, new byte[] { (byte) -key[0] });
}

public int strength() {
return 1;
}
}
  • serviceLoader/Main.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package serviceLoader;

import java.util.ServiceLoader;

public class Main {
public static ServiceLoader<Cipher> cipherLoader = ServiceLoader.load(Cipher.class);

public static void main(String[] args) {
System.out.println(cipherLoader);
System.out.println("---");
Cipher cipher = getCipher(0);
System.out.println(cipher);
}


public static Cipher getCipher(int minStrength) {
for(Cipher cipher: cipherLoader) { // 隐含调用 cipherLoader.iterator()
if(cipher.strength() >= minStrength) {
return cipher;
}
}
return null;
}
}
  • META-INF/services/serviceLoader.Cipher
1
serviceLoader.impl.CaesarCipher

编译和执行

进入到根目录。

(1) 编译接口文件:

1
javac serviceLoader/Cipher.java

(2) 编译实现文件:

1
javac serviceLoader/impl/CaesarCipher.java

(3) 将实现类打包:

1
jar cvf mycipher.jar META-INF/ serviceLoader/impl/

此时 JAR 包下的内容(通过 jar tvf mycipher.jar 查看)如下:

1
2
3
4
5
6
7
META-INF/
META-INF/MANIFEST.MF
serviceLoader/impl/
serviceLoader/impl/CaesarCipher.java
serviceLoader/impl/CaesarCipher.class
META-INF/services/
META-INF/services/serviceLoader.Cipher

(4) 编译客户端文件:

1
javac serviceLoader/Main.java

(5) 设置依赖的实现类

1
export CLASSPATH=.:mycipher.jar

(6) 执行

1
java serviceLoader.Main

结果如下:

1
2
3
java.util.ServiceLoader[serviceLoader.Cipher]
---
serviceLoader.impl.CaesarCipher@5caf905d

完整脚本:

1
2
3
4
5
6
javac serviceLoader/Cipher.java
javac serviceLoader/impl/CaesarCipher.java
jar cvf mycipher.jar META-INF/ serviceLoader/impl/
javac serviceLoader/Main.java
export CLASSPATH=.:mycipher.jar
java serviceLoader.Main

java.util.ServiceLoader<S> API 总结

1
2
3
4
5
// 创建一个服务加载器来加载实现给定服务接口的类。
static <S> ServiceLoader<S> load(Class<S> service)

// 生成一个以“懒”方式加载服务类的迭代器。也就是说,迭代器推进时类才会加载。
Iterator<S> iterator()

Share