Java外围技术全解析
1. Java系统属性
Java 提供了多种机制,帮助应用程序与本地操作系统环境进行交互,系统属性便是其中之一。系统属性以小型哈希表的形式存在,在应用程序的任何位置都可访问。这个哈希表包含了许多有用的信息,涵盖了 Java 环境和操作系统的相关内容。
1.1 获取系统属性
可以使用 java.lang.System 类的 getProperties 方法来获取完整的系统属性集:
Properties existingProperties = System.getProperties();
java.util.Properties 类继承自 java.util.Hashtable 类,它新增了一些管理方法和实用方法。管理方法可用于从文件加载属性集,实用方法则简化了对哈希表的访问。
1.2 常用系统属性
以下是一些较为常用的系统属性:
| 属性名称 | 描述 |
| — | — |
| Java.class.path | Java 类路径的值 |
| Java.io.tmpdir | 默认临时文件路径 |
| file.separator | 文件分隔符(Unix 下为 / ,Windows 下为 \ ) |
| path.separator | 路径中条目的分隔符(Unix 下为 : ,Windows 下为 ; ) |
| line.separator | 行分隔符(Unix 下为 \n ,Windows 下为 \r\n ) |
| user.home | 当前用户的主目录 |
| user.dir | 当前工作目录 |
需要注意的是,虽然可以设置 Java.class.path 属性的值,但它对当前 Java 执行没有影响,Java 虚拟机不会重新读取该值。若需要更改类路径,可能需要构建自定义类加载器。
1.3 获取单个系统属性
使用 System 类的 getProperty 方法可以获取单个系统属性的值:
assertEquals("1.5.0-beta3", System.getProperty("java.version"));
系统属性是全局的,可在代码的任何位置访问。
1.4 添加自定义系统属性
可以向系统属性列表中添加自定义的全局属性。通常,数据库更适合存储这类信息。使用属性的主要原因是在应用程序的不同执行之间动态更改少量信息。例如,部署的应用程序大部分时间可能需要以“信息亭”模式运行,循环并全屏显示,无边框和标题栏;而在开发过程中,可能需要快速切换执行模式。
添加自定义属性有以下两种方式:
- 编程方式:
System.setProperty("kioskMode", "loopFullscreen");
- 作为 VM 参数设置:
java -DkioskMode=noloopFullscreen sis.Sis
如果属性未设置, getProperty 方法将返回 null 。可以为 getProperty 方法提供一个默认值,当属性未设置时返回该默认值:
public void testGetPropertyWithDefault() {
Properties existingProperties = System.getProperties();
try {
System.setProperties(new Properties()); // 移除所有属性
assertEquals(
"default", System.getProperty("noSuchProperty", "default"));
} finally {
System.setProperties(existingProperties);
}
}
2. 属性文件
在许多项目和应用程序中,会经常遇到使用属性文件的情况。属性文件类似于资源文件,是键值对的集合,格式为 key=value 。开发人员常将属性文件用作可配置值的快速存储方式,例如驱动程序的类名、配色方案和布尔标志等都会存储在属性文件中。
2.1 属性文件的使用场景
属性文件的一个主要驱动因素是,当软件从生产环境迁移到测试环境、集成环境或其他环境时,需要更改某些值。例如,代码可能需要连接到 Web 服务器,开发环境和生产环境使用的 Web 服务器名称可能不同。
2.2 属性文件的建议使用方式
建议尽量减少属性文件的使用,或者至少减少其中的条目。属性文件中的条目与应用程序是分离的,它们代表了一堆随意聚集在一起的全局变量。更好的做法是将可配置值与受其影响的类仔细关联起来,通过使用数据库来实现这一点。
如果必须使用属性文件, Properties 类提供了从输入流加载属性的方法,输入流可以是键值文本格式或 XML 格式。但不要尝试在执行的应用程序中动态更新此文件,可考虑使用下一节讨论的 Preferences API。
3. Preferences API
对于需要在应用程序的不同执行之间保留的关键信息,应使用数据库或其他可靠的持久化机制。然而,通常希望能够快速简单地存储各种非关键信息,这些信息通常与用户的一些小偏好有关,如首选颜色、窗口位置、最近访问的网站等。即使这些信息丢失,也只会给用户带来一些不便,但不会影响其准确完成工作。
3.1 Preferences API 示例
Java 的 Preferences API 为这些数据提供了一种简单的本地持久化方案。以下是一个 PersistentFrameTest 示例:
package sis.ui;
import junit.framework.*;
import java.awt.*;
import java.util.prefs.BackingStoreException;
import static sis.ui.PersistentFrame.*;
public class PersistentFrameTest extends TestCase {
private PersistentFrame frame;
protected void setUp() throws BackingStoreException {
frame = new PersistentFrame();
frame.clearPreferences();
frame.initialize();
frame.setVisible(true);
}
protected void tearDown() {
frame.dispose();
}
public void testCreate() {
assertEquals(
new Rectangle(
DEFAULT_X, DEFAULT_Y, DEFAULT_WIDTH, DEFAULT_HEIGHT),
frame.getBounds());
}
public void testMove() {
int x = 600;
int y = 650;
int width = 150;
int height = 160;
frame.setBounds(x, y, width, height);
frame.dispose();
PersistentFrame frame2 = new PersistentFrame();
frame2.initialize();
frame2.setVisible(true);
assertEquals(
new Rectangle(x, y, width, height), frame2.getBounds());
}
}
3.2 PersistentFrame 类实现
package sis.ui;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.prefs.*;
public class PersistentFrame extends JFrame {
static final int DEFAULT_X = 100;
static final int DEFAULT_Y = 101;
static final int DEFAULT_WIDTH = 300;
static final int DEFAULT_HEIGHT = 400;
private static final String X = "x";
private static final String Y = "y";
private static final String WIDTH = "width";
private static final String HEIGHT = "height";
private Preferences preferences =
Preferences.userNodeForPackage(this.getClass()); // 1
public void initialize() {
int x = preferences.getInt(X, DEFAULT_X); // 2
int y = preferences.getInt(Y, DEFAULT_Y);
int width = preferences.getInt(WIDTH, DEFAULT_WIDTH);
int height = preferences.getInt(HEIGHT, DEFAULT_HEIGHT);
setBounds(x, y, width, height);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
saveWindowPosition();
}
});
}
private void saveWindowPosition() {
Rectangle bounds = getBounds();
preferences.putInt(X, bounds.x); // 3
preferences.putInt(Y, bounds.y);
preferences.putInt(WIDTH, bounds.width);
preferences.putInt(HEIGHT, bounds.height);
try {
preferences.flush(); // 4
} catch (BackingStoreException e) {
// not crucial; log message
}
}
// for testing
void clearPreferences() throws BackingStoreException {
preferences.clear();
preferences.flush();
}
}
3.3 Preferences 对象的创建
通过将类名传递给 Preferences 的两个工厂方法之一( userNodeForPackage 或 systemNodeForPackage )来创建 Preference 对象,分别对应于用户和系统的偏好数据树。可以使用系统偏好数据树存储特定于应用程序的信息,如属性文件的位置;使用用户偏好数据树存储特定于用户的信息,如颜色选择。
Java 将偏好存储在类似于文件系统目录树结构的树中,根据平台和具体实现,偏好的实际存储位置可能是操作系统注册表、文件系统、目录服务器或数据库。
4. 系统环境变量
在极少数情况下,可能需要了解预定义系统属性未提供的操作系统环境信息。Unix 和 Windows 操作系统都有环境变量,这也是一种键值对方案。
4.1 获取系统环境变量
在命令行提示符下,可以使用 set 命令查看环境变量。在 Java 中, System 类提供了两个 getEnv 方法,用于检索这些环境变量及其值:
Map<String,String> env = System.getenv();
for (Map.Entry entry: env.entrySet())
System.out.printf("%s->%s%n", entry.getKey(), entry.getValue());
System.out.println(System.getenv("SystemRoot")); // Unix: try "SHELL"
4.2 避免依赖系统环境变量
应尽量避免依赖系统环境变量,因为这会增加将应用程序移植到其他平台的难度。Windows 和 Unix 之间几乎没有共同的环境变量。如果需要创建自己的系统级环境变量,建议使用 Java 属性机制。
5. 执行其他应用程序
在极少数情况下,可能需要启动另一个操作系统进程。例如,让应用程序启动系统计算器应用程序(Windows 下为 calc.exe )。Java 允许执行单独的进程,但应尽量避免这样做,因为这通常会使应用程序依赖于单一操作系统。在处理多个平台时,大多数情况下需要维护条件逻辑。
5.1 创建和执行进程的方法
有两种方法可以在 Java 中创建和执行单独的进程:
- 旧技术:需要使用 java.lang.Runtime 类的 getRuntime 方法获取单例实例,然后将命令行字符串和可选参数传递给 exec 方法。
- 新技术:J2SE 5.0 引入了 ProcessBuilder 类,通过命令行字符串创建 ProcessBuilder 对象,然后调用 start 方法启动进程。
5.2 示例代码
以下是一个 CommandTest 示例:
package sis.util;
import junit.framework.TestCase;
import java.util.*;
public class CommandTest extends TestCase {
private static final String TEST_SINGLE_LINE = "testOneLine";
private static final String TEST_MULTIPLE_LINES = "testManyLines";
private static final String TEST_LOTS_OF_LINES = "testLotsLines";
private static Map<String, String> output =
new HashMap<String, String>();
private static final String COMMAND = // 1
"java " +
"-classpath \"" + System.getProperty("java.class.path") + "\" " +
"sis.util.CommandTest %s";
private Command command;
static { // 2
output.put(TEST_SINGLE_LINE, "a short line of text");
output.put(TEST_MULTIPLE_LINES, "line 1\\nline 2\\");
output.put(TEST_LOTS_OF_LINES, lotsOfLines());
}
static String lotsOfLines() {
final int lots = 1024;
StringBuilder lotsBuffer = new StringBuilder();
for (int i = 0; i < lots; i++)
lotsBuffer.append("" + i);
return lotsBuffer.toString();
}
public static void main(String[] args) { // 3
String methodName = args[0];
String text = output.get(methodName);
// redirected output:
System.out.println(text);
System.err.println(text);
}
public void testSingleLine() throws Exception {
verifyExecuteCommand(TEST_SINGLE_LINE);
}
public void testMultipleLines() throws Exception {
verifyExecuteCommand(TEST_MULTIPLE_LINES);
}
public void testLotsOfLines() throws Exception {
verifyExecuteCommand(TEST_LOTS_OF_LINES);
}
private void verifyExecuteCommand(String text) throws Exception {
command = new Command(String.format(COMMAND, text));
command.execute();
assertEquals(output.get(text), command.getOutput());
}
}
Command 类的实现如下:
package sis.util;
import java.io.*;
public class Command {
private String command;
private Process process;
private StringBuilder output = new StringBuilder();
private StringBuilder errorOutput = new StringBuilder();
public Command(String command) {
this.command = command;
}
public void execute() throws Exception {
process = new ProcessBuilder(command).start(); // 1
collectOutput();
collectErrorOutput();
process.waitFor(); // 2
}
private void collectErrorOutput() { // 3
Runnable runnable = new Runnable() {
public void run() {
try {
collectOutput(process.getErrorStream(), // 4
errorOutput);
} catch (IOException e) {
errorOutput.append(e.getMessage());
}
}
};
new Thread(runnable).start();
}
private void collectOutput() { // 5
Runnable runnable = new Runnable() {
public void run() {
try {
collectOutput(process.getInputStream(), // 6
output);
} catch (IOException e) {
output.append(e.getMessage());
}
}
};
new Thread(runnable).start();
}
private void collectOutput( // 7
InputStream inputStream, StringBuilder collector)
throws IOException {
BufferedReader reader = null;
try {
reader =
new BufferedReader(new InputStreamReader(inputStream));
String line;
while ((line = reader.readLine()) != null)
collector.append(line);
} finally {
reader.close();
}
}
public String getOutput() throws IOException {
return output.toString();
}
public String getErrorOutput() throws IOException {
return output.toString();
}
public String toString() {
return command;
}
}
5.3 注意事项
在执行新进程时,不会看到控制台窗口。捕获新进程的输出或输入稍微有些棘手,因为操作系统的缓冲区大小可能较小,必须及时处理每个输出流的数据,否则应用程序在执行时可能会挂起。同时,应避免使用命令行重定向( > 和 < )或管道( | ),因为它们会被视为命令输入字符串的一部分,操作系统无法正确处理。 ProcessBuilder 类允许向新进程传递新的环境变量和值,还可以更改进程执行的当前工作目录。此外, ProcessBuilder 提供了合并 stdout 和 stderr 流的功能,而 Runtime.exec 技术则不具备此功能。
6. Java 其他高级主题和 API
6.1 自定义类加载器
当首次访问一个类时,Java 虚拟机使用三个默认类加载器之一来加载该类。首先使用的是引导类加载器,它会在系统 JAR 文件 rt.jar 和 i18n.jar 中查找核心 Java API 库中的 Java 类。若未找到,会接着在 JRE 安装目录下的 ./lib/ext 扩展目录中查找。必要时,最后会使用类路径上的目录来加载类。
可以创建额外的自定义类加载器,它能从其他位置加载类,比如数据库、网站、FTP 站点或动态生成的字节码。若想了解更多信息,可参考文章 Create a Custom Java 1.2 - Style Classloader ,也可从 java.lang.ClassLoader 类的 API 文档开始。
6.2 弱引用
Java 的垃圾回收机制会回收不再使用的对象。但如果想编写一个监控内存使用的应用程序,为了监控对象,必须持有对它的引用,而只要监控应用程序保留该引用,垃圾回收器就无法回收该对象。同样,在实现缓存方案时,由于缓存数据结构必须引用缓存对象,垃圾回收器也无法回收这些对象,需要编写复杂的额外代码来定期管理从缓存中移除对象,否则缓存会不断增长直至出现内存不足错误。
为解决这些问题,Java 允许使用弱引用。弱引用不会影响垃圾回收,垃圾回收器会回收只有弱引用而没有强引用的对象。Java 提供了三个级别的弱引用,从最弱到最强依次为:虚引用、弱引用和软引用。虚引用可用于对象终结后的特殊清理处理;弱引用在垃圾回收时会移除被引用的对象;软引用只有在垃圾回收器确定确实需要内存时才会移除被引用的对象。
java.util.WeakHashMap 是一个使用弱键的 Map 实现,可参考其 API 文档获取更多详细信息。若想了解各种弱引用类型的更多信息,可查看 java.lang.ref 包的 Java API 文档。
6.3 finalize 方法
在 Java 中,垃圾回收器会自动回收不再被其他对象引用的对象。有时,可能希望在对象被垃圾回收时执行某些操作,这时可以考虑定义 finalize 方法:
protected void finalize() throws Throwable {
try {
// do some cleanup work
} finally {
super.finalize();
}
}
垃圾回收器在回收对象时会调用 finalize 方法,但无法保证该方法一定会被调用。即使 Java 虚拟机关闭(即应用程序退出), finalize 方法也可能不会被调用。因此,不应在 finalize 方法中编写必须执行的代码,例如在 finalize 方法中关闭文件或数据库连接不是一个好主意,因为这些资源可能永远不会被释放。
如果认为需要使用 finalize 方法,可尝试重新设计代码,或者考虑使用虚引用作为替代方案。若实现了 finalize 方法,最好确保始终调用超类的 finalize 方法。
6.4 仪器化(Instrumentation)
利用 Java 的仪器化功能,可以向 Java 类文件添加信息性字节码,这些信息可用于辅助记录日志消息、分析方法执行时间或构建代码覆盖率工具。目标是在不改变现有应用程序功能的情况下插入此类信息。
要使用仪器化,需实现 ClassFileTransformer 接口,并在 java 命令行使用 -javaagent 开关注册该实现。当类加载器尝试加载类时,会调用 ClassFileTransformer 实现中的 transform 方法,该方法的目标是返回一个表示替换类文件的字节数组。若想了解更多信息,可参考 java.lang.instrument 包的 Java API 文档。
6.5 管理 API(Management)
管理 API 允许监控和管理 Java 虚拟机,还能对虚拟机所在的操作系统进行一定程度的管理。通过管理 API,可以比较垃圾回收器的性能特征、确定可用处理器的数量、监控内存使用情况、监控线程或从外部视角监控类加载等。若想了解更多信息,可参考 java.lang.management 包的 API 文档。
6.6 网络编程(Networking)
java.net 包提供了多个类,帮助构建网络应用程序。该包基于双向通信机制(即套接字)的通用概念,支持底层通信,套接字允许访问 TCP/IP 协议,是大多数主机到主机通信的基础。
此外, java.net 包还提供了许多以 Web 编程和 URL(统一资源定位符)为中心的高级 API。若想了解更多信息,可参考 java.net 包的 API 文档。
6.7 NIO(New I/O)
Java NIO 是一种高级、高性能的输入 - 输出操作工具。如果需要提高大规模数据传输的性能,可以考虑使用 NIO。虽然 Sun 已经对许多 java.io 流进行了改造以使用 NIO,但为了实现最快的数据传输,可能需要直接与 NIO API 进行交互。
NIO 不是基于流的,而是使用通道,通道类似于流,是数据的源和汇。缓冲区包含通道读写的数据块,NIO 的主要速度提升来自于使用直接缓冲区。通常,数据会在 Java 数组和 VM 缓冲区之间复制,而直接缓冲区直接在 VM 内分配,允许代码直接访问,避免了昂贵的复制操作。
NIO 是非阻塞的。通常,执行 I/O 操作时,调用代码会被阻塞,必须等待操作完成。使用非阻塞 I/O,即使读写操作正在进行,代码也可以继续执行。可以使用非阻塞套接字通道作为套接字服务器的基础,这提高了套接字服务器的可扩展性,简化了传入连接和请求的管理。阻塞 I/O 需要谨慎使用多线程,而 NIO 允许在单个线程中管理所有传入请求。
概念上,NIO 比标准的 java.io 库更复杂,但如果性能是首要考虑因素,这种权衡是值得的。对于套接字服务器,NIO 无疑是首选。对于大多数其他需求,标准 I/O 库就足够了。若想了解更多信息,可参考 Sun NIO 文档 。
6.8 JNI(Java Native Interface)
Java 本地接口(JNI)是与外部世界交互的接口。可能需要与用 C 编写的硬件 API 交互,或者调用超出 Java 功能的操作系统例程。使用 JNI,可以调用用其他语言(包括 C++ 和 C)编写的库(Windows 下为 DLL,Unix 下为 so)。
若想了解更多信息, Sun 教程 是一个不错的起点,但可能有些过时。与任何允许与外部资源交互的 Java 技术一样,应尽量减少 JNI 的使用,以最大程度提高跨平台部署的可能性。
6.9 RMI(Remote Method Invocation)
远程方法调用(RMI)是一种 API,允许调用在另一个 Java 虚拟机上下文中执行的 Java 对象的方法。另一个虚拟机可以在同一台机器上,也可以在远程机器上。RMI 是 EJB(企业 Java Bean)中使用的分布式组件计算的基础。
RMI 使用代理设计模式。客户端对象可以通过公共接口与位于另一台机器上的服务器方法进行交互。实际上,客户端与客户端存根交互,存根将 Java 消息转换为可通过网络发送的序列化对象,然后与服务器端骨架交互,服务器端骨架的任务是将这些对象流转换为对实际服务器类的方法调用。
可以使用位于 Java bin 目录中的 RMI 编译器 rmic 生成存根和骨架的 Java 源代码。使用 RMI 可以避免客户端编写远程通信代码,对客户端代码来说,就像在同一虚拟机中与另一个 Java 对象交互一样。但网络限制会使应用程序变慢,因此在设计分布式应用程序时,主要目标是尽量减少分布式处理的量,除非必要,否则不要分布对象。
6.10 JavaBeans
JavaBeans 用于构建可插拔的 GUI 组件,但不要与企业 Java Bean(EJBs)混淆。例如,可能构建一个新的 Java 交通信号灯 GUI 控件并出售给 Java GUI 开发人员。交通信号灯 JavaBean 与其他 Java 类一样,但为了让开发人员在 JBuilder 等工具中加载和使用该控件,它必须符合 JavaBeans 规范。
要成为可视化 JavaBean,类必须继承自 java.awt.Component 并可序列化,必须通过遵循标准 Java 命名约定的访问器方法(例如 getName 用于返回 name 字段的值)公开其信息。工具基于这些约定收集 Bean 信息的能力称为内省。Bean 有一个相关的 BeanInfo 类,提供相关的 Bean 元数据。Beans 之间通过事件机制进行通信。若想了解更多信息,可从 Sun 教程 开始。
6.11 安全(Security)
Java 安全是一个可以写一本书的主题。J2SE 安全 API 位于以 java.security 开头的各个包中,包括用于证书管理、密钥库管理、策略文件、加密/解密算法和访问控制列表等的类。
Java 安全模型基于沙箱的概念,沙箱是一个可定制的虚拟空间,Java 程序可以在其中执行,而不会对底层系统或用户造成不利影响。J2SE 核心安全 API 随着 Java 的每个新版本不断发展。若想了解可用的全部信息,可参考 Java API 和版本文档。
6.12 J2EE(Java 2 Platform Extended Edition)
Java 2 平台扩展版(J2EE)是各种企业级 API 的集合,可用于创建可扩展的基于组件的应用程序。通常人们提到 J2EE 时,具体指的是企业 Java Bean(EJBs),EJBs 是组件,其目标是消除应用程序开发人员重新创建部署、安全、事务和持久化服务的需求。EJB 应用程序服务器(如 BEA 的 WebSphere、IBM 的 WebLogic 或 JBoss)是为 EJBs 提供这些服务的容器。
J2EE 大量使用 XML 作为通用数据语言,有四个以 XML 为中心的 API:Java API for XML Processing(JAXP)、Java API for XML - based RPC(JAX - RPC)、SOAP with Attachments API for Java(SAAJ)和 Java API for XML Registries(JAXR)。
另一组 J2EE API 以 Web 开发为中心,Java Servlet API 是 Web 应用程序的基础,JavaServer Pages(JSPs)简化了 Web 应用程序的表示层,JSP 标准标签库(JSTL)提供了常用的自定义标签以简化 JSP 的编写,JavaServer Faces 提供了一个框架,用于简化 Web 应用程序的模型和应用方面的开发。
其他各种 J2EE API 支持事务、资源连接和安全,Java 消息服务(JMS)为异步消息需求提供了接口。有很多关于 J2EE 的书籍, J2EE 教程 是一个不错的起点。在进入极其复杂的 J2EE 世界之前,必须从功能需求、开发成本、复杂性要求、性能需求和可扩展性需求等方面证明决策的合理性。许多公司在 J2EE 上投入了大量资金,却发现他们并不需要它提供的大部分功能,通常自己的自定义实现就能满足需求,而且更简单、更灵活。
6.13 更多 Java API
还有数十个其他 Java API,支持各种功能,如图像处理、语音、小程序、密码学、共享数据、电话、可访问性和自动 Web 安装等。Java 是一项根深蒂固的技术,短期内不会消失,几乎对于每一种计算需求,都有相应的 Java API。可以从 Sun 的 Java 网站 http://java.sun.com 开始,然后使用 Google 搜索更多信息。
综上所述,Java 提供了丰富的功能和多样的 API 来满足不同的开发需求。在开发过程中,应根据具体需求合理选择和使用这些技术,同时要考虑跨平台部署、性能等因素,尽量避免引入不必要的依赖,以构建高效、稳定的应用程序。
以下是一个简单的 mermaid 流程图,展示了 Java 不同技术的选择流程:
graph LR
A[开发需求] --> B{是否需要与外部资源交互}
B -- 是 --> C{是否跨平台重要}
C -- 是 --> D[尽量避免 JNI]
C -- 否 --> E[可考虑使用 JNI]
B -- 否 --> F{是否需要高性能数据传输}
F -- 是 --> G[NIO]
F -- 否 --> H{是否需要分布式处理}
H -- 是 --> I[RMI]
H -- 否 --> J{是否构建 GUI 组件}
J -- 是 --> K[JavaBeans]
J -- 否 --> L{是否企业级应用}
L -- 是 --> M[J2EE]
L -- 否 --> N[根据具体功能选其他 API]
这个流程图展示了在面对不同开发需求时,如何选择合适的 Java 技术。通过这种方式,可以更清晰地了解各种技术的适用场景,帮助开发者做出更合理的决策。
超级会员免费看

1万+

被折叠的 条评论
为什么被折叠?



