Java学习day085 部署Java程序(二)((应用首选项的存储:属性映射、首选项API)、服务加载器)

使用的教材是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;
}

            


 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值