使用的教材是java核心技术卷1,我将跟着这本书的章节同时配合视频资源来进行学习基础java知识。
day085 部署Java程序(二)((应用首选项的存储:属性映射、首选项API)、服务加载器)
应用用户通常希望能保存他们的首选项和定制信息,以后再次启动应用时再恢复这些配置。首先我们来学习Java应用的传统做法,这是一种简单的方法,将配置信息保存在属性文件中。然后我们学习首选项API,它提供了一个更加健壮的解决方案。
1.属性映射
属性映射(propertymap)是一种存储键/值对的数据结构。属性映射通常用来存储配置信息,它有3个特性:
•键和值是字符串。
•映射可以很容易地存人文件以及从文件加载。
•有一个二级表保存默认值。
实现属性映射的Java类名为Properties。
属性映射对于指定程序的配置选项很有用。例如:
Properties settings=newProperties();
settings.setProperty("width","200");
settings.setProperty("title","Hello,World!");
可以使用store方法将属性映射列表保存到一个文件中。在这里,我们将属性映射保存在文件program.properties中。第二个参数是包含在这个文件中的注释。
OutputStream out = new FileOutputStream("program.properties");
settings.store(out, "Program Properties");
这个示例会给出以下输出:
Progran Properties
#Mon Apr 30 07:22:52 2007
width=200
title=Hello, World!
要从文件加载属性,可以使用以下调用:
InputStrean in = new Filei叩utStream("prograni.properties");
settings.load(in);
习惯上,会把程序属性存储在用户主目录的一个子目录中。目录名通常以一个点号开头(在UNIX系统中),这个约定说明这是一个对用户隐藏的系统目录。示例程序就遵循这个约定。要找出用户的主目录,可以调用System.getProperties方法,它恰好也使用一个Properties对象描述系统信息。主目录包含键"usenhome"。还有一个便利方法可以读取单个键:
String userDir=System.getProperty("user.home");
可以为程序属性提供默认值,这是一个很好的想法,因为用户有可能手动编辑这个文件。Properties类有两种提供默认值的机制。第一种方法是,查找一个字符串的值时可以指定一个默认值,这样当键不存在时就会自动使用这个默认值。
String title=settings.getProperty("title","Defaulttitle");
如果属性映射中有一个"title"属性,title就会设置为相应的字符串。否则,title会设置为"Defaulttitle"。
如果觉得在每个getProperty调用中指定默认值太过麻烦,可以把所有默认值都放在一个二级属性映射中,并在主属性映射的构造器中提供这个二级映射。
Properties defaultSettings=new Properties();
defaultSettings.setProperty("ividth","300");
defaultSettings.setProperty("height","200");
defaultSettings.setProperty("titie","Defaulttitle");
...
Propertiessettings=newProperties(defaultSettings);
没错,如果为defaultSettings构造器提供另一个属性映射参数,甚至可以为默认值指定默认值,不过一般不会这么做。
下面的程序显示了如何使用属性来存储和加载程序状态。程序会记住框架位置、大小和标题。
/**
*@author zzehao
*/
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.
public class PropertiesTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
PropertiesFrame frame = new PropertiesFrame();
frame.setVisible(true);
});
}
}
//A frame that restores position and size from a properties file and updates he properties upon exit.
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()
{
//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.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);
//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.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);
}
});
}
}
运行的结果:(自己输入之后的显示)
2.首选项API
我们已经看到,利用Properties类可以很容易地加载和保存配置信息。不过,使用属性文件有以下缺点:
•有些操作系统没有主目录的概念,所以很难找到一个统一的配置文件位置。
•关于配置文件的命名没有标准约定,用户安装多个Java应用时,就更容易发生命名冲突。
有些操作系统有一个存储配置信息的中心存储库。最著名的例子就是MicrosoftWindows中的注册表。Preferences类以一种平台无关的方式提供了这样一个中心存储库。在Windows中,Preferences类使用注册表来存储信息;在Linux上,信息则存储在本地文件系统中。当然,存储库实现对使用Preferences类的程序员是透明的。
Preferences存储库有一个树状结构,节点路径名类似于/com/mycompany/myapp。类似于包名,只要程序员用逆置的域名作为路径的开头,就可以避免命名冲突。实际上,API的设计者就建议配置节点路径要与程序中的包名一致。存储库的各个节点分别有一个单独的键/值对表,可以用来存储数值、字符串或字节数组,但不能存储可串行化的对象。API设计者认为对于长期存储来说,串行化格式过于脆弱,并不合适。当然,如果你不同意这种看法,也可以用字节数组保存串行化对象。
为了增加灵活性,可以有多个并行的树。每个程序用户分别有一棵树;另外还有一棵系统树,可以用于存放所有用户的公共信息。Preferences类使用操作系统的“当前用户”概念来访问适当的用户树。
若要访问树中的一个节点,需要从用户或系统根开始:
Preferences root=Preferences.userRoot();
或
Preferences root=Preferences.systemRoot():
然后访问节点。可以直接提供一个节点路径名:
Preferencesnode=root.node("/com/mycompany/myapp"):
如果节点的路径名等于类的包名,还有一种便捷方式来获得这个节点。只需要得到这个类的一个对象,然后调用:
Preferences node=Preferences.userNodeForPackage(obj.getClass());
或
Preferences node=Preferences.systemNodeForPackage(obj.getClass()):
一般来说,Obj往往是this引用。
一旦得到了节点,可以用以下方法访问键/值表:
Stringget(Stringkey,Stringdefval)
intgetInt(Stringkey,intdefval)
longgetLong(Stringkey,longdefval)
floatgetFIoat(Stringkey,floatdefval)
doublegetDouble(Stringkey,doubledefval)
booleangetBoolean(Stringkey,booleandefval)
byte[]getByteArray(Stringkey,byte[]defval)
需要说明的是,读取信息时必须指定一个默认值,以防止没有可用的存储库数据。之所以必须有默认值,有很多原因。可能由于用户从未指定过首选项,所以没有相应的数据。某些资源受限的平台可能没有存储库,移动设备有可能与存储库暂时断开了连接。
相对应地,可以用如下的put方法向存储库写数据:
put(Stringkey,Stringvalue)
putInt(Stringkey,intvalue)等等。
可以用以下方法枚举一个节点中存储的所有键:
String[]keys()
目前没有办法找出一个特定键对应的值的类型。
类似Windows注册表这样的中心存储库通常都存在两个问题:
•它们会变成充斥着过期信息的“垃圾场”。
•配置数据与存储库纠缠在一起,以至于很难把首选项迁移到新平台。
Preferences类为第二个问题提供了一个解决方案。可以通过调用以下方法导出一个子树(或者比较少见的,也可以是一个节点)的首选项:
void exportSubtree(OutputStream out)
void exportNode(OutputStream out)
数据用XML格式保存。可以通过调用以下方法将数据导人到另一个存储库:
void importPreferences(InputStreain in)
如果你的程序使用首选项,要让用户有机会导出和导人首选项,从而可以很容易地将设置从一台计算机迁移到另一台计算机。
下面的程序展示了这种技术。这个程序只保存了主窗口的位置、大小和标题。试着调整窗口的大小,然后退出并重启应用。窗口的状态与之前退出时是一样的。
/**
*@author zzehao
*/
import java.awt.*;
import java.io.*;
import java.util.prefs.*;
import javax.swing.*;
import javax.swing.filechooser.*;
//A program to test properties. The program remembers the frame position, size and title.
public class PreferencesTest
{
public static void main(String[] args)
{
EventQueue.invokeLater(() -> {
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 preferences upon exit.
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()
{
//get position,size,title from properties
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("."));
chooser.setFileFilter(new FileNameExtensionFilter("XML files","xml"));
//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(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());
}
}
运行的结果:
3.服务加载器
有时会开发采用插件体系结构的应用。有些平台支持这种方法,如OSGi(http://0Sgi.org),并用于开发环境、应用服务器和其他复杂的应用中。不过 JDK 还提供了一个加载插件的简单机制。
通常,提供一个插件时,程序希望插件设计者能有一些自由来确定如何实现插件的特性。另外还可以有多个实现以供选择。利用ServiceLoader类可以很容易地加载符合一个公共接口的插件。
定义一个接口(或者,如果愿意也可以定义一个超类),其中包含服务的各个实例应当提供的方法。例如,假设你的服务要提供加密。
package serviceLoader;
public interface Cipher
{
byte[] encrypt(byte[] source, byte[] key);
byte[] decrypt(byte[] source, byte[] key);
int strength();
}
服务提供者可以提供一个或多个实现这个服务的类,例如:
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 strength0 { return 1; }
}
实现类可以放在任意包中,而不一定是服务接口所在的包。每个实现类必须有一个无参数构造器。
现在把这些类的类名增加到META-INF/services目录下的一个UTF-8编码文本文件中,文件名必须与完全限定类名一致。在我们的例子中,文件META-INF/services/serviceLoader.Cipher必须包含这样一行:
serviceLoader.impl.CaesarCipher
在这个例子中,我们提供了一个实现类。还可以提供多个类,以后可以从中选择。
完成这个准备工作之后,程序可以如下初始化一个服务加载器:
public static ServiceLoadercipherLoader<Ciher>=ServiceLoader.1oad(Cipher.class);
这个初始化工作只在程序中完成一次。服务加载器的iterator方法会对服务提供的所有实现返冋一个迭代器。最容易的做法是使用一个增强的for循环进行遍历。在循环中,选择一个适当的对象来完成服务。
public static Cipher getCipher(int minStrength)
{
for (Cipher cipher : cipherLoader) // Implicitly calls cipherLoader.iteratorO
{
if (cipher.strength() >= minStrength) return cipher;
}
return null;
}