铁文整理
10.4 应用程序存储的配置
应用程序的用户通常期待能够自行对应用程序进行配置,并能够将其保存起来。日后再次运行这个应用程序时将能够读取这些配置。下面首先介绍一种直接将配置信息存储在属性文件中的传统方式。然后,再介绍Java SE 1.4中的一种功能更加强大的机制。
10.4.1 属性映射
属性映射(property map)是一种存储键/值对的数据结构。属性映射经常被用来存放配置信息&它有三个特性:
-
键和值都是字符串
-
键/值对可以很容易地写入文件或从文件读出
-
用二级表存放默认值。
实现属性映射的Java类被称为Properties。
属性映射对指定程序的配置选项非常有用。例如:
Properties settings = new Properties();
settings.put("width", "200");
settings.put("title", "Hello, World!");
可以使用store方法将这个属性映射列表保存到文件中。在这里,将属性映射保存在
Myprog.properties文件中,第二个参数是包含到这个文件的注释。
FileOutputStream out = new FileOutputStream("Myprog.propefties");
settings.store(out, "Program Properties");
上述示例将会输出如下结果:
#Program Properties
#Mon Apr 30 07:22:52 2007
width=200
title=Hello, World!
要想从文件中加载这些属性,可以使用:
FileInputStream in = new FileInputStream("Myprog.properties");
settings.load(in);
习惯上,将程序属性存储在用户主目录的某个子目录下。目录名通常选择由一个(在UNIX系统中)圆点开始。这个约定表示来自用户的一个隐藏的系统目录。在示例程序中将遵守这个约定。
要想査看用户的主目录,可以调用System.getProperties方法。很巧,还可以使用Properties对象描述系统信息。主目录包含键user.home。还有一个很有用的方法,它用于读取单键:
String userDir = System.getProperty("user.home");
一旦用户手工地编辑文件,那么为应用程序提供默认值就是一种很好的想法。Properties类有两种提供默认值的机制。第一种是在试图获得字符串值时指定默认值,当键值不存在时,就会自动地使用它。
String title = settings.getProperty("title", "Default title");
如果在属性映射中有title属性,则title将设置成字符串。否则,title将设置成"Default title"。
如果觉得在每次调用getProperty方法时指定默认值太麻烦,那么就可以将所有的默认值放在一个二级属性映射中,并在主属牲映射的构造器中提供映射。且用它来构造查询表。
Properties defaultSettings = new Properties();
defaultSettings.put("width", "300");
defaultSettings.put("height", "200");
defaultSettings.put("title", "Default title");
Properties settings = new Properties(defaultSettings);
甚至,如果为defaultSettings的构造器提供另一个属性映射参数,可以为默认值指定默认值。但是一般不会这样做。例10-7展示了利用这些属性存储和加载程序状态的方式。程序将记住框架的位置、大小和标题。还可以手工地编辑主目录中的文件.corejava/program.properties,用以将程序的外观变成所希望的那样。
注释:属性映射是一个没有层次结构的简单表。通过引入window.main.color、window.main.title等键名可以伪装层次结构。但是Properties类没有提供组织这种层次结构的方法。如果存储复杂的配置信息,就应该使用Preferences类,请看下—节。
例10-7 PropertiesTest.java
import java.awt.EventQueue;
import java.awt.event.*;
import java.io.*;
import java.util.Properties;
import javax.swing.*;
/**
* A program to test properties. The program remembers the frame position, size,
* and title.
*
* @version 1.00 2007-04-29
* @author Cay Horstmann
*/
public class PropertiesTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
PropertiesFrame frame = new PropertiesFrame();
frame.setVisible(true);
}
});
}
}
/**
* A frame that restores position and size from a properties file and updates
* the properties upon exit.
*/
class PropertiesFrame extends JFrame {
public PropertiesFrame() {
// get position, size, title from properties
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.put("left", "0");
defaultSettings.put("top", "0");
defaultSettings.put("width", "" + DEFAULT_WIDTH);
defaultSettings.put("height", "" + DEFAULT_HEIGHT);
defaultSettings.put("title", "");
settings = new Properties(defaultSettings);
if (propertiesFile.exists())
try {
FileInputStream 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);
// if no title given, ask user
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.put("left", "" + getX());
settings.put("top", "" + getY());
settings.put("width", "" + getWidth());
settings.put("height", "" + getHeight());
settings.put("title", getTitle());
try {
FileOutputStream out = new FileOutputStream(propertiesFile);
settings.store(out, "Program Properties");
} catch (IOException ex) {
ex.printStackTrace();
}
System.exit(0);
}
});
}
private File propertiesFile;
private Properties settings;
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
API:java.util.Properties 1.0
-
Properties():创建一个空的属性映射。
-
Properties(Properties defaults):用一组默认值创建空的属性映射。
-
String getProperty(String key):获得一个属性映射。返回对应键的字符串。如果键没有在表中出现,返回在默认值表中与键对应的字符串。如果键也没有出现在默认值表中,则返回null。
-
String getProperty(String key, String defaultValue):搜索默认值表吗?
-
void load(InputStreani in) throws IOException
-
void store(OutputStream outf String header) 1.2
API:java.lang.System 1.0
-
Properties getProperties():获得全部系统属性。应用程序必须有访问全部系统属性的权限,否则将抛出安全异常。
-
String getProperty(String key):返回具有给定键名的系统属性。应用程序必须有访问全部系统属性的权限,否则将抛出安全异常。下列特性无论怎样都可以得到:
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.nane
注释:可以在Java运行时的目录下的security/java.policy文件中找到免费访问系故特性的名字。
10.4.2 Preferences API
正如读者所看到的,Properties类能够简化读取和保存配置信息的过程。但是,使用属性文件存在下列不足:
-
酡置文件不能存放在用户的主目录中。这是因为某些操作系统(如Windows 9x)没有主目录的概念。
-
没有标准的为配置文件命名的规则。当用户安装了多个Java应用程序时,会増加配置文件名冲突的可能性。
有些操作系统提供一个存储配置信息的中心知识库。其中广为人知的就是Windows。Java SE 1.4中的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 node = Preferences.userNodeForPackage(obj.getClass());
或者
Preferences node - Preferences.systefflNodeForPackage(obj.getClass());
通常情况下,obj就是this引用。
—旦获得了节点,就可以使用下列方法访问键/值表。
String get(String key, String defval);
int getInt(String key, int defval);
long getLong(String key, long defval);
float getFloat(String key, float defva1);
double getDouble(String key, double defval);
boolean getBoolean(String key, boolean defval);
byte[] getByteArray(String key, byte[] defval);
注意,在读取信息时,必须指定默认值,以防止中心知识库中的值不可用。有几种情况需要默认值。由于用户始终没有指定配置信息,所以数据可能找不到。在某些对资源受限的平台上可能没有中心知识库,并且移动设备可能暂时与中心知识库断开连接。
与之对应,可以利用put方法将数据写入中心知识库。
使用下列方法可以枚举出节点中全部的键:
String[] keys
但是目前无法査看给定键值的类型。
类似于Windows注册信息表这样的中心知识库通常会遇到下面两个问题:
-
其中添满了陈旧的信息,成为了“垃圾场”。
-
配置数据与中心知识库相关,使得数据不易迁移至新平台上。
Preferences类解决了第二个问题。可以调用下列方法将子树(或者一个节点)的全部值显示输出。
void exportSubtree(OutputStream out);
void exportNode(OutputStream out);
数据以XML格式存储。可以调用下列方法将它们导入到另一个中心知识库中:
void importPrefererces(InputStream in);
下面是一个示例文件:……
如果程序使用配置信息,那么应该允许用户导入和导出,这样就可以将这些信息从一台机器上迁移到另一台上。例10-8程序演示了这个技术。程序只保存主窗口的位置,大小和标题。然后改变窗口的大小,退出并重启应用程序。窗口的大小和位置将与关闭窗口时一样。
例10-8 PreferencesTest.java
import java.awt.EventQueue;
import java.awt.event.*;
import java.io.*;
import java.util.prefs.*;
import javax.swing.*;
/**
* A program to test preference settings. The program remembers the frame
* position, size, and title.
*
* @version 1.02 2007-06-12
* @author Cay Horstmann
*/
public class PreferencesTest {
public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
public void run() {
PreferencesFrame frame = new PreferencesFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
}
});
}
}
/**
* A frame that restores position and size from user preferences and updates the
* preferences upon exit.
*/
class PreferencesFrame extends JFrame {
public PreferencesFrame() {
// get position, size, title from preferences
Preferences root = Preferences.userRoot();
final Preferences node = root.node("/com/horstmann/corejava");
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);
// if no title given, ask user
String title = node.get("title", "");
if (title.equals(""))
title = JOptionPane.showInputDialog("Please supply a frame title:");
if (title == null)
title = "";
setTitle(title);
// set up file chooser that shows XML files
final JFileChooser chooser = new JFileChooser();
chooser.setCurrentDirectory(new File("."));
// accept all files ending with .xml
chooser.setFileFilter(new javax.swing.filechooser.FileFilter() {
public boolean accept(File f) {
return f.getName().toLowerCase().endsWith(".xml") || f.isDirectory();
}
public String getDescription() {
return "XML files";
}
});
// set up menus
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(new ActionListener() {
public void actionPerformed(ActionEvent event) {
if (chooser.showSaveDialog(PreferencesFrame.this) == JFileChooser.APPROVE_OPTION) {
try {
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(new ActionListener() {
public void actionPerformed(ActionEvent 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(new ActionListener() {
public void actionPerformed(ActionEvent event) {
node.putInt("left", getX());
node.putInt("top", getY());
node.putInt("width", getWidth());
node.putInt("height", getHeight());
node.put("title", getTitle());
System.exit(0);
}
});
}
public static final int DEFAULT_WIDTH = 300;
public static final int DEFAULT_HEIGHT = 200;
}
API:java.util.prefs.Preferences 1.4
-
Preferences userRoot():返回调用应用程序的用户配置信息的根节点。
-
Preferences systemRoot():返回系统范围的配置信息的根节点。
-
Preferences node(String path):返回从当前节点经过给定路径可以到达的节点。如果path是绝对路径(以/开始),则从包含配置信息的树根开始查找这个节点。如果给定的路径不存在这样的节点,则创建它。
-
Preferences userNodeForPackage(Class cl)
-
Preferences systemNodeForPackage(Class cl):返回当前用户树或者系统树中的节点。这个节点的绝对路径符合类cl的包名。
-
String[] keys():返回属于这个节点的所有键。
-
String get(String key, String defval)
-
int getInt(String key, int defvaI)
-
long getLong(String key, long defval)
-
float getFloat(String key, float defval)
-
double getDouble(String key, double rtefval)
-
boolean getBoolean(Str1ng key, boolean defval)
-
byte[] getByteArray(Strfng 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)
-
votd putBoolean(String key, boolean value)
-
void putByteArray(Strtng key, byte[] value):将键/值存放在节点中。
-
void exportSubtree(OutputStream out):将这个节点及子节点的配置信息写到指定流中。
-
void exportNode(OutputStream out):将这个节点(不包括子节点)配置信息写到指定流中。
-
void importPreferences(InputStream in):导入指定流中的配置信息。
至此,结束了对Java软件部署的讨论,下一章将介绍如何利用异常告诉程序在出现运行问题时如何进行处理。另外,还将介绍一些测试和调试的技巧,以保证程序运行时不会出现过多的错误。