Java-中文官方教程-2022-版-五-
Java 中文官方教程 2022 版(五)
原文:
docs.oracle.com/javase/tutorial/reallybigindex.html
将 DOM 写出为 XML 文件
原文:
docs.oracle.com/javase/tutorial/jaxp/xslt/writingDom.html
在构建了一个 DOM(通过解析 XML 文件或以编程方式构建)之后,您经常希望将其保存为 XML。本节将向您展示如何使用 Xalan 转换包来实现这一点。
使用该包,您将创建一个转换器对象,将DOMSource
连接到StreamResult
。然后,您将调用转换器的transform()
方法将 DOM 写出为 XML 数据。
读取 XML
第一步是通过解析 XML 文件在内存中创建一个 DOM。到目前为止,您应该已经对这个过程感到熟悉了。
注意:
本节讨论的代码位于文件TransformationApp01.java
中。下载 XSLT 示例并将其解压缩到install-dir/jaxp-1_4_2-
release-date/samples
目录中。
以下代码提供了一个基本的模板供参考。基本上,这与文档对象模型课程开头使用的代码相同。
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.FactoryConfigurationError;
import javax.xml.parsers.ParserConfigurationException;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import java.io.*;
public class TransformationApp01 {
static Document document;
public static void main(String argv[]) {
if (argv.length != 1) {
System.err.println("Usage: java TransformationApp01 filename");
System.exit (1);
}
DocumentBuilderFactory factory =
DocumentBuilderFactory.newInstance();
try {
File f = new File(argv[0]);
DocumentBuilder builder =
factory.newDocumentBuilder();
document = builder.parse(f);
}
catch (SAXParseException spe) {
// Error generated by the parser
System.out.println("\n** Parsing error"
+ ", line " + spe.getLineNumber()
+ ", uri " + spe.getSystemId());
System.out.println(" " + spe.getMessage() );
// Use the contained exception, if any
Exception x = spe;
if (spe.getException() != null)
x = spe.getException();
x.printStackTrace();
}
catch (SAXException sxe) {
// Error generated by this application
// (or a parser-initialization error)
Exception x = sxe;
if (sxe.getException() != null)
x = sxe.getException();
x.printStackTrace();
}
catch (ParserConfigurationException pce) {
// Parser with specified options
// cannot be built
pce.printStackTrace();
}
catch (IOException ioe) {
// I/O error
ioe.printStackTrace();
}
}
}
创建一个转换器
下一步是创建一个转换器,您可以使用它将 XML 传输到System.out
。首先,需要以下导入语句。
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
在这里,您添加了一系列类,这些类现在应该形成一个标准模式:一个实体(Transformer
)、用于创建它的工厂(TransformerFactory
)以及每个类可能生成的异常。因为转换始终有一个源和一个结果,所以您导入了使用 DOM 作为源(DOMSource
)和用于结果的输出流(StreamResult
)所需的类。
接下来,添加执行转换的代码:
try {
File f = new File(argv[0]);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(f);
// Use a Transformer for output
TransformerFactory tFactory =
TransformerFactory.newInstance();
Transformer transformer =
tFactory.newTransformer();
DOMSource source = new DOMSource(document);
StreamResult result = new StreamResult(System.out);
transformer.transform(source, result);
}
在这里,您创建一个转换器对象,使用 DOM 构造一个源对象,并使用System.out
构造一个结果对象。然后告诉转换器操作源对象并输出到结果对象。
在这种情况下,“转换器”实际上并没有改变任何内容。在 XSLT 术语中,您使用的是身份转换,这意味着“转换”生成源的副本,未更改。
注意:
您可以为转换器对象指定各种输出属性,如 W3C 规范中定义的www.w3.org/TR/xslt#output
。例如,要获得缩进输出,可以调用以下方法:
% transformer.setOutputProperty(OutputKeys.INDENT, "yes");
最后,以下突出显示的代码捕获可能生成的新错误:
}
catch (TransformerConfigurationException tce) {
System.out.println("* Transformer Factory error");
System.out.println(" " + tce.getMessage());
Throwable x = tce;
if (tce.getException() != null)
x = tce.getException();
x.printStackTrace();
}
catch (TransformerException te) {
System.out.println("* Transformation error");
System.out.println(" " + te.getMessage());
Throwable x = te;
if (te.getException() != null)
x = te.getException();
x.printStackTrace();
}
catch (SAXParseException spe) {
// ...
}
注意
-
转换器对象会抛出
TransformerExceptions
。 -
工厂会抛出
TransformerConfigurationExceptions
。 -
要保留 XML 文档的
DOCTYPE
设置,还需要添加以下代码:import javax.xml.transform.OutputKeys; ... if (document.getDoctype() != null) { String systemValue = (new File (document.getDoctype().getSystemId())).getName(); transformer.setOutputProperty(OutputKeys.DOCTYPE_SYSTEM, systemValue); }
要了解更多关于配置工厂和处理验证错误的信息,请参阅将 XML 数据读入 DOM。
运行TransformationApp01
示例
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。% cd xslt
-
编译
TransformationApp01
示例。输入以下命令:
% javac TransformationApp01.java
-
在 XML 文件上运行
TransformationApp01
示例。在下面的情况下,在解压示例包后,
TransformationApp01
在xslt/data
目录中的foo.xml
文件上运行。% java TransformationApp01 data/foo.xml
你将看到以下输出:
<?xml version="1.0" encoding="UTF-8" standalone="no"?><doc> <name first="David" last="Marston"/> <name first="David" last="Bertoni"/> <name first="Donald" last="Leslie"/> <name first="Emily" last="Farmer"/> <name first="Joseph" last="Kesselman"/> <name first="Myriam" last="Midy"/> <name first="Paul" last="Dick"/> <name first="Stephen" last="Auriemma"/> <name first="Scott" last="Boag"/> <name first="Shane" last="Curcuru"/>
如创建转换器中所述,这个转换器实际上并没有改变任何内容,而只是执行了恒等变换,生成了源的副本。真正的转换将在从任意数据结构生成 XML 中执行。
写出 DOM 的子树
也可以操作 DOM 的子树。在本节中,你将尝试这个选项。
注意:
本节讨论的代码在 TranformationApp02.java 中。如果你还没有这样做,下载 XSLT 示例并将其解压缩到install-dir/jaxp-1_4_2-
release-date/samples
目录中。
过程中唯一的区别是现在将使用 DOM 中的一个节点创建DOMSource
,而不是整个 DOM。第一步是导入你需要获取的节点的类,如下面突出显示的代码所示:
import org.w3c.dom.Document;
import org.w3c.dom.DOMException;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
下一步是找到一个好的节点进行实验。以下突出显示的代码选择第一个<name>
元素。
try {
File f = new File(argv[0]);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(f);
NodeList list = document.getElementsByTagName("name");
Node node = list.item(0);
}
在创建转换器中,源对象是通过以下代码行从整个文档构造的
DOMSource source = new DOMSource(document);
然而,下面突出显示的代码行构造了一个由特定节点为根的子树组成的源对象。
DOMSource source = new DOMSource(node);
StreamResult result = new StreamResult(System.out);
transformer.transform(source, result);
运行TranformationApp02
示例
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。cd xslt
-
编译
TranformationApp02
示例。输入以下命令:
% javac xslt/TranformationApp02.java
-
在 XML 文件上运行
TranformationApp02
示例。在下面的情况下,在解压示例包后,
TranformationApp02
在xslt/data
目录中的foo.xml
文件上运行。% java TranformationApp02 data/foo.xml
你将看到以下输出:
<?xml version="1.0" encoding="UTF-8" standalone="no"?><doc><name first="David" last="Marston"/>
这次,只打印出了第一个
<name>
元素。到目前为止,你已经看到如何使用转换器写出 DOM,以及如何使用 DOM 的子树作为转换中的源对象。在下一节中,你将看到如何使用转换器从你能够解析的任何数据结构创建 XML。
从任意数据结构生成 XML
原文:
docs.oracle.com/javase/tutorial/jaxp/xslt/generatingXML.html
本节使用 XSLT 将任意数据结构转换为 XML。
这是整个过程的概述:
-
修改一个现有的读取数据的程序,使其生成 SAX 事件。(此时,该程序是一个真正的解析器还是一个数据过滤器都无关紧要)。
-
使用 SAX“解析器”构建一个
SAXSource
进行转换。 -
使用与上一个练习中创建的相同的
StreamResult
对象来显示结果。(但请注意,你也可以轻松地创建一个DOMResult
对象来在内存中创建一个 DOM)。 -
使用转换器对象将源连接到结果,进行转换。
首先,你需要一个要转换的数据集和一个能够读取数据的程序。接下来的两个部分创建了一个简单的数据文件和一个读取它的程序。
创建一个简单的文件
这个例子使用了一个地址簿的数据集,PersonalAddressBook.ldif
。如果你还没有这样做,下载 XSLT 示例
并解压到install-dir/jaxp-1_4_2-
release-date/samples
目录中。这里显示的文件是通过在 Netscape Messenger 中创建一个新的地址簿,给它一些虚拟数据(一个地址卡),然后以 LDAP 数据交换格式(LDIF)格式导出而生成的。解压 XSLT 示例后,它位于xslt/data
目录中。
下面的图示显示了创建的通讯录条目。
图示 通讯录条目
导出通讯录会生成一个类似下面显示的文件。我们关心的文件部分以粗体显示。
dn: cn=Fred Flintstone,mail=fred@barneys.house
modifytimestamp: 20010409210816Z
cn: Fred Flintstone
xmozillanickname: Fred
mail: Fred@barneys.house
xmozillausehtmlmail: TRUE
givenname: Fred
sn: Flintstone
telephonenumber: 999-Quarry
homephone: 999-BedrockLane
facsimiletelephonenumber: 888-Squawk
pagerphone: 777-pager
cellphone: 555-cell
xmozillaanyphone: 999-Quarry
objectclass: top
objectclass: person
请注意,文件的每一行都包含一个变量名,一个冒号和一个空格,后面跟着变量的值。sn
变量包含人的姓氏(姓),变量cn
包含通讯录条目中的DisplayName
字段。
创建一个简单的解析器
下一步是创建一个解析数据的程序。
注意 - 本节讨论的代码在AddressBookReader01.java
中,解压XSLT 示例
到install-dir/jaxp-1_4_2-
release-date/samples
目录后,可以在xslt
目录中找到。
接下来显示程序的文本。这是一个非常简单的程序,甚至不会为多个条目循环,因为毕竟它只是一个演示。
import java.io.*;
public class AddressBookReader01 {
public static void main(String argv[]) {
// Check the arguments
if (argv.length != 1) {
System.err.println("Usage: java AddressBookReader01 filename");
System.exit (1);
}
String filename = argv[0];
File f = new File(filename);
AddressBookReader01 reader = new AddressBookReader01();
reader.parse(f);
}
// Parse the input file
public void parse(File f) {
try {
// Get an efficient reader for the file
FileReader r = new FileReader(f);
BufferedReader br = new BufferedReader(r);
// Read the file and display its contents.
String line = br.readLine();
while (null != (line = br.readLine())) {
if (line.startsWith("xmozillanickname: "))
break;
}
output("nickname", "xmozillanickname", line);
line = br.readLine();
output("email", "mail", line);
line = br.readLine();
output("html", "xmozillausehtmlmail", line);
line = br.readLine();
output("firstname","givenname", line);
line = br.readLine();
output("lastname", "sn", line);
line = br.readLine();
output("work", "telephonenumber", line);
line = br.readLine();
output("home", "homephone", line);
line = br.readLine();
output("fax", "facsimiletelephonenumber", line);
line = br.readLine();
output("pager", "pagerphone", line);
line = br.readLine();
output("cell", "cellphone", line);
}
catch (Exception e) {
e.printStackTrace();
}
}
}
这个程序包含三个方法:
main
main
方法从命令行获取文件名,创建解析器的实例,并开始解析文件。当我们将程序转换为 SAX 解析器时,这个方法将消失。(这是将解析代码放入单独方法的一个原因)。
parse
此方法在主例程发送的 File
对象上运行。正如您所看到的,这非常简单。唯一的效率让步是使用 BufferedReader
,当您开始操作大文件时,这可能变得重要。
output
输出方法包含了一行结构的逻辑。它接受三个参数。第一个参数给出一个要显示的方法名称,因此它可以将 html
输出为一个变量名,而不是 xmozillausehtmlmail
。第二个参数给出存储在文件中的变量名(xmozillausehtmlmail
)。第三个参数给出包含数据的行。然后该例程从行的开头剥离变量名并输出所需的名称,加上数据。
运行 AddressBookReader01
示例
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例 并将其解压缩到 install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。cd xslt
-
编译
AddressBookReader01
示例。输入以下命令:
% javac AddressBookReader01.java
-
在数据文件上运行
AddressBookReader01
示例。在下面的情况下,
AddressBookReader01
在上面显示的文件PersonalAddressBook.ldif
上运行,在解压缩示例包后在xslt/data
目录中找到。% java AddressBookReader01 data/PersonalAddressBook.ldif
您将看到以下输出:
nickname: Fred email: Fred@barneys.house html: TRUE firstname: Fred lastname: Flintstone work: 999-Quarry home: 999-BedrockLane fax: 888-Squawk pager: 777-pager cell: 555-cell
这比 创建简单文件 中显示的文件更易读。
创建生成 SAX 事件的解析器
本节展示了如何使解析器生成 SAX 事件,以便您可以将其用作 XSLT 转换中的 SAXSource
对象的基础。
注意 - 本节讨论的代码在 AddressBookReader02.java
中,该文件在您将 XSLT examples
解压缩到 install-dir/jaxp-1_4_2-
release-date/samples
目录后的 xslt
目录中找到。AddressBookReader02.java
是从 AddressBookReader01.java
改编而来,因此这里只讨论两个示例之间的代码差异。
AddressBookReader02
需要以下未在 AddressBookReader01
中使用的突出显示的类。
import java.io.*;
import org.xml.sax.*;
import org.xml.sax.helpers.AttributesImpl;
该应用程序还扩展了 XmlReader
。此更改将应用程序转换为生成适当的 SAX 事件的解析器。
public class AddressBookReader02 implements XMLReader { /* ... */ }
与 AddressBookReader01
示例不同,此应用程序没有 main
方法。
以下全局变量将在本节后面使用:
public class AddressBookReader02 implements XMLReader {
ContentHandler handler;
String nsu = "";
Attributes atts = new AttributesImpl();
String rootElement = "addressbook";
String indent = "\n ";
// ...
}
SAX ContentHandler
是将获得解析器生成的 SAX 事件的对象。为了将应用程序转换为 XmlReader
,应用程序定义了一个 setContentHandler
方法。处理程序变量将保存在调用 setContentHandler
时发送的对象的引用。
当解析器生成 SAX 元素事件时,它将需要提供命名空间和属性信息。因为这是一个简单的应用程序,它为这两者定义了空值。
应用程序还为数据结构(addressbook
)定义了一个根元素,并设置了一个缩进字符串以提高输出的可读性。
此外,解析方法被修改为接受 InputSource
(而不是 File
)作为参数,并考虑了它可能生成的异常:
public void parse(InputSource input) throws IOException, SAXException
现在,不再像在 AddressBookReader01
中那样创建新的 FileReader
实例,而是将读取器封装在 InputSource
对象中:
try {
java.io.Reader r = input.getCharacterStream();
BufferedReader Br = new BufferedReader(r);
// ...
}
注意 - 下一节将展示如何创建输入源对象,实际放入其中的将是一个缓冲读取器。但是 AddressBookReader
可能会被其他人在某个地方使用。这一步确保处理将是高效的,无论给定的读取器是什么。
下一步是修改解析方法以生成文档和根元素的 SAX 事件。以下突出显示的代码实现了这一点:
public void parse(InputSource input) {
try {
// ...
String line = br.readLine();
while (null != (line = br.readLine())) {
if (line.startsWith("xmozillanickname: "))
break;
}
if (handler == null) {
throw new SAXException("No content handler");
}
handler.startDocument();
handler.startElement(nsu, rootElement, rootElement, atts);
output("nickname", "xmozillanickname", line);
// ...
output("cell", "cellphone", line);
handler.ignorableWhitespace("\n".toCharArray(),
0, // start index
1 // length
);
handler.endElement(nsu, rootElement, rootElement);
handler.endDocument();
}
catch (Exception e) {
// ...
}
}
在这里,应用程序检查解析器是否正确配置了 ContentHandler
。(对于这个应用程序,我们不关心其他任何内容)。然后生成文档和根元素的开始事件,并通过发送根元素的结束事件和文档的结束事件来完成。
此时有两个值得注意的地方:
-
setDocumentLocator
事件未发送,因为这是可选的。如果它很重要,那么该事件将在startDocument
事件之前立即发送。 -
在根元素结束之前生成了一个
ignorableWhitespace
事件。这也是可选的,但它极大地提高了输出的可读性,很快就会看到。(在这种情况下,空白包括一个单独的换行符,它以与将字符发送到字符方法相同的方式发送:作为字符数组、起始索引和长度)。
现在生成了文档和根元素的 SAX 事件,下一步是修改输出方法以为每个数据项生成适当的元素事件。删除对 System.out.println(name + ": " + text)
的调用,并添加以下突出显示的代码来实现:
void output(String name, String prefix, String line)
throws SAXException {
int startIndex =
prefix.length() + 2; // 2=length of ": "
String text = line.substring(startIndex);
int textLength = line.length() - startIndex;
handler.ignorableWhitespace (indent.toCharArray(),
0, // start index
indent.length()
);
handler.startElement(nsu, name, name /*"qName"*/, atts);
handler.characters(line.toCharArray(),
startIndex,
textLength;
);
handler.endElement(nsu, name, name);
}
因为 ContentHandler
方法可以将 SAXExceptions
发送回解析器,所以解析器必须准备好处理它们。在这种情况下,不会有任何异常,所以如果出现任何异常,应用程序将简单地允许失败。
然后计算数据的长度,再次生成一些可忽略的空白以提高可读性。在这种情况下,只有一个数据级别,所以我们可以使用一个固定的缩进字符串。(如果数据结构更加结构化,我们将不得不根据数据的嵌套计算缩进空间)。
注意 - 缩进字符串对数据本身没有影响,但会使输出更容易阅读。如果没有该字符串,所有元素将被连接在一起:
<addressbook>
<nickname>Fred</nickname>
<email>...
接下来,以下方法配置解析器,以接收其生成的事件的 ContentHandler
:
void output(String name, String prefix, String line)
throws SAXException {
// ...
}
// Allow an application to register a content event handler.
public void setContentHandler(ContentHandler handler) {
this.handler = handler;
}
// Return the current content handler.
public ContentHandler getContentHandler() {
return this.handler;
}
还必须实现几个其他方法以满足 XmlReader
接口的要求。为了这个练习,所有这些方法都生成了空方法。然而,一个生产应用程序需要实现错误处理程序方法以生成更健壮的应用程序。不过,在这个例子中,以下代码为它们生成了空方法:
// Allow an application to register an error event handler.
public void setErrorHandler(ErrorHandler handler) { }
// Return the current error handler.
public ErrorHandler getErrorHandler() {
return null;
}
然后,以下代码为剩余的 XmlReader
接口生成了空方法。(其中大部分对于真实的 SAX 解析器是有价值的,但对于像这样的数据转换应用程序却没有太大影响)。
// Parse an XML document from a system identifier (URI).
public void parse(String systemId) throws IOException, SAXException
{ }
// Return the current DTD handler.
public DTDHandler getDTDHandler() { return null; }
// Return the current entity resolver.
public EntityResolver getEntityResolver() { return null; }
// Allow an application to register an entity resolver.
public void setEntityResolver(EntityResolver resolver) { }
// Allow an application to register a DTD event handler.
public void setDTDHandler(DTDHandler handler) { }
// Look up the value of a property.
public Object getProperty(String name) { return null; }
// Set the value of a property.
public void setProperty(String name, Object value) { }
// Set the state of a feature.
public void setFeature(String name, boolean value) { }
// Look up the value of a feature.
public boolean getFeature(String name) { return false; }
现在您有一个可以用来生成 SAX 事件的解析器。在下一节中,您将使用它来构造一个 SAX 源对象,从而将数据转换为 XML。
使用解析器作为 SAXSource
给定一个用作事件源的 SAX 解析器,您可以构造一个转换器来生成一个结果。在本节中,TransformerApp
将被更新以生成一个流输出结果,尽管它也可以轻松地生成一个 DOM 结果。
注意 - 注意:本节讨论的代码位于 TransformationApp03.java
中,在将 XSLT 示例
解压缩到 install-dir/jaxp-1_4_2-
release-date/samples
目录后,可以在 xslt
目录中找到。
要开始,TransformationApp03
与 TransformationApp02
的不同之处在于构建 SAXSource
对象所需导入的类。这些类如下所示。在这一点上,DOM 类已不再需要,因此已被丢弃,尽管保留它们不会造成任何伤害。
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.ContentHandler;
import org.xml.sax.InputSource;
import javax.xml.transform.sax.SAXSource;
import javax.xml.transform.stream.StreamResult;
接下来,应用程序不再创建 DOM DocumentBuilderFactory
实例,而是创建了一个 SAX 解析器,即 AddressBookReader
的实例:
public class TransformationApp03 {
static Document document;
public static void main(String argv[]) {
// ...
// Create the sax "parser".
AddressBookReader saxReader = new AddressBookReader();
try {
File f = new File(argv[0]);
// ...
}
// ...
}
}
然后,以下突出显示的代码构造了一个 SAXSource
对象
// Use a Transformer for output
// ...
Transformer transformer = tFactory.newTransformer();
// Use the parser as a SAX source for input
FileReader fr = new FileReader(f);
BufferedReader br = new BufferedReader(fr);
InputSource inputSource = new InputSource(br);
SAXSource source = new SAXSource(saxReader, inputSource);
StreamResult result = new StreamResult(System.out);
transformer.transform(source, result);
在这里,TransformationApp03
构造了一个缓冲读取器(如前所述),并将其封装在一个输入源对象中。然后创建了一个 SAXSource
对象,将读取器和 InputSource
对象传递给它,然后将其传递给转换器。
当应用程序运行时,转换器将自身配置为 SAX 解析器(AddressBookReader
)的 ContentHandler
,并告诉解析器对 inputSource
对象进行操作。解析器生成的事件然后传递给转换器,转换器执行适当的操作并将数据传递给结果对象。
最后,TransformationApp03
不会生成异常,因此在 TransformationApp02
中看到的异常处理代码不再存在。
运行 TransformationApp03
示例
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例 并将其解压缩到 install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。cd xslt
-
编译
TransformationApp03
示例。输入以下命令:
% javac TransformationApp03.java
-
运行
TransformationApp03
示例,转换您希望转换为 XML 的数据文件。在下面的情况下,在您解压缩示例包后,在
xslt/data
目录中找到的PersonalAddressBook.ldif
文件上运行TransformationApp03
。% java TransformationApp03 data/PersonalAddressBook.ldif
您将看到以下输出:
<?xml version="1.0" encoding="UTF-8"?> <addressbook> <nickname>Fred</nickname> <email>Fred@barneys.house</email> <html>TRUE</html> <firstname>Fred</firstname> <lastname>Flintstone</lastname> <work>999-Quarry</work> <home>999-BedrockLane</home> <fax>888-Squawk</fax> <pager>777-pager</pager> <cell>555-cell</cell> </addressbook>
正如您所看到的,LDIF 格式文件
PersonalAddressBook
已被转换为 XML!
使用 XSLT 转换 XML 数据
原文:
docs.oracle.com/javase/tutorial/jaxp/xslt/transformingXML.html
可以使用可扩展样式表语言转换(XSLT)API 来实现多种目的。例如,通过一个足够智能的样式表,你可以从 XML 数据生成 PDF 或 PostScript 输出。但通常情况下,XSLT 用于生成格式化的 HTML 输出,或者创建数据的另一种 XML 表示。
在这一部分中,使用 XSLT 转换将 XML 输入数据转换为 HTML 输出。
注意 - XSLT 规范庞大而复杂,因此本教程只能触及表面。它将为您提供一些背景知识,以便您理解简单的 XSLT 处理任务,但不会详细讨论如何编写 XSLT 转换,而是集中于如何使用 JAXP 的 XSLT 转换 API。要深入了解 XSLT,请参考一本好的参考手册,例如 Michael Kay 的XSLT 2.0 和 XPath 2.0:程序员参考(Wrox,2008)。
定义一个简单的文档类型
首先定义一个非常简单的文档类型,可用于撰写文章。我们的article
文档将包含这些结构标签:
-
<TITLE>
: 文章的标题 -
<SECT>
: 一个包含标题和主体的节 -
<PARA>
: 一个段落 -
<LIST>
: 一个列表 -
<ITEM>
: 列表中的一个条目 -
<NOTE>
: 一个与主文本分隔开的旁注
这种结构稍微不同寻常的地方在于,我们不会为节标题创建单独的元素标签。这样的元素通常被创建用于区分标题文本(及其包含的任何标签)和节的主体(即标题下面的任何结构元素)。
相反,我们将允许标题与节的主体无缝融合。这种安排增加了样式表的一些复杂性,但它将让我们有机会探索 XSLT 的模板选择机制。它也符合我们对文档结构的直觉期望,即标题文本直接后跟结构元素,这种安排可以简化基于大纲的编辑。
注意 - 这种结构不容易验证,因为 XML 的混合内容模型允许在节的任何位置出现文本,而我们希望将文本和内联元素限制在只出现在节主体的第一个结构元素之前。基于断言的验证器可以做到,但大多数其他模式机制无法。因此,我们将放弃为文档类型定义 DTD。
在这种结构中,部分可以嵌套。嵌套的深度将决定用于部分标题的 HTML 格式(例如,h1
或h2
)。使用普通的SECT
标签(而不是编号的部分)在面向大纲的编辑中也很有用,因为它允许您随意移动部分,而无需担心更改任何受影响部分的编号。
对于列表,我们将使用一个类型属性来指定列表条目是无序的(带项目符号)、alpha(用小写字母符号)、ALPHA(用大写字母编号)还是编号的。
我们还将允许一些内联标签来改变文本的外观。
-
<B>
:粗体 -
<I>
:斜体 -
<U>
:下划线 -
<DEF>
:定义 -
<LINK>
:链接到 URL
注意 - 内联标签不会生成换行符,因此内联标签引起的样式更改不会影响页面上文本的流动(尽管它会影响该文本的外观)。另一方面,结构标签划分了文本的新段落,因此至少总是会生成一个换行符,除了其他格式更改。
<DEF>
标签将用于文本中定义的术语。这些术语将以斜体显示,就像它们在文档中通常显示的那样。但是在 XML 中使用特殊标签将允许索引程序找到这些定义,并将它们添加到索引中,以及标题中的关键字。例如,在前面的注释中,内联标签和结构标签的定义可以用<DEF>
标签标记以供将来索引。
最后,LINK
标签有两个目的。首先,它将让我们创建到 URL 的链接,而无需两次放置 URL;因此我们可以编写<link>http//...</link>
而不是<a href="http//...">http//...</a>
。当然,我们还希望允许一个看起来像<link target="...">...name...</link>
的形式。这导致了<link>
标签的第二个原因。它将让我们有机会在 XSLT 中使用条件表达式。
注意 - 尽管文章结构非常简单(仅由十一个标签组成),但它引发了足够多有趣的问题,让我们对 XSLT 的基本功能有了很好的了解。但是我们仍然会留下大部分规范未触及的领域。在还有什么其他 XSLT 可以做?中,我们将指出我们跳过的主要功能。
创建一个测试文档
在这里,您将使用嵌套的<SECT>
元素、一些<PARA>
元素、一个<NOTE>
元素、一个<LINK>
和一个<LIST type="unordered">
创建一个简单的测试文档。这个想法是创建一个包含各种元素的文档,以便我们可以探索更有趣的翻译机制。
注意 - 本节讨论的代码位于article1.xml
中,在您解压XSLT 示例
到install-dir/jaxp-1_4_2-
release-date/samples
目录后,可以在xslt/data
目录中找到。
要创建测试文档,请创建一个名为article.xml
的文件,并输入以下 XML 数据。
<?xml version="1.0"?>
<ARTICLE>
<TITLE>A Sample Article</TITLE>
<SECT>The First Major Section
<PARA>This section will introduce a subsection.</PARA>
<SECT>The Subsection Heading
<PARA>This is the text of the subsection.
</PARA>
</SECT>
</SECT>
</ARTICLE>
请注意,在 XML 文件中,子部分完全包含在主要部分中。(而在 HTML 中,标题不包含部分的正文)。结果是一个在纯文本形式下更难编辑的大纲结构,像这样,但在面向大纲的编辑器中更容易编辑。
某一天,如果有一个理解内联标记(如<B>
和<I>
)的树形 XML 编辑器,那么就可以以大纲形式编辑这种文章,而不需要复杂的样式表。(这样的编辑器将允许作者专注于文章的结构,将布局留到后期)。在这样的编辑器中,文章片段将看起来像这样:
<ARTICLE>
<TITLE>A Sample Article
<SECT>The First Major Section
<PARA>This section will
introduce a subsection.
<SECT>The Subheading
<PARA>This is the text of the subsection.
Note that ...
注意 - 目前存在树形结构编辑器,但它们将内联标记(如<B>
和<I>
)与结构标记一样处理,这可能会使“大纲”有点难以阅读。
编写 XSLT 转换
现在是时候开始编写一个 XSLT 转换,将转换 XML 文章并在 HTML 中呈现出来。
注意 - 本节讨论的代码位于article1a.xsl
中,在将XSLT 示例
解压缩到install-dir/jaxp-1_4_2-
release-date/samples
目录后,在xslt/data
目录中找到。
从创建一个普通的 XML 文档开始:
<?xml version="1.0" encoding="ISO-8859-1"?>
然后添加以下突出显示的行以创建一个 XSL 样式表:
<?xml version="1.0" encoding="ISO-8859-1"?>
<xsl:stylesheet
xmlns:xsl=
"http://www.w3.org/1999/XSL/Transform"
version="1.0"
>
**</xsl:stylesheet>**
**现在设置它以生成兼容 HTML 的输出。
<xsl:stylesheet
[...]
>
<xsl:output method="html"/>
[...]
</xsl:stylesheet>
我们将在本节稍后详细讨论该条目的原因。现在,请注意,如果你想输出除了格式良好的 XML 之外的任何内容,那么你将需要一个像所示的<xsl:output>
标记,指定text
或html
。(默认值为xml
)。
注意 - 当你指定 XML 输出时,你可以添加缩进属性以生成漂亮的缩进 XML 输出。规范如下:<xsl:output method="xml" indent="yes"/>
。
处理基本结构元素
你将开始填写样式表,处理用于创建目录的元素:根元素、标题元素和标题。你还将处理测试文档中定义的PARA
元素。
注意 - 如果你在第一次阅读时跳过了讨论 XPath 寻址机制的部分,XPath 工作原理,现在是回去复习那部分的好时机。
从添加处理根元素的主要指令开始:
<xsl:template match="/">
<html><body>
<xsl:apply-templates/>
</body></html>
</xsl:template>
</xsl:stylesheet>
新的 XSL 命令以粗体显示。(请注意,它们在xsl
命名空间中定义)。指令<xsl:apply-templates>
处理当前节点的子节点。在这种情况下,当前节点是根节点。
尽管这个例子很简单,但它阐明了许多重要的概念,因此值得彻底理解。第一个概念是样式表包含许多模板,用<xsl:template>
标签定义。每个模板包含一个匹配属性,该属性使用 XPath 工作原理中描述的 XPath 寻址机制来选择模板将应用于的元素。
在模板内部,不以xsl: namespace
前缀开头的标签只是简单地复制。随后的换行符和空格也会被复制,这有助于使生成的输出可读。
注意 - 当没有换行符时,通常会忽略空格。要在这种情况下包含输出中的空格,或者包含其他文本,您可以使用<xsl:text>
标签。基本上,XSLT 样式表期望处理标签。因此,它看到的一切都需要是<xsl:..>
标签、其他标签或空格。
在这种情况下,非 XSL 标签是 HTML 标签。因此,当匹配根标签时,XSLT 输出 HTML 起始标签,处理适用于根节点子元素的任何模板,然后输出 HTML 结束标签。
处理<TITLE>
元素
接下来,添加一个模板来处理文章标题:
<xsl:template match="/ARTICLE/TITLE">
<h1 align="center">
<xsl:apply-templates/> </h1>
</xsl:template>
</xsl:stylesheet>
在这种情况下,您指定了到 TITLE 元素的完整路径,并输出一些 HTML 以使标题文本成为一个大的、居中的标题。在这种情况下,apply-templates
标签确保如果标题包含任何内联标签,如斜体、链接或下划线,它们也将被处理。
更重要的是,apply-templates
指令导致标题文本被处理。与 DOM 数据模型一样,XSLT 数据模型基于包含在元素节点中的文本节点的概念(这些元素节点又可以包含在其他元素节点中,依此类推)。这种分层结构构成了源树。还有一个包含输出的结果树。
XSLT 通过将源树转换为结果树来工作。要可视化 XSLT 操作的结果,了解这些树的结构及其内容是有帮助的。(有关此主题的更多信息,请参见 XSLT/XPath 数据模型)。
处理标题
要继续处理基本结构元素,添加一个模板来处理顶级标题:
<xsl:template match=
"/ARTICLE/SECT">
<h2> <xsl:apply-templates
select="text()|B|I|U|DEF|LINK"/>
</h2>
<xsl:apply-templates select=
"SECT|PARA|LIST|NOTE"/>
</xsl:template>
</xsl:stylesheet>
在这里,您指定了到最顶层SECT
元素的路径。但这次,您使用select
属性在两个阶段应用模板。在第一阶段,您使用 XPath 的text()
函数选择文本节点,以及加粗和斜体等内联标签(竖线(|
)用于匹配多个项目:文本或加粗标签或斜体标签等)。在第二阶段,您选择文件中包含的其他结构元素,如章节、段落、列表和注释。
使用 select 属性可以让您将文本和内联元素放在<h2>...</h2>
标签之间,同时确保随后处理该部分中的所有结构标签。换句话说,您确保 XML 文档中标题的嵌套不会反映在 HTML 格式中,这是对 HTML 输出很重要的区别。
一般来说,使用 select 子句可以让您将所有模板应用于当前上下文中可用信息的子集。作为另一个示例,此模板选择当前节点的所有属性:
<xsl:apply-templates select="@*"/></attributes>
接下来,添加几乎相同的模板来处理嵌套一级更深的子标题:
<xsl:template match=
"/ARTICLE/SECT/SECT">
<h3> <xsl:apply-templates
select="text()|B|I|U|DEF|LINK"/>
</h3>
<xsl:apply-templates select=
"SECT|PARA|LIST|NOTE"/>
</xsl:template>
</xsl:stylesheet>
生成运行时消息
您也可以为更深层的标题添加模板,但在某个时候必须停止,即使只是因为 HTML 只支持五级标题。在本例中,您将在两级部分标题处停止。但是,如果 XML 输入恰好包含第三级,您将希望向用户提供错误消息。本节将向您展示如何做到这一点。
注意 - 我们可以继续处理更深层的SECT
元素,通过使用表达式/SECT/SECT//SECT
选择它们。//
选择任何SECT
元素,深度由 XPath 寻址机制定义。但我们将利用这个机会来玩弄消息传递。
添加以下模板以在遇到嵌套过深的部分时生成错误:
<xsl:template match=
"/ARTICLE/SECT/SECT/SECT">
<xsl:message terminate="yes">
Error: Sections can only be nested 2 deep.
</xsl:message>
</xsl:template>
</xsl:stylesheet>
terminate="yes"
子句导致转换过程在生成消息后停止。如果没有它,处理仍然会继续,该部分中的所有内容都将被忽略。
作为额外的练习,您可以扩展样式表以处理嵌套高达四个部分的部分,生成<h2>...<h5>
标签。在任何嵌套五级深的部分上生成错误。
最后,通过添加一个模板来处理PARA
标签来完成样式表:
<xsl:template match="PARA">
<p><xsl:apply-templates/></p>
</xsl:template>
</xsl:stylesheet>
编写基本程序
现在,您将修改使用 XSLT 的程序,以原样回显 XML 文件,并将其修改为使用您的样式表。
注意 - 本节讨论的代码位于Stylizer.java
中,在将XSLT examples
解压缩到install-dir/jaxp-1_4_2-
release-date/samples
目录后,可以在xslt/data
中找到stylizer1a.html
。
Stylizer
示例改编自TransformationApp02
,该示例解析 XML 文件并写入System.out
。这两个程序之间的主要区别如下所述。
首先,Stylizer
在创建Transformer
对象时使用样式表。
// ...
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.transform.stream.StreamResult;
// ...
public class Stylizer {
// ...
public static void main (String argv[]) {
// ...
try {
File stylesheet = new File(argv[0]);
File datafile = new File(argv[1]);
DocumentBuilder builder = factory.newDocumentBuilder();
document = builder.parse(datafile);
// ...
StreamSource stylesource = new StreamSource(stylesheet);
Transformer transformer = Factory.newTransformer(stylesource);
}
}
}
此代码使用文件创建StreamSource
对象,然后将源对象传递给工厂类以获取转换器。
注意 - 通过消除DOMSource
类,您可以简化代码。不要为 XML 文件创建DOMSource
对象,而是为其创建StreamSource
对象,以及为样式表创建StreamSource
对象。
运行Stylizer
示例
-
导航至
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例,并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航至
xslt
目录。cd xslt
-
编译
Stylizer
示例。输入以下命令:
% javac Stylizer.java
-
使用样式表
article1a.xsl
在article1.xml
上运行Stylizer
示例。% java Stylizer data/article1a.xsl data/article1.xml
您将看到以下输出:
<html> <body> <h1 align="center">A Sample Article</h1> <h2>The First Major Section </h2> <p>This section will introduce a subsection.</p> <h3>The Subsection Heading </h3> <p>This is the text of the subsection. </p> </body> </html>
此时,输出中有相当多的多余空格。在下一节中,您将看到如何消除大部分空格。
修剪空白
请记住,当您查看 DOM 的结构时,有许多只包含可忽略空格的文本节点。输出中的大部分多余空格来自这些节点。幸运的是,XSL 提供了一种消除它们的方法。(有关节点结构的更多信息,请参阅 XSLT/XPath 数据模型。)
注意 - 本节讨论的样式表在解压缩XSLT 示例
到install-dir/jaxp-1_4_2-
release-date/samples
目录后,在xslt/data
目录中找到的article1b.xsl
中。结果是在xslt/data
中找到的stylizer1b.html
。
要去除一些多余的空白,将以下突出显示的行添加到样式表中。
<xsl:stylesheet ...
>
<xsl:output method="html"/>
<xsl:strip-space elements="SECT"/>
[...]
此指令告诉 XSL 删除SECT
元素下只包含空格的文本节点。不包含空格以外文本的节点不会受影响,也不会影响其他类型的节点。
运行带有修剪空白的Stylizer
示例
-
导航至
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例,并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航至
xslt
目录。cd xslt
-
编译
Stylizer
示例。输入以下命令:
% javac Stylizer.java
-
使用样式表
article1b.xsl
在article1.xml
上运行Stylizer
示例。% java Stylizer data/article1b.xsl data/article1.xml
您将看到以下输出:
<html> <body> <h1 align="center">A Sample Article</h1> <h2>The First Major Section </h2> <p>This section will introduce a subsection.</p> <h3>The Subsection Heading </h3> <p>This is the text of the subsection. </p> </body> </html>
这是一个相当大的改进。标题后仍然有换行符和空格,但这些来自 XML 的编写方式:
<SECT>The First Major Section ____<PARA>This section will introduce a subsection.</PARA> ^^^^
在这里,您可以看到节标题以换行符和缩进空格结束,然后是 PARA 条目开始。这不是一个大问题,因为处理 HTML 的浏览器通常会压缩和忽略多余的空格。但我们还有一个格式化工具可供使用。
移除最后的空白
注意 - 本节讨论的样式表在article1c.xsl
中,解压XSLT examples
到install-dir/jaxp-1_4_2-
release-date/samples
目录后,可以在xslt/data
目录中找到。结果是在xslt/data
中找到的stylizer1c.html
。
最后那一点空白空间通过将以下内容添加到样式表中来处理:
<xsl:template match="text()">
<xsl:value-of select="normalize-space()"/>
</xsl:template>
</xsl:stylesheet>
使用此样式表运行Stylizer
将删除所有剩余的空白空间。
使用所有空白空间修剪的Stylizer
示例运行
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。cd xslt
-
编译
Stylizer
示例。输入以下命令:
% javac Stylizer.java
-
使用样式表
article1c.xsl
在article1.xml
上运行Stylizer
示例。% java Stylizer data/article1c.xsl data/article1.xml
输出现在看起来像这样:
<html> <body> <h1 align="center">A Sample Article </h1> <h2>The First Major Section</h2> <p>This section will introduce a subsection. </p> <h3>The Subsection Heading</h3> <p>This is the text of the subsection. </p> </body> </html>
那已经好多了。当然,如果能缩进会更好,但事实证明这比预期的要困难一些。以下是一些可能的攻击途径,以及困难所在:
缩进选项
不幸的是,可以应用于 XML 输出的
indent="yes"
选项对 HTML 输出不可用。即使该选项可用,也不会有帮助,因为 HTML 元素很少嵌套!尽管 HTML 源经常缩进以显示暗示的结构,但 HTML 标记本身并未以创建真实结构的方式嵌套。缩进变量
<xsl:text>
函数允许您添加任何文本,包括空格。因此,它可能被用于输出缩进空间。问题在于变化缩进空间的数量。XSLT 变量似乎是一个好主意,但在这里不起作用。原因是当您在模板中为变量赋值时,该值仅在该模板内(在编译时静态地)被知晓。即使变量在全局范围内定义,分配的值也不会以一种使其在运行时动态地被其他模板知晓的方式存储。当<apply-templates/>
调用其他模板时,这些模板不知道其他地方设置的任何变量设置。参数化模板
使用参数化模板是修改模板行为的另一种方法。但确定要传递的缩进空间量作为参数仍然是问题的关键。
目前,似乎没有任何好的方法来控制 HTML 格式化输出的缩进。如果您需要将 HTML 显示或编辑为纯文本,那将会很不方便。但如果您在 XML 表单上进行编辑,仅在浏览器中显示 HTML 版本,则不是问题(例如,当您查看
stylizer1c.html
时,您会看到预期的结果)。
处理剩余的结构元素
在这一部分,您将处理 LIST
和 NOTE
元素,这些元素为文章添加了更多结构。
注意 - 本节描述的示例文档是 article2.xml
,用于操作它的样式表是 article2.xsl
。结果是 stylizer2.html
。解压缩 XSLT examples
到 install-dir/jaxp-1_4_2-
release-date/samples
目录后,这些文件可以在 xslt/data
目录中找到。
首先向示例文档添加一些测试数据:
<?xml version="1.0"?>
<ARTICLE>
<TITLE>A Sample Article</TITLE>
<SECT>The First Major Section
...
</SECT>
<SECT>The Second Major Section
<PARA>This section adds a LIST and a NOTE.
<PARA>Here is the LIST:
<LIST type="ordered">
<ITEM>Pears</ITEM>
<ITEM>Grapes</ITEM>
</LIST>
</PARA>
<PARA>And here is the NOTE:
<NOTE>Don't forget to go to the
hardware store on your way
to the grocery!
</NOTE>
</PARA>
</SECT>
</ARTICLE>
注意 - 虽然 XML 文件中的 list
和 note
包含在各自的段落中,但它们是否包含并不重要;生成的 HTML 无论如何都是相同的。但是包含它们会使它们在面向大纲的编辑器中更容易处理。
修改 <PARA>
处理
接下来,修改 PARA
模板以考虑我们现在允许一些结构元素嵌入到段落中的事实:
<xsl:template match="PARA">
<p> <xsl:apply-templates select=
"text()|B|I|U|DEF|LINK"/>
</p>
<xsl:apply-templates select=
"PARA|LIST|NOTE"/>
</xsl:template>
这种修改使用了你用于节标题的相同技术。唯一的区别是 SECT
元素不应该出现在段落中。(但是,一个段落很容易存在于另一个段落内,例如,作为引用材料)。
处理 <LIST>
和 <ITEM>
元素
现在你已经准备好添加一个模板来处理 LIST
元素:
<xsl:template match="LIST">
<xsl:if test="@type='ordered'">
<ol>
<xsl:apply-templates/>
</ol>
</xsl:if>
<xsl:if test="@type='unordered'">
<ul>
<xsl:apply-templates/>
</ul>
</xsl:if>
</xsl:template>
</xsl:stylesheet>
<xsl:if>
标签使用 test=""
属性来指定一个布尔条件。在这种情况下,测试值是 type 属性,并且生成的列表会根据值是有序还是无序而改变。
在这个例子中注意两个重要的事情:
-
没有 else 子句,也没有 return 或 exit 语句,因此需要两个
<xsl:if>
标签来覆盖两个选项。(或者可以使用<xsl:choose>
标签,它提供了 case 语句功能)。 -
在属性值周围需要使用单引号。否则,XSLT 处理器会尝试将 ordered 作为 XPath 函数来解释,而不是作为字符串。
现在通过处理 ITEM 元素来完成 LIST 处理:
<xsl:template match="ITEM">
<li><xsl:apply-templates/>
</li>
</xsl:template>
</xsl:stylesheet>
在样式表中排序模板
到目前为止,你应该已经了解到模板是彼此独立的,所以它们通常出现在文件中的位置并不重要。因此,从这一点开始,我们只会展示你需要添加的模板。(为了比较,它们总是添加在示例样式表的末尾)。
当两个模板可以应用于同一节点时,顺序确实很重要。在这种情况下,最后定义的模板是被找到和处理的。例如,要将缩进列表的排序更改为使用小写字母表,可以指定一个看起来像这样的模板模式://LIST//LIST
。在该模板中,您将使用 HTML 选项生成字母枚举,而不是数字枚举。
但这样的元素也可以通过模式//LIST
来识别。为了确保正确处理,指定//LIST
的模板必须出现在指定//LIST//LIST
的模板之前。
处理<NOTE>
元素
唯一剩下的结构元素是NOTE
元素。添加以下模板来处理它。
<xsl:template match="NOTE">
<blockquote><b>Note:</b><br/>
<xsl:apply-templates/>
</p></blockquote>
</xsl:template>
</xsl:stylesheet>
这段代码引发了一个有趣的问题,这是由于包含<br/>
标签导致的。为了使文件成为格式良好的 XML,必须在样式表中指定该标签为<br/>
,但许多浏览器不识别该标签。虽然大多数浏览器识别序列<br></br>
,但它们都将其视为段落换行而不是单个换行。
换句话说,转换必须生成一个<br>
标签,但样式表必须指定<br/>
。这就是我们在样式表中早期添加的特殊输出标签的主要原因:
<xsl:stylesheet ... >
<xsl:output method="html"/>
[...]
</xsl:stylesheet>
该输出规范将空标签(如<br/>
)转换为它们的 HTML 形式<br>
,在输出时。这种转换很重要,因为大多数浏览器不识别空标签。以下是受影响的标签列表:
area frame isindex
base hr link
basefont img meta
br input param
col
总之,默认情况下,XSLT 在输出时生成格式良好的 XML。因为 XSL 样式表本身就是格式良好的 XML,所以你不能轻易地在其中间放置<br>
这样的标签。<xsl:output method="html"/>
标签解决了这个问题,这样你可以在样式表中编码<br/>
,但在输出中得到<br>
。
指定<xsl:output method="html"/>
的另一个主要原因是,与指定<xsl:output method="text"/>
一样,生成的文本不会被转义。例如,如果样式表包含<
实体引用,它将出现为生成文本中的<
字符。另一方面,当生成 XML 时,样式表中的<
实体引用将保持不变,因此在生成的文本中会显示为<
。
注意 - 如果你希望<
实际上作为 HTML 输出的一部分生成,你需要将其编码为<
。这个序列在输出时变为<
,因为只有&
被转换为&
字符。
使用定义了LIST
和NOTE
元素的Stylizer
示例运行
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例,并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录中。 -
导航到
xslt
目录。cd xslt
-
编译
Stylizer
示例。输入以下命令:
% javac Stylizer.java
-
使用样式表
article2.xsl
在article2.xml
上运行Stylizer
示例。% java Stylizer data/article2.xsl data/article2.xml
这是当你现在运行程序时为第二部分生成的 HTML:
... <h2>The Second Major Section </h2> <p>This section adds a LIST and a NOTE. </p> <p>Here is the LIST: </p> <ol> <li>Pears</li> <li>Grapes</li> </ol> <p>And here is the NOTE: </p> <blockquote> <b>Note:</b> <br>Do not forget to go to the hardware store on your way to the grocery! </blockquote>
处理内联(内容)元素
ARTICLE
类型中唯一剩下的标签是内联标签-它们不会在输出中创建换行,而是被整合到它们所属的文本流中。
内联元素与结构元素不同,内联元素是标签内容的一部分。 如果将元素视为文档树中的节点,则每个节点都具有内容和结构。 内容由它包含的文本和内联标记组成。 结构由标签下的其他元素(结构元素)组成。
注意 - 本节描述的示例文档是article3.xml
,用于操作它的样式表是article3.xsl
。 结果是stylizer3.html
。
首先向示例文档添加一些测试数据:
<?xml version="1.0"?>
<ARTICLE>
<TITLE>A Sample Article</TITLE>
<SECT>The First Major Section
[...]
</SECT>
<SECT>The Second Major Section
[...]
</SECT>
<SECT>The <i>Third</i>
Major Section
<PARA>In addition to the inline tag
in the heading,
this section defines the term
<DEF>inline</DEF>,
which literally means "no line break".
It also adds a simple link to the main page
for the Java platform
(<LINK>http://java.sun.com</LINK>),
as well as a link to the
<LINK target="http://java.sun.com/xml">
XML </LINK>
page.
</PARA>
</SECT>
</ARTICLE>
现在处理段落中的内联<DEF>
元素,将它们重命名为 HTML 斜体标记:
<xsl:template match="DEF">
<i> <xsl:apply-templates/> </i>
</xsl:template>
接下来,注释掉文本节点规范化。它已经达到了它的目的,现在你需要保留重要的空格:
<!--
<xsl:template match="text()">
<xsl:value-of select="normalize-space()"/>
</xsl:template>
-->
这个修改使我们不会丢失在<I>
和<DEF>
等标签之前的空格。 (尝试在没有此修改的情况下运行程序,看看结果)。
现在处理基本的内联 HTML 元素,如<B>
,<I>
和<U>
,用于加粗,斜体和下划线。
<xsl:template match="B|I|U">
<xsl:element name="{name()}">
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<xsl:element>
标签允许您计算要生成的元素。 在这里,您使用当前元素的名称生成适当的内联标记。 特别要注意在name=".."
表达式中使用大括号({}
)。 这些大括号导致引号内的文本被处理为 XPath 表达式,而不是被解释为字面字符串。 在这里,它们导致 XPath name()
函数返回当前节点的名称。
大括号可以出现在属性值模板可以出现的任何地方。 (属性值模板在 XSLT 规范的第 7.6.2 节中定义,并且它们出现在模板定义的几个地方)。 在这种表达式中,大括号也可以用于引用属性的值,{@foo}
,或元素的内容{foo}
。
注意 - 您还可以使用<xsl:attribute>
生成属性。 有关更多信息,请参阅 XSLT 规范的第 7.1.3 节。
最后剩下的元素是LINK
标签。处理该标签的最简单方法是设置一个带有参数的命名模板:
<xsl:template name="htmLink">
<xsl:param name="dest"
select="UNDEFINED"/>
<xsl:element name="a">
<xsl:attribute name="href">
<xsl:value-of select=""/>
</xsl:attribute>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
这个模板的主要区别在于,不是指定匹配子句,而是使用name=""
子句为模板指定一个名称。 因此,只有在调用它时才会执行此模板。
在模板内部,还使用<xsl:param>
标签指定一个名为dest
的参数。 为了进行一些错误检查,您使用 select 子句为该参数指定了一个默认值UNDEFINED
。 要在<xsl:value-of>
标签中引用变量,您指定.
。
`* * *
注意 - 请记住,引号中的条目被解释为表达式,除非它进一步被单引号括起来。 这就是为什么之前需要在"@type='ordered'"
中使用单引号来确保 ordered 被解释为字符串。
<xsl:element>
标签生成一个元素。之前,你可以简单地通过编写类似<html>
的代码来指定我们想要的元素。但是在这里,你正在动态生成<xsl:element>
标签的主体中 HTML 锚点(<a>
)的内容。并且你正在使用<xsl:attribute>
标签动态生成锚点的href
属性。
模板的最后一个重要部分是<apply-templates>
标签,它插入LINK
元素下文本节点中的文本。如果没有它,生成的 HTML 链接中将没有文本。
接下来,添加LINK
标签的模板,并在其中调用命名模板:
<xsl:template match="LINK">
<xsl:if test="@target">
<!--Target attribute specified.-->
<xsl:call-template
name="htmLink">
<xsl:with-param name="dest"
select="@target"/>
</xsl:call-template>
</xsl:if>
</xsl:template>
<xsl:template name="htmLink">
[...]
test="@target"
子句在 LINK 标签中存在 target 属性时返回 true。因此,当链接的文本和为其定义的目标不同时,这个<xsl-if>
标签会生成 HTML 链接。
<xsl:call-template>
标签调用命名模板,而<xsl:with-param>
使用name
子句指定参数,并使用select
子句指定其值。
在样式表构建过程的最后一步中,添加<xsl-if>
标签来处理没有target
属性的LINK
标签。
<xsl:template match="LINK">
<xsl:if test="@target">
[...]
</xsl:if>
<xsl:if test="not(@target)">
<xsl:call-template name="htmLink">
<xsl:with-param name="dest">
<xsl:apply-templates/>
</xsl:with-param>
</xsl:call-template>
</xsl:if>
</xsl:template>
not(...)
子句反转了先前的测试(记住,没有 else 子句)。因此,当未指定目标属性时,模板的这部分被解释。这次,参数值不是来自select
子句,而是来自<xsl:with-param>
元素的内容。
注意 - 仅仅是为了明确:参数和变量(稍后在 XSLT 还能做什么?中讨论)的值可以通过select
子句指定,让你可以使用 XPath 表达式,也可以通过元素的内容指定,让你可以使用 XSLT 标签。
在这种情况下,参数的内容是由<xsl:apply-templates/>
标签生成的,它插入LINK
元素下文本节点的内容。
运行带有内联元素定义的Stylizer
示例
-
导航到
samples
目录。% cd *install-dir*/jaxp-1_4_2-*release-date*/samples.
-
点击此链接下载 XSLT 示例并将其解压缩到install-dir
/jaxp-1_4_2-
release-date/samples
目录。 -
导航到
xslt
目录。cd xslt
-
编译
Stylizer
示例。输入以下命令:
% javac Stylizer.java
-
使用样式表
article3.xsl
在article3.xml
上运行Stylizer
示例。% java Stylizer data/article3.xsl data/article3.xml
现在运行程序,结果应该看起来像这样:
[...] <h2>The <i>Third</i> Major Section</h2> <p>In addition to the inline tag in the heading, this section defines the term <i>inline</i>, which literally means "no line break". It also adds a simple link to the main page for the Java platform (<a href="http://java.sun.com">http://java.sun.com</a>), as well as a link to the <a href="http://java.sun.com/xml">XML</a> page.</p>
干得好!你现在已经将一个相当复杂的 XML 文件转换为 HTML。(尽管一开始看起来很简单,但它确实提供了很多探索的机会)。
打印 HTML
您现在已将 XML 文件转换为 HTML。总有一天,会有人制作出一个了解 HTML 的打印引擎,您将能够通过 Java 打印服务 API 找到并使用它。到那时,您将能够通过生成 HTML 打印任意 XML 文件。您只需设置一个样式表并使用您的浏览器。
XSLT 还能做什么?
尽管本节内容很长,但只是触及了 XSLT 的能力表面。XSLT 规范中还有许多额外的可能性等待着您。以下是一些值得关注的事项:
import
(第 2.6.2 节)和 include
(第 2.6.1 节)
rt
(第 2.6.2 节)和 include(第 2.6.1 节)使用这些语句对 XSLT 样式表进行模块化和组合。include 语句只是插入所包含文件中的任何定义。import 语句允许您用自己样式表中的定义覆盖导入文件中的定义。
for-each
循环(第 8 节)
遍历一系列项目并依次处理每个项目。
choose
(case 语句)用于条件处理(第 9.2 节)
根据输入值分支到多个处理路径之一。
生成数字(第 7.7 节)
动态生成编号的章节、编号元素和数字文字。XSLT 提供三种编号模式:
-
Single:将项目编号在单个标题下,就像 HTML 中的有序列表
-
Multiple:生成多级编号,如"A.1.3"
-
Any:无论项目出现在何处,都连续编号,就像课程中的脚注。
格式化数字(第 12.3 节)
控制枚举格式,以便获得数字(format="1"
)、大写字母(format="A"
)、小写字母(format="a"
)或复合数字,如"A.1",以及适合特定国际区域设置的数字和货币金额。
排序输出(第 10 节)
按照所需的排序顺序生成输出。
基于模式的模板(第 5.7 节)
多次处理一个元素,每次在不同的“模式”中。您可以向模板添加一个模式属性,然后指定<apply-templates mode="...">
来仅应用具有匹配模式的模板。结合<apply-templates select="...">
属性,将基于模式的处理应用于输入数据的子集。
变量(第 11 节)
变量类似于方法参数,可以控制模板的行为。但它们并不像你想象的那样有价值。变量的值仅在定义它的当前模板或<xsl:if>
标签(例如)的范围内才可知。你无法将一个值从一个模板传递到另一个模板,甚至无法将一个模板的封闭部分的值传递到同一模板的另一部分。
即使是“全局”变量,这些说法也是正确的。你可以在模板中更改它的值,但更改仅适用于该模板。当用于定义全局变量的表达式被评估时,该评估发生在结构的根节点的上下文中。换句话说,全局变量本质上是运行时常量。这些常量可以用于改变模板的行为,特别是与包含和导入语句结合使用时。但变量不是通用的数据管理机制。
变量的问题
创建一个单一模板并为链接的目标设置一个变量,而不是费力地设置一个带参数的模板并以两种不同的方式调用它,这是一个诱人的想法。这个想法是将变量设置为默认值(比如,LINK
标签的文本),然后,如果目标属性存在,将目的地变量设置为目标属性的值。
如果这个方法行得通就好了。但问题在于变量只在定义它们的范围内才被知晓。因此,当你编写一个<xsl:if>
标签来改变变量的值时,该值只在<xsl:if>
标签的上下文中被知晓。一旦遇到</xsl:if>
,对变量设置的任何更改都会丢失。
一个同样诱人的想法是用一个变量()来替换
text()|B|I|U|DEF|LINK规范。但由于变量的值取决于它的定义位置,全局内联变量的值由文本节点、
`节点等组成,这些节点恰好存在于根级别。换句话说,在这种情况下,这样一个变量的值是空的。**
课程:XML 的流式 API
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/index.html
本课程专注于 XML 的流式 API(StAX),这是一种基于 Java 技术的流式、事件驱动、拉取解析的 API,用于读取和写入 XML 文档。StAX 使您能够创建快速、相对易于编程且具有轻量级内存占用的双向 XML 解析器。
为什么选择 StAX?
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/why.html
StAX 项目由 BEA 主导,得到了 Sun Microsystems 的支持,JSR 173 规范于 2004 年 3 月通过了 Java 社区流程的最终批准投票。StAX API 的主要目标是通过公开一个简单的基于迭代器的 API,将“解析控制权交给程序员。这允许程序员请求下一个事件(拉取事件),并允许以过程化方式存储状态。” StAX 的创建是为了解决两种最常见解析 API,SAX 和 DOM,的限制。
流式处理与 DOM
一般来说,处理 XML 信息集有两种编程模型:流式处理和文档对象模型(DOM)。
DOM 模型涉及创建代表整个文档树和 XML 文档的完整信息集状态的内存对象。一旦在内存中,DOM 树可以自由导航和任意解析,因此为开发人员提供了最大的灵活性。然而,这种灵活性的代价是潜在的大内存占用和显著的处理器需求,因为整个文档的表示必须作为对象在内存中保持,以便在文档处理期间使用。在处理小型文档时,这可能不是问题,但随着文档大小的增加,内存和处理器需求可能会迅速升高。
流式处理是指一种编程模型,在应用程序运行时串行传输和解析 XML 信息集,通常是实时的,并且通常来自动态来源,其内容事先并不完全知晓。此外,基于流的解析器可以立即开始生成输出,并且信息集元素在使用后可以立即丢弃和进行垃圾回收。虽然提供了较小的内存占用、降低的处理器需求和在某些情况下更高的性能,但流处理的主要折衷是您只能在文档中的一个位置看到信息集状态。您基本上受限于文档的“纸板筒”视图,这意味着您需要在阅读 XML 文档之前知道要进行哪些处理。
在处理 XML 时,流式处理模型特别适用于应用程序具有严格的内存限制,比如在运行 Java 平台微版(Java ME 平台)的手机上,或者当应用程序需要同时处理多个请求时,比如在应用服务器上。实际上,可以说大多数 XML 业务逻辑都可以从流式处理中受益,并且不需要在内存中维护整个 DOM 树。
拉取解析与推送解析
流拉取解析是一种编程模型,其中客户端应用程序在需要与 XML 信息集交互时调用 XML 解析库的方法,即客户端只有在明确请求时才会获取(拉取)XML 数据。
流推送解析是一种编程模型,其中 XML 解析器在遇到 XML 信息集中的元素时向客户端发送(推送)XML 数据,即使客户端此时还没有准备好使用它。
在处理 XML 流时,拉取解析相比于推送解析提供了几个优势:
-
在拉取解析中,客户端控制应用程序线程,并且可以在需要时调用解析器的方法。相比之下,在推送处理中,解析器控制应用程序线程,客户端只能接受解析器的调用。
-
拉取解析库可以比推送库更小,与这些库交互的客户端代码也更简单,即使对于更复杂的文档。
-
拉取客户端可以使用单个线程同时读取多个文档。
-
StAX 拉取解析器可以过滤 XML 文档,使客户端不需要的元素被忽略,并且可以支持非 XML 数据的 XML 视图。
StAX 使用案例
StAX 规范定义了 API 的许多用例:
-
数据绑定
-
反编组 XML 文档
-
将 XML 文档编组
-
并行文档处理
-
无线通信
-
-
简单对象访问协议(SOAP)消息处理
-
解析简单可预测的结构
-
解析具有前向引用的图形表示
-
解析 Web 服务描述语言(WSDL)
-
-
虚拟数据源
-
查看存储在数据库中的 XML 数据
-
查看由 XML 数据绑定创建的 Java 对象中的数据
-
将 DOM 树作为事件流导航
-
-
解析特定的 XML 词汇
-
管道化 XML 处理
对所有这些用例的完整讨论超出了本课程的范围。请参考 StAX 规范以获取更多信息。
将 StAX 与其他 JAXP API 进行比较
作为 JAXP 家族中的一个 API,StAX 可以与 SAX、TrAX 和 JDOM 等其他 API 进行比较。在后两者中,StAX 不像 TrAX 或 JDOM 那样强大或灵活,但也不需要太多内存或处理器负载才能发挥作用,并且在许多情况下,StAX 可以胜过基于 DOM 的 API。上面概述的相同论点,权衡 DOM 模型与流模型的成本/效益,在这里同样适用。
有鉴于此,最接近的比较可以在 StAX 和 SAX 之间进行,正是在这里 StAX 提供了许多情况下有益的功能;其中一些包括:
-
使用 StAX 的客户端通常比使用 SAX 的客户端更容易编码。虽然可以说 SAX 解析器稍微更容易编写,但 StAX 解析器的代码可能更小,客户端与解析器交互所需的代码更简单。
-
StAX 是一个双向 API,意味着它既可以读取又可以写入 XML 文档。SAX 只能读取,所以如果你想要写入 XML 文档,就需要另一个 API。
-
SAX 是一个推送 API,而 StAX 是一个拉取 API。上面概述的推送和拉取 API 之间的权衡在这里也适用。
以下表格总结了 StAX、SAX、DOM 和 TrAX 的比较特性。(表格改编自 Jeff Ryan 的文章Does StAX Belong in Your XML Toolbox?)。
XML 解析器 API 特性摘要
特性 | StAX | SAX | DOM | TrAX |
---|---|---|---|---|
API 类型 | 拉取,流式 | 推送,流式 | 内存树 | XSLT 规则 |
使用便捷性 | 高 | 中 | 高 | 中 |
XPath 能力 | 否 | 否 | 是 | 是 |
CPU 和内存效率 | 良好 | 良好 | 各异 | 各异 |
仅向前 | 是 | 是 | 否 | 否 |
读取 XML | 是 | 是 | 是 | 是 |
写入 XML | 是 | 否 | 是 | 是 |
创建,读取,更新,删除 | 否 | 否 | 是 | 否 |
StAX API
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/api.html
StAX API 公开了用于 XML 文档的迭代式、基于事件的处理的方法。XML 文档被视为一系列经过过滤的事件,并且信息集状态可以以过程化方式存储。此外,与 SAX 不同,StAX API 是双向的,可以实现对 XML 文档的读取和写入。
StAX API 实际上是两个不同的 API 集:一个光标 API 和一个迭代器 API。这两个 API 集将在本课程的后面更详细地解释,但它们的主要特点如下所述。
光标 API
如其名称所示,StAX 光标 API 表示一个光标,您可以使用它从头到尾遍历 XML 文档。这个光标一次只能指向一件事,并且总是向前移动,从不后退,通常一次移动一个信息集元素。
两个主要的光标接口是XMLStreamReader
和XMLStreamWriter
。XMLStreamReader
包括了从 XML 信息模型中检索所有可能信息的访问方法,包括文档编码、元素名称、属性、命名空间、文本节点、起始标记、注释、处理指令、文档边界等等;例如:
public interface XMLStreamReader {
public int next() throws XMLStreamException;
public boolean hasNext() throws XMLStreamException;
public String getText();
public String getLocalName();
public String getNamespaceURI();
// ... other methods not shown
}
您可以在XMLStreamReader
上调用诸如getText
和getName
之类的方法,以获取当前光标位置的数据。XMLStreamWriter
提供了与StartElement
和EndElement
事件类型对应的方法;例如:
public interface XMLStreamWriter {
public void writeStartElement(String localName) throws XMLStreamException;
public void writeEndElement() throws XMLStreamException;
public void writeCharacters(String text) throws XMLStreamException;
// ... other methods not shown
}
光标 API 与 SAX 在许多方面相似。例如,可以直接访问字符串和字符信息的方法可用,并且可以使用整数索引访问属性和命名空间信息。与 SAX 一样,光标 API 方法将 XML 信息作为字符串返回,这减少了对象分配的需求。
迭代器 API
StAX 迭代器 API 将 XML 文档流表示为一组离散的事件对象。这些事件由应用程序拉取,并由解析器按照它们在源 XML 文档中读取的顺序提供。
基本的迭代器接口称为XMLEvent
,并且为事件迭代器 API 表中列出的每种事件类型都有子接口。用于读取迭代器事件的主要解析器接口是XMLEventReader
,用于写入迭代器事件的主要接口是XMLEventWriter
。XMLEventReader
接口包含五种方法,其中最重要的是nextEvent
,它返回 XML 流中的下一个事件。XMLEventReader
实现了java.util.Iterator
,这意味着从XMLEventReader
返回的内容可以被缓存或传递给可以与标准 Java 迭代器一起工作的程序;例如:
public interface XMLEventReader extends Iterator {
public XMLEvent nextEvent() throws XMLStreamException;
public boolean hasNext();
public XMLEvent peek() throws XMLStreamException;
// ...
}
类似地,在迭代器 API 的输出端,你有:
public interface XMLEventWriter {
public void flush() throws XMLStreamException;
public void close() throws XMLStreamException;
public void add(XMLEvent e) throws XMLStreamException;
public void add(Attribute attribute) throws XMLStreamException;
// ...
}
迭代器事件类型
XMLEvent
在事件迭代器 API 中定义的类型
事件类型 | 描述 |
---|---|
StartDocument |
报告一组 XML 事件的开始,包括编码、XML 版本和独立属性。 |
StartElement |
报告元素的开始,包括任何属性和命名空间声明;还提供了开始标记的前缀、命名空间 URI 和本地名称的访问。 |
EndElement |
报告元素的结束标记。如果已在相应的 StartElement 上显式设置了命名空间,则在此处可以调用已经超出范围的命名空间。 |
Characters |
对应于 XML CData 部分和 CharacterData 实体。请注意,可忽略的空格和重要的空格也被报告为 Character 事件。 |
EntityReference |
字符实体可以作为独立事件报告,应用程序开发人员可以选择解析或传递未解析的实体。默认情况下,实体会被解析。或者,如果不想将实体报告为事件,则可以替换文本并报告为 Characters 。 |
ProcessingInstruction |
报告底层处理指令的目标和数据。 |
Comment |
返回注释的文本。 |
EndDocument |
报告一组 XML 事件的结束。 |
DTD |
报告与流相关联的(如果有的话)DTD 的信息,并提供一种返回在 DTD 中找到的自定义对象的方法。 |
Attribute |
属性通常作为 StartElement 事件的一部分报告。然而,有时希望将属性作为独立的 Attribute 事件返回;例如,当命名空间作为 XQuery 或 XPath 表达式的结果返回时。 |
Namespace |
与属性一样,命名空间通常作为 StartElement 的一部分报告,但有时希望将命名空间作为独立的 Namespace 事件报告。 |
请注意,只有在处理的文档包含 DTD 时,才会创建 DTD
、EntityDeclaration
、EntityReference
、NotationDeclaration
和 ProcessingInstruction
事件。
事件映射示例
作为事件迭代器 API 如何映射 XML 流的示例,请考虑以下 XML 文档:
<?xml version="1.0"?>
<BookCatalogue >
<Book>
<Title>Yogasana Vijnana: the Science of Yoga</Title>
<ISBN>81-40-34319-4</ISBN>
<Cost currency="INR">11.50</Cost>
</Book>
</BookCatalogue>
此文档将被解析为十八个主要和次要事件,如下表所示。请注意,通常从主要事件而不是直接访问,可以访问用大括号({}
)显示的次要事件。
迭代器 API 事件映射示例
# | 元素/属性 | 事件 |
---|---|---|
1 | version="1.0" |
StartDocument |
2 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
3 | qname = BookCatalogue:http://www.publishing.org 属性 = null 命名空间 = {BookCatalogue" -> http://www.publishing.org"} |
StartElement |
4 | qname = 书 属性 = null 命名空间 = null |
StartElement |
5 | qname = 标题 属性 = null 命名空间 = null |
StartElement |
6 | isCData = false data = "Yogasana Vijnana: the Science of Yoga\n\t" IsWhiteSpace = false |
Characters |
7 | qname = Title namespaces = null |
EndElement |
8 | qname = ISBN attributes = null namespaces = null |
StartElement |
9 | isCData = false data = "81-40-34319-4\n\t" IsWhiteSpace = false |
Characters |
10 | qname = ISBN namespaces = null |
EndElement |
11 | qname = Cost attributes = {"currency" -> INR} namespaces = null |
StartElement |
12 | isCData = false data = "11.50\n\t" IsWhiteSpace = false |
Characters |
13 | qname = Cost namespaces = null |
EndElement |
14 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
15 | qname = Book namespaces = null |
EndElement |
16 | isCData = false data = "\n" IsWhiteSpace = true |
Characters |
17 | qname = BookCatalogue:http://www.publishing.org namespaces = {BookCatalogue" -> http://www.publishing.org"} |
EndElement |
18 | EndDocument |
在这个例子中有几个重要的事项需要注意:
-
事件按照文档中遇到相应的 XML 元素的顺序创建,包括元素的嵌套、打开和关闭元素、属性顺序、文档开始和文档结束等。
-
与正确的 XML 语法一样,所有容器元素都有相应的开始和结束事件;例如,每个
StartElement
都有一个对应的EndElement
,即使是空元素也是如此。 -
Attribute
事件被视为次要事件,并且可以从其对应的StartElement
事件中访问。 -
与
Attribute
事件类似,Namespace
事件被视为次要事件,但在事件流中出现两次,并且可以从它们对应的StartElement
和EndElement
中分别访问两次。 -
所有元素都指定了
Character
事件,即使这些元素没有字符数据。同样,Character
事件可以跨事件分割。 -
StAX 解析器维护一个命名空间堆栈,其中保存了当前元素及其祖先元素定义的所有 XML 命名空间信息。通过
javax.xml.namespace.NamespaceContext
接口暴露的命名空间堆栈可以通过命名空间前缀或 URI 访问。
在游标和迭代器 API 之间进行选择
此时合理地问一下,“我应该选择哪个 API?我应该创建 XMLStreamReader
还是 XMLEventReader
的实例?为什么会有两种类型的 API?”
开发目标
StAX 规范的作者针对三种类型的开发者:
-
图书馆和基础设施开发者:创建应用服务器、JAXM、JAXB、JAX-RPC 等实现;需要高效、低级别的 API,并且具有最小的可扩展性要求。
-
Java ME 开发者:需要小型、简单的拉取解析库,并且具有最小的可扩展性需求。
-
Java 平台企业版(Java EE)和 Java 平台标准版(Java SE)开发人员:需要干净、高效的拉取解析库,同时需要灵活性来读取和写入 XML 流,创建新的事件类型,并扩展 XML 文档元素和属性。
鉴于这些广泛的开发类别,StAX 的作者认为定义两个小型、高效的 API 比过载一个更大、必然更复杂的 API 更有用。
比较游标和迭代器 API
在选择游标和迭代器 API 之间之前,你应该注意一些你可以使用迭代器 API 而不能使用游标 API 的事项:
-
从
XMLEvent
子类创建的对象是不可变的,可以在数组、列表和映射中使用,并且可以在解析器继续处理后传递到你的应用程序中。 -
你可以创建
XMLEvent
的子类型,这些子类型可以是全新的信息项,也可以是现有项目的扩展,但具有额外的方法。 -
你可以以比游标 API 更简单的方式向 XML 事件流中添加和删除事件。
同样,在做出选择时,请记住一些一般性建议:
-
如果你正在为特别受内存限制的环境编程,比如 Java ME,你可以使用游标 API 创建更小、更高效的代码。
-
如果性能是你的最高优先级——例如,在创建低级库或基础设施时——游标 API 更有效率。
-
如果你想创建 XML 处理管道,请使用迭代器 API。
-
如果你想修改事件流,请使用迭代器 API。
-
如果你希望你的应用程序能够处理事件流的可插拔处理,请使用迭代器 API。
-
一般来说,如果你没有明确偏好,建议使用迭代器 API,因为它更灵活、可扩展,从而“未雨绸缪”你的应用程序。
使用 StAX
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/using.html
一般来说,StAX 程序员通过使用 XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
类来创建 XML 流读取器、写入器和事件。通过在工厂上设置属性来进行配置,可以通过在工厂上使用 setProperty
方法将特定于实现的设置传递给底层实现。类似地,可以使用 getProperty
工厂方法查询特定于实现的设置。
下面描述了 XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
类,然后讨论了资源分配、命名空间和属性管理、错误处理,最后使用游标和迭代器 API 读取和写入流。
StAX 工厂类
StAX 工厂类。XMLInputFactory
、XMLOutputFactory
和 XMLEventFactory
,让您定义和配置 XML 流读取器、流写入器和事件类的实现实例。
XMLInputFactory
XMLInputFactory
类允许您配置由工厂创建的 XML 流读取器处理器的实现实例。通过在类上调用 newInstance
方法来创建抽象类 XMLInputFactory
的新实例。然后使用静态方法 XMLInputFactory.newInstance
来创建新的工厂实例。
派生自 JAXP,XMLInputFactory.newInstance
方法通过以下查找过程确定要加载的特定 XMLInputFactory
实现类:
-
使用
javax.xml.stream.XMLInputFactory
系统属性。 -
使用 Java SE 平台的 Java Runtime Environment (JRE) 目录中的
lib/xml.stream.properties
文件。 -
如果可用,使用 Services API 通过查找 JRE 中可用的 JAR 文件中的
META-INF/services/javax.xml.stream.XMLInputFactory
文件确定类名。 -
使用平台默认的
XMLInputFactory
实例。
在获取适当的 XMLInputFactory
引用之后,应用程序可以使用工厂来配置和创建流实例。以下表格列出了 XMLInputFactory
支持的属性。详细列表请参阅 StAX 规范。
javax.xml.stream.XMLInputFactory
属性
属性 | 描述 |
---|---|
isValidating |
打开实现特定的验证。 |
isCoalescing |
(必需) 要求处理器合并相邻的字符数据。 |
isNamespaceAware |
关闭命名空间支持。所有实现必须支持命名空间。对非命名空间感知文档的支持是可选的。 |
isReplacingEntityReferences |
(必需) 要求处理器用其替换值替换内部实体引用,并将其报告为字符或描述实体的事件集。 |
isSupportingExternalEntities |
(必需) 要求处理器解析外部解析实体。 |
reporter |
(必需) 设置并获取XMLReporter 接口的实现。 |
resolver |
(必需) 设置并获取XMLResolver 接口的实现。 |
allocator |
(必需) 设置并获取XMLEventAllocator 接口的实现。 |
XMLOutputFactory
通过在类上调用newInstance
方法来创建抽象类XMLOutputFactory
的新实例。然后使用静态方法XMLOutputFactory.newInstance
来创建一个新的工厂实例。用于获取实例的算法与XMLInputFactory
相同,但引用javax.xml.stream.XMLOutputFactory
系统属性。
XMLOutputFactory
只支持一个属性,即javax.xml.stream.isRepairingNamespaces
。此属性是必需的,其目的是创建默认前缀并将其与命名空间 URI 关联起来。有关更多信息,请参阅 StAX 规范。
XMLEventFactory
通过在类上调用newInstance
方法来创建抽象类XMLEventFactory
的新实例。然后使用静态方法XMLEventFactory.newInstance
来创建一个新的工厂实例。此工厂引用javax.xml.stream.XMLEventFactory
属性来实例化工厂。用于获取实例的算法与XMLInputFactory
和XMLOutputFactory
相同,但引用javax.xml.stream.XMLEventFactory
系统属性。
XMLEventFactory
没有默认属性。
资源、命名空间和错误
StAX 规范处理资源解析、属性和命名空间,以及错误和异常,如下所述。
资源解析
XMLResolver
接口提供了在 XML 处理期间解析资源的方法。应用程序在XMLInputFactory
上设置接口,然后该工厂实例创建的所有处理器都设置该接口。
属性和命名空间
属性由 StAX 处理器使用游标接口中的查找方法和字符串以及迭代器接口中的Attribute
和Namespace
事件报告。请注意,命名空间被视为属性,尽管在游标和迭代器 API 中,命名空间与属性分开报告。还要注意,命名空间处理对于 StAX 处理器是可选的。有关命名空间绑定和可选命名空间处理的完整信息,请参阅 StAX 规范。
错误报告和异常处理
所有致命错误都通过javax.xml.stream.XMLStreamException
接口报告。所有非致命错误和警告都使用javax.xml.stream.XMLReporter
接口报告。
读取 XML 流
正如在本课程前面所描述的,使用 StAX 处理器读取 XML 流的方式——更重要的是,您得到的内容——取决于您是使用 StAX 游标 API 还是事件迭代器 API,这两个部分描述了如何使用这两个 API 读取 XML 流。
使用 XMLStreamReader
StAX 游标 API 中的XMLStreamReader
接口只允许您以向前方向读取 XML 流或文档,每次只能读取信息集中的一个项目。以下方法可用于从流中提取数据或跳过不需要的事件:
-
获取属性的值
-
读取 XML 内容
-
确定一个元素是否有内容或为空
-
获取对属性集合的索引访问
-
获取对命名空间集合的索引访问
-
获取当前事件的名称(如果适用)
-
获取当前事件的内容(如果适用)
XMLStreamReader
的实例在任何时候都有一个当前事件,其方法在其上操作。当您在流上创建一个XMLStreamReader
实例时,初始当前事件是START_DOCUMENT
状态。然后可以使用XMLStreamReader.next
方法来跳到流中的下一个事件。
读取属性、属性和命名空间
XMLStreamReader.next
方法加载流中下一个事件的属性。然后,您可以通过调用XMLStreamReader.getLocalName
和XMLStreamReader.getText
方法来访问这些属性。
当XMLStreamReader
游标位于StartElement
事件上时,它读取事件的名称和任何属性,包括命名空间。可以使用索引值访问事件的所有属性,并且还可以通过命名空间 URI 和本地名称查找。但请注意,只有当前StartEvent
上声明的命名空间可用;之前声明的命名空间不会被保留,重新声明的命名空间也不会被移除。
XMLStreamReader 方法
XMLStreamReader
提供以下方法来检索有关命名空间和属性的信息:
int getAttributeCount();
String getAttributeNamespace(int index);
String getAttributeLocalName(int index);
String getAttributePrefix(int index);
String getAttributeType(int index);
String getAttributeValue(int index);
String getAttributeValue(String namespaceUri, String localName);
boolean isAttributeSpecified(int index);
也可以使用三种额外的方法访问命名空间:
int getNamespaceCount();
String getNamespacePrefix(int index);
String getNamespaceURI(int index);
实例化一个 XMLStreamReader
这个示例取自 StAX 规范,展示了如何实例化一个输入工厂,创建一个读取器,并遍历 XML 流的元素:
XMLInputFactory f = XMLInputFactory.newInstance();
XMLStreamReader r = f.createXMLStreamReader( ... );
while(r.hasNext()) {
r.next();
}
使用 XMLEventReader
StAX 事件迭代器 API 中的XMLEventReader
API 提供了将 XML 流中的事件映射到可以自由重用的分配的事件对象的方法,并且 API 本身可以扩展以处理自定义事件。
XMLEventReader
提供了四种方法来迭代解析 XML 流:
-
next
:返回流中的下一个事件 -
nextEvent
:返回下一个类型化的 XMLEvent -
hasNext
:如果流中有更多事件要处理,则返回 true -
peek
:返回事件但不迭代到下一个事件
例如,以下代码片段说明了XMLEventReader
方法声明:
package javax.xml.stream;
import java.util.Iterator;
public interface XMLEventReader extends Iterator {
public Object next();
public XMLEvent nextEvent() throws XMLStreamException;
public boolean hasNext();
public XMLEvent peek() throws XMLStreamException;
// ...
}
要读取流上的所有事件然后打印它们,您可以使用以下方法:
while(stream.hasNext()) {
XMLEvent event = stream.nextEvent();
System.out.print(event);
}
读取属性
您可以从其关联的javax.xml.stream.StartElement
中访问属性,如下所示:
public interface StartElement extends XMLEvent {
public Attribute getAttributeByName(QName name);
public Iterator getAttributes();
}
您可以使用StartElement
接口上的getAttributes
方法来使用在该StartElement
上声明的所有属性的Iterator
。
读取命名空间
与读取属性类似,命名空间是通过调用StartElement
接口上的getNamespaces
方法创建的Iterator
来读取的。仅返回当前StartElement
的命名空间,并且应用程序可以通过使用StartElement.getNamespaceContext
来获取当前命名空间上下文。
写入 XML 流
StAX 是一个双向 API,游标和事件迭代器 API 都有自己的一套接口用于写入 XML 流。与读取流的接口一样,写入器 API 对于游标和事件迭代器之间存在显著差异。以下部分描述了如何使用这些 API 之一来写入 XML 流。
使用 XMLStreamWriter
StAX 游标 API 中的XMLStreamWriter
接口允许应用程序写回到 XML 流或创建全新的流。XMLStreamWriter 具有让您执行以下操作的方法:
-
写入格式良好的 XML
-
刷新或关闭输出
-
写入限定名称
请注意,XMLStreamWriter
实现不需要对输入执行格式良好性或有效性检查。虽然一些实现可能执行严格的错误检查,但其他可能不会。您实现的规则适用于XMLOutputFactory
类中定义的属性。
使用writeCharacters
方法转义字符,如&
、<
、>
和"
。绑定前缀可以通过传递前缀的实际值,使用setPrefix
方法,或设置默认命名空间声明的属性来处理。
以下示例取自 StAX 规范,展示了如何实例化输出工厂,创建写入器并写入 XML 输出:
XMLOutputFactory output = XMLOutputFactory.newInstance();
XMLStreamWriter writer = output.createXMLStreamWriter( ... );
writer.writeStartDocument();
writer.setPrefix("c","http://c");
writer.setDefaultNamespace("http://c");
writer.writeStartElement("http://c","a");
writer.writeAttribute("b","blah");
writer.writeNamespace("c","http://c");
writer.writeDefaultNamespace("http://c");
writer.setPrefix("d","http://c");
writer.writeEmptyElement("http://c","d");
writer.writeAttribute("http://c", "chris","fry");
writer.writeNamespace("d","http://c");
writer.writeCharacters("Jean Arp");
writer.writeEndElement();
writer.flush();
此代码生成以下 XML(新行不是规范性的):
<?xml version=’1.0’ encoding=’utf-8’?>
<a b="blah" >
<d:d d:chris="fry" />Jean Arp</a>
使用 XMLEventWriter
StAX 事件迭代器 API 中的XMLEventWriter
接口允许应用程序写回到 XML 流或创建全新的流。此 API 可以扩展,但主要 API 如下:
public interface XMLEventWriter {
public void flush() throws XMLStreamException;
public void close() throws XMLStreamException;
public void add(XMLEvent e) throws XMLStreamException;
// ... other methods not shown.
}
XMLEventWriter
的实例是由XMLOutputFactory
的实例创建的。流事件被迭代地添加,一旦添加到事件写入器实例后,事件就不能被修改。
属性、转义字符、绑定前缀
StAX 实现需要缓冲最后一个StartElement
,直到在流中添加或遇到除Attribute
或Namespace
之外的事件。这意味着当您向流中添加Attribute
或Namespace
时,它会附加到当前的StartElement
事件。
您可以使用Characters
方法转义字符如&
、<
、>
和"
。
setPrefix(...)
方法可用于显式绑定输出时使用的前缀,而 getPrefix(...)
方法可用于获取当前前缀。请注意,默认情况下,XMLEventWriter
会将命名空间绑定添加到其内部命名空间映射中。前缀在绑定它们的事件对应的 EndElement
后会失效。
Oracle 的流式 XML 解析器实现
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/parser.html
应用服务器 9.1 包含 Sun 微系统的 JSR 173(StAX)实现,称为 Sun Java 流式 XML 解析器(简称为流式 XML 解析器)。流式 XML 解析器是一个高速、非验证的、符合 W3C XML 1.0 和 Namespace 1.0 标准的流式 XML 拉取解析器,构建在 Xerces2 代码库之上。
在 Sun 的流式 XML 解析器实现中,Xerces2 的底层,特别是 Scanner 和相关类,已经重新设计为拉取方式。除了底层的更改外,流式 XML 解析器还包括额外的与 StAX 相关的功能和许多性能增强改进。流式 XML 解析器实现在 appserv-ws.jar
和 javaee.jar
文件中,这两个文件位于 install_dir/lib/
目录中。
JAXP 参考实现中包含了 StAX 代码示例,位于 INSTALL_DIR/jaxp-
version/samples/stax
目录中,展示了 Sun 的流式 XML 解析器实现的工作原理。这些示例在 示例代码 中有描述。
在继续使用示例代码之前,有两个关于流式 XML 解析器的方面需要注意:
-
报告 CDATA 事件
-
流式 XML 解析器工厂实现
下面将讨论这些主题。
报告 CDATA 事件
流式 XML 解析器中实现的 javax.xml.stream.XMLStreamReader
不报告 CDATA 事件。如果您有一个需要接收此类事件的应用程序,请配置 XMLInputFactory
来设置以下特定于实现的 report-cdata-event
属性:
XMLInputFactory factory = XMLInptuFactory.newInstance();
factory.setProperty("report-cdata-event", Boolean.TRUE);
流式 XML 解析器工厂实现
大多数应用程序不需要知道工厂实现类名。对于大多数应用程序,只需将 javaee.jar
和 appserv-ws.jar
文件添加到类路径即可,因为这两个 jar 文件在 META-INF/services
目录下提供了各种流式 XML 解析器属性的工厂实现类名,例如 javax.xml.stream.XMLInputFactory
、javax.xml.stream.XMLOutputFactory
和 javax.xml.stream.XMLEventFactory
,这是应用程序请求工厂实例时查找操作的第三步。有关查找机制的更多信息,请参阅 XMLInputFactory.newInstance
方法的 Javadoc。
但是,在某些情况下,应用程序可能希望了解工厂实现类名并显式设置属性。这些情况可能包括类路径中存在多个 JSR 173 实现,应用程序希望选择其中一个,也许是性能更好的一个,包含了关键的错误修复,或类似情况。
如果一个应用程序设置了SystemProperty
,那么这是查找操作的第一步,因此获取工厂实例相对于其他选项来说会更快;例如:
javax.xml.stream.XMLInputFactory -->
com.sun.xml.stream.ZephyrParserFactory
javax.xml.stream.XMLOutputFactory -->
com.sun.xml.stream.ZephyrWriterFactor
javax.xml.stream.XMLEventFactory -->
com.sun.xml.stream.events.ZephyrEventFactory
示例代码
原文:
docs.oracle.com/javase/tutorial/jaxp/stax/example.html
本节逐步介绍了 JAXP 参考实现包中包含的示例 StAX 代码。本节中使用的所有示例目录均位于INSTALL_DIR/jaxp-
version/samples/stax
目录中。
本节涵盖的主题如下:
-
示例代码组织
-
示例 XML 文档
-
游标示例
-
游标到事件示例
-
事件示例
-
过滤器示例
-
读写示例
-
写入示例
示例代码组织
INSTALL_DIR/jaxp-
version/samples/stax
目录包含六个 StAX 示例目录:
-
游标示例:
cursor
目录包含CursorParse.java
,演示如何使用XMLStreamReader
(游标)API 读取 XML 文件。 -
游标到事件示例:
cursor2event
目录包含CursorApproachEventObject.java
,演示应用程序如何在使用游标 API 时将信息作为XMLEvent
对象获取。 -
事件示例:
event
目录包含EventParse.java
,演示如何使用XMLEventReader
(事件迭代器)API 读取 XML 文件。 -
过滤器示例:
filter
目录包含MyStreamFilter.java
,演示如何使用 StAX 流过滤器 API。在此示例中,过滤器仅接受StartElement
和EndElement
事件,并过滤掉其余事件。 -
读写示例:
readnwrite
目录包含EventProducerConsumer.java
,演示了如何使用 StAX 生产者/消费者机制同时读取和写入 XML 流。 -
写入示例:
writer
目录包含CursorWriter.java
,演示如何使用XMLStreamWriter
以编程方式编写 XML 文件。
除了写入示例外,本节中的所有 StAX 示例均使用示例 XML 文档BookCatalog.xml
。
示例 XML 文档
大多数 StAX 示例类使用的示例 XML 文档BookCatalog.xml
是一个基于常见BookCatalogue
命名空间的简单图书目录。BookCatalog.xml
的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<BookCatalogue >
<Book>
<Title>Yogasana Vijnana: the Science of Yoga</Title>
<author>Dhirendra Brahmachari</Author>
<Date>1966</Date>
<ISBN>81-40-34319-4</ISBN>
<Publisher>Dhirendra Yoga Publications</Publisher>
<Cost currency="INR">11.50</Cost>
</Book>
<Book>
<Title>The First and Last Freedom</Title>
<Author>J. Krishnamurti</Author>
<Date>1954</Date>
<ISBN>0-06-064831-7</ISBN>
<Publisher>Harper & Row</Publisher>
<Cost currency="USD">2.95</Cost>
</Book>
</BookCatalogue>
游标示例
位于INSTALL_DIR/jaxp-
version/samples/stax/cursor/
目录中,CursorParse.java
演示了如何使用 StAX 游标 API 读取 XML 文档。在游标示例中,应用程序通过调用next()
指示解析器读取 XML 输入流中的下一个事件。
请注意,next()
只返回与解析器所处位置对应的整数常量。应用程序需要调用相关函数以获取与底层事件相关的更多信息。
您可以将这种方法想象成虚拟游标在 XML 输入流中移动。当虚拟游标位于特定事件时,可以调用各种访问器方法。
逐个事件地进行步进
在这个示例中,客户端应用程序通过在解析器上调用next
方法来拉取 XML 流中的下一个事件;例如:
try {
for (int i = 0 ; i < count ; i++) {
// pass the file name.. all relative entity
// references will be resolved against this
// as base URI.
XMLStreamReader xmlr = xmlif.createXMLStreamReader(filename,
new FileInputStream(filename));
// when XMLStreamReader is created,
// it is positioned at START_DOCUMENT event.
int eventType = xmlr.getEventType();
printEventType(eventType);
printStartDocument(xmlr);
// check if there are more events
// in the input stream
while(xmlr.hasNext()) {
eventType = xmlr.next();
printEventType(eventType);
// these functions print the information
// about the particular event by calling
// the relevant function
printStartElement(xmlr);
printEndElement(xmlr);
printText(xmlr);
printPIData(xmlr);
printComment(xmlr);
}
}
}
请注意,next
只是返回与当前游标位置下的事件对应的整数常量。应用程序调用相关函数以获取与底层事件相关的更多信息。当游标位于特定事件时,可以调用各种访问器方法。
返回字符串表示形式
因为next
方法只返回与底层事件类型对应的整数,通常需要将这些整数映射到事件的字符串表示形式;例如:
public final static String getEventTypeString(int eventType) {
switch (eventType) {
case XMLEvent.START_ELEMENT:
return "START_ELEMENT";
case XMLEvent.END_ELEMENT:
return "END_ELEMENT";
case XMLEvent.PROCESSING_INSTRUCTION:
return "PROCESSING_INSTRUCTION";
case XMLEvent.CHARACTERS:
return "CHARACTERS";
case XMLEvent.COMMENT:
return "COMMENT";
case XMLEvent.START_DOCUMENT:
return "START_DOCUMENT";
case XMLEvent.END_DOCUMENT:
return "END_DOCUMENT";
case XMLEvent.ENTITY_REFERENCE:
return "ENTITY_REFERENCE";
case XMLEvent.ATTRIBUTE:
return "ATTRIBUTE";
case XMLEvent.DTD:
return "DTD";
case XMLEvent.CDATA:
return "CDATA";
case XMLEvent.SPACE:
return "SPACE";
}
return "UNKNOWN_EVENT_TYPE , " + eventType;
}
运行游标示例
-
要编译和运行游标示例,在终端窗口中,转到INSTALL_DIR
/jaxp-
version/samples/
目录,并输入以下内容:javac stax/cursor/*.java
-
在
BookCatalogue.xml
文件上运行CursorParse
示例,使用以下命令。CursorParse
将打印出BookCatalogue.xml
文件的每个元素。
java stax/event/CursorParse stax/data/BookCatalogue.xml
游标到事件示例
位于tut-install/javaeetutorial5/examples/stax/cursor2event/
目录中,CursorApproachEventObject.java
演示了如何在使用游标 API 时获取XMLEvent
对象返回的信息。
这里的想法是,游标 API 的XMLStreamReader
返回与特定事件对应的整数常量,而事件迭代器 API 的XMLEventReader
返回不可变且持久的事件对象。 XMLStreamReader
更有效率,但XMLEventReader
更易于使用,因为与特定事件相关的所有信息都封装在返回的XMLEvent
对象中。然而,事件方法的缺点是为每个事件创建对象的额外开销,这既消耗时间又消耗内存。
有了这个想法,即使使用游标 API,也可以使用XMLEventAllocator
来获取事件信息作为XMLEvent
对象。
实例化一个 XMLEventAllocator
第一步是创建一个新的XMLInputFactory
并实例化一个XMLEventAllocator
:
XMLInputFactory xmlif = XMLInputFactory.newInstance();
System.out.println("FACTORY: " + xmlif);
xmlif.setEventAllocator(new XMLEventAllocatorImpl());
allocator = xmlif.getEventAllocator();
XMLStreamReader xmlr = xmlif.createXMLStreamReader(filename,
new FileInputStream(filename));
创建一个事件迭代器
下一步是创建一个事件迭代器:
int eventType = xmlr.getEventType();
while (xmlr.hasNext()) {
eventType = xmlr.next();
// Get all "Book" elements as XMLEvent object
if (eventType == XMLStreamConstants.START_ELEMENT
&& xmlr.getLocalName().equals("Book")) {
// get immutable XMLEvent
StartElement event = getXMLEvent(xmlr).asStartElement();
System.out.println ("EVENT: " + event.toString());
}
}
创建分配器方法
最后一步是创建XMLEventAllocator
方法:
private static XMLEvent getXMLEvent(XMLStreamReader reader)
throws XMLStreamException {
return allocator.allocate(reader);
}
运行游标到事件示例
-
要编译和运行游标到事件示例,在终端窗口中,转到INSTALL_DIR
/jaxp-
version/samples/
目录,并输入以下内容:javac -classpath ../lib/jaxp-ri.jar stax/cursor2event/*.java
-
在
BookCatalogue.xml
文件上运行CursorApproachEventObject
示例,使用以下命令。java stax/cursor2event/CursorApproachEventObject stax/data/BookCatalogue.xml
CursorApproachEventObject
将打印出BookCatalogue.xml
文件中定义的事件列表。
事件示例
位于INSTALL_DIR/jaxp-
version/samples/stax/event/
目录中,EventParse.java
演示了如何使用 StAX 事件 API 读取 XML 文档。
创建一个输入工厂
第一步是创建一个新的XMLInputFactory
实例:
XMLInputFactory factory = XMLInputFactory.newInstance();
System.out.println("FACTORY: " + factory);
创建一个事件读取器
下一步是创建一个 XMLEventReader
实例:
XMLEventReader r = factory.createXMLEventReader
(filename, new FileInputStream(filename));
创建一个事件迭代器
第三步是创建一个事件迭代器:
XMLEventReader r = factory.createXMLEventReader
(filename, new FileInputStream(filename));
while (r.hasNext()) {
XMLEvent e = r.nextEvent();
System.out.println(e.toString());
}
获取事件流
最后一步是获取底层事件流:
public final static String getEventTypeString(int eventType) {
switch (eventType) {
case XMLEvent.START_ELEMENT:
return "START_ELEMENT";
case XMLEvent.END_ELEMENT:
return "END_ELEMENT";
case XMLEvent.PROCESSING_INSTRUCTION:
return "PROCESSING_INSTRUCTION";
case XMLEvent.CHARACTERS:
return "CHARACTERS";
case XMLEvent.COMMENT:
return "COMMENT";
case XMLEvent.START_DOCUMENT:
return "START_DOCUMENT";
case XMLEvent.END_DOCUMENT:
return "END_DOCUMENT";
case XMLEvent.ENTITY_REFERENCE:
return "ENTITY_REFERENCE";
case XMLEvent.ATTRIBUTE:
return "ATTRIBUTE";
case XMLEvent.DTD:
return "DTD";
case XMLEvent.CDATA:
return "CDATA";
case XMLEvent.SPACE:
return "SPACE";
}
return "UNKNOWN_EVENT_TYPE," + eventType;
}
返回输出
当你运行事件示例时,EventParse
类被编译,XML 流被解析为事件并返回到 STDOUT
。例如,Author
元素的一个实例被返回为:
<[’http://www.publishing.org’]::Author>
Dhirendra Brahmachari
</[’http://www.publishing.org’]::Author>
请注意,在这个示例中,事件包括一个包含命名空间的开标签和闭标签,两者都包含元素的内容作为字符串返回在标签内。
同样,一个 Cost
元素的一个实例被返回如下:
<[’http://www.publishing.org’]::Cost currency=’INR’>
11.50
</[’http://www.publishing.org’]::Cost
在这种情况下,currency
属性和值在事件的开标签中返回。
运行事件示例
-
要编译和运行事件示例,在终端窗口中,转到 INSTALL_DIR
/jaxp-
version/samples/
目录并输入以下内容:javac -classpath ../lib/jaxp-ri.jar stax/event/*.java
-
对
BookCatalogue.xml
文件运行EventParse
示例,使用以下命令。java stax/event/EventParse stax/data/BookCatalogue.xml
EventParse
将打印出由BookCatalogue.xml
文件定义的所有元素的数据。
过滤器示例
位于 INSTALL_DIR/jaxp-
version/samples/stax/filter/
目录中,MyStreamFilter.java
演示了如何使用 StAX 流过滤器 API 过滤应用程序不需要的事件。在这个示例中,解析器过滤掉除了 StartElement
和 EndElement
之外的所有事件。
实现 StreamFilter 类
MyStreamFilter
类实现了 javax.xml.stream.StreamFilter
:
public class MyStreamFilter implements javax.xml.stream.StreamFilter {
// ...
}
创建一个输入工厂
下一步是创建一个 XMLInputFactory
实例。在这种情况下,还在工厂上设置了各种属性:
XMLInputFactory xmlif = null ;
try {
xmlif = XMLInputFactory.newInstance();
xmlif.setProperty(
XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES,
Boolean.TRUE);
xmlif.setProperty(
XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES,
Boolean.FALSE);
xmlif.setProperty(
XMLInputFactory.IS_NAMESPACE_AWARE,
Boolean.TRUE);
xmlif.setProperty(
XMLInputFactory.IS_COALESCING,
Boolean.TRUE);
}
catch (Exception ex) {
ex.printStackTrace();
}
System.out.println("FACTORY: " + xmlif);
System.out.println("filename = "+ filename);
创建过滤器
下一步是实例化一个文件输入流并创建流过滤器:
FileInputStream fis = new FileInputStream(filename);
XMLStreamReader xmlr = xmlif.createFilteredReader(
xmlif.createXMLStreamReader(fis),
new MyStreamFilter());
int eventType = xmlr.getEventType();
printEventType(eventType);
while (xmlr.hasNext()) {
eventType = xmlr.next();
printEventType(eventType);
printName(xmlr,eventType);
printText(xmlr);
if (xmlr.isStartElement()) {
printAttributes(xmlr);
}
printPIData(xmlr);
System.out.println("-----------------------");
}
捕获事件流
下一步是捕获事件流。这与事件示例中的方式基本相同。
过滤流
最后一步是过滤流:
public boolean accept(XMLStreamReader reader) {
if (!reader.isStartElement() && !reader.isEndElement())
return false;
else
return true;
}
返回输出
当你运行过滤器示例时,MyStreamFilter
类被编译,XML 流被解析为事件并返回到 STDOUT
。例如,一个 Author
事件被返回如下:
EVENT TYPE(1):START_ELEMENT
HAS NAME: Author
HAS NO TEXT
HAS NO ATTRIBUTES
-----------------------------
EVENT TYPE(2):END_ELEMENT
HAS NAME: Author
HAS NO TEXT
-----------------------------
同样,一个 Cost
事件被返回如下:
EVENT TYPE(1):START_ELEMENT
HAS NAME: Cost
HAS NO TEXT
HAS ATTRIBUTES:
ATTRIBUTE-PREFIX:
ATTRIBUTE-NAMESP: null
ATTRIBUTE-NAME: currency
ATTRIBUTE-VALUE: USD
ATTRIBUTE-TYPE: CDATA
-----------------------------
EVENT TYPE(2):END_ELEMENT
HAS NAME: Cost
HAS NO TEXT
-----------------------------
查看 迭代器 API 和 读取 XML 流 以获取更详细的 StAX 事件解析讨论。
运行过滤器示例
-
要编译和运行过滤器示例,在终端窗口中,转到 INSTALL_DIR
/jaxp-
version/samples/
目录并输入以下内容:javac -classpath ../lib/jaxp-ri.jar stax/filter/*.java
-
在
java.endorsed.dirs
系统属性设置为指向samples/lib
目录的情况下,对BookCatalogue.xml
文件运行MyStreamFilter
示例,使用以下命令。java -Djava.endorsed.dirs=../lib stax/filter/MyStreamFilter -f stax/data/BookCatalogue.xml
MyStreamFilter
将打印出由BookCatalogue.xml
文件定义的事件作为 XML 流。
读写示例
位于INSTALL_DIR/jaxp-
version/samples/stax/readnwrite/
目录中,EventProducerConsumer.java
演示了如何同时将 StAX 解析器用作生产者和消费者。
StAX XMLEventWriter
API 扩展自XMLEventConsumer
接口,并被称为事件消费者。相比之下,XMLEventReader
是一个事件生产者。StAX 支持同时读取和写入,因此可以顺序地从一个 XML 流中读取并同时写入到另一个流中。
读写示例展示了如何使用 StAX 生产者/消费者机制同时读取和写入。该示例还展示了如何修改流以及如何动态添加新事件,然后写入到不同的流中。
创建一个事件生产者/消费者
第一步是实例化一个事件工厂,然后创建一个事件生产者/消费者的实例:
XMLEventFactory m_eventFactory = XMLEventFactory.newInstance();
public EventProducerConsumer() {
// ...
try {
EventProducerConsumer ms = new EventProducerConsumer();
XMLEventReader reader = XMLInputFactory.newInstance().
createXMLEventReader(new java.io.FileInputStream(args[0]));
XMLEventWriter writer =
XMLOutputFactory.newInstance().createXMLEventWriter(System.out);
}
// ...
}
创建一个迭代器
下一步是创建一个迭代器来解析流:
while (reader.hasNext()) {
XMLEvent event = (XMLEvent)reader.next();
if (event.getEventType() == event.CHARACTERS) {
writer.add(ms.getNewCharactersEvent(event.asCharacters()));
}
else {
writer.add(event);
}
}
writer.flush();
创建一个写入器
最后一步是创建一个流写入器,形式为一个新的Character
事件:
Characters getNewCharactersEvent(Characters event) {
if (event.getData().equalsIgnoreCase("Name1")) {
return m_eventFactory.createCharacters(
Calendar.getInstance().getTime().toString());
}
// else return the same event
else {
return event;
}
}
返回输出
运行读写示例时,EventProducerConsumer
类被编译,并且 XML 流被解析为事件并写回到STDOUT
。输出是示例 XML 文档中描述的BookCatalog.xml
文件的内容。
运行读写示例
-
要编译和运行读写示例,在终端窗口中,转到INSTALL_DIR
/jaxp-
version/samples/
目录并输入以下内容:javac -classpath ../lib/jaxp-ri.jar stax/readnwrite/*.java
-
在
BookCatalogue.xml
文件上运行EventProducerConsumer
示例,使用以下命令。java stax/readnwrite/EventProducerConsumer stax/data/BookCatalogue.xml
EventProducerConsumer
将打印出BookCatalogue.xml
文件的内容。
写入器示例
位于INSTALL_DIR/jaxp-
version/samples/stax/writer/
目录中,CursorWriter.java
演示了如何使用 StAX 游标 API 编写 XML 流。
创建输出工厂
第一步是创建一个XMLOutputFactory
的实例:
XMLOutputFactory xof = XMLOutputFactory.newInstance();
创建一个流写入器
下一步是创建一个XMLStreamWriter
的实例:
XMLStreamWriter xtw = null;
写入流
最后一步是写入 XML 流。请注意,在写入最终的EndDocument
后,流会被刷新并关闭:
xtw = xof.createXMLStreamWriter(new FileWriter(fileName));
xtw.writeComment("all elements here are explicitly in the HTML namespace");
xtw.writeStartDocument("utf-8","1.0");
xtw.setPrefix("html", "http://www.w3.org/TR/REC-html40");
xtw.writeStartElement("http://www.w3.org/TR/REC-html40","html");
xtw.writeNamespace("html", "http://www.w3.org/TR/REC-html40");
xtw.writeStartElement("http://www.w3.org/TR/REC-html40", "head");
xtw.writeStartElement("http://www.w3.org/TR/REC-html40", "title");
xtw.writeCharacters("Frobnostication");
xtw.writeEndElement();
xtw.writeEndElement();
xtw.writeStartElement("http://www.w3.org/TR/REC-html40", "body");
xtw.writeStartElement("http://www.w3.org/TR/REC-html40", "p");
xtw.writeCharacters("Moved to");
xtw.writeStartElement("http://www.w3.org/TR/REC-html40", "a");
xtw.writeAttribute("href","http://frob.com");
xtw.writeCharacters("here");
xtw.writeEndElement();
xtw.writeEndElement();
xtw.writeEndElement();
xtw.writeEndElement();
xtw.writeEndDocument();
xtw.flush();
xtw.close();
返回输出
运行写入器示例时,CursorWriter
类被编译,并且 XML 流被解析为事件并写入到名为dist/CursorWriter-Output
的文件中:
<!--all elements here are explicitly in the HTML namespace-->
<?xml version="1.0" encoding="utf-8"?>
<html:html >
<html:head>
<html:title>Frobnostication</html:title></html:head>
<html:body>
<html:p>Moved to <html:a href="http://frob.com">here</html:a>
</html:p>
</html:body>
</html:html>
在实际的dist/CursorWriter-Output
文件中,该流是连续写入的,没有任何换行符;这里添加了换行符以便更容易阅读清单。在这个示例中,与事件示例中的对象流一样,命名空间前缀被添加到 HTML 标签的开头和结尾。虽然 StAX 规范不要求添加这个前缀,但是当输出流的最终范围不明确时,这是一个良好的实践。
运行写入器示例
-
要编译和运行 Writer 示例,在终端窗口中,转到 INSTALL_DIR
/jaxp-
version/samples/
目录,并输入以下内容:javac -classpath \ ../lib/jaxp-ri.jar stax/writer/*.java
-
运行
CursorWriter
示例,指定输出应写入的文件名。java stax/writer/CursorWriter -f *output_file*
CursorWriter
将创建一个包含 返回输出 中显示的数据的相应名称的输出文件。
更多信息
docs.oracle.com/javase/tutorial/jaxp/stax/info.html
有关 StAX 的更多信息,请参见:
-
Java 社区进程页面.
-
W3C 推荐 可扩展标记语言(XML)1.0
-
XML 信息集
-
jcp.org 上的 JAXB 规范:JSR-222 Java XML 绑定架构(JAXB)
-
W3C 推荐 文档对象模型
-
SAX 简单 XML API
-
DOM 文档对象模型
-
W3C 推荐 XML 中的命名空间
有关使用 StAX 的一些有用文章,请参见:
-
Jeff Ryan, StAX 是否应该成为您的 XML 工具箱中的一部分?
-
Elliott Rusty Harold, StAX 简介
课程:JAXP 1.5 和新属性
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/index.html
本课程重点介绍了 JAXP 1.5 中引入的新属性。
JAXP 1.5 被添加到了 7u40 和 JDK 8 版本中。你可以从java.net
下载当前的JDK 8 快照。
背景
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/backgnd.html
JAXP 安全处理功能对 XML 处理器施加资源限制,以抵御某些类型的拒绝服务攻击。 但是,它并不限制获取外部资源的方式,这在尝试安全处理 XML 文档时也是有用的。 当前的 JAXP 实现支持特定于实现的属性,可用于强制执行此类限制,但需要一种标准方法来实现。
JAXP 1.5 添加了三个新属性以及它们对应的系统属性,允许用户指定可以或不可以允许的外部连接类型。属性值是协议列表。 JAXP 处理器通过将协议与列表中的协议进行匹配来检查给定的外部连接是否被允许。 如果连接在列表中,则处理器将尝试建立连接,否则将拒绝连接。
JAXP 1.5 已经集成到 7u40 和 JDK8 中。
外部资源
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/resources.html
XML、Schema 和 XSLT 标准支持以下需要外部资源的构造。JDK XML 处理器的默认行为是建立连接并按照指定的方式获取外部资源。
-
外部 DTD:引用外部文档类型定义(DTD),示例:
<!DOCTYPE root_element SYSTEM "url">
-
外部实体引用:引用外部数据,语法:
<!ENTITY name SYSTEM "url">
通用实体引用如下:
<?xml version="1.0" standalone="no" ?> <!DOCTYPE doc [<!ENTITY otherFile SYSTEM "otherFile.xml">]> <doc> <foo> <bar>&otherFile;</bar> </foo> </doc>
-
外部参数实体,语法
<!ENTITY % name SYSTEM uri>
。例如:<?xml version="1.0" standalone="no"?> <!DOCTYPE doc [ <!ENTITY % foo SYSTEM "http://www.example.com/student.dtd"< %foo; ]>
-
XInclude:在 XML 文档中包含外部信息集
-
使用
schemaLocation
属性、import
和include
元素引用 XML Schema 组件。示例:schemaLocation="http://www.example.com/schema/bar.xsd"
-
使用
import
或include
元素合并样式表:语法:<xsl:include href="include.xsl"/>
-
xml-stylesheet 处理指令:用于在 xml 文档中包含样式表,语法:
<?xml-stylesheet href="foo.xsl" type="text/xsl"?>
-
XSLT
document()
函数:用于访问外部 XML 文档中的节点。例如,<xsl:variable name="dummy" select="document('DocumentFunc2.xml')/>
。
新属性
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/properties.html
JAXP 1.5 定义了三个新属性,用于调节 XML 处理器是否解析上述外部资源。这些属性是:
-
javax.xml.XMLConstants.ACCESS_EXTERNAL_DTD
-
javax.xml.XMLConstants.ACCESS_EXTERNAL_SCHEMA
-
javax.xml.XMLConstants.ACCESS_EXTERNAL_STYLESHEET
这些 API 属性具有相应的系统属性和 jaxp.properties。
ACCESS_EXTERNAL_DTD
名称:http://javax.xml.XMLConstants/property/accessExternalDTD
定义:限制对外部 DTD、外部实体引用到指定协议的访问。
值:参见属性的值
默认值:all
,允许连接到所有协议。
系统属性:javax.xml.accessExternalDTD
ACCESS_EXTERNAL_SCHEMA
名称:http://javax.xml.XMLConstants/property/accessExternalSchema
定义:限制对由schemaLocation
属性、Import 和 Include 元素设置的外部引用协议的访问。
值:参见属性的值
默认值:all
,允许连接到所有协议。
系统属性:javax.xml.accessExternalSchema
ACCESS_EXTERNAL_STYLESHEET
名称:http://javax.xml.XMLConstants/property/accessExternalStylesheet
定义:限制对由样式表处理指令、文档函数、Import 和 Include 元素设置的外部引用协议的访问。
值:参见属性的值
默认值:all
,允许连接到所有协议。
系统属性:javax.xml.accessExternalStylesheet
${java.home}/lib/jaxp.properties
这些属性可以在jaxp.properties
中指定,以定义所有使用 Java Runtime 的应用程序的行为。格式为property-name=[value][,value]*
。例如:
javax.xml.accessExternalDTD=file,http
属性名称与系统属性相同:javax.xml.accessExternalDTD
、javax.xml.accessExternalSchema
和javax.xml.accessExternalStylesheet
。
属性的值
所有属性的值格式相同。
值:由逗号分隔的协议列表。协议是 URI 的 scheme 部分,或者在 JAR 协议的情况下,由冒号分隔的"jar"加上 scheme 部分。协议定义为:
scheme = alpha *( alpha | digit | "+" | "-" | "." )
其中 alpha = a-z 和 A-Z。
以及 JAR 协议:
jar[:scheme]
协议不区分大小写。值中由Character.isSpaceChar
定义的任何空格将被忽略。协议的示例包括file
、http
、jar:file
。
默认值:默认值是实现特定的。在 JAXP 1.5 RI、Java SE 7u40 和 Java SE 8 中,默认值为all
,授予所有协议的权限。
授予所有访问:关键字all
授予所有协议的权限。例如,在jaxp.properties
中设置javax.xml.accessExternalDTD=all
将允许系统像以前一样工作,无限制地访问外部 DTD 和实体引用。
拒绝任何访问:空字符串,即"",表示不授予任何协议权限。例如,在jaxp.properties
中设置javax.xml.accessExternalDTD=""
将指示 JAXP 处理器拒绝任何外部连接。
范围和顺序
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/scope.html
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING
(FSP)是包括 DOM、SAX、Schema Validation、XSLT 和 XPath 在内的 XML 处理器的必需功能。当设置为true
时,建议实现启用由上述新属性定义的访问限制。为了兼容性,尽管对于 DOM、SAX 和 Schema Validation,默认情况下 FSP 为 true,但 JAXP 1.5 不会启用新的限制。
对于 JDK 8,建议将新的accessExternal*
属性在 FSP 被明确设置时设置为空字符串。这仅在通过 API 设置 FSP 时才会发生,例如factory.setFeature(FSP, true)
。尽管对于 DOM、SAX 和 Schema Validation,默认情况下 FSP 为 true,但 JDK 8 并不将其视为“明确”设置,因此默认情况下不会设置限制。
在jaxp.properties
文件中指定的属性会影响 JDK 或 JRE 的所有调用,并将覆盖其默认值,或者可能已经由 FEATURE_SECURE_PROCESSING 设置的值。
当设置系统属性时,将仅影响一个调用,并将覆盖默认设置或在 jaxp.properties 中设置的设置,或者可能已经由 FEATURE_SECURE_PROCESSING 设置的设置。
通过 JAXP 工厂或SAXParser
指定的 JAXP 属性优先于系统属性,jaxp.properties
文件以及javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING
。
新的 JAXP 属性在以下情况下对其试图限制的相关构造没有影响:
-
当存在解析器并且解析器返回的源不为 null 时。这适用于可能设置在 SAX 和 DOM 解析器上的实体解析器,StAX 解析器上的 XML 解析器,SchemaFactory 上的 LSResourceResolver,验证器或 ValidatorHandler,或者转换器上的 URIResolver。
-
当通过调用
SchemaFactory
的newSchema
方法显式创建模式时。 -
当不需要外部资源时。例如,以下功能/属性由参考实现支持,并可用于指示处理器不加载外部 DTD 或解析外部实体。
http://apache.org/xml/features/disallow-doctype-decl true http://apache.org/xml/features/nonvalidating/load-external-dtd false http://xml.org/sax/features/external-general-entities false http://xml.org/sax/features/external-parameter-entities false
与安全管理器的关系
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/security.html
在尝试连接之前,将首先检查 JAXP 属性,无论是否存在SecurityManager
。这意味着即使SecurityManager
授予权限,连接也可能被阻止。例如,如果 JAXP 属性被设置为禁止 http 协议,它们将有效地阻止任何连接尝试,即使应用程序具有SocketPermission
。
为了限制连接,SecurityManager
可以被视为处于较低级别。在评估 JAXP 属性之后,权限将被检查。例如,如果一个应用程序没有SocketPermission
,即使 JAXP 属性被设置为允许 http 连接,也会抛出SecurityException
。
当存在SecurityManager
时,JAXP FEATURE_SECURE_PROCESSING
被设置为 true。这种行为不会启用新的限制。
JDK 中的属性设置
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/propSettings.html
以下表格显示了 JDK 中新属性的默认值和行为。
访问属性的值 | 默认值 | 设置 FSP(a) | jaxp.properties | 系统属性 | API 属性 |
---|---|---|---|---|---|
7u40 | all |
无更改 | 覆盖 | 覆盖 | 覆盖 |
JDK8 | all |
更改为 "" | 覆盖 | 覆盖 | 覆盖 |
(a) 设置 FSP 意味着明确使用 JAXP 工厂的 setFeature
方法设置 FEATURE_SECURE_PROCESSING。
(b) 7u40 和 JDK8 之间唯一的行为差异是,在 7u40 中设置 FSP 不会更改 accessExternal*
属性,但在 JDK8 中会将值设置为空字符串。在 JDK8 中,设置 FSP 被视为选择加入。
(c) 表中从左到右的顺序反映了覆盖顺序。例如,如果通过 API 设置了 accessExternal
属性,则会覆盖其他可能已通过其他方式设置的属性。
使用属性
译文:
docs.oracle.com/javase/tutorial/jaxp/properties/usingProps.html
本节重点介绍了 JAXP 1.5 中引入的新属性。
何时使用属性
只有处理不受信任的 XML 内容的应用程序才需要限制获取外部资源。不处理不受信任内容的内部系统和应用程序不需要关注新的限制或进行任何更改。自 7u40 和 JDK8 默认没有对此类限制的要求,应用程序在升级到 7u40 和 JDK8 时不会出现行为变化。
对于处理不受信任的 XML 输入、Schema 或样式表的应用程序,如果已经存在安全措施,比如启用 Java 安全管理器仅授予受信任的外部连接,或者使用解析器解析实体,则不需要 JAXP 1.5 中添加的新功能。
然而,JAXP 1.5 确实为没有安全管理器运行的系统和应用程序提供了直接的保护。对于这类应用程序,可以考虑使用下面详细描述的新功能来进行限制。
通过 API 设置属性
当改变代码可行时,通过 JAXP 工厂或解析器设置新属性是启用限制的最佳方式。属性可以通过以下接口设置:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setAttribute(name, value);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.setProperty(name, value);
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(name, value);
SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage);
schemaFactory.setProperty(name, value);
TransformerFactory factory = TransformerFactory.newInstance();
factory.setAttribute(name, value);
以下是一个将 DOM 解析器限制为仅本地连接的外部 DTD 的示例:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
try {
dbf.setAttribute({{XMLConstants.ACCESS_EXTERNAL_DTD}}, "file, jar:file");
} catch (IllegalArgumentException e) {
//jaxp 1.5 feature not supported
}
当代码更改可行,并且对于新开发,建议设置新属性如上所示。通过这种方式设置属性,应用程序可以确保无论部署到较旧还是较新版本的 JDK,或者通过系统属性或jaxp.properties
设置属性,都能保持所需的行为。
使用系统属性
如果改变代码不可行,系统属性可能会有用。
如果希望为整个 JDK/JRE 调用设置限制,可以在命令行上设置系统属性;如果仅需要部分应用程序,可以在该部分之前设置系统属性,然后在之后清除。例如,以下代码展示了如何使用系统属性:
//allow resolution of external schemas
System.setProperty("javax.xml.accessExternalSchema", "file, http");
//this setting will affect all processing after it's set
some processing here
//after it's done, clear the property
System.clearProperty("javax.xml.accessExternalSchema");
使用 jaxp.properties
jaxp.properties
是一个普通的配置文件。它位于${java.home}/lib/jaxp.properties
,其中 java.home
是 JRE 安装目录,例如,[安装目录路径]/jdk7/jre
。
可通过将以下行添加到 jaxp.properties 文件来设置外部访问限制:
javax.xml.accessExternalStylesheet=file, http
设置此项后,所有 JDK/JRE 的调用将遵守加载外部样式表的限制。
对于不希望允许 XML 处理器进行任何外部连接的系统,此功能可能很有用,此时,所有三个属性可以设置为,例如,仅文件。
错误处理
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/error.html
由于这些属性是当前版本的新功能,建议应用程序捕获适合接口的异常,例如,在以下示例中捕获 SAXException。在旧版本上,应用程序可能正常工作,例如,示例代码包含以下方法,用于检测是否使用支持新属性的 JDK 版本或 JAXP 实现运行示例:
public boolean isNewPropertySupported() {
try {
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "file");
} catch (ParserConfigurationException ex) {
fail(ex.getMessage());
} catch (SAXException ex) {
String err = ex.getMessage();
if (err.indexOf("Property 'http://javax.xml.XMLConstants/property/accessExternalDTD' is not recognized.") > -1)
{
//expected, jaxp 1.5 not supported
return false;
}
}
return true;
}
如果由于新属性设置的限制而拒绝访问外部资源,则将以以下格式抛出异常并带有错误信息:
[type of construct]: Failed to read [type of construct] "[name of the external resource]", because "[type of restriction]" access is not allowed due to restriction set by the [property name] property.
例如,如果由于限制只允许使用 http 协议而拒绝获取外部 DTD,如下所示:parser.setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "file");
,并且解析器解析包含对"http://java.sun.com/dtd/properties.dtd"
的外部引用的 XML 文件,则错误消息将如下所示:
External DTD: Failed to read external DTD ''http://java.sun.com/dtd/properties.dtd'', because ''http'' access is not allowed due to restriction set by the accessExternalDTD property.
StAX
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/stax.html
StAX、JSR 173 的规范尚不支持新属性。然而,在 JAXP 的上下文中,StAX 确实包括对这些属性的支持。设置新属性类似于 SAX 或 DOM,但通过 XMLInputFactory,如下所示:
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty("http://javax.xml.XMLConstants/property/accessExternalDTD", "file");
存在于 StAX、JSR 173 规范中指定的属性和特性将优先于新的 JAXP 属性。例如,当 SupportDTD 属性设置为 false 时,将导致程序在输入文件包含 DTD 之前无法解析时抛出异常。对于使用 SupportDTD 属性禁用 DTD 的应用程序,新属性的添加不会产生影响。
结论
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/conclusion.html
JAXP 1.5 提供了新的属性,让用户可以控制获取外部资源。使用新属性与其他现有属性相同,只是这些属性与相应的系统属性和jaxp.properties
一起提供,以便它们可以用于系统范围的限制或权限。
参考资料
原文:
docs.oracle.com/javase/tutorial/jaxp/properties/references.html
欲了解更多信息,请参阅以下资源:
-
JSR 206 JAXP 1.5 maintenance review
-
7u40 Release Notes
-
JDK 8 API and Documentation
-
JDK 7 API and Documentation
-
JEP 185
Lesson: 处理限制
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/index.html
XML 处理有时可能是一个消耗大量内存的操作。应用程序,特别是那些接受来自不受信任来源的 XML、XSD 和 XSL 的应用程序,应该通过使用 JDK 提供的 JAXP 处理限制来防范过度的内存消耗。
开发人员应该评估他们应用程序的需求和运行环境,以确定系统配置的可接受限制,并相应地设置这些限制。与大小相关的限制可用于防止处理畸形的 XML 源时消耗大量内存,而EntityExpansionLimit
将允许应用程序在可接受水平下控制内存消耗。
在本教程中,您将了解这些限制,并学习如何正确使用它们。
处理限制定义
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/limits.html
以下列表描述了 JDK 支持的 JAXP XML 处理限制。这些限制可以通过工厂 API、系统属性和jaxp.properties
文件指定。
entityExpansionLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/entityExpansionLimit |
定义 | 限制实体扩展的数量。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 64000 |
系统属性 | jdk.xml.entityExpansionLimit |
自从 | 7u45, 8 |
elementAttributeLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/elementAttributeLimit |
定义 | 限制元素可以拥有的属性数量。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 10000 |
系统属性 | jdk.xml.elementAttributeLimit |
自从 | 7u45, 8 |
maxOccurLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/maxOccurLimit |
定义 | 限制在构建包含值不是"unbounded"的maxOccurs 属性的 W3C XML Schema 的语法时可以创建的内容模型节点的数量。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 5000 |
系统属性 | jdk.xml.maxOccurLimit |
自从 | 7u45, 8 |
totalEntitySizeLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/totalEntitySizeLimit |
定义 | 限制包含通用实体和参数实体的所有实体的总大小。大小是所有实体的聚合计算。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 5x10⁷ |
系统属性 | jdk.xml.totalEntitySizeLimit |
自从 | 7u45, 8 |
maxGeneralEntitySizeLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/maxGeneralEntitySizeLimit |
定义 | 限制任何通用实体的最大大小。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 0 |
系统属性 | jdk.xml.maxGeneralEntitySizeLimit |
自从 | 7u45, 8 |
maxParameterEntitySizeLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/maxParameterEntitySizeLimit |
定义 | 限制任何参数实体的最大大小,包括嵌套多个参数实体的结果。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 1000000 |
系统属性 | jdk.xml.maxParameterEntitySizeLimit |
自 JDK 7u45, 8 起 |
entityReplacementLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/entityReplacementLimit |
定义 | 限制所有实体引用中节点的总数。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 3000000 |
系统属性 | jdk.xml.entityReplacementLimit |
自 JDK 7u111, 8u101 起 |
maxElementDepth
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/maxElementDepth |
定义 | 限制最大元素深度。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 0 |
系统属性 | jdk.xml.maxElementDepth |
自 JDK 7u65, 8u11 起 |
maxXMLNameLimit
属性 | 描述 |
---|---|
名称 | http://www.oracle.com/xml/jaxp/properties/maxXMLNameLimit |
定义 | 限制 XML 名称的最大大小,包括元素名称、属性名称和命名空间前缀和 URI。 |
值 | 一个正整数。小于或等于 0 的值表示没有限制。如果值不是整数,则会抛出NumericFormatException 异常。 |
默认值 | 1000 |
系统属性 | jdk.xml.maxXMLNameLimit |
自 JDK 7u91, 8u65 起 |
旧版系统属性
这些属性自 JDK 5.0 和 6 起被引入,并继续为向后兼容性而受支持。
系统属性 | 自 JDK 5.0 和 6 起 | 新系统属性 |
---|---|---|
entityExpansionLimit | 1.5 | jdk.xml.entityExpansionLimit |
elementAttributeLimit | 1.5 | jdk.xml.elementAttributeLimit |
maxOccurLimit | 1.6 | jdk.xml.maxOccur |
{java.home}/lib/jaxp.properties
可以在jaxp.properties
文件中指定系统属性,以定义 JDK 或 JRE 的所有调用的行为。格式为system-property-name=value
。例如:
jdk.xml.maxGeneralEntitySizeLimit=1024
范围和顺序
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/scope.html
javax.xml.XMLConstants#FEATURE_SECURE_PROCESSING
(FSP)功能对包括 DOM、SAX、模式验证、XSLT 和 XPath 在内的 XML 处理器是必需的。当 FSP 设置为true
时,建议的默认限制将被强制执行。将 FSP 设置为false
不会改变这些限制。
当 Java 安全管理器存在时,FSP 被设置为 true 且无法关闭。因此,建议的默认限制将被强制执行。
在jaxp.properties
文件中指定的属性会影响 JDK 和 JRE 的所有调用,并将覆盖它们的默认值,或者可能已被 FSP 设置的值。
系统属性在设置时会影响 JDK 和 JRE 的调用,并覆盖默认设置或者在jaxp.properties
中设置的值,或者可能已被 FSP 设置的值。
通过 JAXP 工厂或SAXParser
指定的 JAXP 属性优先于系统属性,jaxp.properties
文件以及FEATURE_SECURE_PROCESSING
。
使用限制
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/using.html
环境评估
评估包括在系统级别考虑应用程序可用的内存量,是否接受和处理来自不受信任来源的 XML、XSD 或 XSL 源,以及在应用程序级别考虑是否使用某些构造(如 DTD)。
内存设置和限制
XML 处理可能非常消耗内存。允许消耗的内存量取决于特定环境中应用程序的要求。必须防止处理格式不正确的 XML 数据消耗过多内存。
默认限制通常设置为允许大多数应用程序的合法 XML 输入,并允许小型硬件系统(如 PC)的内存使用。建议将限制设置为可能的最小值,以便在消耗大量内存之前捕获任何格式不正确的输入。
这些限制是相关的,但并非完全冗余。您应为所有限制设置适当的值:通常限制应设置为比默认值小得多的值。
例如,可以设置ENTITY_EXPANSION_LIMIT
和GENERAL_ENTITY_SIZE_LIMIT
来防止过多的实体引用。但是当扩展和实体大小的确切组合未知时,TOTAL_ENTITY_SIZE_LIMIT
可以作为整体控制。同样,虽然TOTAL_ENTITY_SIZE_LIMIT
控制替换文本的总大小,但如果文本是一个非常大的 XML 块,ENTITY_REPLACEMENT_LIMIT
会限制文本中可以出现的节点总数,并防止系统过载。
通过使用getEntityCountInfo
属性估计限制
为帮助您分析应设置的限制值,提供了一个名为http://www.oracle.com/xml/jaxp/properties/getEntityCountInfo
的特殊属性。以下代码片段显示了使用该属性的示例:
parser.setProperty("http://www.oracle.com/xml/jaxp/properties/getEntityCountInfo", "yes");
查看示例以获取有关下载示例代码的更多信息。
当程序在 W3C MathML 3.0 中运行时,将打印出以下表格:
属性 | 限制 | 总大小 | 大小 | 实体名称 |
---|---|---|---|---|
ENTITY_EXPANSION_LIMIT |
64000 | 1417 | 0 | null |
MAX_OCCUR_NODE_LIMIT |
5000 | 0 | 0 | null |
ELEMENT_ATTRIBUTE_LIMIT |
10000 | 0 | 0 | null |
TOTAL_ENTITY_SIZE_LIMIT |
50000000 | 55425 | 0 | null |
GENERAL_ENTITY_SIZE_LIMIT |
0 | 0 | 0 | null |
PARAMETER_ENTITY_SIZE_LIMIT |
1000000 | 0 | 7303 | %MultiScriptExpression |
MAX_ELEMENT_DEPTH_LIMIT |
0 | 2 | 0 | null |
MAX_NAME_LIMIT |
1000 | 13 | 13 | null |
ENTITY_REPLACEMENT_LIMIT |
3000000 | 0 | 0 | null |
在此示例中,实体引用的总数,或实体扩展,为 1417;默认限制为 64000。所有实体的总大小为 55425;默认限制为 50000000。在解析所有引用后,最大的参数实体是 %MultiScriptExpression
,长度为 7303;默认限制为 1000000。
如果这是应用程序预计要处理的最大文件,请建议将限制设置为较小的数字。例如,ENTITY_EXPANSION_LIMIT
设置为 2000,TOTAL_ENTITY_SIZE_LIMIT
设置为 100000,PARAMETER_ENTITY_SIZE_LIMIT
设置为 10000。
设置限制
限制可以像其他 JAXP 属性一样设置。它们可以通过工厂方法或解析器设置:
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setAttribute(name, value);
SAXParserFactory spf = SAXParserFactory.newInstance();
SAXParser parser = spf.newSAXParser();
parser.setProperty(name, value);
XMLInputFactory xif = XMLInputFactory.newInstance();
xif.setProperty(name, value);
SchemaFactory schemaFactory = SchemaFactory.newInstance(schemaLanguage);
schemaFactory.setProperty(name, value);
TransformerFactory factory = TransformerFactory.newInstance();
factory.setAttribute(name, value);
以下示例显示了如何使用 DocumentBuilderFactory
设置限制:
dbf.setAttribute(JDK_ENTITY_EXPANSION_LIMIT, "2000");
dbf.setAttribute(TOTAL_ENTITY_SIZE_LIMIT, "100000");
dbf.setAttribute(PARAMETER_ENTITY_SIZE_LIMIT, "10000");
dbf.setAttribute(JDK_MAX_ELEMENT_DEPTH, "100");
使用系统属性
如果更改代码不可行,系统属性可能很有用。
要为整个 JDK 或 JRE 的调用设置限制,请在命令行上设置系统属性。要仅为应用程序的一部分设置限制,可以在该部分之前设置系统属性,并在之后清除。以下代码显示了如何使用系统属性:
public static final String SP_GENERAL_ENTITY_SIZE_LIMIT = "jdk.xml.maxGeneralEntitySizeLimit";
//set limits using system property
System.setProperty(SP_GENERAL_ENTITY_SIZE_LIMIT, "2000");
//this setting will affect all processing after it's set
...
//after it is done, clear the property
System.clearProperty(SP_GENERAL_ENTITY_SIZE_LIMIT);
请注意,属性的值应为整数。如果输入的值不包含可解析的整数,将抛出 NumberFormatException
;请参阅方法 parseInt(String)
。
查看 示例 以获取有关下载示例代码的更多信息。
使用 jaxp.properties
文件
jaxp.properties
文件是一个配置文件。通常位于 ${*java.home*}/lib/jaxp.properties
,其中 *java.home*
是 JRE 安装目录,例如,[安装目录路径]/jdk8/jre。
可通过向 jaxp.properties
文件添加以下行来设置限制:
jdk.xml.maxGeneralEntitySizeLimit=2000
请注意,属性名称与系统属性相同,并具有前缀 jdk.xml
。属性的值应为整数。如果输入的值不包含可解析的整数,将抛出 NumberFormatException
;请参阅方法 parseInt(String)
。
当在文件中设置属性时,所有 JDK 和 JRE 的调用都将遵守限制。
错误处理
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/error.html
建议应用程序在设置新属性时捕获org.xml.sax.SAXNotRecognizedException
异常,以便应用程序在不支持这些属性的旧版本上正常工作。例如,可下载的示例代码包含以下方法,isNewPropertySupported
,用于检测示例是否在支持JDK_GENERAL_ENTITY_SIZE_LIMIT
属性的 JDK 版本上运行:
public boolean isNewPropertySupported() {
try {
SAXParser parser = getSAXParser(false, false, false);
parser.setProperty(JDK_GENERAL_ENTITY_SIZE_LIMIT, "10000");
} catch (ParserConfigurationException ex) {
fail(ex.getMessage());
} catch (SAXException ex) {
String err = ex.getMessage();
if (err.indexOf("Property '" + JDK_GENERAL_ENTITY_SIZE_LIMIT +
"' is not recognized.") > -1) {
//expected before this patch
debugPrint("New limit properties not supported. Samples not run.");
return false;
}
}
return true;
}
当输入文件包含导致超出限制异常的结构时,应用程序可以检查错误代码以确定失败的性质。以下错误代码适用于这些限制:
-
EntityExpansionLimit
: JAXP00010001 -
ElementAttributeLimit
: JAXP00010002 -
MaxEntitySizeLimit
: JAXP00010003 -
TotalEntitySizeLimit
: JAXP00010004 -
MaxXMLNameLimit
: JAXP00010005 -
maxElementDepth
: JAXP00010006 -
EntityReplacementLimit
: JAXP00010007
错误代码的格式如下:
"JAXP" + components (two digits) + error category (two digits) + sequence number
因此,代码 JAXP00010001 代表了 JAXP 基本解析器安全限制EntityExpansionLimit
。
StAX
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/stax.html
StAX,JSR 173,不支持 FSP。然而,JDK 中的 StAX 实现支持新的限制属性及其对应的系统属性。这意味着,虽然没有 FSP 来开启和关闭限制,但描述的限制和系统属性的工作方式完全相同。
为了兼容性,StAX 特定属性总是在新的 JAXP 限制之前生效。例如,将SupportDTD
属性设置为 false 会导致在输入文件包含Entity
引用时抛出异常。因此,对于使用SupportDTD
属性禁用 DTD 的应用程序,新限制的添加不会产生影响。
Samples
原文:
docs.oracle.com/javase/tutorial/jaxp/limits/sample.html
你可以下载包含两个 Java 应用程序文件LimitSamples.java
和SampleBase.java
以及一个包含示例 xml 文件的目录samples
的samples.zip
文件。
要运行示例,请解压samples.zip
文件,编译,并在命令行上执行以下操作:
java LimitSamples
Trail: RMI
原文:
docs.oracle.com/javase/tutorial/rmi/index.html
Java 远程方法调用(RMI)系统允许在一个 Java 虚拟机中运行的对象调用另一个 Java 虚拟机中运行的对象的方法。RMI 提供了 Java 编程语言编写的程序之间的远程通信。
注意: 如果你要连接到一个现有的 IDL 程序,应该使用 Java IDL 而不是 RMI。
本教程简要介绍了 RMI 系统,然后演示了一个完整的客户端/服务器示例,该示例利用了 RMI 的独特功能,在运行时加载和执行用户定义的任务。示例中的服务器实现了一个通用的计算引擎,客户端用它来计算 的值。
描述了 RMI 系统并列出了其优势。此外,本节介绍了典型的 RMI 应用程序,由服务器和客户端组成,并介绍了重要术语。
演示了计算引擎服务器的代码。本节将教你如何设计和实现一个 RMI 服务器。
着眼于一个可能的计算引擎客户端,并用它来说明 RMI 客户端的重要特性。
展示了如何编译和运行计算引擎服务器及其客户端。
RMI 应用概述
原文:
docs.oracle.com/javase/tutorial/rmi/overview.html
RMI 应用程序通常由两个独立的程序组成,一个服务器和一个客户端。典型的服务器程序创建一些远程对象,使这些对象的引用可访问,并等待客户端调用这些对象的方法。典型的客户端程序获取服务器上一个或多个远程对象的远程引用,然后调用这些对象的方法。RMI 提供了服务器和客户端进行通信和传递信息的机制。这样的应用有时被称为分布式对象应用。
分布式对象应用需要执行以下操作:
-
定位远程对象。 应用程序可以使用各种机制获取对远程对象的引用。例如,应用程序可以使用 RMI 的简单命名设施,即 RMI 注册表,注册其远程对象。另外,应用程序可以将远程对象引用作为其他远程调用的一部分传递和返回。
-
与远程对象通信。 远程对象之间的通信细节由 RMI 处理。对于程序员来说,远程通信看起来类似于常规的 Java 方法调用。
-
加载传递的对象的类定义。 因为 RMI 允许对象来回传递,它提供了加载对象的类定义以及传输对象数据的机制。
以下插图描述了一个使用 RMI 注册表获取远程对象引用的 RMI 分布式应用程序。服务器调用注册表将名称与远程对象关联(或绑定)。客户端在服务器的注册表中按名称查找远程对象,然后调用其方法。插图还显示了 RMI 系统使用现有的 Web 服务器在需要时从服务器到客户端和从客户端到服务器加载类定义的过程。
动态代码加载的优势
RMI 的一个核心和独特特性是,如果接收方的 Java 虚拟机中未定义类的定义,它可以下载对象类的定义。一个对象的所有类型和行为,以前仅在单个 Java 虚拟机中可用,可以传输到另一个可能是远程的 Java 虚拟机。RMI 通过其实际类传递对象,因此当它们被发送到另一个 Java 虚拟机时,对象的行为不会改变。这种能力使得可以将新类型和行为引入到远程 Java 虚拟机中,从而动态扩展应用程序的行为。本教程中的计算引擎示例使用了这种能力来向分布式程序引入新行为。
远程接口、对象和方法
与任何其他 Java 应用程序一样,使用 Java RMI 构建的分布式应用程序由接口和类组成。接口声明方法。类实现接口中声明的方法,并且可能还声明其他方法。在分布式应用程序中,一些实现可能驻留在一些 Java 虚拟机中,而另一些则不在。具有可以在 Java 虚拟机之间调用的方法的对象称为远程对象。
通过实现远程接口,对象变成远程对象,具有以下特征:
-
一个远程接口扩展接口
java.rmi.Remote
。 -
接口的每个方法在其
throws
子句中声明java.rmi.RemoteException
,除了任何特定于应用程序的异常。
当对象从一个 Java 虚拟机传递到另一个 Java 虚拟机时,RMI 会将远程对象与非远程对象区别对待。RMI 不会在接收 Java 虚拟机中复制实现对象,而是传递一个远程对象的远程存根。存根充当远程对象的本地代表或代理,并且基本上是客户端的远程引用。客户端在本地存根上调用方法,本地存根负责在远程对象上执行方法调用。
一个远程对象的存根实现了远程对象实现的相同一组远程接口。这个属性使得一个存根可以被转换为远程对象实现的任何接口。然而,只有在远程接口中定义的方法才能从接收 Java 虚拟机中调用。
使用 RMI 创建分布式应用程序
使用 RMI 开发分布式应用程序涉及以下一般步骤:
-
设计和实现分布式应用程序的组件。
-
编译源代码。
-
使类可网络访问。
-
启动应用程序。
设计和实现应用程序组件
首先,确定您的应用程序架构,包括哪些组件是本地对象,哪些组件是可远程访问的。这一步包括:
-
定义远程接口。 远程接口指定客户端可以远程调用的方法。客户端编程针对远程接口,而不是针对这些接口的实现类。这些接口的设计包括确定将用作这些方法的参数和返回值的对象类型。如果这些接口或类中的任何一个尚不存在,您也需要定义它们。
-
实现远程对象。 远程对象必须实现一个或多个远程接口。远程对象类可能包括其他仅在本地可用的接口和方法的实现。如果要将任何本地类用作这些方法的参数或返回值,那么它们也必须被实现。
-
实现客户端。 使用远程对象的客户端可以在定义远程接口之后的任何时间实现,包括在部署远程对象之后。
编译源代码
与任何 Java 程序一样,您使用javac
编译器来编译源文件。源文件包含远程接口的声明、它们的实现、任何其他服务器类以及客户端类。
注意: 在 Java 平台标准版 5.0 之前的版本中,需要通过使用rmic
编译器来构建存根类,但现在不再需要这一步骤。
使类能够在网络中访问
在这一步中,您需要使某些类定义能够在网络中访问,例如远程接口及其关联类型的定义,以及需要下载到客户端或服务器的类的定义。通常通过 Web 服务器使类定义能够在网络中访问。
启动应用程序
启动应用程序包括运行 RMI 远程对象注册表、服务器和客户端。
本节的其余部分将介绍创建计算引擎所使用的步骤。
构建通用计算引擎
本教程专注于一个简单但强大的分布式应用程序,称为计算引擎。计算引擎是服务器上的一个远程对象,它接收来自客户端的任务,运行这些任务,并返回任何结果。这些任务在运行服务器的机器上执行。这种类型的分布式应用程序可以让多台客户端机器利用特别强大或具有专门硬件的机器。
计算引擎的新颖之处在于它运行的任务不需要在编写或启动计算引擎时定义。可以随时创建新类型的任务,然后将其交给计算引擎运行。任务的唯一要求是其类实现特定接口。RMI 系统可以将执行任务所需的代码下载到计算引擎中。然后,计算引擎在运行它的机器上利用资源运行任务。
执行任意任务的能力是由 Java 平台的动态特性实现的,通过 RMI 扩展到网络。RMI 动态加载任务代码到计算引擎的 Java 虚拟机中,并在没有实现任务的类的先验知识的情况下运行任务。这种具有动态下载代码能力的应用程序通常被称为基于行为的应用程序。这些应用程序通常需要完整的代理启用基础设施。在 Java 平台上,这些应用程序是分布式计算的基本机制之一。
编写一个 RMI 服务器
原文:
docs.oracle.com/javase/tutorial/rmi/server.html
计算引擎服务器接受来自客户端的任务,运行这些任务,并返回任何结果。服务器代码由一个接口和一个类组成。接口定义了可以从客户端调用的方法。实质上,接口定义了客户端对远程对象的视图。类提供了实现。
设计一个远程接口
这一部分解释了Compute
接口,它提供了客户端和服务器之间的连接。您还将了解支持此通信的 RMI API。
实现一个远程接口
这一部分探讨了实现Compute
接口的类,从而实现了一个远程对象。这个类还提供了组成服务器程序的其余代码,包括一个创建远程对象实例的main
方法,将其注册到 RMI 注册表,并设置安全管理器。
设计远程接口
原文:
docs.oracle.com/javase/tutorial/rmi/designing.html
计算引擎的核心是一种协议,使得任务可以提交到计算引擎,计算引擎可以运行这些任务,并将这些任务的结果返回给客户端。这个协议在支持计算引擎的接口中表达。该协议的远程通信在下图中有所体现。
每个接口包含一个方法。计算引擎的远程接口Compute
允许任务提交到引擎。客户端接口Task
定义了计算引擎如何执行提交的任务。
compute.Compute
接口定义了远程访问部分,即计算引擎本身。这里是Compute
接口的源代码:
package compute;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Compute extends Remote {
<T> T executeTask(Task<T> t) throws RemoteException;
}
通过扩展接口java.rmi.Remote
,Compute
接口将自身标识为一个可以从另一个 Java 虚拟机中调用其方法的接口。实现这个接口的任何对象都可以是一个远程对象。
作为远程接口的成员,executeTask
方法是一个远程方法。因此,该方法必须被定义为能够抛出java.rmi.RemoteException
的方法。这个异常是由 RMI 系统从远程方法调用中抛出的,用于指示通信失败或协议错误。RemoteException
是一个受检异常,因此任何调用远程方法的代码都需要通过捕获它或在其throws
子句中声明来处理这个异常。
计算引擎所需的第二个接口是Task
接口,它是Compute
接口中executeTask
方法的参数类型。compute.Task
接口定义了计算引擎和需要执行的工作之间的接口,提供了开始工作的方式。这里是Task
接口的源代码:
package compute;
public interface Task<T> {
T execute();
}
Task
接口定义了一个方法execute
,该方法没有参数,也不会抛出异常。因为该接口没有扩展Remote
,所以在该接口中的方法不需要在throws
子句中列出java.rmi.RemoteException
。
Task
接口有一个类型参数T
,代表任务计算的结果类型。该接口的execute
方法返回计算的结果,因此其返回类型是T
。
Compute
接口的executeTask
方法反过来返回传递给它的Task
实例的执行结果。因此,executeTask
方法有自己的类型参数T
,将其自己的返回类型与传递的Task
实例的结果类型关联起来。
RMI 使用 Java 对象序列化机制在 Java 虚拟机之间按值传输对象。要使对象被视为可序列化,其类必须实现java.io.Serializable
标记接口。因此,实现Task
接口的类必须也实现Serializable
,任务结果所使用的对象的类也必须实现。
只要它们是Task
类型的实现,不同类型的任务可以由Compute
对象运行。实现这个接口的类可以包含任务计算所需的任何数据以及计算所需的任何其他方法。
这就是 RMI 如何使这个简单的计算引擎成为可能。因为 RMI 可以假定Task
对象是用 Java 编程语言编写的,之前未知于计算引擎的Task
对象的实现会根据需要通过 RMI 下载到计算引擎的 Java 虚拟机中。这种能力使得计算引擎的客户端能够定义新的任务类型,而无需将代码明确安装在该机器上。
由ComputeEngine
类实现的计算引擎实现了Compute
接口,通过调用其executeTask
方法,使不同的任务可以提交给它。这些任务使用任务的execute
方法的实现来运行,并将结果返回给远程客户端。
实现远程接口。
原文:
docs.oracle.com/javase/tutorial/rmi/implementing.html
本节讨论实现计算引擎类的任务。一般来说,实现远程接口的类至少应该执行以下操作:
-
声明正在实现的远程接口。
-
为每个远程对象定义构造函数。
-
为远程接口中的每个远程方法提供实现。
RMI 服务器程序需要创建初始远程对象并将其导出到 RMI 运行时,使其可用于接收传入的远程调用。此设置过程可以封装在远程对象实现类本身的方法中,也可以完全包含在另一个类中。设置过程应执行以下操作:
-
创建并安装安全管理器
-
创建并导出一个或多个远程对象。
-
至少在 RMI 注册表(或其他命名服务,如通过 Java 命名和目录接口访问的服务)中注册一个远程对象,用于引导目的。
计算引擎的完整实现如下。engine.ComputeEngine
类实现了远程接口Compute
,并包括用于设置计算引擎的main
方法。以下是ComputeEngine
类的源代码:
package engine;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import compute.Compute;
import compute.Task;
public class ComputeEngine implements Compute {
public ComputeEngine() {
super();
}
public <T> T executeTask(Task<T> t) {
return t.execute();
}
public static void main(String[] args) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine, 0);
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
System.out.println("ComputeEngine bound");
} catch (Exception e) {
System.err.println("ComputeEngine exception:");
e.printStackTrace();
}
}
}
以下各节讨论计算引擎实现的每个组件。
声明正在实现的远程接口。
计算引擎的实现类声明如下:
public class ComputeEngine implements Compute
此声明说明该类实现了Compute
远程接口,因此可以用作远程对象。
ComputeEngine
类定义了一个远程对象实现类,该类实现了一个单独的远程接口,没有其他接口。ComputeEngine
类还包含两个仅可在本地调用的可执行程序元素。其中一个元素是用于ComputeEngine
实例的构造函数。另一个元素是用于创建ComputeEngine
实例并使其对客户端可用的main
方法。
为远程对象定义构造函数。
ComputeEngine
类有一个不带参数的构造函数。构造函数的代码如下:
public ComputeEngine() {
super();
}
此构造函数只调用了超类构造函数,即Object
类的无参数构造函数。尽管即使在ComputeEngine
构造函数中省略了超类构造函数,超类构造函数也会被调用,但为了清晰起见,还是包含在内。
为每个远程方法提供实现。
远程对象的类为远程接口中指定的每个远程方法提供实现。Compute
接口包含一个单独的远程方法,executeTask
,其实现如下:
public <T> T executeTask(Task<T> t) {
return t.execute();
}
此方法实现了ComputeEngine
远程对象与其客户端之间的协议。每个客户端都向ComputeEngine
提供一个具有Task
接口的特定实现的Task
对象的execute
方法。ComputeEngine
执行每个客户端的任务,并将任务的execute
方法的结果直接返回给客户端。
在 RMI 中传递对象
远程方法的参数或返回值几乎可以是任何类型,包括本地对象、远程对象和基本数据类型。更准确地说,只要实体是基本数据类型、远程对象或可序列化对象的实例,就可以将任何类型的实体传递给远程方法或从远程方法传递出来,这意味着它实现了接口java.io.Serializable
。
一些对象类型不符合这些标准,因此无法传递给远程方法或从远程方法返回。其中大多数对象,如线程或文件描述符,封装的信息只在单个地址空间内有意义。许多核心类,包括java.lang
和java.util
包中的类,实现了Serializable
接口。
关于如何传递参数和返回值的规则如下:
-
远程对象本质上是通过引用传递的。远程对象引用是一个存根,是一个客户端代理,实现了远程对象实现的完整远程接口集。
-
本地对象通过对象序列化进行复制传递。默认情况下,除了标记为
static
或transient
的字段外,所有字段都会被复制。可以按类覆盖默认序列化行为。
通过引用传递远程对象意味着通过远程方法调用对对象状态所做的任何更改都会反映在原始远程对象中。当传递远程对象时,只有接收者可用的是远程接口。在实现类中定义的方法或类实现的非远程接口中定义的任何方法对接收者不可用。
例如,如果您要传递对ComputeEngine
类实例的引用,则接收方只能访问计算引擎的executeTask
方法。该接收方将看不到ComputeEngine
构造函数、其main
方法或其对java.lang.Object
的任何方法的实现。
在远程方法调用的参数和返回值中,不是远程对象的对象是按值传递的。因此,在接收方的 Java 虚拟机中创建对象的副本。接收方对对象状态的任何更改仅反映在接收方的副本中,而不是发送方的原始实例中。发送方对对象状态的任何更改仅反映在发送方的原始实例中,而不是接收方的副本中。
实现服务器的main
方法
ComputeEngine
实现中最复杂的方法是 main
方法。main
方法用于启动 ComputeEngine
,因此需要进行必要的初始化和管理工作,以准备服务器接受来自客户端的调用。这个方法不是一个远程方法,这意味着它不能从不同的 Java 虚拟机中调用。由于 main
方法声明为 static
,该方法根本不与对象关联,而是与类 ComputeEngine
关联。
创建和安装安全管理器
main
方法的第一个任务是创建和安装安全管理器,以保护来自 Java 虚拟机内运行的不受信任的下载代码对系统资源的访问。安全管理器确定下载的代码是否可以访问本地文件系统或执行任何其他特权操作。
如果一个 RMI 程序没有安装安全管理器,RMI 将不会为作为参数接收的对象或远程方法调用的返回值下载类(除了从本地类路径)。这个限制确保下载代码执行的操作受安全策略约束。
这是创建和安装安全管理器的代码:
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
使远程对象对客户端可用
接下来,main
方法创建了一个 ComputeEngine
实例,并使用以下语句将其导出到 RMI 运行时:
Compute engine = new ComputeEngine();
Compute stub =
(Compute) UnicastRemoteObject.exportObject(engine, 0);
静态的 UnicastRemoteObject.exportObject
方法导出提供的远程对象,以便它可以接收来自远程客户端的远程方法调用。第二个参数是一个 int
,指定用于监听对象的传入远程调用请求的 TCP 端口。通常使用值零,指定使用匿名端口。实际端口将由 RMI 或底层操作系统在运行时选择。但也可以使用非零值指定用于监听的特定端口。一旦 exportObject
调用成功返回,ComputeEngine
远程对象就准备好处理传入的远程调用。
exportObject
方法返回导出的远程对象的存根。请注意,变量 stub
的类型必须是 Compute
,而不是 ComputeEngine
,因为远程对象的存根只实现导出的远程对象实现的远程接口。
exportObject
方法声明可以抛出 RemoteException
,这是一个已检查的异常类型。main
方法使用其 try
/catch
块处理此异常。如果不以这种方式处理异常,则必须在 main
方法的 throws
子句中声明 RemoteException
。如果请求的端口已绑定到其他用途,尝试导出远程对象可能会抛出 RemoteException
,例如,如果必要的通信资源不可用。
在客户端可以调用远程对象的方法之前,必须首先获取对远程对象的引用。获取引用可以通过与程序中获取任何其他对象引用的方式相同完成,例如通过将引用作为方法的返回值的一部分或作为包含这样一个引用的数据结构的一部分。
系统提供了一种特定类型的远程对象,即 RMI 注册表,用于查找其他远程对象的引用。RMI 注册表是一个简单的远程对象命名服务,使客户端能够通过名称获取对远程对象的引用。注册表通常仅用于定位 RMI 客户端需要使用的第一个远程对象。然后,该第一个远程对象可能提供支持以查找其他对象。
java.rmi.registry.Registry
远程接口是在注册表中绑定(或注册)和查找远程对象的 API。java.rmi.registry.LocateRegistry
类提供了用于在特定网络地址(主机和端口)合成远程引用到注册表的静态方法。这些方法创建包含指定网络地址的远程引用对象,而不执行任何远程通信。LocateRegistry
还提供了用于在当前 Java 虚拟机中创建新注册表的静态方法,尽管此示例未使用这些方法。一旦远程对象在本地主机上的 RMI 注册表中注册,任何主机上的客户端都可以按名称查找远程对象,获取其引用,然后调用对象上的远程方法。注册表可以被所有运行在主机上的服务器共享,或者单个服务器进程可以创建和使用自己的注册表。
ComputeEngine
类使用以下语句为对象创建名称:
String name = "Compute";
代码然后将名称添加到运行在服务器上的 RMI 注册表中。此步骤稍后通过以下语句完成:
Registry registry = LocateRegistry.getRegistry();
registry.rebind(name, stub);
此rebind
调用会对本地主机上的 RMI 注册表进行远程调用。与任何远程调用一样,此调用可能导致抛出RemoteException
,该异常由main
方法末尾的catch
块处理。
注意Registry.rebind
调用的以下内容:
-
LocateRegistry.getRegistry
的无参数重载在本地主机上和默认注册表端口 1099 上合成对注册表的引用。如果注册表在除 1099 之外的端口上创建,则必须使用具有int
参数的重载。 -
当对注册表进行远程调用时,传递的是远程对象的存根而不是远程对象本身的副本。远程实现对象,例如
ComputeEngine
的实例,永远不会离开它们被创建的 Java 虚拟机。因此,当客户端在服务器的远程对象注册表中执行查找时,会返回存根的副本。在这种情况下,远程对象实际上是通过(远程)引用而不是通过值传递的。 -
出于安全原因,应用程序只能在运行在同一主机上的注册表上
bind
、unbind
或rebind
远程对象引用。这种限制防止远程客户端删除或覆盖服务器注册表中的任何条目。然而,可以从任何主机(本地或远程)请求lookup
。
一旦服务器向本地 RMI 注册表注册,它会打印一条消息,指示它已准备好开始处理调用。然后,main
方法完成。不需要有一个线程等待来保持服务器处于活动状态。只要在另一个 Java 虚拟机中有对ComputeEngine
对象的引用,无论是本地还是远程,ComputeEngine
对象都不会被关闭或垃圾回收。因为程序在注册表中绑定了对ComputeEngine
的引用,所以它可以从远程客户端,即注册表本身,访问。RMI 系统保持ComputeEngine
的进程运行。ComputeEngine
可用于接受调用,并且在其绑定从注册表中移除且没有远程客户端持有对ComputeEngine
对象的远程引用时才会被回收。
在ComputeEngine.main
方法中的最后一段代码处理可能出现的任何异常。代码中可能抛出的唯一已检查异常类型是RemoteException
,可能是由UnicastRemoteObject.exportObject
调用或注册表rebind
调用引起的。在任何情况下,程序在打印错误消息后不能做更多事情,只能退出。在一些分布式应用中,可以从远程调用失败中恢复。例如,应用程序可以尝试重试操作或选择另一个服务器继续操作。
创建客户端程序
原文:
docs.oracle.com/javase/tutorial/rmi/client.html
计算引擎是一个相对简单的程序:它运行交给它的任务。计算引擎的客户端更加复杂。客户端需要调用计算引擎,但也必须定义计算引擎执行的任务。
在我们的示例中,客户端由两个单独的类组成。第一个类ComputePi
查找并调用Compute
对象。第二个类Pi
实现Task
接口并定义计算引擎执行的工作。Pi
类的工作是计算到某个小数位数的的值。
非远程Task
接口定义如下:
package compute;
public interface Task<T> {
T execute();
}
调用Compute
对象方法的代码必须获取对该对象的引用,创建一个Task
对象,然后请求执行该任务。稍后将显示任务类Pi
的定义。使用单个参数构造Pi
对象,该参数是所需结果的精度。任务执行的结果是表示计算到指定精度的的
java.math.BigDecimal
。
这里是client.ComputePi
的源代码,主要客户端类:
package client;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.math.BigDecimal;
import compute.Compute;
public class ComputePi {
public static void main(String args[]) {
if (System.getSecurityManager() == null) {
System.setSecurityManager(new SecurityManager());
}
try {
String name = "Compute";
Registry registry = LocateRegistry.getRegistry(args[0]);
Compute comp = (Compute) registry.lookup(name);
Pi task = new Pi(Integer.parseInt(args[1]));
BigDecimal pi = comp.executeTask(task);
System.out.println(pi);
} catch (Exception e) {
System.err.println("ComputePi exception:");
e.printStackTrace();
}
}
}
像ComputeEngine
服务器一样,客户端首先安装安全管理器。这一步是必要的,因为接收服务器远程对象存根的过程可能需要从服务器下载类定义。为了让 RMI 下载类,必须启用安全管理器。
安装安全管理器后,客户端构造一个名称用于查找Compute
远程对象,使用与ComputeEngine
绑定其远程对象相同的名称。此外,客户端使用LocateRegistry.getRegistry
API 合成服务器主机上注册表的远程引用。第一个命令行参数args[0]
的值是Compute
对象运行的远程主机的名称。然后客户端在注册表上调用lookup
方法,通过名称在服务器主机的注册表中查找远程对象。使用的LocateRegistry.getRegistry
重载版本具有单个String
参数,返回命名主机和默认注册表端口 1099 的注册表引用。如果注册表在除 1099 之外的端口上创建,则必须使用具有int
参数的重载。
接下来,客户端创建一个新的Pi
对象,将第二个命令行参数args[1]
解析为整数传递给Pi
构造函数。这个参数指示计算中要使用的小数位数。最后,客户端调用Compute
远程对象的executeTask
方法。传入executeTask
调用的对象返回一个BigDecimal
类型的对象,程序将其存储在变量result
中。最后,程序打印结果。下图描述了ComputePi
客户端、rmiregistry
和ComputeEngine
之间消息流的过程。
Pi
类实现了Task
接口,并计算了的值到指定的小数位数。对于这个示例,实际算法并不重要。重要的是算法是计算密集型的,这意味着你希望它在一个能力强大的服务器上执行。
这是client.Pi
的源代码,该类实现了Task
接口:
package client;
import compute.Task;
import java.io.Serializable;
import java.math.BigDecimal;
public class Pi implements Task<BigDecimal>, Serializable {
private static final long serialVersionUID = 227L;
/** constants used in pi computation */
private static final BigDecimal FOUR =
BigDecimal.valueOf(4);
/** rounding mode to use during pi computation */
private static final int roundingMode =
BigDecimal.ROUND_HALF_EVEN;
/** digits of precision after the decimal point */
private final int digits;
/**
* Construct a task to calculate pi to the specified
* precision.
*/
public Pi(int digits) {
this.digits = digits;
}
/**
* Calculate pi.
*/
public BigDecimal execute() {
return computePi(digits);
}
/**
* Compute the value of pi to the specified number of
* digits after the decimal point. The value is
* computed using Machin's formula:
*
* pi/4 = 4*arctan(1/5) - arctan(1/239)
*
* and a power series expansion of arctan(x) to
* sufficient precision.
*/
public static BigDecimal computePi(int digits) {
int scale = digits + 5;
BigDecimal arctan1_5 = arctan(5, scale);
BigDecimal arctan1_239 = arctan(239, scale);
BigDecimal pi = arctan1_5.multiply(FOUR).subtract(
arctan1_239).multiply(FOUR);
return pi.setScale(digits,
BigDecimal.ROUND_HALF_UP);
}
/**
* Compute the value, in radians, of the arctangent of
* the inverse of the supplied integer to the specified
* number of digits after the decimal point. The value
* is computed using the power series expansion for the
* arc tangent:
*
* arctan(x) = x - (x³)/3 + (x⁵)/5 - (x⁷)/7 +
* (x⁹)/9 ...
*/
public static BigDecimal arctan(int inverseX,
int scale)
{
BigDecimal result, numer, term;
BigDecimal invX = BigDecimal.valueOf(inverseX);
BigDecimal invX2 =
BigDecimal.valueOf(inverseX * inverseX);
numer = BigDecimal.ONE.divide(invX,
scale, roundingMode);
result = numer;
int i = 1;
do {
numer =
numer.divide(invX2, scale, roundingMode);
int denom = 2 * i + 1;
term =
numer.divide(BigDecimal.valueOf(denom),
scale, roundingMode);
if ((i % 2) != 0) {
result = result.subtract(term);
} else {
result = result.add(term);
}
i++;
} while (term.compareTo(BigDecimal.ZERO) != 0);
return result;
}
}
注意,所有可序列化的类,无论它们是否直接或间接实现了Serializable
接口,都必须声明一个名为serialVersionUID
的private
static
final
字段,以确保在不同版本之间的序列化兼容性。如果该类之前没有发布过版本,则该字段的值可以是任何long
值,类似于Pi
使用的227L
,只要该值在未来版本中一致使用即可。如果该类的先前版本已发布但没有显式声明serialVersionUID
,但与该版本的序列化兼容性很重要,则必须使用先前版本的默认隐式计算值作为新版本显式声明的值。可以运行serialver
工具来确定先前版本的默认计算值。
这个示例最有趣的特点是Compute
实现对象在Pi
对象作为参数传递给executeTask
方法之前从不需要Pi
类的定义。在那时,RMI 会将该类的代码加载到Compute
对象的 Java 虚拟机中,调用execute
方法,并执行任务的代码。结果,对于Pi
任务来说是一个BigDecimal
对象,会被传回给调用客户端,在那里用于打印计算结果。
供给的Task
对象计算Pi
值这一事实对于ComputeEngine
对象来说并不重要。你也可以实现一个任务,比如通过使用概率算法生成一个随机素数。这个任务也会需要大量计算,因此是传递给ComputeEngine
的一个很好的选择,但它需要非常不同的代码。当Task
对象传递给Compute
对象时,这段代码也可以被下载。就像在需要时引入计算的算法一样,生成随机素数的代码也会在需要时被引入。
Compute
对象只知道它接收到的每个对象都实现了execute
方法。Compute
对象不知道,也不需要知道实现的具体内容。
编译和运行示例
原文:
docs.oracle.com/javase/tutorial/rmi/example.html
现在,计算引擎示例的代码已经编写完成,需要进行编译和运行。
编译示例程序
在这一部分,您将学习如何编译组成计算引擎示例的服务器和客户端程序。
运行示例程序
最后,你运行服务器和客户端程序,从而计算出的值。
编译示例程序
原文:
docs.oracle.com/javase/tutorial/rmi/compiling.html
在一个真实的场景中,例如部署了像计算引擎这样的服务,开发人员可能会创建一个包含 Compute
和 Task
接口的 Java 存档(JAR)文件,供服务器类实现和客户端程序使用。接下来,一个开发人员,也许是接口 JAR 文件的相同开发人员,会编写 Compute
接口的实现,并将该服务部署在客户端可用的机器上。客户端程序的开发人员可以使用 JAR 文件中包含的 Compute
和 Task
接口,独立开发一个任务和客户端程序,使用 Compute
服务。
在本节中,您将学习如何设置 JAR 文件、服务器类和客户端类。您将看到客户端的 Pi
类将在运行时下载到服务器上。此外,Compute
和 Task
接口将在运行时从服务器下载到注册表中。
该示例将接口、远程对象实现和客户端代码分为三个包:
-
compute
–Compute
和Task
接口 -
engine
–ComputeEngine
实现类 -
client
–ComputePi
客户端代码和Pi
任务实现
首先,您需要构建接口 JAR 文件,以提供给服务器和客户端开发人员。
构建接口类的 JAR 文件
首先,您需要编译 compute
包中的接口源文件,然后构建一个包含它们类文件的 JAR 文件。假设用户 waldo
已经编写了这些接口,并将源文件放在 Windows 的目录 c:\home\waldo\src\compute
或 Solaris OS 或 Linux 的目录 /home/waldo/src/compute
中。给定这些路径,您可以使用以下命令编译接口并创建 JAR 文件:
微软 Windows:
cd c:\home\waldo\src
javac compute\Compute.java compute\Task.java
jar cvf compute.jar compute\*.class
Solaris OS 或 Linux:
cd /home/waldo/src
javac compute/Compute.java compute/Task.java
jar cvf compute.jar compute/*.class
jar
命令由于 -v
选项显示如下输出:
added manifest
adding: compute/Compute.class(in = 307) (out= 201)(deflated 34%)
adding: compute/Task.class(in = 217) (out= 149)(deflated 31%)
现在,您可以将 compute.jar
文件分发给服务器和客户端应用程序的开发人员,以便他们可以利用这些接口。
使用 javac
编译器构建服务器端或客户端类后,如果其中任何类需要由其他 Java 虚拟机动态下载,您必须确保它们的类文件放在网络可访问的位置。在本例中,对于 Solaris OS 或 Linux,这个位置是 /home/*user*/public_html/classes
,因为许多 Web 服务器允许通过构造为 http://host/~*user*/
的 HTTP URL 访问用户的 public_html
目录。如果您的 Web 服务器不支持这种约定,您可以在 Web 服务器的层次结构中使用不同的位置,或者您可以使用文件 URL。在 Solaris OS 或 Linux 上,文件 URL 的形式为 file:/home/*user*/public_html/classes/
,在 Windows 上的形式为 file:/c:/home/*user*/public_html/classes/
。您也可以根据需要选择另一种类型的 URL。
类文件的网络可访问性使得 RMI 运行时在需要时可以下载代码。RMI 不会为代码下载定义自己的协议,而是使用 Java 平台支持的 URL 协议(例如 HTTP)来下载代码。请注意,使用完整的重量级 Web 服务器来提供这些类文件是不必要的。例如,可以在 找到一个简单的 HTTP 服务器,提供了在 RMI 通过 HTTP 下载类所需的功能。
另请参阅 远程方法调用主页。
构建服务器类
engine
包仅包含一个服务器端实现类 ComputeEngine
,这是远程接口 Compute
的实现。
假设开发 ComputeEngine
类的用户 ann
已经将 ComputeEngine.java
放在 Windows 的目录 c:\home\ann\src\engine
或 Solaris OS 或 Linux 的目录 /home/ann/src/engine
中。她正在部署供客户下载的类文件在她的 public_html
目录的子目录中,Windows 上为 c:\home\ann\public_html\classes
或 Solaris OS 或 Linux 上为 /home/ann/public_html/classes
。这个位置可以通过一些 Web 服务器访问,如 http://*host*:*port*/~ann/classes/
。
ComputeEngine
类依赖于 Compute
和 Task
接口,这些接口包含在 compute.jar
JAR 文件中。因此,在构建服务器类时,您需要将 compute.jar
文件放在类路径中。假设 compute.jar
文件位于 Windows 的目录 c:\home\ann\public_html\classes
或 Solaris OS 或 Linux 的目录 /home/ann/public_html/classes
中。有了这些路径,您可以使用以下命令构建服务器类:
Microsoft Windows:
cd c:\home\ann\src
javac -cp c:\home\ann\public_html\classes\compute.jar
engine\ComputeEngine.java
Solaris OS 或 Linux:
cd /home/ann/src
javac -cp /home/ann/public_html/classes/compute.jar
engine/ComputeEngine.java
ComputeEngine
的存根类实现了Compute
接口,该接口引用了Task
接口。因此,这两个接口的类定义需要对其他 Java 虚拟机(如注册表的 Java 虚拟机)可访问,以便存根能够接收。客户端 Java 虚拟机将已经在其类路径中包含了这些接口,因此实际上不需要下载它们的定义。public_html
目录下的compute.jar
文件可以起到这个作用。
现在,计算引擎已经准备好部署。你现在可以这样做,或者等到构建客户端之后再部署。
构建客户端类
client
包含两个类,ComputePi
,主要客户端程序,和Pi
,客户端对Task
接口的实现。
假设开发客户端类的用户jones
已经将ComputePi.java
和Pi.java
放置在 Windows 系统的c:\home\jones\src\client
目录下,或者 Solaris OS 或 Linux 系统的/home/jones/src/client
目录下。他正在将计算引擎的类文件部署在他的public_html
目录的子目录中,Windows 系统为c:\home\jones\public_html\classes
,Solaris OS 或 Linux 系统为/home/jones/public_html/classes
。这个位置可以通过一些 Web 服务器访问,如http://*host*:*port*/~jones/classes/
。
客户端类依赖于Compute
和Task
接口,这些接口包含在compute.jar
JAR 文件中。因此,在构建客户端类时,你需要将compute.jar
文件放在类路径中。假设compute.jar
文件位于 Windows 系统的c:\home\jones\public_html\classes
目录下,或者 Solaris OS 或 Linux 系统的/home/jones/public_html/classes
目录下。有了这些路径,你可以使用以下命令来构建客户端类:
Microsoft Windows:
cd c:\home\jones\src
javac -cp c:\home\jones\public_html\classes\compute.jar
client\ComputePi.java client\Pi.java
mkdir c:\home\jones\public_html\classes\client
cp client\Pi.class
c:\home\jones\public_html\classes\client
Solaris OS 或 Linux:
cd /home/jones/src
javac -cp /home/jones/public_html/classes/compute.jar
client/ComputePi.java client/Pi.java
mkdir /home/jones/public_html/classes/client
cp client/Pi.class
/home/jones/public_html/classes/client
只有Pi
类需要放置在public_html\classes\client
目录中,因为只有Pi
类需要在计算引擎的 Java 虚拟机中可用以供下载。现在,你可以先运行服务器,然后再运行客户端。
运行示例程序
原文:
docs.oracle.com/javase/tutorial/rmi/running.html
关于安全性的说明
服务器和客户端程序都安装了安全管理器。当您运行任一程序时,您需要指定一个安全策略文件,以便代码被授予其运行所需的安全权限。这是一个用于服务器程序的示例策略文件。
grant codeBase "file:/home/ann/src/" {
permission java.security.AllPermission;
};
这是一个用于客户端程序的示例策略文件。
grant codeBase "file:/home/jones/src/" {
permission java.security.AllPermission;
};
对于这两个示例策略文件,所有权限都授予程序本地类路径中的类,因为本地应用程序代码是受信任的,但不授予从其他位置下载的代码任何权限。因此,计算引擎服务器限制其执行的任务(其代码未知是否受信任且可能具有敌意)执行需要安全权限的任何操作。示例客户端的Pi
任务不需要任何权限来执行。
在此示例中,服务器程序的策略文件名为server.policy
,客户端程序的策略文件名为client.policy
。
启动服务器
在启动计算引擎之前,您需要启动 RMI 注册表。RMI 注册表是一个简单的服务器端引导命名工具,使远程客户端能够获取对初始远程对象的引用。它可以通过rmiregistry
命令启动。在执行rmiregistry
之前,您必须确保您将运行rmiregistry
的 shell 或窗口要么没有设置CLASSPATH
环境变量,要么具有不包括您希望下载到远程对象的客户端的任何类路径的CLASSPATH
环境变量。
要在服务器上启动注册表,请执行rmiregistry
命令。此命令不会产生任何输出,并且通常在后台运行。在此示例中,注册表在主机mycomputer
上启动。
Microsoft Windows(如果start
不可用,请使用javaw
):
start rmiregistry
Solaris 操作系统或 Linux:
rmiregistry &
默认情况下,注册表在端口 1099 上运行。要在不同端口上启动注册表,请在命令行上指定端口号。不要忘记取消设置CLASSPATH
环境变量。
Microsoft Windows:
start rmiregistry 2001
Solaris 操作系统或 Linux:
rmiregistry 2001 &
一旦注册表启动,你可以启动服务器。你需要确保compute.jar
文件和远程对象实现类都在你的类路径中。当你启动计算引擎时,你需要使用java.rmi.server.codebase
属性指定服务器类的网络访问位置。在这个例子中,要提供下载的服务器端类是Compute
和Task
接口,在用户ann
的public_html\classes
目录中的compute.jar
文件中可用。计算引擎服务器在主机mycomputer
上启动,与注册表启动的主机相同。
Microsoft Windows:
java -cp c:\home\ann\src;c:\home\ann\public_html\classes\compute.jar
-Djava.rmi.server.codebase=file:/c:/home/ann/public_html/classes/compute.jar
-Djava.rmi.server.hostname=mycomputer.example.com
-Djava.security.policy=server.policy
engine.ComputeEngine
Solaris OS 或 Linux:
java -cp /home/ann/src:/home/ann/public_html/classes/compute.jar
-Djava.rmi.server.codebase=http://mycomputer/~ann/classes/compute.jar
-Djava.rmi.server.hostname=mycomputer.example.com
-Djava.security.policy=server.policy
engine.ComputeEngine
上述java
命令定义了以下系统属性:
-
java.rmi.server.codebase
属性指定了一个代码库 URL,从这个服务器可以下载源自*的类的定义。如果代码库指定了一个目录层次结构(而不是一个 JAR 文件),你必须在代码库 URL 的末尾包含一个斜杠。 -
java.rmi.server.hostname
属性指定了要放在此 Java 虚拟机中导出的远程对象存根中的主机名或地址。当客户端尝试通信远程方法调用时,客户端使用的值是主机名或地址。默认情况下,RMI 实现使用服务器的 IP 地址,如java.net.InetAddress.getLocalHost
API 所示。然而,有时,这个地址对于所有客户端都不合适,一个完全合格的主机名会更有效。为了确保 RMI 使用一个对所有潜在客户端都可路由的主机名(或 IP 地址)作为服务器,设置java.rmi.server.hostname
属性。 -
java.security.policy
属性用于指定包含您打算授予的权限的策略文件。
启动客户端
一旦注册表和计算引擎运行起来,你可以启动客户端,指定以下内容:
-
客户端提供其类(
Pi
类)的位置,使用java.rmi.server.codebase
属性 -
java.security.policy
属性用于指定包含您打算授予各种代码片段的权限的安全策略文件 -
作为命令行参数,服务器的主机名(以便客户端知道在哪里找到
Compute
远程对象)和在计算中使用的小数位数
在另一台主机上启动客户端(例如名为mysecondcomputer
的主机)如下:
Microsoft Windows:
java -cp c:\home\jones\src;c:\home\jones\public_html\classes\compute.jar
-Djava.rmi.server.codebase=file:/c:/home/jones/public_html/classes/
-Djava.security.policy=client.policy
client.ComputePi mycomputer.example.com 45
Solaris OS 或 Linux:
java -cp /home/jones/src:/home/jones/public_html/classes/compute.jar
-Djava.rmi.server.codebase=http://mysecondcomputer/~jones/classes/
-Djava.security.policy=client.policy
client.ComputePi mycomputer.example.com 45
注意,类路径是在命令行上设置的,以便解释器可以找到客户端类和包含接口的 JAR 文件。还要注意,java.rmi.server.codebase
属性的值,指定一个目录层次结构,以斜杠结尾。
在启动客户端后,将显示以下输出:
3.141592653589793238462643383279502884197169399
以下图示说明了在程序执行期间rmiregistry
、ComputeEngine
服务器和ComputePi
客户端获取类的位置。
当ComputeEngine
服务器在注册表中绑定其远程对象引用时,注册表会下载存根类依赖的Compute
和Task
接口。这些类是从ComputeEngine
服务器的 Web 服务器或文件系统下载的,具体取决于启动服务器时使用的代码库 URL 的类型。
因为ComputePi
客户端在其类路径中同时具有Compute
和Task
接口的定义,它从其类路径加载它们的定义,而不是从服务器的代码库加载。
最后,当Pi
对象在executeTask
远程调用中传递给ComputeEngine
对象时,Pi
类被加载到ComputeEngine
服务器的 Java 虚拟机中。Pi
类是由服务器从客户端的 Web 服务器或文件系统加载的,具体取决于启动客户端时使用的代码库 URL 的类型。
教程:Java SE 中的安全功能
原文:
docs.oracle.com/javase/tutorial/security/index.html
在本教程中,您将了解内置的 Java™安全功能如何保护您免受恶意程序的侵害。您将看到如何使用工具来控制对资源的访问,生成和检查数字签名,以及创建和管理用于签名生成和检查所需的密钥。您还将看到如何将加密服务(如数字签名生成和检查)整合到您的程序中。
Java 开发工具包(JDK™)提供的安全功能面向各种受众:
-
运行程序的用户:
内置的安全功能可保护您免受恶意程序(包括病毒)的侵害,保护您文件的隐私和关于您的信息,并验证每个代码提供者的身份。当您需要时,您可以对应用程序和小程序进行安全控制。
-
开发人员:
您可以使用 API 方法将安全功能整合到您的程序中,包括加密服务和安全检查。API 框架使您能够定义和整合自己的权限(控制对特定资源的访问)、加密服务实现、安全管理器实现和策略实现。此外,还提供了用于管理您信任的人的公钥/私钥对和公钥证书的类。
-
系统管理员、开发人员和用户:
JDK 工具管理您的密钥库(密钥和证书的数据库);为 JAR 文件生成数字签名,并验证这些签名的真实性和已签名内容的完整性;以及创建和修改定义安装安全策略的策略文件。
注意: 对于想要创建小程序和 Java Web 启动应用程序的开发人员,请参阅 Java 小程序以获取安全信息。
教程课程
创建策略文件 展示了如何通过策略文件控制资源访问。有关策略配置文件的最新信息,请参阅策略指南 页面。
快速浏览控制应用程序 在前一课程的基础上展示了资源访问的控制,例如对于在安全管理器下运行的应用程序,除非在策略文件中明确允许,否则不允许读取或写入文件等资源访问。
用于安全代码和文件交换的 API 和工具使用定义了数字签名、证书和密钥库,并讨论了它们为何需要。它还回顾了适用于接下来三个课程的信息,这些课程通常需要使用工具或 API 生成签名、导出/导入证书等步骤。
签署代码并授予权限展示了所有与安全相关的工具的使用。它展示了开发人员签署和分发代码供他人运行的步骤。本课程还展示了运行代码的人(或系统管理员)如何在策略文件中添加条目以授予代码所需的资源访问权限。
文件交换展示了一个人使用工具签署重要文件,如合同,并导出与用于签署合同的私钥对应的公钥证书。然后本课程展示了另一个人如何导入证书并验证签名,该人收到了合同、签名和公钥证书。
生成和验证签名逐步引导您通过一个示例,使用 JDK 安全 API 编写 Java 程序生成密钥,使用私钥为数据生成数字签名,并将公钥和签名导出到文件。然后示例展示了编写第二个程序,该程序可能预期在另一个人的计算机上运行,导入公钥并验证签名的真实性。最后,示例讨论了基本程序使用的方法的潜在弱点,并演示了可能的替代方法和提供和导入密钥的方法,包括在证书中。
实现自己的权限演示了如何编写一个定义自己特殊权限的类。
更多信息
JDK 安全发布文档可以在安全指南页面找到。此索引页面列出了规范,提供了关于最新安全功能的详细信息,包括架构规范、使用指南、API 文档和工具文档。
教训:安全功能概述
原文:
docs.oracle.com/javase/tutorial/security/overview/index.html
安全功能帮助您保护程序和数据免受伤害,保持数据受到保护和私密,并在安全意识的运行时环境中部署新应用程序。
Java 还提供了几个工具,帮助您管理对系统资源的访问;创建、存储和维护加密的公共和私有密码(密钥对)和证书;以及在部署过程中创建和签名 jar 文件。
注意: 要了解 Java SE 7 中支持的安全功能的简要概述,请参阅安全指南目录中的安全概述白皮书。
课程:创建策略文件
原文:
docs.oracle.com/javase/tutorial/security/tour1/index.html
这节课展示了如何创建一个控制资源访问的策略文件。
本课程的步骤是:
- 设置策略文件以授予所需权限
设置策略文件以授予所需权限
原文:
docs.oracle.com/javase/tutorial/security/tour1/step2.html
策略文件是一个 ASCII 文本文件,可以通过文本编辑器或本节中演示的图形化策略工具来编写。策略工具可以节省您的输入时间,消除您需要了解策略文件所需语法的需求,从而减少错误。
本课程使用策略工具创建名为 examplepolicy
的策略文件,在其中您将添加一个 策略条目,授予来自 examples
目录的代码写入权限。
按照以下步骤创建和修改您的新策略文件:
-
启动策略工具
-
授予所需权限
-
保存策略文件
UNIX 用户注意事项: 这些步骤说明了如何为 Windows 系统创建策略文件。如果您在 UNIX 系统上工作,步骤完全相同。当文本中说要将策略文件存储在 C:\Test
目录中时,您可以将其存储在其他目录中。本课程中的示例假定您将其存储在 ~/test
目录中。
启动 Policy Tool
原文:
docs.oracle.com/javase/tutorial/security/tour1/wstep1.html
要启动 Policy Tool,只需在命令行中输入以下内容:
policytool
这将打开 Policy Tool 窗口。
每次启动 Policy Tool 时,它会尝试从用户策略文件中填充此窗口中的策略信息。用户策略文件默认在您的主目录中命名为.java.policy
。如果 Policy Tool 找不到用户策略文件,它会发出警告并显示一个空白的 Policy Tool 窗口(一个带有标题和按钮但没有数据的窗口),如下图所示。
然后,您可以选择打开现有的策略文件或创建新的策略文件。
第一次运行 Policy Tool 时,您会看到空白的 Policy Tool 窗口,因为用户策略文件尚不存在。您可以立即继续创建新的策略文件,如下一步所述。
授予所需权限
原文:
docs.oracle.com/javase/tutorial/security/tour1/wstep2.html
要创建新条目,请在主策略工具窗口中单击 添加策略条目 按钮。这将显示如下图所示的策略条目对话框。
策略条目指定了来自特定代码源的一个或多个权限--来自特定位置(URL)的代码,由特定实体签名的代码,或两者兼有。
CodeBase 和 SignedBy 文本框指定您要授予权限的代码,这些权限将添加到文件中。
-
CodeBase 值表示代码源位置;您授予来自该位置的代码的权限。空的 CodeBase 条目表示“任何代码”--代码的来源并不重要。
-
SignedBy 值表示存储在密钥库中的证书的别名。该证书内的公钥用于验证代码上的数字签名。您授予由与别名指定的密钥库条目中的公钥对应的私钥签名的代码的权限。SignedBy 条目是可选的;省略它表示“任何签名者”--代码是否签名或由谁签名并不重要。
如果您同时拥有 CodeBase 和 SignedBy 条目,权限仅授予来自指定位置且由指定别名签名的代码。
您可以授予存储示例的位置(URL)的所有代码权限。
在策略条目对话框的 CodeBase 文本框中键入以下 URL:
https://docs.oracle.com/javase/tutorial/security/tour1/examples/
注意: 这是一个 URL。因此,它必须始终使用斜杠作为分隔符,而不是反斜杠。
将 SignedBy 文本框留空,因为您不需要代码签名。
注意: 要授予权限给任何代码(.class
文件),不仅仅是从先前指定的目录,而是从 security
目录及其子目录中,将以下 URL 键入 CodeBase 框中:
https://docs.oracle.com/javase/tutorial/security/
您已经指定了代码的来源(CodeBase),并且代码不需要签名(因为没有 SignedBy 值)。
您现在已经指定了此策略条目,因此在策略条目对话框中单击 完成 按钮。策略工具窗口现在包含代表策略条目的一行,显示 CodeBase
值。
注意: 我们将在下一课中授予此新策略条目的权限。
保存策略文件
原文:
docs.oracle.com/javase/tutorial/security/tour1/wstep3.html
要保存您正在创建的新策略文件,请从文件菜单中选择另存为命令。这将显示另存为对话框。
在控制应用程序快速入门课程中的示例假设您将策略文件存储在C:
驱动器上的Test
目录中。
导航到Test
目录。键入文件名examplepolicy
,然后点击保存。
策略文件现在已保存,其名称和路径显示在标记为Policy File
的文本框中。
通过从文件菜单中选择退出来退出策略工具。
课程:快速浏览控制应用程序
原文:
docs.oracle.com/javase/tutorial/security/tour2/index.html
先决条件课程: 创建策略文件
本课程展示如何使用安全管理器为应用程序授予或拒绝对系统资源的访问权限。本课程还展示了资源访问,如读取或写入文件,对于使用安全管理器运行的应用程序,除非在策略文件中明确允许,否则是不允许的。
本课程的步骤为:
-
观察应用程序自由
-
查看如何限制应用程序
-
设置策略文件以授予所需权限
-
查看策略文件的效果
观察应用程序自由
原文:
docs.oracle.com/javase/tutorial/security/tour2/step1.html
当应用程序运行时,不会自动安装安全管理器。在下一步中,您将看到如何将相同的安全策略应用于在本地文件系统上找到的应用程序和下载的沙箱小程序。但首先,让我们证明默认情况下未安装安全管理器用于应用程序,因此应用程序可以完全访问资源。
在您的计算机上创建一个名为GetProps.java
的文件,可以通过复制或下载GetProps.java
源代码来实现。
本课程中的示例假定您将GetProps.java
放在C:\Test
目录中(如果您使用 Windows 系统)或在 UNIX 上的~/test
目录中。
如您所见,如果检查源文件,此程序尝试获取(读取)属性值,其名称为"os.name"
,"java.version"
,"user.home"
和"java.home"
。
现在编译并运行GetProps.java
。您应该看到类似以下的输出:
C:\TEST>java GetProps
About to get os.name property value
The name of your operating system is:
Windows XP
About to get java.version property value
The version of the JVM you are running is:
1.6.0
About to get user.home property value
Your user home directory is: C:\WINDOWS
About to get java.home property value
Your JRE installation directory is:
C:\JDK7.0.0\JRE
这表明应用程序被允许访问所有属性值,如下图所示。
查看如何限制应用程序
原文:
docs.oracle.com/javase/tutorial/security/tour2/step2.html
正如您在上一步骤中看到的,当 Java 运行时运行一个应用程序时,它不会自动安装安全管理器。要将相同的安全策略应用于本地文件系统中找到的应用程序和下载的沙箱小程序,您可以使用新的-Djava.security.manager
命令行参数调用解释器。
要使用默认安全管理器执行GetProps
应用程序,请键入以下内容:
java -Djava.security.manager GetProps
以下是程序的输出:
C:\TEST>java -Djava.security.manager GetProps
About to get os.name property value
The name of your operating system is: SunOS
About to get java.version property value
The version of the JVM you are running is: 1.7.0
About to get user.home property value
Caught exception java.security.AccessControlException:
access denied ("java.util.PropertyPermission"
"user.home" "read")
过程如下图所示。
安全敏感属性
Java 运行时默认加载默认策略文件,并授予所有代码访问一些常用属性(如"os.name"
和"java.version"
)的权限。这些属性不是安全敏感的,因此授予这些权限通常不会构成安全风险。
GetProps
尝试访问的其他属性"user.home"
和"java.home"
不在系统策略文件授予读取权限的属性之列。因此,一旦GetProps
尝试访问这些属性中的第一个("user.home"
)时,安全管理器会阻止访问并报告AccessControlException
。此异常表示当前生效的策略,其中包含一个或多个策略文件中的条目,不允许读取"user.home"
属性的权限。
注意: 代码始终可以从与其所在目录相同的目录(或该目录的子目录)中读取文件;它不需要明确的权限来这样做。代码还可以获取其执行目录的路径名,而此路径名可能包含敏感信息。例如,如果代码是从主目录(或主目录的子目录)执行的,则路径名可能会显示当前用户的名称。
默认策略文件
默认策略文件,java.policy
默认位于:
-
Windows:
*java.home*\lib\security\java.policy
-
UNIX:
*java.home*/lib/security/java.policy
请注意,java.home代表"java.home"
属性的值,该属性是指定 JRE 安装目录的系统属性。因此,如果 JRE 安装在 Windows 上名为C:\jdk\jre
,在 UNIX 上名为/jdk/jre
的目录中,则系统策略文件位于:
-
Windows:
C:\jdk\jre\lib\security\java.policy
-
UNIX:
/jdk/jre/lib/security/java.policy
设置策略文件以授予所需的权限
原文:
docs.oracle.com/javase/tutorial/security/tour2/step3.html
这一步使用策略工具实用程序打开名为examplepolicy
的策略文件,该文件是在创建策略文件课程中创建的。您将添加一个新的策略条目,允许来自存储GetProps.class
的目录的代码读取"user.home"
和"java.home"
属性值,如下图所示。
步骤如下。
-
打开策略文件
-
授予所需的权限
-
保存策略文件
UNIX 用户注意: 本说明演示了为 Windows 系统创建策略文件。如果您在 UNIX 系统上工作,则步骤完全相同,只有以下区别。
-
您从您的主目录中的
test
目录中检索examplepolicy
文件。 -
对于授予所需权限的步骤中的CodeBase URL,您可以将
file:${user.home}/test/
替换为file:/C:/Test/
。或者,您可以直接指定您的主目录,而不是引用"user.home"
属性,如file:/home/jones/test/
。
打开策略文件
原文:
docs.oracle.com/javase/tutorial/security/tour2/wstep1.html
通过在命令行中键入以下内容启动策略工具:
policytool
这将打开策略工具窗口。要打开examplepolicy
策略文件,请在文件菜单中使用打开命令。这将显示一个打开对话框,您可以使用它来浏览目录结构,直到找到包含策略文件的目录(即C:\Test\
目录)。
选择该目录中的examplepolicy
文件,然后选择打开按钮。
这将从examplepolicy
策略文件中填充策略工具窗口中的信息,包括策略文件名和由创建策略文件课程创建的CodeBase部分的策略条目。
授予所需权限
原文:
docs.oracle.com/javase/tutorial/security/tour2/wstep2.html
要授予GetProps
应用程序读取"user.home"
和"java.home"
属性值的权限,必须创建一个授予这些权限的策略条目。在主策略工具窗口中选择添加策略条目按钮。这将弹出策略条目对话框,如下图所示。
在CodeBase文本框中键入以下文件 URL,以指示您将授予来自指定目录中的代码的权限,该目录是存储GetProps.class
的目录。
file:/C:/Test/
(注意,这是一个 URL,因此必须始终使用斜杠,而不是反斜杠。)
将SignedBy文本框留空,因为您不需要代码签名。
要添加读取"user.home"
属性值的权限,请选择添加权限按钮。这将弹出权限对话框。
执行以下操作。
-
从权限下拉列表中选择属性权限。完整的权限类型名称(
java.util.PropertyPermission
)现在出现在下拉列表右侧的文本框中。 -
在标有目标名称列表右侧的文本框中键入以下内容,以指定
"user.home"
属性:user.home
-
通过从操作下拉列表中选择读取选项来指定读取此属性的权限。
现在权限对话框如下所示。
选择确定按钮。新权限将出现在策略条目窗口中的一行中。
要添加读取"java.home"
属性值的权限,请再次选择添加权限按钮。在权限对话框中,执行以下操作:
-
从权限下拉列表中选择属性权限。
-
在标有目标名称列表右侧的文本框中键入以下内容,以指定
"java.home"
属性:java.home
-
通过从操作下拉列表中选择读取选项来指定读取此属性的权限。
现在权限对话框如下所示。
选择确定按钮。新权限和先前添加的权限将出现在策略条目窗口中的行中,如下图所示。
您现在已经完成了指定此策略条目的操作,因此在策略条目对话框中选择完成按钮。策略工具窗口现在包括表示新策略条目的一行,显示CodeBase值。
保存策略文件
原文:
docs.oracle.com/javase/tutorial/security/tour2/wstep3.html
要保存策略文件,只需在文件菜单中选择保存命令。
然后从文件菜单中选择退出命令退出策略工具。
查看策略文件效果
原文:
docs.oracle.com/javase/tutorial/security/tour2/step4.html
现在您已经向examplepolicy
策略文件添加了所需的策略条目,当您使用安全管理器执行GetProps
应用程序时,您应该能够读取指定的属性,如下图所示。
每当您运行一个小程序,或者一个带有安全管理器的应用程序时,默认加载和使用的策略文件是位于以下目录之一的"安全属性文件"中指定的文件。
-
Windows:
*java.home*\lib\security\java.security
-
UNIX:
*java.home*/lib/security/java.security
注意: java.home
环境变量指定了 JRE 安装的目录。
策略文件的位置是指定为属性值的形式为:
policy.url.*n*
其中变量n
表示一个数字。请在以下形式的行中指定每个属性值:
policy.url.*n*=*URL*
其中URL是 URL 规范。例如,默认的策略文件,有时分别称为系统和用户策略文件,在安全属性文件中定义为
policy.url.1=file:${java.home}/lib/security/java.policy
policy.url.2=file:${user.home}/.java.policy
注意: 在安全属性文件中使用${propName}
符号是指定属性值的一种方式。因此${java.home}
将在运行时被实际的"java.home"
属性值替换,该属性值指示了 JRE 安装的目录,${user.home}
将被"user.home"
属性的值替换,例如,C:\Windows
。
有两种可能的方式可以使examplepolicy
文件被视为整体策略的一部分,除了在安全属性文件中指定的策略文件。您可以通过将附加策略文件指定为传递给运行时系统的属性,如方法 1 中所述,或者在安全属性文件中添加指定附加策略文件的行,如方法 2 中所讨论的那样。
方法 1
您可以使用-Djava.security.policy
解释器命令行参数来指定一个应该在安全属性文件中指定的策略文件之外使用的策略文件。
确保您在包含GetProps.class
和examplepolicy
的目录中。然后,您可以运行GetProps
应用程序,并通过在一行上键入以下命令将examplepolicy
策略文件传递给解释器:
java -Djava.security.manager -Djava.security.policy=examplepolicy GetProps
注意: 请记住,为了使用安全管理器运行应用程序,需要-Djava.security.manager
,如查看如何限制应用程序步骤中所示。
该程序报告了"user.home"
和"java.home"
属性的值。
如果应用程序仍然报告错误,则策略文件中存在问题。使用策略工具检查您在设置策略文件以授予所需权限步骤中创建的策略条目。
方法 2
您可以在安全属性文件中指定多个 URL,所有指定的策略文件都将被加载。因此,让java
解释器考虑您的examplepolicy
文件的策略条目的一种方法是在安全属性文件中添加指定该策略文件的条目。
重要提示: 如果您正在运行自己的 JDK 副本,您可以轻松编辑安全属性文件。如果您正在运行与其他用户共享的版本,只有在具有写入权限或在适当时向系统管理员请求修改文件时,您才能修改系统范围的安全属性文件。但是,对于本教程测试,您可能不应该对系统范围的策略文件进行修改。我们建议您只需阅读以下内容,看看如何操作,或者安装您自己的私人版本的 JDK 用于教程课程。
要修改安全属性文件,请在适合编辑 ASCII 文本文件的编辑器中打开它。然后在包含policy.url.2
的行之后添加以下行:如果您在 Windows 系统上,请添加
policy.url.3=file:/C:/Test/examplepolicy
如果您在 UNIX 系统上,请添加
policy.url.3=file:${user.home}/test/examplepolicy
在 UNIX 系统上,您还可以显式指定您的主目录,如下所示
policy.url.3=file:/home/jones/test/examplepolicy
运行应用程序
现在,您应该能够成功运行以下内容。
java -Djava.security.manager GetProps
与方法 1 一样,如果仍然出现安全异常,则策略文件中存在问题。使用策略工具检查您在设置策略文件以授予所需权限步骤中创建的策略条目。然后修复任何拼写错误或其他错误。
重要提示: 除非您正在运行本教程课程,否则无需包含examplepolicy
文件。要排除此文件,请打开安全属性文件并删除刚刚添加的行。
在继续之前,您可能希望删除您刚刚在安全属性文件中添加的行(或将其注释掉),因为您可能不希望在不运行教程课程时包含examplepolicy
文件。
课程:用于安全代码和文件交换的 API 和工具使用
原文:
docs.oracle.com/javase/tutorial/security/sigcert/index.html
本课程解释了为什么需要数字签名、证书和密钥库。该课程还比较了使用这些工具与 JDK 安全 API 生成签名的情况。这些工具的使用在接下来的两节课中进行演示,签署代码并授予权限 和 文件交换。API 的使用在 生成和验证签名 课程中进行演示。
本课程包含以下部分
-
代码和文档安全
-
工具和 API 注释
-
使用 JDK 安全 API 签署文档
-
使用工具签署代码或文档
代码和文档安全
如果您通过电子方式向某人发送重要文件(或文件)、或要运行的小程序或应用程序,接收方需要一种方法来验证文件或代码是否来自您,并且在传输过程中未被修改(例如,被恶意用户拦截)。数字签名、证书和密钥库都有助于确保您发送的文件的安全性。
数字签名
使用数字签名的基本思想如下。
-
您使用您可以通过
keytool
或安全 API 方法生成的私钥对文档或代码进行“签名”。也就是说,您使用jarsigner
工具或安全 API 方法为文档或代码生成数字签名。 -
您将签名的文档发送给接收方。
-
您还向接收方提供您的公钥。这个公钥对应于您最初用于生成签名的私钥。
-
接收方使用您的公钥来验证您的文件是否来自您,并且在到达之前未被修改。
接收方需要确保您的公钥本身是真实的,然后才能使用它来验证您的签名是否真实。因此,您通常会提供一个包含您的公钥以及可以为您的密钥真实性作证的证书颁发机构的密钥的证书。有关详细信息,请参见下一节。
有关签名和验证术语和概念的更多信息,以及有关好处的进一步解释,请参见“JAR 文件打包程序”课程的 签署 JAR 文件 部分。
证书
证书包含:
-
一个公钥。
-
证书的“显著名称”信息是指证书的实体(个人、公司等)的信息。这个实体被称为证书主体或所有者。显著名称信息包括以下属性(或其子集):实体的名称、组织单位、组织、城市或地点、州或省和国家代码。
-
数字签名。证书由一个实体,颁发者,签名,以证明封闭的公钥是另一个实体,所有者的实际公钥。
-
签名者(颁发者)的显著名称信息。
收件人检查证书是否有效的一种方法是使用颁发者(签名者)的公钥验证其数字签名。该密钥本身可以存储在另一个证书中,该证书的签名也可以通过使用下一个证书的颁发者的公钥进行验证,而该密钥可能也存储在另一个证书中,依此类推。当你到达一个你已经信任的公钥并用它来验证相应证书上的签名时,你可以停止检查。
如果收件人无法建立信任链,那么他/她可以使用 keytool
的 -import
或 -printcert
命令计算证书的指纹。指纹是一个相对较短的数字,可以唯一且可靠地识别证书。(从技术上讲,指纹是证书信息的哈希值,使用消息摘要函数。)然后,收件人可以打电话给证书所有者,并将收到的证书的指纹值与发送的证书进行比较。如果指纹相同,则证书相同。
因此,你可以确保证书在传输过程中没有被修改。在处理证书时的另一个潜在不确定性是发送者的身份。有时证书是自签名的,即使用与证书中的公钥对应的私钥签名;颁发者与主体相同。
自签名证书对于开发和测试应用程序很有用。然而,在部署给用户之前,从一个受信任的第三方(称为认证机构(CA))那里获取证书是必要的。为此,你向 CA 发送一个自签名证书签名请求(CSR)。CA 验证 CSR 上的签名和你的身份,可能通过检查你的驾驶执照或其他信息。然后,CA 通过使用自己的(CA 的)私钥签发证书并为你作为公钥所有者背书。任何信任颁发 CA 的公钥的人现在都可以验证证书上的签名。在许多情况下,颁发 CA 本身可能有一个来自 CA 层次结构更高层的证书,导致证书链。
你信任的实体的证书通常被导入到你的密钥库中作为“受信任的证书”。每个这样的证书中的公钥可以用来验证使用相应私钥生成的签名。这样的验证可以通过以下方式完成:
-
jarsigner
工具(如果文档/代码和签名出现在一个 JAR 文件中), -
API 方法,或
-
运行时系统,在尝试访问资源时,如果策略文件指定允许代码尝试访问的资源访问,且其签名是真实的,则允许访问。代码的类文件和签名必须在一个 JAR 文件中。
如果您要向他人发送签名的代码或文档,则需要向他们提供包含与用于签署代码/文档的私钥对应的公钥的证书。keytool
的 -export
命令或 API 方法可以将您的证书从密钥库导出到文件中,然后可以将该文件发送给需要的任何人。接收证书的人可以将其导入到密钥库中作为受信任的证书,例如使用 API 方法或 keytool
的 -import
命令。
如果您使用 jarsigner
工具为 JAR 文件生成签名,则该工具会从您的密钥库中检索您的证书及其支持的证书链。然后,该工具将它们与签名一起存储在 JAR 文件中。
密钥库
私钥及其相关的公钥证书存储在受密码保护的数据库中,称为密钥库。密钥库可以包含两种类型的条目:上述讨论的受信任证书条目,以及密钥/证书条目,每个条目包含一个私钥和相应的公钥证书。密钥库中的每个条目都由一个别名标识。
密钥库所有者可以在密钥库中拥有多个密钥,通过不同的别名访问。别名通常以密钥库所有者在其中使用相关密钥的特定角色命名。别名也可以标识密钥的用途。例如,别名 signPersonalEmail
可能用于标识一个用于签署个人电子邮件的密钥库条目,而别名 signJarFiles
可能用于标识一个用于签署 JAR 文件的条目。
keytool
工具可用于
-
创建私钥及其相关的公钥证书
-
发出证书请求,然后将其发送给适当的认证机构
-
导入从您联系的认证机构获得的证书回复
-
导入属于其他方的公钥证书作为受信任的证书
-
管理您的密钥库
API 方法也可用于访问和修改密钥库。
工具和 API 注意事项
请注意以下与数字签名相关的工具和 API 使用。
-
您可以使用 JDK 安全 API、工具或组合来生成密钥和签名,并导入证书。您可以使用这些 API 或工具功能与他人安全地交换文档。
-
要使用工具进行文档交换,文档必须放在 JAR(Java ARchive)文件中,可以通过
jar
工具创建。JAR 文件是将多个文件封装在一个位置的好方法。当文件被“签名”时,生成的数字签名字节需要存储在某个地方。当 JAR 文件被签名时,签名可以放在 JAR 文件本身中。这就是当您使用jarsigner
工具对 JAR 文件进行签名时发生的情况。 -
如果您正在创建将要签署的小程序代码,它需要放在 JAR 文件中。如果您正在创建可能受到安全管理器限制的应用程序代码,同样需要放在 JAR 文件中。您需要 JAR 文件的原因是,当策略文件指定由特定实体签名的代码允许一个或多个操作,例如特定文件读取或写入时,预期代码来自已签名的 JAR 文件。(术语“已签名代码”是指“出现在已签名 JAR 文件中的类文件中的代码”的简称。)
-
为了使运行时系统检查代码签名,将运行代码的人/组织首先需要将验证用于签署代码的私钥对应的公钥的证书导入其密钥库中。
-
为了使
jarsigner
工具验证 JAR 文件签名的真实性,首先需要将接收到的 JAR 文件的人/组织导入其密钥库中,以验证与用于签署代码的私钥对应的公钥的证书。 -
目前还没有用于证书创建的 API。
使用 JDK 安全 API 签署文档
生成和验证签名展示了如何使用 JDK 安全 API 签署文档。该课程展示了由拥有原始文档的人执行的一个程序会做什么
-
生成密钥,
-
使用私钥为数据生成数字签名,然后
-
导出公钥和签名到文件。
然后展示了另一个程序的示例,由数据、签名和公钥的接收者执行。展示了该程序如何
-
导入公钥
-
验证签名的真实性。
本课程还展示了导入和提供密钥的替代方法,包括证书。
使用工具签署代码或文档
签署代码并授予权限课程展示了如何使用 Java 安全工具将您的代码放入 JAR 文件中,对其进行签名,并导出您的公钥。然后展示了您的接收方如何使用相同的 Java 工具导入您的公钥证书,然后向策略文件添加条目,以授予您的代码访问受接收方控制的系统资源所需的权限。
文件交换课程教你如何使用 Java 安全工具签署文档,然后使用keytool
导出公钥证书,对应于使用keytool
签署该文档的私钥。然后它展示了如何接收者可以通过安装您的公钥证书并使用jarsigner
工具验证您的签名。
这两个课程有很多共同之处。在两种情况下,发送代码或文档的前两个步骤是:
-
使用
jar
工具创建包含文档或类文件的 JAR 文件。 -
生成密钥(如果尚不存在),使用
keytool
的-genkey
命令。
接下来的两个步骤是可选的:
-
使用
keytool
的-certreq
命令;然后将生成的证书签名请求发送给认证机构(CA),如 VeriSign。 -
使用
keytool
的-import
命令导入 CA 的响应。
接下来的两个步骤是必需的:
-
使用之前生成的私钥,使用
jarsigner
工具对 JAR 文件进行签名。 -
使用
keytool
的-export
命令导出公钥证书。然后将签名的 JAR 文件和证书提供给接收者。
在两种情况下,签署的 JAR 文件和证书的接收者应该使用keytool
的-import
命令将证书导入为受信任的证书。keytool
将尝试从要导入的证书到密钥库中已受信任的证书构建信任链。如果失败,keytool
将显示证书指纹并提示您进行验证。
如果发送的是代码,则接收者还需要修改策略文件以允许由导入证书中的公钥对应的私钥签名的代码访问所需资源。可以使用策略工具来执行此操作。
如果发送的是一个或多个文档,则接收者需要使用jarsigner
工具验证 JAR 文件签名的真实性。
本课程讨论了两个可选步骤。其他步骤在接下来的两个课程中涵盖,签署代码并授予权限和文件交换。
为公钥证书生成证书签名请求(CSR)
当使用keytool
生成公私钥对时,它会创建一个包含私钥和公钥的自签名证书的密钥库条目。(即,证书使用相应的私钥进行签名。)这在开发和测试应用程序时是足够的。
然而,如果证书由认证机构(CA)签名,其他人更有可能信任该证书。要获得由 CA 签名的证书,首先需要生成证书签名请求(CSR),通过类似以下命令:
keytool -certreq -alias *alias* -file *csrFile*
这里alias用于访问包含私钥和公钥证书的密钥库条目,csrFile指定此命令创建的 CSR 使用的名称。
然后,你将此文件提交给 CA,如 VeriSign, Inc.。CA 对你(请求者/"主体")进行验证,然后签署并返回一个验证你的公钥的证书。通过签署证书,CA 保证你是公钥的所有者。
在某些情况下,CA 会返回一系列证书,每个证书都用于验证链中前一个证书签发者的公钥。
导入来自 CA 的响应
在向认证机构(CA)提交证书签名请求(CSR)后,你需要通过导入 CA 返回给你的证书(或证书链)来用证书链替换密钥库中的原始自签名证书。
但首先,你需要在你的密钥库中(或下面描述的cacerts
密钥库文件中)有一个"受信任证书"条目,用于验证CA的公钥。有了这样的条目,可以验证 CA 对证书的签名。也就是说,可以验证 CA 对证书的签名,或者验证 CA 发送给你作为 CSR 响应中的最终证书链上的最终证书的签名。
从 CA 导入证书作为"受信任证书"
在导入来自 CA 的证书回复之前,你需要在你的密钥库或cacerts
文件中拥有一个或多个"受信任证书"。
-
如果证书回复是一个证书链,你只需要链中的顶层证书 -- "根" CA 证书,用于验证该 CA 的公钥。
-
如果证书回复是单个证书,你需要签发它的 CA 的证书。如果该证书不是自签名的,你需要其签发者的证书,依此类推,直到自签名的"根" CA 证书。
cacerts
文件代表一个系统范围的带有 CA 证书的密钥库。该文件位于 JRE 安全属性目录*java.home*/lib/security
中,其中java.home是 JRE 安装目录。
重要提示:验证你的cacerts
文件
由于你信任cacerts
文件中的 CA 作为签署和颁发证书给其他实体的实体,你必须仔细管理cacerts
文件。cacerts
文件应只包含你信任的 CA 的证书。你有责任验证cacerts
文件中捆绑的受信任根 CA 证书,并做出自己的信任决定。要从cacerts
文件中删除一个不受信任的 CA 证书,使用keytool
命令的删除选项。你可以在 JRE 安装目录中找到cacerts
文件。如果没有权限编辑此文件,请联系系统管理员。
cacerts
文件包含许多受信任的 CA 证书。如果您将您的 CSR 发送给了这些受信任的供应商之一(比如 VeriSign),您就不需要将供应商的根证书作为受信任证书导入到您的密钥库中;您可以继续到下一个部分,查看如何从 CA 导入证书回复。
从 CA 获得的证书通常是自签名的或者由另一个 CA 签名的,此时您还需要一个证书来验证该 CA 的公钥。假设 ABC 公司是一个 CA,并且您获得了一个名为ABCCA.cer
的文件,据称是来自 ABC 公司的自签名证书,用于验证该 CA 的公钥。
在将证书导入为“受信任”证书之前,请务必确保证书是有效的!首先查看它(使用keytool
的-printcert
命令或者keytool
的-import
命令,不带-noprompt
选项),并确保显示的证书指纹与预期的一致。您可以联系发送证书的人,并将您看到的指纹与他们展示的或者安全的公钥存储库展示的指纹进行比较。只有当指纹相等时,才能保证证书在传输过程中没有被替换为其他人(例如,攻击者)的证书。如果发生这样的攻击,并且您在导入证书之前没有检查证书,那么您将信任攻击者签署的任何内容。
如果您相信该证书是有效的,您可以通过类似以下命令将其添加到您的密钥库中:
keytool -import -alias *alias* -file ABCCA.cer -keystore *storefile*
此命令在密钥库中创建一个名为storefile指定的“受信任证书”条目。该条目包含来自文件ABCCA.cer
的数据,并分配指定的别名。
从 CA 导入证书回复
一旦您已经导入了所需的受信任证书,就像前一节中描述的那样,或者它们已经存在于您的密钥库中或者在cacerts
文件中,您可以导入证书回复,从而用证书链替换您的自签名证书。这个链将是 CA 对您的请求的回复中返回的(如果 CA 的回复是一个链)或者通过使用证书回复和已经在密钥库中或者cacerts
密钥库文件中可用的受信任证书构建的(如果 CA 的回复是一个单一证书)。
举例来说,假设您将您的证书签名请求发送给了 VeriSign。然后您可以通过以下方式导入回复,假设返回的证书在certReplyFile指定的文件中:
keytool -import -trustcacerts
-keystore *storefile*
-alias *alias*
-file *certReplyFile*
在一行上输入此命令。
证书回复通过使用密钥库中的受信任证书进行验证,并可选择使用cacerts
密钥库文件中配置的证书进行验证(如果指定了-trustcacerts
选项)。每个链中的证书都会被验证,使用链中下一级别的证书。您只需要信任链中顶级的“根”CA 证书。如果您尚未信任顶级证书,keytool
将显示该证书的指纹,并询问您是否要信任它。
指定(通过别名)条目的新证书链将替换与该条目关联的旧证书(或链)。只有在提供有效的keypass,用于保护条目私钥的密码时,才能替换旧链。如果未提供密码,并且私钥密码与密钥库密码不同,则会提示用户输入。
关于生成证书签名请求(CSR)和导入证书回复的更详细信息,请参阅keytool
文档:
-
带有 Windows 示例的 keytool 文档
-
带有 UNIX 示例的 keytool 文档
课程:签署代码并授予权限
原文:
docs.oracle.com/javase/tutorial/security/toolsign/index.html
本课程展示如何使用keytool
、jarsigner
、策略工具
和jar
将文件放入 JAR(Java ARchive)文件,以便后续由jarsigner
工具进行签名。
本课程分为两部分。首先,您将创建和部署一个应用程序。其次,您将作为签名应用程序的接收者。
以下是创建和部署应用程序的步骤:
注意: 为方便起见,您假装是一个名为 Susan Jones 的用户/开发者。您需要在生成密钥时定义 Susan Jones。
-
将包含应用程序的 Java 类文件放入 JAR 文件中
-
对 JAR 文件进行签名
-
导出与用于签署 JAR 文件的私钥对应的公钥证书
以下是向应用程序授予权限的步骤
注意: 为方便起见,您假装是一个名为 Ray 的用户。
-
您可以看到,当在安全管理器下运行时,签名应用程序通常无法读取文件。
-
使用
keytool
将证书导入 Ray 的密钥库中,别名为susan
-
使用策略工具在 Ray 的策略文件中创建一个条目,以允许由
susan
签名的代码读取指定文件。 -
最后,您将看到在安全管理器下运行的应用程序现在可以读取文件,因为已经被授予了相应的权限。
有关数字签名、证书、密钥库和工具的更多信息,请参阅用于安全代码和文件交换的 API 和工具使用课程。
重要: 您需要在存储示例应用程序的目录中执行本课程中的任务,但应将应用程序所需的数据文件存储在不同的目录中。本教程中的所有示例都假定您正在C:\Test
目录中工作,并且数据文件位于C:\TestData
目录中。
如果您在 UNIX 系统上工作,请用您自己的目录名称替换。
以下是步骤:
-
代码签署者的步骤
-
代码接收者的步骤
Code Signer 的步骤
原文:
docs.oracle.com/javase/tutorial/security/toolsign/signer.html
Code Signer 执行以下步骤:
-
下载并尝试示例应用程序。
-
创建包含类文件的 JAR 文件,使用
jar
工具。 -
生成密钥(如果尚不存在),使用
keytool
的-genkey
命令。
可选步骤 为公钥证书生成证书签名请求(CSR),并导入认证机构(CA)的响应。为简单起见(并且因为你只是假装是苏珊·琼斯),这一步被省略了。有关更多信息,请参阅为公钥证书生成证书签名请求(CSR)。
-
对 JAR 文件进行签名,使用
jarsigner
工具和私钥。 -
导出公钥证书,使用
keytool
的-export
命令。然后将签名的 JAR 文件和证书提供给接收者雷。
下载并尝试示例应用程序
原文:
docs.oracle.com/javase/tutorial/security/toolsign/step1.html
本课程使用我们提供给您的一个简单应用程序。
-
通过复制或下载
Count.java
源代码,在本地计算机上创建一个名为Count.java
的文件。本课程中的示例假定您将count
放在C:\Test
目录中。 -
count
应用程序需要访问包含其将处理的数据的文本文件。下载一个示例数据文件
,或使用任何其他方便的文本文件作为数据。
重要:将数据文件放入一个不同于包含已下载
count
类文件的目录中。我们建议使用C:\TestData\data
。在本课程的后面部分,您将看到在安全管理器下运行的应用程序除非有明确的权限,否则无法读取文件。但是,应用程序始终可以从相同目录(或子目录)中读取文件。它不需要明确的权限。
-
编译然后运行
Count
应用程序以查看其功能。当您运行
count
应用程序时,您需要指定(作为参数)要读取的文件的路径名。java Count C:\TestData\data
这里是一个示例运行:
C:\Test>java Count C:\TestData\data
Counted 65 chars.
创建一个包含类文件的 JAR 文件
原文:
docs.oracle.com/javase/tutorial/security/toolsign/step2.html
接下来,在命令窗口中输入以下内容,创建一个包含Count.class
文件的 JAR 文件:
jar cvf Count.jar Count.class
这将创建一个名为Count.jar
的 JAR 文件,并将Count.class
文件放入其中。
生成密钥
原文:
docs.oracle.com/javase/tutorial/security/toolsign/step3.html
如果代码签名者尚未拥有适合签署代码的私钥,必须首先生成该私钥,以及一个相应的公钥,供代码接收方的运行时系统用于验证签名。
由于这节课假设你还没有这样的密钥,你将创建一个名为examplestore
的密钥库,并创建一个带有新生成的公钥/私钥对的条目(其中公钥在证书中)。
在命令窗口中输入以下命令以创建一个名为examplestore
的密钥库并生成密钥:
keytool -genkey -alias signFiles -keystore examplestore
您将被提示输入密钥和密钥库的密码。
keytool
命令的子部分
让我们看看keytool
的每个子部分的含义。
-
生成密钥的命令是-genkey。
-
-alias signFiles子部分指示将来用于引用包含将生成的密钥的密钥库条目的别名。
-
-keystore examplestore子部分指示您正在创建或已经使用的密钥库的名称(和可选路径)。
-
你被提示输入的storepass值指定了密钥库密码。
-
你被提示输入的keypass值指定了即将生成的私钥的密码。您始终需要此密码才能访问包含该密钥的密钥库条目。该条目不必有自己的密码。当提示您输入密钥密码时,您可以选择让它与密钥库密码相同。
注意:出于安全原因,您不应在命令行上设置密钥或密钥库密码,因为这样更容易被拦截。
专有名称信息
如果您使用上述keystore
命令,将提示您输入您的专有名称信息。以下是提示信息;粗体表示您应该输入的内容。
What is your first and last name?
[Unknown]: Susan Jones
What is the name of your organizational unit?
[Unknown]: Purchasing
What is the name of your organization?
[Unknown]: ExampleCompany
What is the name of your City or Locality?
[Unknown]: Cupertino
What is the name of your State or Province?
[Unknown]: CA
What is the two-letter country code for this unit?
[Unknown]: US
Is <CN=Susan Jones, OU=Purchasing, O=ExampleCompany,
L=Cupertino, ST=CA, C=US> correct?
[no]: y
命令结果
keytool
命令在执行命令的同一目录中创建名为examplestore
的密钥库(如果尚不存在)。该命令为具有 Susan Jones 作为通用名称和采购部门的实体生成公钥/私钥对。
该命令创建一个包含公钥和专有名称信息的自签名证书。(您提供的专有名称将用作证书中的“主题”字段。)该证书将在 90 天内有效,如果您不指定-validity选项,则为默认有效期。该证书与密钥库条目中的私钥关联,该条目由别名signFiles
引用。
自签名证书对于开发和测试应用程序非常有用。然而,用户会收到警告,指出应用程序是使用不受信任的证书签名的,并询问他们是否要运行该应用程序。为了让用户更有信心地运行您的应用程序,请使用由认可的证书颁发机构颁发的证书。
注意: 如果接受选项的默认值或希望提示输入各种值,命令可能会更短。每当执行keytool
命令时,对于未指定具有默认值的选项,将使用默认值,并提示您输入任何必需的值。对于genkey
命令,具有默认值的选项包括别名(默认为mykey
)、有效期(90 天)和密钥库(位于您的主目录中名为.keystore
的文件)。必需的值包括dname、storepass和keypass。
签署 JAR 文件
原文:
docs.oracle.com/javase/tutorial/security/toolsign/step4.html
现在,您可以准备签署 JAR 文件。在命令窗口中键入以下内容,以使用密钥库条目中别名为signFiles
的私钥签署 JAR 文件Count.jar
,并将生成的签名 JAR 文件命名为sCount.jar
:
jarsigner -keystore examplestore -signedjar sCount.jar Count.jar signFiles
您将被要求输入存储密码和私钥密码。
注意: jarsigner
工具从别名为signFiles
的密钥库条目中提取证书,并将其附加到已签名 JAR 文件的生成签名中。
建议为签名加上时间戳,如果签名未加时间戳,则会显示警告。时间戳用于验证用于签署 JAR 文件的证书在签署时是否有效。有关使用jarsigner
选项在签名中包含时间戳的信息,请参阅签署 JAR 文件。
导出公钥证书
原文:
docs.oracle.com/javase/tutorial/security/toolsign/step5.html
现在您有一个已签名的 JAR 文件sCount.jar
。当签名的 JAR 文件中的Count
应用程序尝试读取文件并且策略文件授予该权限给这个已签名代码时,代码接收者(Ray)的运行时系统将需要验证签名。
为了使运行时系统能够验证签名,Ray 的密钥库需要具有与用于生成签名的私钥对应的公钥。您可以通过将证书复制到名为Example.cer
的文件中,向 Ray 发送验证公钥的证书。通过以下方式从密钥库examplestore
复制该证书:
keytool -export -keystore examplestore -alias signFiles -file Example.cer
你将被提示输入存储密码。
代码接收者的步骤
原文:
docs.oracle.com/javase/tutorial/security/toolsign/receiver.html
在这节课中,你将扮演接收者的角色,接收包含count.class
文件的已签名的 jar 文件。它请求访问你系统资源的权限,这是它通常没有权限访问的。
这个过程需要你执行下面列出的步骤。
-
观察受限应用程序。在导入 Susan 的证书并创建策略文件之前,该应用程序将无法访问你的系统资源。
-
使用
keytool -import
命令将 Susan 的证书导入为受信任的证书,并将其别名设置为susan
。 -
设置策略文件以授予权限给由
susan
签名的count
应用程序读取你系统上指定的文件。 -
测试你重新配置的
count
应用程序,以验证具有受信任证书和访问你新策略文件的权限后,count
现在可以读取你的data
文件。
观察受限应用程序
原文:
docs.oracle.com/javase/tutorial/security/toolsign/rstep1.html
控制应用程序快速浏览课程的最后一部分展示了如何通过使用新的-Djava.security.manager
命令行参数调用解释器来在安全管理器下运行应用程序。但是,如果要调用的应用程序位于 JAR 文件中怎么办?
解释器选项之一是-cp
(用于类路径)选项,它允许您为应用程序类和资源指定搜索路径。因此,要在sCount.jar
JAR 文件内执行Count
应用程序,并将文件C:\TestData\data
指定为其参数,您可以在包含sCount.jar
的目录中键入以下命令:
java -cp sCount.jar Count C:\TestData\data
要使用安全管理器执行应用程序,请添加-Djava.security.manager
,如下所示:
java -Djava.security.manager -cp sCount.jar Count C:\TestData\data
重要: 运行此命令时,您的 Java 解释器将抛出下面显示的异常:
Exception in thread "main" java.security.AccessControlException:
access denied (java.io.FilePermission C:\TestData\data read)
at java.security.AccessControlContext.checkPermission(Compiled Code)
at java.security.AccessController.checkPermission(Compiled Code)
at java.lang.SecurityManager.checkPermission(Compiled Code)
at java.lang.SecurityManager.checkRead(Compiled Code)
at java.io.FileInputStream.<init>(Compiled Code)
at Count.main(Compiled Code)
在此示例中,AccessControlException
报告说count
应用程序没有权限读取文件C:\TestData\data
。您的解释器引发此异常,因为除非在policy
文件中包含的grant
语句中明确允许,否则它不会允许任何在安全管理器下运行的应用程序读取文件或访问其他资源。
将证书导入为受信任的证书
原文:
docs.oracle.com/javase/tutorial/security/toolsign/rstep2.html
在授予已签署代码读取指定文件的权限之前,您需要将 Susan 的证书作为受信任的证书导入到您的密钥库中。
假设您已经从 Susan 那里收到了
-
已签署的 JAR 文件
sCount.jar
,其中包含Count.class
文件,以及 -
包含与用于签署 JAR 文件的私钥对应的公钥证书的
Example.cer
文件。
即使您创建了这些文件并且它们实际上还没有被传输到任何地方,您也可以模拟成不是创建者和发送者 Susan 的其他人。假装您现在是 Ray。作为 Ray,您将创建一个名为exampleraystore
的密钥库,并将其用于将证书导入到具有别名susan
的条目中。
每当您使用指定尚不存在的密钥库的keytool
命令时,都会创建一个密钥库。因此,我们可以通过单个keytool
命令创建exampleraystore
并导入证书。在命令窗口中执行以下操作。
-
转到包含公钥证书文件
Example.cer
的目录。(实际上,您应该已经在那里,因为本课程假设您在整个过程中都保持在一个目录中。) -
在一行上输入以下命令:
keytool -import -alias susan -file Example.cer -keystore exampleraystore
由于密钥库尚不存在,将会创建它,并提示您输入密钥库密码;输入任何您想要的密码。
keytool
命令将打印出证书信息,并要求您验证它,例如,通过将显示的证书指纹与从另一个(受信任的)信息源获得的指纹进行比较。 (每个指纹是一个相对较短的数字,可以唯一且可靠地识别证书。)例如,在现实世界中,您可能会打电话给 Susan,并询问她应该是什么指纹。她可以通过执行以下命令获取她创建的Example.cer
文件的指纹
keytool -printcert -file Example.cer
如果她看到的指纹与keytool
向您报告的指纹相同,则证书在传输过程中未被修改。在这种情况下,您可以让keytool
继续将受信任的证书条目放入密钥库中。该条目包含来自文件Example.cer
的公钥证书数据,并被分配别名susan
。
设置策略文件以授予所需的权限。
原文:
docs.oracle.com/javase/tutorial/security/toolsign/rstep3.html
接下来,您将使用策略工具创建一个名为exampleraypolicy
的策略文件,并在其中授予来自已签名 JAR 文件的代码权限。
JAR 文件必须使用与上一步中导入到 Ray 的密钥库(exampleraystore
)中的公钥对应的私钥进行签名。包含公钥的证书在密钥库中被别名为susan
。我们将授予此类代码权限以读取C:\TestData\
目录中的任何文件。
步骤如下:
-
启动策略工具
-
指定密钥库
-
使用 SignedBy 别名添加策略条目
-
保存策略文件
启动策略工具
原文:
docs.oracle.com/javase/tutorial/security/toolsign/wstep1.html
要启动策略工具,只需在命令行中键入以下内容:
policytool
这将打开策略工具窗口。每次启动策略工具时,它会尝试从通常称为“用户策略文件”的文件中填充此窗口中的策略信息,默认情况下,该文件名为.java.policy
,位于您的主目录中。如果策略工具找不到用户策略文件,它会报告这种情况并显示一个空白的策略工具窗口(即,一个带有标题和按钮但没有数据的窗口,如下图所示。
由于本教程的课程不需要对您的官方用户策略文件进行修改,因此您将创建并使用一个与用户策略文件不同的策略文件。
假设您看到了空白的策略工具窗口(如果没有,请在文件菜单中选择新建),您可以立即开始创建一个新的策略文件。
指定密钥库
原文:
docs.oracle.com/javase/tutorial/security/toolsign/wstep2.html
对于本课程,您将授予别名为 susan 的 JAR 文件中的所有代码对C:\TestData\
目录中所有文件的读取访问权限。您需要
-
指定包含别名为 susan 的证书信息的密钥库
-
创建授予权限的策略条目
密钥库是在将证书导入为受信任的证书步骤中创建的名为exampleraystore
的密钥库。
要指定密钥库,请在主策略工具窗口的编辑菜单中选择更改密钥库命令。这将弹出一个对话框,您可以在其中指定密钥库 URL 和密钥库类型。
要指定名为exampleraystore
的密钥库,位于C:
驱动器上的Test
目录中,请在标记为“New KeyStore URL”的文本框中键入以下file
URL
file:/C:/Test/exampleraystore
如果密钥库类型是默认类型,可以将标记为“New KeyStore Type”的文本框留空,如安全属性文件中所指定的那样。您的密钥库将是默认类型,因此请将文本框留空。
注意: “New KeyStore URL”值是一个 URL,因此应始终使用斜杠(而不是反斜杠)作为目录分隔符。
当您完成指定密钥库 URL 后,请选择确定。标记为密钥库的文本框现在填入了 URL。
接下来,您需要指定新的策略条目。
添加带有 SignedBy 别名的策略条目
原文:
docs.oracle.com/javase/tutorial/security/toolsign/wstep3.html
要授予由susan
签名的代码权限以读取C:\TestData
目录中的任何文件,您需要创建一个授予此权限的策略条目。请注意,“由susan
签名的代码”是指“包含在 JAR 文件中的类文件中的代码,其中 JAR 文件是使用与出现在别名为susan
的 keystore 证书中的公钥对应的私钥签名的。”
在主策略工具窗口中选择添加策略条目按钮。这将打开策略条目对话框:
在此对话框中,将以下别名键入到SignedBy文本框中:
susan
将CodeBase文本框留空,以授予由susan
签名的所有代码权限,无论其来源。
注意: 如果您想将权限限制为仅来自C:\Test\
目录的susan
签名代码,您将在CodeBase文本框中键入以下 URL:
file:/C:/Test/*
要添加权限,请选择添加权限按钮。这将打开权限对话框。
执行以下操作。
-
从权限下拉列表中选择文件权限。完整的权限类型名称(
java.io.FilePermission
)现在显示在下拉列表右侧的文本框中。 -
在标记为目标名称的列表右侧的文本框中键入以下内容,以指定
C:\TestData\
目录中的所有文件:C:\TestData\*
-
通过从操作下拉列表中选择读取选项来指定读取权限。
现在权限对话框看起来像下面这样。
选择确定按钮。新的权限出现在策略条目对话框中的一行中,如下所示。
注意: 你在文件路径中键入的每个反斜杠都已替换为两个反斜杠,以方便您使用。策略文件中的字符串由一个标记器处理,允许使用 \ 作为转义字符(例如,\n
表示换行),因此策略文件需要两个反斜杠来表示一个反斜杠。如果您使用单个反斜杠作为目录分隔符,策略工具会自动将其转换为双反斜杠。
现在已经完成指定此策略条目的操作,请在策略条目对话框中选择完成按钮。策略工具窗口现在包含表示策略条目的一行,显示SignedBy值。
保存策略文件
原文:
docs.oracle.com/javase/tutorial/security/toolsign/wstep4.html
要保存你正在创建的新策略文件,请从文件菜单中选择另存为命令。这将弹出另存为对话框。
浏览目录结构,找到要保存策略文件的目录:C:
驱动器上的Test
目录。输入文件名。
exampleraypolicy
然后选择保存按钮。策略文件现在已保存,其名称和路径显示在标有策略文件的文本框中。
然后通过从文件菜单中选择退出命令退出策略工具。
查看策略文件效果
原文:
docs.oracle.com/javase/tutorial/security/toolsign/rstep4.html
在之前的步骤中,你在exampleraypolicy
策略文件中创建了一个条目,授予由susan
签名的代码从C:\TestData\
目录(或者如果你在 UNIX 上工作,则是你的主目录中的testdata
目录)读取文件的权限。现在,你应该能够成功地执行Count
程序,从指定目录中读取文件并计算字符数,即使在使用安全管理器运行应用程序时也是如此。
如在创建策略文件课程的结尾所述,有两种可能的方式可以使exampleraypolicy
文件作为整体策略的一部分被考虑,除了在安全属性文件中指定的策略文件之外。第一种方法是在传递给运行时系统的属性中指定额外的策略文件。第二种方法是在安全属性文件中添加一行指定额外的策略文件。
方法 1
你可以使用-Djava.security.policy
命令行参数来指定一个策略文件,该文件应该被用来补充或替代安全属性文件中指定的文件。
要运行Count
应用程序并包含exampleraypolicy
策略文件,请在包含sCount.jar
和exampleraypolicy
文件的目录中键入以下内容:
java -Djava.security.manager
-Djava.security.policy=exampleraypolicy
-cp sCount.jar Count C:\TestData\data
注意:在一行上键入命令,-D
和-cp
之前加上一个空格。
程序应该报告指定文件中的字符数。
如果仍然报错,那么策略文件中可能存在问题。使用策略工具检查你在上一步中创建的权限,并更正任何拼写错误或其他错误。
方法 2
你可以在安全属性文件中的policy.url.n
属性中指定多个 URL,包括形如"http://"的 URL,所有指定的策略文件都将被加载。
因此,让解释器考虑你的exampleraypolicy
文件的策略条目的一种方法是在安全属性文件中添加指示该文件的条目。
重要提示:如果你正在运行自己的 JDK 副本,你可以轻松编辑你的安全属性文件。如果你正在运行与他人共享的版本,只有在你有写入权限或在适当时向系统管理员请求修改文件时,你才能修改系统范围的安全属性文件。然而,在本教程测试中,对于你来说可能不适合修改系统范围的策略文件;我们建议你只是阅读以下内容以了解如何操作,或者安装你自己的私人版本的 JDK 以供教程课程使用。
安全属性文件位于
-
Windows:
*java.home*\lib\security\java.security
-
UNIX:
*java.home*/lib/security/java.security
java.home
部分表示 JRE 安装的目录。
要修改安全属性文件,请在适合编辑 ASCII 文本文件的编辑器中打开它。然后在以policy.url.2
开头的行后添加以下行:
-
Windows:
**policy.url.3=file:/C:/Test/exampleraypolicy**
-
UNIX:
**policy.url.3=file:${user.home}/test/exampleraypolicy**
在 UNIX 系统上,您还可以显式指定您的主目录,如
policy.url.3=file:/home/susanj/test/exampleraypolicy
接下来,在您的命令窗口中,转到包含sCount.jar
文件的目录,即C:\Test
或~/test
目录。在一行上键入以下命令:
java -Djava.security.manager
-cp sCount.jar Count C:\TestData\data
与第一种方法一样,如果程序仍然报告错误,则可能是策略文件出现问题。使用策略工具检查您在上一步中创建的权限,并更正任何拼写错误或其他错误。
重要提示:在继续之前,您可能希望删除您刚刚在安全属性文件中添加的行(或将其注释掉),因为您可能不希望在不运行教程课程时包含exampleraypolicy
文件。
课程:文件交换
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/index.html
如果您想将重要文件(如合同)电子发送给他人,最好对文件进行数字“签名”,以便您的接收方可以检查文件确实来自您,并在传输过程中未被更改。
本课程向您展示如何使用安全工具交换重要文件,本例中为合同。
首先,您假装自己是合同发送方,斯坦·史密斯。本课程展示了斯坦将使用的步骤,将合同放入 JAR 文件中,签名并导出与用于签署 JAR 文件的私钥对应的公钥证书。
然后,你假装自己是鲁思,已经收到签名的 JAR 文件和证书。你将使用keytool
将证书导入鲁思的密钥库中,别名为stan
,并使用jarsigner
工具验证签名。
有关数字签名、证书、密钥库和工具的更多信息,请参阅用于安全代码和文件交换的 API 和工具使用课程。
注意:本课程假设您从同一目录中执行所有命令。
以下是步骤:
-
发送方操作步骤
-
接收方操作步骤
合同发送方的步骤
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/sender.html
此处为合同发送方概述的步骤与签署代码并授予权限课程中列出的代码签署者的步骤基本相同。然而,在这里,您假装是 Stan Smith 而不是 Susan Jones,并且在要签名的 JAR 文件中存储数据文件而不是类文件。
作为合同发送方,您需要执行以下步骤。
-
创建包含合同的 JAR 文件,使用
jar
工具。 -
生成密钥(如果尚未存在),使用
keytool
的-genkey
命令。可选步骤:为公钥证书生成证书签名请求(CSR),并导入认证机构的响应。为简单起见,由于您只是假装是 Stan Smith,因此省略了此步骤。有关更多信息,请参见为公钥证书生成证书签名请求(CSR)。
-
对 JAR 文件进行签名,使用
jarsigner
工具和第 2 步生成的私钥。 -
导出公钥证书,使用
keytool
的-export
命令。然后将签名的 JAR 文件和证书提供给接收方 Ruth。
创建包含合同的 JAR 文件
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/step1.html
你需要的第一件事是一个合同文件。你可以下载并使用这个非常基本的示例文件,命名为contract
。或者你也可以使用其他任何你喜欢的文件。只需确保将文件命名为contract
,这样它就可以与本课程中指定的命令一起使用。
一旦你有了合同文件,将其放入一个 JAR 文件中。在你的命令窗口中输入以下内容:
jar cvf Contract.jar contract
这个命令创建一个名为Contract.jar
的 JAR 文件,并将contract
文件放入其中。
生成密钥
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/step2.html
在签署包含contract
文件的Contract.jar
JAR 文件之前,如果您尚未拥有合适的密钥,则需要生成密钥。您需要使用私钥对 JAR 文件进行签名,您的接收方需要您相应的公钥来验证您的签名。
本课程假设您尚未拥有密钥对。您将创建一个名为examplestanstore
的密钥库,并创建一个具有新生成的公钥/私钥对的条目(其中公钥在证书中)。
现在假设你是 Stan Smith,并且在 Example2 公司的法律部门工作。在命令窗口中输入以下内容,创建一个名为examplestanstore
的密钥库,并为 Stan Smith 生成密钥:
keytool -genkey -alias signLegal -keystore examplestanstore
密钥库工具会提示您输入密钥库密码、专有名称信息和密钥密码。以下是提示信息;粗体表示您应该输入的内容。
Enter keystore password: *<password>*
What is your first and last name?
[Unknown]: Stan Smith
What is the name of your organizational unit?
[Unknown]: Legal
What is the name of your organization?
[Unknown]: Example2
What is the name of your City or Locality?
[Unknown]: New York
What is the name of your State or Province?
[Unknown]: NY
What is the two-letter country code for this unit?
[Unknown]: US
Is <CN=Stan Smith, OU=Legal, O=Example2, L=New York, ST=NY, C=US> correct?
[no]: y
Enter key password for <signLegal>
(RETURN if same as keystore password):
前面的keytool
命令在执行命令的同一目录中(假设指定的密钥库尚不存在)创建名为examplestanstore
的密钥库,并为具有Stan Smith通用名称和Legal组织单位的实体生成公钥/私钥对。
你刚刚创建的自签名证书包括公钥和专有名称信息。(自签名证书是由与证书中公钥对应的私钥签名的证书。)该证书有效期为 90 天。如果不指定* -validity*选项,则默认有效期为 90 天。该证书与别名为signLegal
的密钥库条目中的私钥相关联。私钥分配了输入的密码。
自签名证书对于开发和测试应用程序非常有用。但是,用户会收到警告,应用程序是使用不受信任的证书签名的,并询问他们是否要运行该应用程序。为了让用户更有信心运行您的应用程序,请使用由认可的证书颁发机构颁发的证书。
签署 JAR 文件
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/step3.html
现在,您已经准备好签署 JAR 文件了。
在命令窗口中一行输入以下内容,以使用密钥库条目中别名为signLegal
的私钥签署 JAR 文件Contract.jar
,并将生成的签名附加到命名为sContract.jar
的结果签名的 JAR 文件中:
jarsigner -keystore examplestanstore
-signedjar sContract.jar
Contract.jar signLegal
系统会提示您输入存储密码和私钥密码。
jarsigner
工具从别名为signLegal
的密钥库条目中提取证书,并将其附加到已签名 JAR 文件的生成签名中。
导出公钥证书
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/step4.html
您现在拥有一个已签名的 JAR 文件sContract.jar
。想要使用此文件的接收方也希望验证您的签名。为此,他们需要与您用于生成签名的私钥对应的公钥。您可以通过将包含您的公钥的证书副本发送给他们来提供您的公钥。通过以下方式,将该证书从密钥库examplestanstore
复制到名为StanSmith.cer
的文件中:
keytool -export -keystore examplestanstore
-alias signLegal -file StanSmith.cer
系统会提示您输入存储密码。
一旦他们获得了该证书和签名的 JAR 文件,您的接收方可以使用jarsigner
工具来验证您的签名。请参阅合同接收方的步骤。
合同接收方的步骤
docs.oracle.com/javase/tutorial/security/toolfilex/receiver.html
现在扮演接收来自 Stan 的签名 JAR 文件和证书文件的 Ruth,执行以下步骤。
-
使用
keytool
的-import
命令将证书导入为受信任的证书。 -
使用
jarsigner
工具验证 JAR 文件签名。
将证书导入为受信任的证书
原文:
docs.oracle.com/javase/tutorial/security/toolfilex/rstep1.html
假设您是 Ruth,并已从 Stan Smith 那里收到
-
签名的 JAR 文件
sContract.jar
包含一个合同 -
文件
StanSmith.cer
包含与用于签署 JAR 文件的私钥对应的公钥证书
在您可以使用jarsigner
工具检查 JAR 文件签名的真实性之前,您需要将 Stan 的证书导入您的密钥库。
即使您(扮演 Stan)创建了这些文件,它们实际上还没有被传输到任何地方,您可以模拟成除创建者和发送者 Stan 之外的其他人。作为 Ruth,输入以下命令创建一个名为exampleruthstore
的密钥库,并将证书导入到别名为stan
的条目中。
keytool -import -alias stan -file StanSmith.cer -keystore exampleruthstore
由于密钥库尚不存在,keytool
将为您创建它。它会提示您输入密钥库密码。
keytool
打印证书信息并要求您验证它;例如,通过将显示的证书指纹与从另一个(受信任的)信息源获得的指纹进行比较。(每个指纹是一个相对较短的数字,可以唯一且可靠地识别证书。)例如,在现实世界中,您可以打电话给 Stan 并询问他应该是什么指纹。他可以通过执行命令获取他创建的StanSmith.cer
文件的指纹
keytool -printcert -file StanSmith.cer
如果他看到的指纹与keytool
向您报告的指纹相同,则您都可以假定证书在传输过程中未被修改。您可以放心地让keytool
继续将一个“受信任的证书”条目放入您的密钥库中。该条目包含来自文件StanSmith.cer
的公钥证书数据。keytool
为这个新条目分配别名stan
。
验证 JAR 文件签名。
docs.oracle.com/javase/tutorial/security/toolfilex/rstep2.html
作为 Ruth,您现在已将 Stan 的公钥证书导入到exampleruthstore
密钥库中作为“受信任的证书”。您现在可以使用jarsigner
工具来验证 JAR 文件签名的真实性。
当您验证已签名的 JAR 文件时,您验证签名是否有效,以及 JAR 文件是否未被篡改。您可以通过以下命令对sContract.jar
文件进行此操作:
jarsigner -verify -verbose -keystore exampleruthstore sContract.jar
您应该看到类似以下内容:
183 Fri Jul 31 10:49:54 PDT 1998 META-INF/SIGNLEGAL.SF
1542 Fri Jul 31 10:49:54 PDT 1998 META-INF/SIGNLEGAL.DSA
0 Fri Jul 31 10:49:18 PDT 1998 META-INF/
smk 1147 Wed Jul 29 16:06:12 PDT 1998 contract
s = signature was verified
m = entry is listed in manifest
k = at least one certificate was found in keystore
i = at least one certificate was found in identity scope
jar verified.
请务必使用-verbose
选项运行命令,以获取足够的信息以确保以下内容:
-
合同文件是 JAR 文件中的文件之一,已签名并验证其签名(这就是
s
的意思)。 -
用于验证签名的公钥位于指定的密钥库中,因此您信任它(这就是
k
的意思)。
课程:生成和验证签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/index.html
本课程将指导您使用 JDK 安全 API 为数据生成数字签名并验证签名的步骤。本课程适用于希望将安全功能纳入其程序中的开发人员,包括密码服务。
本课程演示了使用 JDK 安全 API 签署文档。该课程展示了一个程序,由拥有原始文档的人执行,用于生成密钥、使用私钥为文档生成数字签名,并将公钥和签名导出到文件。
然后展示了另一个程序的示例,由文档、签名和公钥的接收者执行。展示了程序如何导入公钥并验证签名的真实性。该课程还讨论并演示了可能的替代方法和提供和导入密钥的方法,包括在证书中。
欲了解有关概念和术语(数字签名、证书、密钥库)的更多信息,请参阅用于安全代码和文件交换的 API 和工具使用课程。
在本课程中,您将创建两个基本应用程序,一个用于数字签名生成,另一个用于验证。接着讨论和演示了潜在的增强功能。本课程包含三个部分。
-
生成数字签名展示了使用 API 生成数据的密钥和数字签名,使用私钥并将公钥和签名导出到文件。应用程序从命令行获取数据文件名。
-
验证数字签名展示了使用 API 导入一个公钥和一个据称是指定数据文件签名的签名,并验证签名的真实性。数据、公钥和签名文件名在命令行中指定。
-
弱点和替代方案讨论了基本程序使用的方法可能存在的弱点。然后介绍并演示了可能的替代方法和提供和导入密钥的方法,包括使用包含编码密钥字节的文件和使用包含公钥的证书。
生成数字签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/gensig.html
即将创建的GenSig
程序将使用 JDK 安全 API 生成密钥和使用私钥为数据生成数字签名,并将公钥和签名导出到文件中。应用程序从命令行获取数据文件名。
以下步骤创建GenSig
示例程序。
-
准备初始程序结构
创建一个名为
GenSig.java
的文本文件。输入初始程序结构(导入语句、类名、main
方法等)。 -
生成公钥和私钥
生成一对密钥(公钥和私钥)。私钥用于对数据进行签名。公钥将被
VerSig
程序用于验证签名。 -
对数据进行签名
获取一个
Signature
对象并初始化以进行签名。提供要签名的数据,并生成签名。 -
保存签名和公钥到文件中
将签名字节保存在一个文件中,将公钥字节保存在另一个文件中。
-
编译并运行程序
准备初始程序结构
原文:
docs.oracle.com/javase/tutorial/security/apisign/step1.html
这是GenSig
程序的基本结构。将其放在名为GenSig.java
的文件中。
import java.io.*;
import java.security.*;
class GenSig {
public static void main(String[] args) {
/* Generate a DSA signature */
if (args.length != 1) {
System.out.println("Usage: GenSig nameOfFileToSign");
}
else try {
// the rest of the code goes here
} catch (Exception e) {
System.err.println("Caught exception " + e.toString());
}
}
}
注意:
-
签署数据的方法位于
java.security
包中,因此程序从该包中导入所有内容。程序还导入了java.io
包,其中包含输入要签名的文件数据所需的方法。 -
期望提供一个参数,指定要签名的数据文件。
-
后续步骤中编写的代码将放在
try
和catch
块之间。
生成公钥和私钥
原文:
docs.oracle.com/javase/tutorial/security/apisign/step2.html
要能够创建数字签名,您需要一个私钥。(为了验证签名的真实性,还需要相应的公钥。)
在某些情况下,密钥对(私钥和相应的公钥)已经存在于文件中。在这种情况下,程序可以导入并使用私钥进行签名,如 Weaknesses and Alternatives 中所示。
在其他情况下,程序需要生成密钥对。通过使用KeyPairGenerator
类生成密钥对。
在此示例中,您将为数字签名算法(DSA)生成公钥/私钥对。您将生成长度为 1024 位的密钥。
生成密钥对需要几个步骤:
创建密钥对生成器
第一步是获取用于生成 DSA 签名算法密钥的密钥对生成器对象。
与所有引擎类一样,获取特定类型算法的KeyPairGenerator
对象的方法是在KeyPairGenerator
类上调用getInstance
静态工厂方法。该方法有两种形式,都有一个String algorithm
作为第一个参数;其中一种形式还有一个String provider
作为第二个参数。
调用者可以选择指定提供程序的名称,这将确保所请求的算法实现来自指定的提供程序。本课程的示例代码始终指定内置于 JDK 中的默认 SUN 提供程序。
在上述声明之后放置
else try {
在上一步创建的文件中的行,Prepare Initial Program Structure:
KeyPairGenerator keyGen = KeyPairGenerator.getInstance("DSA", "SUN");
初始化密钥对生成器
下一步是初始化密钥对生成器。所有密钥对生成器都共享密钥大小和随机源的概念。KeyPairGenerator
类有一个initialize
方法,接受这两种类型的参数。
DSA 密钥生成器的密钥大小是密钥长度(以位为单位),您将设置为 1024。
随机源必须是SecureRandom
类的实例,提供一个密码学强随机数生成器(RNG)。有关SecureRandom
的更多信息,请参阅SecureRandom API Specification和Java Cryptography Architecture Reference Guide。
以下示例请求一个使用内置 SUN 提供程序提供的 SHA1PRNG 算法的SecureRandom
实例。然后将此SecureRandom
实例传递给密钥对生成器初始化方法。
SecureRandom random = SecureRandom.getInstance("SHA1PRNG", "SUN");
keyGen.initialize(1024, random);
有些情况需要强随机值,比如创建高价值和长期存在的秘密,如 RSA 公钥和私钥。为了帮助应用程序选择合适的强SecureRandom
实现,从 JDK 8 开始,Java 发行版在java.security.Security
类的securerandom.strongAlgorithms
属性中包含了已知的强SecureRandom
实现列表。当您创建这样的数据时,应考虑使用SecureRandom.getInstanceStrong()
,因为它获取已知强算法的实例。
生成密钥对
最后一步是生成密钥对,并将密钥存储在PrivateKey
和PublicKey
对象中。
KeyPair pair = keyGen.generateKeyPair();
PrivateKey priv = pair.getPrivate();
PublicKey pub = pair.getPublic();
对数据进行签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/step3.html
现在您已经创建了公钥和私钥,可以准备对数据进行签名。在此示例中,您将对文件中包含的数据进行签名。GenSig
从命令行获取文件名。使用 Signature
类的实例创建数字签名。
对数据进行签名,生成该数据的数字签名,需要执行以下步骤。
获取签名对象:以下获取一个 Signature
对象,用于使用 DSA 算法生成或验证签名,该算法与程序在上一步中生成密钥的算法相同,生成公钥和私钥。
Signature dsa = Signature.getInstance("SHA1withDSA", "SUN");
注意:在指定签名算法名称时,还应包括签名算法使用的消息摘要算法的名称。SHA1withDSA 是指定 DSA 签名算法的一种方式,使用 SHA-1 消息摘要算法。
初始化签名对象
在 Signature
对象用于签名或验证之前,必须进行初始化。签名的初始化方法需要一个私钥。使用前一步中放入名为 priv
的 PrivateKey
对象中的私钥。
dsa.initSign(priv);
向签名对象提供要签名的数据 该程序将使用作为第一个(也是唯一的)命令行参数指定的文件名中的数据。程序将逐个缓冲区读取数据,并通过调用 update
方法将其提供给 Signature
对象。
FileInputStream fis = new FileInputStream(args[0]);
BufferedInputStream bufin = new BufferedInputStream(fis);
byte[] buffer = new byte[1024];
int len;
while ((len = bufin.read(buffer)) >= 0) {
dsa.update(buffer, 0, len);
};
bufin.close();
生成签名
一旦所有数据都已提供给 Signature
对象,就可以生成该数据的数字签名。
byte[] realSig = dsa.sign();
将签名和公钥保存在文件中
原文:
docs.oracle.com/javase/tutorial/security/apisign/step4.html
现在您已经为某些数据生成了签名,您需要将签名字节保存在一个文件中,将公钥字节保存在另一个文件中,这样您就可以通过调制解调器、软盘、邮件等方式将其发送给其他人。
-
生成签名的数据,
-
签名,
-
公钥
接收方可以通过运行您将在接下来的验证数字签名步骤中生成的VerSig
程序来验证数据是否来自您,并且在传输过程中没有被修改。该程序使用公钥来验证接收到的签名是否是接收到的数据的真实签名。
回想一下,签名是放在一个名为realSig
的字节数组中的。您可以通过以下方式将签名字节保存在名为sig
的文件中。
/* save the signature in a file */
FileOutputStream sigfos = new FileOutputStream("sig");
sigfos.write(realSig);
sigfos.close();
从生成公钥和私钥步骤中回想一下,公钥是放在一个名为pub
的 PublicKey 对象中的。您可以通过调用getEncoded
方法获取编码后的密钥字节,然后将编码后的字节存储在一个文件中。您可以随意命名文件。例如,如果您的名字是 Susan,您可以将其命名为suepk
(代表"Sue 的公钥"),如下所示:
/* save the public key in a file */
byte[] key = pub.getEncoded();
FileOutputStream keyfos = new FileOutputStream("suepk");
keyfos.write(key);
keyfos.close();
编译并运行程序
原文:
docs.oracle.com/javase/tutorial/security/apisign/step5.html
这里
是GenSig.java
程序的完整源代码,添加了一些注释。编译并运行它。请记住,您需要指定要签名的文件名,如
java GenSig data
您可以下载并使用名为data
的示例文件或您喜欢的任何其他文件。该文件不会被修改。它将被读取,以便为其生成签名。
执行程序后,您应该看到保存的suepk
(公钥)和sig
(签名)文件。
验证数字签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/versig.html
如果您有生成数字签名的数据,您可以验证签名的真实性。为此,您需要
-
数据
-
签名
-
用于签署数据的私钥对应的公钥
在这个例子中,您编写一个VerSig
程序来验证由GenSig
程序生成的签名。这演示了验证据称签名真实性所需的步骤。
VerSig
导入一个公钥和一个据称是指定数据文件签名的签名,然后验证签名的真实性。公钥、签名和数据文件名在命令行中指定。
创建VerSig
示例程序以导入文件并验证签名的步骤如下。
-
准备初始程序结构
创建一个名为
VerSig.java
的文本文件。输入初始程序结构(导入语句、类名、main
方法等)。 -
输入并转换编码的公钥字节
从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为
PublicKey
。 -
输入签名字节
从指定为第二个命令行参数的文件中输入签名字节。
-
验证签名
获取一个
Signature
对象并用于验证签名的公钥进行初始化。提供要验证签名的数据(来自指定为第三个命令行参数的文件),并验证签名。 -
编译和运行程序
准备初始程序结构
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep1.html
这是在本课程后续部分创建的VerSig
程序的基本结构。将此程序结构放在名为VerSig.java
的文件中。
import java.io.*;
import java.security.*;
import java.security.spec.*;
class VerSig {
public static void main(String[] args) {
/* Verify a DSA signature */
if (args.length != 3) {
System.out.println("Usage: VerSig " +
"publickeyfile signaturefile " + "datafile");
}
else try {
// the rest of the code goes here
} catch (Exception e) {
System.err.println("Caught exception " + e.toString());
}
}
}
注意:
-
用于验证数据的方法位于
java.security
包中,因此程序从该包中导入所有内容。程序还从java.io
包中导入所需的用于输入要签名的文件数据的方法,以及从java.security.spec
包中导入包含X509EncodedKeySpec
类的内容。 -
期望有三个参数,分别指定公钥、签名和数据文件。
-
在本课程后续步骤中编写的代码将放在
try
和catch
块之间。
输入并转换编码的公钥字节
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep2.html
接下来,VerSig
需要从指定为第一个命令行参数的文件中导入编码的公钥字节,并将其转换为PublicKey
。需要一个PublicKey
,因为Signature
的initVerify
方法需要它来初始化用于验证的Signature
对象。
首先,读取编码的公钥字节。
FileInputStream keyfis = new FileInputStream(args[0]);
byte[] encKey = new byte[keyfis.available()];
keyfis.read(encKey);
keyfis.close();
现在字节数组encKey
包含了编码的公钥字节。
你可以使用KeyFactory
类来实例化一个 DSA 公钥,从其编码中。KeyFactory
类提供了不透明密钥(类型为Key
)和密钥规范之间的转换,密钥规范是底层密钥材料的透明表示。通过不透明密钥,你可以获取算法名称、格式名称和编码的密钥字节,但不能获取密钥材料,例如,可能包括密钥本身和用于计算密钥的算法参数。 (请注意,PublicKey
,因为它扩展了Key
,本身也是一个Key
。)
所以,首先你需要一个密钥规范。假设密钥是根据 X.509 标准编码的,你可以通过以下方式获取一个,例如,如果密钥是使用 SUN 提供的内置 DSA 密钥对生成器生成的:
X509EncodedKeySpec pubKeySpec = new X509EncodedKeySpec(encKey);
现在你需要一个KeyFactory
对象来进行转换。该对象必须是一个可以处理 DSA 密钥的对象。
KeyFactory keyFactory = KeyFactory.getInstance("DSA", "SUN");
最后,你可以使用KeyFactory
对象从密钥规范生成一个PublicKey
。
PublicKey pubKey =
keyFactory.generatePublic(pubKeySpec);
输入签名字节
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep3.html
下一步,输入作为第二个命令行参数指定的文件中的签名字节。
FileInputStream sigfis = new FileInputStream(args[1]);
byte[] sigToVerify = new byte[sigfis.available()];
sigfis.read(sigToVerify);
sigfis.close();
现在字节数组sigToVerify
包含了签名字节。
验证签名
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep4.html
您已经向VerSig
程序添加了代码
-
输入编码的密钥字节并将其转换为名为
pubKey
的PublicKey
-
将签名字节输入到名为
sigToVerify
的字节数组中
您现在可以继续进行验证。
初始化用于验证的 Signature 对象
与生成签名一样,签名是通过使用Signature
类的实例来验证的。您需要创建一个使用与生成签名相同的签名算法的Signature
对象。GenSig
程序使用的算法是来自 SUN 提供程序的 SHA1withDSA 算法。
Signature sig = Signature.getInstance("SHA1withDSA", "SUN");
接下来,您需要初始化Signature
对象。验证的初始化方法需要公钥。
sig.initVerify(pubKey);
向签名对象提供要验证的数据 现在,您需要向Signature
对象提供生成签名的数据。这些数据位于以第三个命令行参数指定的文件中。与签名时一样,逐个缓冲区读取数据,并通过调用update
方法将其提供给Signature
对象。
FileInputStream datafis = new FileInputStream(args[2]);
BufferedInputStream bufin = new BufferedInputStream(datafis);
byte[] buffer = new byte[1024];
int len;
while (bufin.available() != 0) {
len = bufin.read(buffer);
sig.update(buffer, 0, len);
};
bufin.close();
验证签名
一旦您向Signature
对象提供了所有数据,您可以验证该数据的数字签名并报告结果。请记住,所谓的签名已读入名为sigToVerify
的字节数组。
boolean verifies = sig.verify(sigToVerify);
System.out.println("signature verifies: " + verifies);
如果所谓的签名(sigToVerify
)是由与公钥pubKey
对应的私钥生成的指定数据文件的实际签名,则verifies
值将为true
。
编译并运行程序
原文:
docs.oracle.com/javase/tutorial/security/apisign/vstep5.html
这里
是VerSig.java
程序的完整源代码,附加了一些注释。
编译并运行程序。请记住,您需要在命令行上指定三个参数:
-
包含编码的公钥字节的文件的名称
-
包含签名字节的文件的名称
-
数据文件的名称(生成签名的文件)
由于您将测试GenSig
程序的输出,您应该使用的文件名是
-
suepk
-
sig
-
data
这是一个示例运行;粗体表示您需要键入的内容。
%java VerSig suepk sig data
signature verifies: true
弱点和替代方案
原文:
docs.oracle.com/javase/tutorial/security/apisign/enhancements.html
本课程中的GenSig
和VerSig
程序演示了使用 JDK 安全 API 生成数据的数字签名以及验证签名的用法。然而,这些程序描绘的实际场景,即发送方使用 JDK 安全 API 生成新的公钥/私钥对,发送方将编码的公钥字节存储在文件中,接收方读取密钥字节,这并不一定是现实的,并且存在一个潜在的重大缺陷。
在许多情况下,密钥不需要生成;它们已经存在,要么作为文件中的编码密钥,要么作为密钥库中的条目。
潜在的重大缺陷在于没有任何保证接收方收到的公钥的真实性,而VerSig
程序只有在提供的公钥本身是真实的情况下才能正确验证签名的真实性!
使用编码的密钥字节
有时,编码的密钥字节已经存在于用于签名和验证的密钥对的文件中。如果是这种情况,GenSig
程序可以导入编码的私钥字节,并将其转换为签名所需的PrivateKey
,通过以下方式,假设包含私钥字节的文件名在privkeyfile
字符串中,并且字节代表已使用 PKCS #8 标准编码的 DSA 密钥。
FileInputStream keyfis = new FileInputStream(privkeyfile);
byte[] encKey = new byte[keyfis.available()];
keyfis.read(encKey);
keyfis.close();
PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(encKey);
KeyFactory keyFactory = KeyFactory.getInstance("DSA");
PrivateKey privKey = keyFactory.generatePrivate(privKeySpec);
GenSig
不再需要将公钥字节保存在文件中,因为它们已经在一个文件中。
在这种情况下,发送方发送接收方
-
包含编码的公钥字节的已存在文件(除非接收方已经拥有此文件)和
-
由
GenSig
导出的数据文件和签名文件。
VerSig
程序保持不变,因为它已经期望文件中存在编码的公钥字节。
但是,如果有恶意用户拦截文件并以一种无法检测到其更换的方式替换它们,会出现什么潜在问题呢?在某些情况下,这不是问题,因为人们已经通过面对面或通过信任的第三方进行了公钥交换。之后,可以远程进行多次文件和签名交换(即在不同位置的两个人之间),并且可以使用公钥来验证其真实性。如果有恶意用户尝试更改数据或签名,这将被VerSig
检测到。
如果无法进行面对面的密钥交换,您可以尝试其他方法来增加正确接收的可能性。例如,您可以在随后的数据和签名文件交换之前,通过尽可能安全的方法发送您的公钥,也许使用不太安全的媒介。
一般来说,将数据和签名与你的公钥分开发送大大降低了攻击的可能性。除非所有三个文件都被更改,并且以下一段讨论的特定方式,否则VerSig
将检测到任何篡改。
如果所有三个文件(数据文档、公钥和签名)被恶意用户拦截,那个人可以用私钥替换文档并签名,然后将替换后的文档、新签名和用于生成新签名的私钥对应的公钥转发给你。然后VerSig
会报告验证成功,你会认为文档来自原始发送者。因此,你应该采取措施确保至少公钥完整接收(VerSig
检测到其他文件的任何篡改),或者可以使用证书来促进公钥的认证,如下一节所述。
使用证书
在密码学中,更常见的是交换包含公钥的证书,而不是公钥本身。
一个好处是,证书由一个实体(颁发者)签名,以验证所包含的公钥是另一个实体(主体或所有者)的实际公钥。通常,一个受信任的第三方认证机构(CA)验证主体的身份,然后通过签署证书来担保其为公钥所有者。
使用证书的另一个好处是,你可以通过使用颁发者(签名者)的公钥验证其数字签名来检查你收到的证书的有效性,该公钥本身可能存储在一个证书中,其签名可以通过使用该证书颁发者的公钥验证;该公钥本身可能存储在一个证书中,依此类推,直到达到你已经信任的公钥。
如果你无法建立信任链(也许因为所需的颁发者证书对你不可用),可以计算证书的指纹。每个指纹是一个相对较短的数字,可以唯一可靠地识别证书。(从技术上讲,它是证书信息的哈希值,使用消息摘要,也称为单向哈希函数。)你可以联系证书所有者,比较你收到的证书的指纹与发送的指纹。如果它们相同,证书也相同。
对于GenSig
来说,更安全的做法是创建包含公钥的证书,然后让VerSig
导入证书并提取公钥。然而,JDK 没有公共证书 API,允许你从公钥创建证书,因此GenSig
程序无法从生成的公钥创建证书。(尽管有从证书中提取公钥的公共 API。)
如果您愿意,您可以使用各种安全工具,而不是 API,对您的重要文档进行签名,并与密钥库中的证书一起使用,就像在文件交换课程中所做的那样。
或者,您可以使用 API 修改您的程序以使用来自密钥库的已存在私钥和相应的公钥(在证书中)。首先,修改GenSig
程序以从密钥库中提取私钥而不是生成新密钥。首先,让我们假设以下内容:
-
密钥库名称在
String``ksName
中 -
密钥库类型为"JKS",这是来自 Oracle 的专有类型。
-
密钥库密码在字符数组
spass
中 -
包含私钥和公钥证书的密钥库条目的别名在
String``alias
中 -
私钥密码在字符数组
kpass
中
然后,您可以通过以下方式从密钥库中提取私钥。
KeyStore ks = KeyStore.getInstance("JKS");
FileInputStream ksfis = new FileInputStream(ksName);
BufferedInputStream ksbufin = new BufferedInputStream(ksfis);
ks.load(ksbufin, spass);
PrivateKey priv = (PrivateKey) ks.getKey(alias, kpass);
您可以从密钥库中提取公钥证书,并将其编码字节保存到名为suecert
的文件中,通过以下方式。
java.security.cert.Certificate cert = ks.getCertificate(alias);
byte[] encodedCert = cert.getEncoded();
// Save the certificate in a file named "suecert"
FileOutputStream certfos = new FileOutputStream("suecert");
certfos.write(encodedCert);
certfos.close();
然后,您将数据文件、签名和证书发送给接收者。接收者通过首先使用keytool -printcert
命令获取证书的指纹来验证证书的真实性。
keytool -printcert -file suecert
Owner: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US
Issuer: CN=Susan Jones, OU=Purchasing, O=ABC, L=Cupertino, ST=CA, C=US
Serial number: 35aaed17
Valid from: Mon Jul 13 22:31:03 PDT 1998 until:
Sun Oct 11 22:31:03 PDT 1998
Certificate fingerprints:
MD5: 1E:B8:04:59:86:7A:78:6B:40:AC:64:89:2C:0F:DD:13
SHA1: 1C:79:BD:26:A1:34:C0:0A:30:63:11:6A:F2:B9:67:DF:E5:8D:7B:5E
然后接收者验证指纹,可能通过给发送者打电话并将其与发送者的证书进行比较,或者通过在公共存储库中查找它们来进行验证。
接收者的验证程序(修改后的VerSig
)然后可以通过以下方式导入证书并从中提取公钥,假设证书文件名(例如,suecert
)在String``certName
中。
FileInputStream certfis = new FileInputStream(certName);
java.security.cert.CertificateFactory cf =
java.security.cert.CertificateFactory.getInstance("X.509");
java.security.cert.Certificate cert = cf.generateCertificate(certfis);
PublicKey pub = cert.getPublicKey();
确保数据机密性
假设您希望保持数据的内容机密性,以便在传输过程中(或在您自己的计算机或磁盘上)无意或恶意尝试查看数据的人无法这样做。为了保持数据的机密性,您应该对其进行加密,仅存储和发送加密结果(称为ciphertext)。接收者可以解密密文以获得原始数据的副本。
课程:实现您自己的权限
原文:
docs.oracle.com/javase/tutorial/security/userperm/index.html
本课程演示了如何编写一个定义自己特殊权限的类。本课程的基本组件包括:
-
一个名为ExampleGame的示例游戏。
-
一个名为HighScore的类,被
ExampleGame
用来存储用户最新的高分。 -
一个名为HighScorePermission的类,用于保护对用户存储的高分值的访问。
-
用户的安全策略文件,授予
ExampleGame
更新他/她的高分的权限。
基本场景如下:
-
用户玩
ExampleGame
。 -
如果用户达到新的高分,
ExampleGame
使用HighScore
类来保存这个新值。 -
HighScore
类查看用户的安全策略,以检查ExampleGame
是否有权限更新用户的高分值。 -
如果
ExampleGame
有权限更新高分,则 HighScore 类更新该值。
我们描述每个基本组件的关键点,然后展示一个示例运行:
-
ExampleGame
-
高分类
-
高分权限类
-
一个示例策略文件
-
将所有内容整合在一起
ExampleGame
原文:
docs.oracle.com/javase/tutorial/security/userperm/game.html
下面是ExampleGame
的源代码。为简单起见,ExampleGame
实际上并不包含玩游戏的代码。它只是检索或更新用户的最高分。
要查看用户当前的最高分值,您可以运行:
java ExampleGame get
要为用户设置新的最高分值,您可以运行:
java ExampleGame set *score*
要检索用户当前的最高分,ExampleGame
只需实例化一个HighScore
对象并调用其getHighScore
方法。要为用户设置新的最高分,ExampleGame
实例化一个HighScore
对象并调用setHighScore
,将用户的新最高分传递给它。
这里是ExampleGame
的源代码,ExampleGame.java
:
package com.gamedev.games;
import java.io.*;
import java.security.*;
import java.util.Hashtable;
import com.scoredev.scores.*;
public class ExampleGame
{
public static void main(String args[])
throws Exception
{
HighScore hs = new HighScore("ExampleGame");
if (args.length == 0)
usage();
if (args[0].equals("set")) {
hs.setHighScore(Integer.parseInt(args[1]));
} else if (args[0].equals("get")) {
System.out.println("score = "+ hs.getHighScore());
} else {
usage();
}
}
public static void usage()
{
System.out.println("ExampleGame get");
System.out.println("ExampleGame set <score>");
System.exit(1);
}
}
高分类
原文:
docs.oracle.com/javase/tutorial/security/userperm/highscore.html
HighScore
类存储并保护用户在ExampleGame
(以及调用它的任何其他游戏)中的高分值的访问。为简单起见,该类将高分值保存到名为.highscore
的文件中,该文件位于用户的主目录中。但是,在允许ExampleGame
检索或更新用户的高分值之前,该类会检查用户是否已在其安全策略文件中授予ExampleGame
访问高分的权限。
检查ExampleGame
是否具有HighScorePermission
要检查ExampleGame
是否具有访问用户高分值的权限,HighScore
类必须:
-
调用
System.getSecurityManager()
以获取当前安装的安全管理器。 -
如果结果不为空(也就是说,存在一个安全管理器,而不是调用者是一个无限制的应用程序),那么
-
构造一个
HighScorePermission
对象,并 -
调用安全管理器的
checkPermission
方法,并传递新构造的HighScorePermission
对象。
-
这是代码:
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(
new HighScorePermission(gameName));
}
checkPermission
方法本质上是询问安全管理器是否ExampleGame
具有指定的HighScorePermission
。换句话说,它询问安全管理器是否ExampleGame
有权限更新指定游戏(ExampleGame
)的用户高分值。底层安全框架将查阅用户的安全策略,以查看ExampleGame
是否确实具有此权限。
高分代码
这里
是HighScore
类的完整源代码。
注意:doPrivileged
方法调用用于使HighScore
能够临时访问对其可用但对调用它的代码(ExampleGame
)不可用的资源。例如,预期策略文件将授予HighScore
访问用户主目录中的.highscore
文件的权限,但不会授予这些权限给游戏,如ExampleGame
。
HighScorePermission 类
原文:
docs.oracle.com/javase/tutorial/security/userperm/perm.html
HighScorePermission
类定义了 ExampleGame
需要更新用户高分的权限。
所有权限类都应该从 java.security.Permission
或 java.security.BasicPermission
中的一个子类化。两者之间的基本区别在于,java.security.Permission
定义了需要名称和操作的更复杂的权限。例如,java.io.FilePermission
扩展自 java.security.Permission
,并需要一个名称(文件名)以及该文件允许的操作(读/写/删除)。
相比之下,java.security.BasicPermission
定义了只需要名称的更简单的权限。例如,java.lang.RuntimePermission
扩展自 java.security.BasicPermission
,只需要一个名称(如 "exitVM"),允许程序退出 Java 虚拟机。
我们的 HighScorePermission
是一个简单的权限,因此可以从 java.security.BasicPermission
扩展。
通常,BasicPermission
类中的方法实现本身不需要被其子类重写。这就是我们的 HighScorePermission
的情况,所以我们只需要实现构造函数,它们只是调用超类的构造函数,如以下
所示:
package com.scoredev.scores;
import java.security.*;
public final class HighScorePermission extends BasicPermission {
public HighScorePermission(String name)
{
super(name);
}
// note that actions is ignored and not used,
// but this constructor is still needed
public HighScorePermission(String name, String actions)
{
super(name, actions);
}
}
一个示例策略文件
原文:
docs.oracle.com/javase/tutorial/security/userperm/policy.html
以下是一个完整的策略文件,供用户运行ExampleGame
使用。
这里不描述策略文件的语法;如果您感兴趣,请参阅默认策略实现和策略文件语法页面。
你不需要了解语法;你可以随时使用策略工具创建策略文件,如创建策略文件,控制应用程序的快速导览,以及签署代码并授予权限课程中所示。
以下是示例策略文件,后面是各个条目的描述。假设
-
策略文件位于 Kim 的计算机上,Kim 的密钥库命名为
kim.keystore
。 -
ExampleGame
已由游戏创建者 Terry 的私钥签名,相应的公钥在别名为"terry"
的密钥库条目中。 -
HighScore
和HighScorePermissions
类是由实现它们的人(Chris)的私钥签名的,相应的公钥在别名为"chris"
的密钥库条目中。
这是策略文件:kim.policy
keystore "kim.keystore";
// Here is the permission ExampleGame needs.
// It grants code signed by "terry" the
// HighScorePermission, if the
// HighScorePermission was signed by "chris"
grant SignedBy "terry" {
permission
com.scoredev.scores.HighScorePermission
"ExampleGame", signedBy "chris";
};
// Here is the set of permissions the HighScore
// class needs:
grant SignedBy "chris" {
// The HighScore class needs permission to read
// "user.home" to find the location of the
// highscore file
permission java.util.PropertyPermission
"user.home", "read";
// It needs permission to read and write the
// high score file itself
permission java.io.FilePermission
"${user.home}${/}.highscore", "read,write";
// It needs to get granted its own permission,
// so it can call checkPermission
// to see if its caller has permission.
// Only grant it the permission
// if the permission itself was signed by
// "chris"
permission
com.scoredev.scores.HighScorePermission
"*", signedBy "chris";
};
密钥库条目
密钥库是密钥和证书的存储库,用于查找策略文件中指定的签名者的公钥(在本例中为"terry"
和"chris"
)。
keytool
实用程序用于创建和管理密钥库。
对于本课程,假设 Kim 想玩ExampleGame
。如果 Kim 的密钥库命名为kim.keystore
,那么 Kim 的策略文件需要在开头加上以下行:
keystore "kim.keystore";
ExampleGame 条目
策略文件条目指定了特定代码源的一个或多个权限 - 来自特定位置(URL)的代码,或者由特定实体签名的代码,或两者兼有。
我们的策略文件需要为每个游戏添加一个条目,为该游戏的创建者签名的代码授予一个名为HighScorePermission
的权限,其名称为游戏名称。该权限允许游戏调用HighScore
方法来获取或更新该特定游戏用户的最高分值。
为ExampleGame
所需的条目是:
grant SignedBy "terry" {
permission
com.scoredev.scores.HighScorePermission
"ExampleGame", signedBy "chris";
};
要求ExampleGame
由"terry"
签名使 Kim 知道该游戏是 Terry 开发的实际游戏。为了使其工作,Kim 必须已经将 Terry 的公钥证书存储到kim.keystore
中,别名为"terry"
。
注意,HighScorePermission
需要由实际实现该权限的"chris"
签名,以确保ExampleGame
被授予由"chris"
实现的实际权限,而不是其他人。与之前一样,为了使其工作,Kim 必须已经将 Chris 的公钥证书存储到kim.keystore
中,别名为"chris"
。
最高分条目
策略文件中的最后一个条目授予HighScore
类权限。更具体地说,它授予由"chris"
签名的代码权限,他创建并签署了这个类。要求类由"chris"
签名确保当ExampleGame
调用这个类来更新用户的高分时,ExampleGame
确切知道它正在使用由"chris"
实现的原始类。
要更新调用它的任何游戏的用户高分值,HighScore
类需要三个权限:
1. 读取"user.home"
属性值的权限。
HighScore
类将用户的高分值存储在用户主目录中的.highscore
文件中。因此,这个类需要一个java.util.PropertyPermission
,允许它读取"user.home"
属性值,以确定用户主目录的确切位置:
permission java.util.PropertyPermission
"user.home", "read";
2. 读写高分文件本身的权限。
这个权限是为了让HighScore
的getHighScore
和 setHighScore
方法可以访问用户的.highscore
文件,分别获取或设置当前游戏的当前高分。
这是所需的权限:
permission java.io.FilePermission
"${user.home}${/}.highscore", "read,write";
注意:${propName}
表示属性的值。因此,${user.home}
将被"user.home"
属性的值替换。${/}
表示文件分隔符的平台无关方式。
3. 所有 HighScorePermissions(即任何名称的 HighScorePermissions)。
这个权限是为了确保HighScore
检查调用游戏是否被授予了一个名为游戏名称的HighScorePermission
。也就是说,HighScore
类必须同样被授予权限,因为权限检查要求堆栈上的所有代码都具有指定的权限。
这是所需的权限:
permission com.scoredev.scores.HighScorePermission
"*", signedBy "chris";
与以前一样,HighScorePermission
本身需要由实际实现权限的"chris"
签名。
将所有内容整合在一起
原文:
docs.oracle.com/javase/tutorial/security/userperm/together.html
在这里,我们模拟依次成为HighScore
开发者(克里斯),ExampleGame
开发者(特里),以及运行游戏的用户(金)。
您可以执行所有指定的步骤,然后(作为金的最后一步)运行ExampleGame
。
这些步骤没有解释。关于代码签名者(如克里斯和特里)和接收此类代码的人(如金)需要采取的进一步信息,请参阅签署代码并授予权限课程。
这里是步骤:
-
HighScore 开发者(克里斯)的步骤
-
ExampleGame 开发者(特里)的步骤
-
运行 ExampleGame 的用户(金)的步骤
高分开发者(克里斯)的步骤
原文:
docs.oracle.com/javase/tutorial/security/userperm/chris.html
克里斯在创建HighScore
和HighScorePermission
类之后将采取的步骤是:
编译这些类
javac HighScore*.java -d .
将类文件放入一个 JAR 文件中
jar cvf hs.jar com/scoredev/scores/HighScore*.class
创建用于签名的密钥库和密钥
keytool -genkey -keystore chris.keystore -alias signJars
指定密码和显著名称信息
签署 JAR 文件
jarsigner -keystore chris.keystore hs.jar signJars
导出公钥证书
keytool -export -keystore chris.keystore
-alias signJars -file Chris.cer
提供游戏开发人员和用户所需的文件和信息
也就是说,提供它们
-
签名的 JAR 文件
hs.jar
, -
公钥证书文件
Chris.cer
, -
HighScore
和HighScorePermission
类在策略文件中必须被授予的权限信息,以便能够正常工作。对于这一点,克里斯可以提供所需的确切授权条目。
示例游戏开发者(Terry)的步骤
原文:
docs.oracle.com/javase/tutorial/security/userperm/terry.html
Terry 创建一个调用 HighScore
的 getHighScore
和 setHighScore
方法来获取和设置用户高分的游戏(ExampleGame
)后,Terry 需要采取的步骤是:
编译游戏类
javac ExampleGame.java -classpath hs.jar -d .
将其类文件放入一个 JAR 文件中
jar cvf terry.jar com/gamedev/games/ExampleGame.class
创建用于签名的密钥库和密钥
keytool -genkey -keystore terry.keystore -alias signTJars
为密码和区分名称信息指定任何你想要的内容。
签署 JAR 文件
jarsigner -keystore terry.keystore terry.jar signTJars
导出公钥证书
keytool -export -keystore terry.keystore
-alias signTJars -file Terry.cer
为用户提供所需的文件和信息
也就是说,向他们提供
-
签名的 JAR 文件
terry.jar,
-
公钥证书文件
Terry.cer
, 和 -
ExampleGame
类所需权限的信息。对于这一点,Terry 可能会提供所需的确切授权条目。
游戏用户还需要来自 Chris 的文件和信息。为了方便他们,Terry 可能会将这些信息转发给他们:
-
签名的 JAR 文件
hs.jar
, -
公钥证书文件
Chris.cer
, 和 -
有关
HighScore
和HighScorePermission
类在策略文件中必须被授予的权限的信息,以便其正常工作。这可能是所需的确切授权条目。
运行 ExampleGame(Kim)的用户步骤
原文:
docs.oracle.com/javase/tutorial/security/userperm/kim.html
用户(比如 Kim)需要执行的步骤包括:
将证书导入为受信任的证书
keytool -import -alias chris -file Chris.cer -keystore kim.keystore
keytool -import -alias terry -file Terry.cer -keystore kim.keystore
设置具有所需权限的策略文件
这里是完整的kim.policy
策略文件,如 A Sample Policy File 中所述。
运行 ExampleGame
设置高分:
java -Djava.security.manager
-Djava.security.policy=kim.policy
-classpath hs.jar;terry.jar
com.gamedev.games.ExampleGame set 456
获取高分:
java -Djava.security.manager
-Djava.security.policy=kim.policy
-classpath hs.jar;terry.jar
com.gamedev.games.ExampleGame get
注意:
-
如果不指定
-Djava.security.manager
,应用程序将无限制地运行(策略文件和权限不会被检查)。 -
-Djava.security.policy=kim.policy
指定了策略文件的位置。注意:还有其他指定策略文件的方法。例如,你可以在安全属性文件中添加一个条目,指定包含kim.policy
,如在查看策略文件效果课程末尾讨论的那样。 -
-classpath hs.jar;terry.jar
指定了包含所需类文件的 JAR 文件。对于 Windows,使用分号(";")分隔 JAR 文件;对于 UNIX,使用冒号(":")。 -
策略文件
kim.policy
指定了密钥库kim.keystore
。由于未提供密钥库的绝对 URL 位置,因此假定密钥库与策略文件位于同一目录中。
教程:扩展机制
原文:
docs.oracle.com/javase/tutorial/ext/index.html
扩展机制提供了一种标准、可扩展的方式,使自定义 API 对在 Java 平台上运行的所有应用程序可用。Java 扩展也被称为可选包。本教程可能会交替使用这两个术语。
扩展是通过扩展机制增强 Java 平台的一组包和类。扩展机制使运行时环境能够找到并加载扩展类,而无需在类路径上命名扩展类。在这方面,扩展类类似于 Java 平台的核心类。这也是扩展得名的原因--它们实际上扩展了平台的核心 API。
由于此机制扩展了平台的核心 API,应谨慎使用。最常见的用途是用于由 Java 社区流程定义的标准化接口,尽管也可能适用于站点范围接口。
如图所示,扩展充当 Java 平台的“附加”模块。它们的类和公共 API 自动对在平台上运行的任何应用程序可用。
扩展机制还提供了一种从远程位置下载扩展类供 applets 使用的方法。
扩展被打包为 Java 存档(JAR)文件,本教程假定您熟悉 JAR 文件格式。如果您对 JAR 文件不熟悉,您可能需要在继续本教程的课程之前查阅一些 JAR 文件文档:
-
本教程中的在 JAR 文件中打包程序课程。
-
JDK™文档中的JAR 指南。
本教程有两个课程:
创建和使用扩展
这一部分向您展示了如何向您的 Java 平台添加扩展,并且 applets 如何通过下载远程扩展类从扩展机制中受益。
使扩展安全
本节描述了在您的平台上授予扩展的安全特权和权限。如果您正在编写自己的扩展类,您将了解如何使用 Java 平台的安全架构。
附加文档
您可以在 JDK 文档的Java 扩展机制部分找到有关扩展的更多信息。
教程:创建和使用扩展
原文:
docs.oracle.com/javase/tutorial/ext/basics/index.html
任何一组包或类都可以轻松地扮演扩展的角色。将一组类转变为扩展的第一步是将它们打包在一个 JAR 文件中。完成这一步后,您可以通过两种方式将软件转变为扩展:
-
通过将 JAR 文件放置在 Java 运行时环境目录结构的特定位置,这种情况下称为已安装扩展。
-
通过以特定方式从另一个 JAR 文件的清单中引用 JAR 文件,这种情况下称为下载扩展。
本课将通过使用一个简单的“玩具”扩展作为示例来展示扩展机制的工作原理。
已安装扩展
在本节中,您将创建一个简单的已安装扩展,并看到扩展软件如何被运行时环境视为平台的一部分。
下载扩展
本节将向您展示如何修改 JAR 文件的清单,以便 JAR 打包的软件可以利用下载扩展。
理解扩展类加载
本节是一个简短的插曲,总结了 Java 平台的类加载委托模型,并展示了它与扩展中类加载的关系。
创建可扩展应用程序
本节讨论了用于扩展应用程序的机制,通过插件或模块,而无需修改其原始代码库。
下一课,使扩展安全 使用相同的扩展来展示 Java 平台如何控制授予扩展的安全权限。
已安装的扩展
原文:
docs.oracle.com/javase/tutorial/ext/basics/install.html
已安装的扩展是 JRE™软件的lib/ext
目录中的 JAR 文件。顾名思义,JRE 是 Java 开发工具包的运行时部分,包含平台的核心 API,但不包括编译器和调试器等开发工具。JRE 可以单独使用,也可以作为 Java 开发工具包的一部分使用。
JRE 是 JDK 软件的严格子集。JDK 软件目录树的子集如下所示:
JRE 由图中突出显示的目录组成。无论您的 JRE 是独立的还是作为 JDK 软件的一部分,JRE 目录中的lib/ext
中的任何 JAR 文件都会被运行时环境自动视为扩展。
由于安装的扩展会扩展平台的核心 API,请谨慎使用。它们很少适用于仅由单个或少量应用程序使用的接口。
此外,由于安装的扩展定义的符号将在所有 Java 进程中可见,因此应注意确保所有可见符号遵循适当的“反向域名”和“类层次结构”约定。例如,com.mycompany.MyClass
。
从 Java 6 开始,扩展 JAR 文件也可以放置在与任何特定 JRE 无关的位置,以便扩展可以被安装在系统上安装的所有 JRE 共享。在 Java 6 之前,java.ext.dirs
的值指的是单个目录,但是从 Java 6 开始,它是一个目录列表(类似于CLASSPATH
),指定扩展被搜索的位置。路径的第一个元素始终是 JRE 的lib/ext
目录。第二个元素是 JRE 之外的目录。这个其他位置允许扩展 JAR 文件只安装一次,并被安装在该系统上安装的几个 JRE 使用。位置因操作系统而异:
-
Solaris™操作系统:
/usr/jdk/packages/lib/ext
-
Linux:
/usr/java/packages/lib/ext
-
Microsoft Windows:
%SystemRoot%\Sun\Java\lib\ext
请注意,放置在上述任一目录中的安装扩展会扩展该系统上每个JRE(Java 6 或更高版本)的平台。
一个简单的例子
让我们创建一个简单的已安装扩展。我们的扩展由一个类RectangleArea
组成,用于计算矩形的面积:
public final class RectangleArea {
public static int area(java.awt.Rectangle r) {
return r.width * r.height;
}
}
此类有一个名为area
的方法,该方法接受一个java.awt.Rectangle
的实例并返回矩形的面积。
假设你想要使用名为AreaApp
的应用程序测试RectangleArea
:
import java.awt.*;
public class AreaApp {
public static void main(String[] args) {
int width = 10;
int height = 5;
Rectangle r = new Rectangle(width, height);
System.out.println("The rectangle's area is "
+ RectangleArea.area(r));
}
}
此应用程序实例化一个 10 x
5 的矩形,然后使用RectangleArea.area
方法打印出矩形的面积。
在没有扩展机制的情况下运行 AreaApp
让我们首先回顾一下如何在不使用扩展机制的情况下运行AreaApp
应用程序。我们假设RectangleArea
类被捆绑在名为area.jar
的 JAR 文件中。
当然,RectangleArea
类不是 Java 平台的一部分,因此您需要将area.jar
文件放在类路径上才能运行AreaApp
而不会出现运行时异常。例如,如果area.jar
在目录/home/user
中,您可以使用以下命令:
java -classpath .:/home/user/area.jar AreaApp
此命令中指定的类路径包含当前目录,其中包含AreaApp.class
,以及包含RectangleArea
包的 JAR 文件的路径。通过运行此命令,您将获得所需的输出:
The rectangle's area is 50
使用扩展机制运行 AreaApp
现在让我们看看如何通过将RectangleArea
类作为扩展来运行AreaApp
。
要将RectangleArea
类变成一个扩展,您需要将文件area.jar
放在 JRE 的lib/ext
目录中。这样做会自动将RectangleArea
赋予已安装扩展的状态。
将area.jar
安装为扩展后,您可以运行AreaApp
而无需指定类路径:
java AreaApp
因为您正在使用area.jar
作为已安装的扩展,运行时环境将能够找到并加载RectangleArea
类,即使您没有在类路径上指定它。同样,任何用户在您的系统上运行的小程序或应用程序都可以找到并使用RectangleArea
类。
如果系统上安装了多个 JRE(Java 6 或更高版本),并且希望RectangleArea
类作为所有 JRE 的扩展可用,而不是将其安装在特定 JRE 的lib/ext
目录中,请将其安装在系统范围的位置。例如,在运行 Linux 的系统上,将area.jar
安装在目录/usr/java/packages/lib/ext
中。然后AreaApp
可以在安装在该系统上的不同 JRE 上运行,例如,如果不同的浏览器配置为使用不同的 JRE。
下载扩展
原文:
docs.oracle.com/javase/tutorial/ext/basics/download.html
下载扩展是 JAR 文件中的一组类(和相关资源)。JAR 文件的清单可以包含引用一个或多个下载扩展的头部。这些扩展可以通过以下两种方式引用:
-
通过
Class-Path
头部 -
通过
Extension-List
头部
请注意,清单中最多只允许一个。通过Class-Path
头部指示的下载扩展仅在下载它们的应用程序(如 Web 浏览器)的生命周期内下载。它们的优点是客户端上没有安装任何内容;缺点是每次需要时都会下载它们。通过Extension-List
头部下载的下载扩展将安装到下载它们的 JRE 的/lib/ext
目录中。它们的优点是第一次需要时下载,随后可以在不下载的情况下使用。但是,正如本教程后面所示,它们部署起来更加复杂。
由于使用Class-Path
头部的下载扩展更简单,让我们先考虑它们。例如假设a.jar
和b.jar
是同一目录中的两个 JAR 文件,并且a.jar
的清单包含了这个头部:
Class-Path: b.jar
那么b.jar
中的类将作为a.jar
中的类的扩展类。a.jar
中的类可以调用b.jar
中的类,而无需将b.jar
中的类命名在类路径中。a.jar
本身可能是扩展,也可能不是。如果b.jar
不在与a.jar
相同的目录中,那么Class-Path
头部的值应设置为b.jar
的相对路径名。
扮演下载扩展角色的类没有任何特殊之处。它们之所以被视为扩展,仅仅是因为它们被某个其他 JAR 文件的清单引用。
为了更好地理解下载扩展的工作原理,让我们创建一个并投入使用。
一个示例
假设你想要创建一个小程序,其中使用了前一节中的RectangleArea
类:
public final class RectangleArea {
public static int area(java.awt.Rectangle r) {
return r.width * r.height;
}
}
在前一节中,你将RectangleArea
类放入 JRE 的lib/ext
目录中,将其转换为已安装扩展。通过将其转换为已安装扩展,任何应用程序都可以使用RectangleArea
类,就好像它是 Java 平台的一部分。
如果你想要在小程序中使用RectangleArea
类,情况会有些不同。例如,假设你有一个名为AreaApplet
的小程序,其中使用了RectangleArea
类:
import java.applet.Applet;
import java.awt.*;
public class AreaApplet extends Applet {
Rectangle r;
public void init() {
int width = 10;
int height = 5;
r = new Rectangle(width, height);
}
public void paint(Graphics g) {
g.drawString("The rectangle's area is "
+ RectangleArea.area(r), 10, 10);
}
}
此小程序实例化一个 10 x
5 的矩形,然后使用RectangleArea.area
方法显示矩形的面积。
然而,你不能假设每个下载并使用你的小程序的人都会在他们的系统上有RectangleArea
类可用,作为已安装的扩展或其他方式。解决这个问题的一种方法是从服务器端提供RectangleArea
类,并且你可以通过将其作为下载扩展来实现。
要了解如何做到这一点,让我们假设你已经将AreaApplet
捆绑在名为AreaApplet.jar
的 JAR 文件中,并且类RectangleArea
捆绑在RectangleArea.jar
中。为了使RectangleArea.jar
被视为下载扩展,RectangleArea.jar
必须在AreaApplet.jar
的清单中的Class-Path
头中列出。例如,AreaApplet.jar
的清单可能如下所示:
Manifest-Version: 1.0
Class-Path: RectangleArea.jar
这个清单中Class-Path
头的值是RectangleArea.jar
,没有指定路径,表示RectangleArea.jar
位于与小程序的 JAR 文件相同的目录中。
关于Class-Path
头的更多信息
如果一个小程序或应用程序使用多个扩展,你可以在清单中列出多个 URL。例如,以下是一个有效的头部:
Class-Path: area.jar servlet.jar images/
在Class-Path
头中,列出的任何不以'/
'结尾的 URL 都被假定为 JAR 文件。以'/
'结尾的 URL 表示目录。在上面的例子中,images/
可能是一个包含小程序或应用程序所需资源的目录。
请注意,清单文件中只允许一个Class-Path
头,并且清单中的每一行不能超过 72 个字符。如果需要指定的类路径条目超过一行的空间,可以将它们延伸到后续的续行上。每个续行都以两个空格开头。例如:
Class-Path: area.jar servlet.jar monitor.jar datasource.jar
provider.jar gui.jar
未来的版本可能会取消每个标题只能有一个实例的限制,以及将行限制为仅有 72 个字符。
下载扩展可以“串联”,意味着一个下载扩展的清单可以有一个引用第二个扩展的Class-Path
头,第二个扩展可以引用第三个扩展,依此类推。
安装下载扩展
在上面的例子中,小程序下载的扩展仅在加载小程序的浏览器仍在运行时可用。然而,如果在小程序和扩展的清单中包含了额外的信息,小程序可以触发扩展的安装。
由于这种机制扩展了平台的核心 API,其使用应谨慎。它很少适用于仅由单个或少量应用程序使用的接口。所有可见的符号应遵循反向域名和类层次结构约定。
基本要求是小程序和它使用的扩展在它们的清单中提供版本信息,并且它们被签名。版本信息允许 Java 插件确保扩展代码具有小程序期望的版本。例如,AreaApplet
可以在其清单中指定一个areatest
扩展:
Manifest-Version: 1.0
Extension-List: areatest
areatest-Extension-Name: area
areatest-Specification-Version: 1.1
areatest-Implementation-Version: 1.1.2
areatest-Implementation-Vendor-Id: com.example
areatest-Implementation-URL: http://www.example.com/test/area.jar
area.jar
中的清单将提供相应的信息:
Manifest-Version: 1.0
Extension-Name: area
Specification-Vendor: Example Tech, Inc
Specification-Version: 1.1
Implementation-Vendor-Id: com.example
Implementation-Vendor: Example Tech, Inc
Implementation-Version: 1.1.2
应用程序和扩展程序都必须由相同的签名者签名。对 jar 文件进行签名会直接修改它们,在清单文件中提供更多信息。签名有助于确保只有可信任的代码被安装。签署 jar 文件的简单方法是首先创建一个密钥库,然后使用该密钥库保存用于应用程序和扩展程序的证书。例如:
keytool -genkey -dname "cn=Fred" -alias test -validity 180
您将被要求输入密钥库和密钥密码。生成密钥后,jar 文件可以被签名:
jarsigner AreaApplet.jar test
jarsigner area.jar test
您将被要求输入密钥库和密钥密码。有关keytool
、jarsigner
和其他安全工具的更多信息,请参阅Java 2 平台安全工具概述。
这里是AreaDemo.html
,它加载应用程序并导致扩展程序代码被下载并安装:
<html>
<body>
<applet code="AreaApplet.class" archive="AreaApplet.jar"/>
</body>
</html>
当页面首次加载时,用户会被告知该应用程序需要安装扩展程序。随后的对话框会通知用户有关已签名的应用程序。接受两者会将扩展程序安装在 JRE 的lib/ext
文件夹中并运行应用程序。
重新启动 Web 浏览器并加载相同的网页后,只会显示有关应用程序签名者的对话框,因为area.jar
已经安装。如果在不同的 Web 浏览器中打开AreaDemo.html
(假设两个浏览器都使用相同的 JRE),情况也是如此。
理解扩展类加载
译文:
docs.oracle.com/javase/tutorial/ext/basics/load.html
扩展框架利用了类加载委托机制。当运行时环境需要为应用程序加载新类时,它按照以下顺序在以下位置查找类:
-
引导类:
rt.jar
中的运行时类,i18n.jar
中的国际化类等。 -
已安装扩展:JRE 的
lib/ext
目录中的 JAR 文件中的类,以及系统范围内的特定于平台的扩展目录(例如在 Solaris™操作系统上的/usr/jdk/packages/lib/ext
,但请注意,此目录仅适用于 Java™ 6 及更高版本)。 -
类路径:包括系统属性
java.class.path
指定路径上的类,包括 JAR 文件中的类。如果类路径上的 JAR 文件具有带有Class-Path
属性的清单,则还将搜索Class-Path
属性指定的 JAR 文件。默认情况下,java.class.path
属性的值为.
,即当前目录。您可以通过使用-classpath
或-cp
命令行选项或设置CLASSPATH
环境变量来更改该值。命令行选项会覆盖CLASSPATH
环境变量的设置。
优先级列表告诉您,例如,只有在要加载的类在rt.jar
、i18n.jar
或已安装扩展中的类中未找到时,才会搜索类路径。
除非您的软件为特殊目的实例化自己的类加载器,否则您实际上不需要了解比记住这个优先级列表更多的内容。特别是,您应该注意可能存在的任何类名冲突。例如,如果您在类路径上列出一个类,如果运行时环境代替加载了安装的扩展中找到的同名另一个类,您将得到意外的结果。
Java 类加载机制
Java 平台使用委托模型来加载类。基本思想是每个类加载器都有一个“父”类加载器。在加载类时,类加载器首先将类的搜索委托给其父类加载器,然后再尝试找到类本身。
以下是类加载 API 的一些亮点:
-
java.lang.ClassLoader
及其子类中的构造函数允许您在实例化新类加载器时指定一个父类加载器。如果您没有明确指定父类加载器,则虚拟机的系统类加载器将被分配为默认父类加载器。 -
当调用
ClassLoader
中的loadClass
方法加载类时,它按顺序执行以下任务:-
如果类已经被加载,它会返回该类。
-
否则,它将搜索新类的任务委托给父类加载器。
-
如果父类加载器未找到类,
loadClass
调用findClass
方法来查找和加载类。
-
-
如果父类加载器未找到类,则
ClassLoader
的findClass
方法将在当前类加载器中搜索该类。当您在应用程序中实例化类加载器子类时,可能需要重写此方法。 -
类
java.net.URLClassLoader
用作扩展和其他 JAR 文件的基本类加载器,覆盖了java.lang.ClassLoader
的findClass
方法,以在一个或多个指定的 URL 中搜索类和资源。
要查看一个使用与 JAR 文件相关的 API 的示例应用程序,请参阅本教程中的使用与 JAR 相关的 API 课程。
类加载和java
命令
Java 平台的类加载机制体现在java
命令中。
-
在
java
工具中,-classpath
选项是设置java.class.path
属性的简便方式。 -
-cp
和-classpath
选项是等效的。 -
-jar
选项用于运行打包在 JAR 文件中的应用程序。有关此选项的描述和示例,请参阅本教程中的运行 JAR 打包软件课程。
创建可扩展应用程序
原文:
docs.oracle.com/javase/tutorial/ext/basics/spi.html
下面涵盖了以下主题:
-
介绍
-
字典服务示例
-
运行 DictionaryServiceDemo 示例
-
编译和运行 DictionaryServiceDemo 示例
-
理解 DictionaryServiceDemo 示例
-
定义服务提供者接口
-
定义检索服务提供者实现的服务
- 单例设计模式
-
实现服务提供者
-
注册服务提供者
-
创建使用服务和服务提供者的客户端
-
将服务提供者、服务和服务客户端打包在 JAR 文件中
-
将服务提供者打包在 JAR 文件中
-
将字典 SPI 和字典服务打包在 JAR 文件中
-
将客户端打包在 JAR 文件中
-
-
运行客户端
-
-
ServiceLoader 类
-
ServiceLoader API 的限制
-
摘要
介绍
可扩展的应用程序是一种可以在不修改其原始代码基础的情况下扩展的应用程序。您可以通过添加新的插件或模块来增强其功能。开发人员、软件供应商和客户可以通过将新的 Java 存档(JAR)文件添加到应用程序类路径或应用程序特定的扩展目录中来添加新功能或应用程序编程接口(API)。
本节描述了如何创建具有可扩展服务的应用程序,这使您或其他人可以提供不需要修改原始应用程序的服务实现。通过设计一个可扩展的应用程序,您提供了一种升级或增强产品特定部分而无需更改核心应用程序的方法。
可扩展应用程序的一个示例是允许最终用户添加新字典或拼写检查器的文字处理器。在这个示例中,文字处理器提供了一个字典或拼写功能,其他开发人员甚至客户可以通过提供自己的功能实现来扩展该功能。
以下是理解可扩展应用程序重要的术语和定义:
服务
一组编程接口和类,提供对某些特定应用功能或特性的访问。服务可以定义功能的接口和检索实现的方法。在文字处理器示例中,字典服务可以定义检索字典和单词定义的方法,但不实现底层功能集。相反,它依赖于服务提供者来实现该功能。
服务提供者接口(SPI)
服务定义的一组公共接口和抽象类。SPI 定义了应用程序可用的类和方法。
服务提供者
实现 SPI。具有可扩展服务的应用程序使您、供应商和客户能够添加服务提供者,而无需修改原始应用程序。
字典服务示例
考虑如何在文字处理器或编辑器中设计一个字典服务。一种方法是定义一个由类DictionaryService
和服务提供者接口Dictionary
表示的服务。DictionaryService
提供一个单例DictionaryService
对象。(有关更多信息,请参见单例设计模式部分。)此对象从Dictionary
提供者那里检索单词的定义。字典服务客户端——您的应用代码——检索此服务的一个实例,服务将搜索、实例化和使用Dictionary
服务提供者。
尽管文字处理器开发人员很可能会在原始产品中提供一个基本的通用字典,但客户可能需要一个包含法律或技术术语的专业字典。理想情况下,客户能够创建或购买新的字典并将其添加到现有应用程序中。
DictionaryServiceDemo
示例向您展示如何实现Dictionary
服务,创建添加额外字典的Dictionary
服务提供者,并创建一个简单的Dictionary
服务客户端来测试该服务。此示例打包在 zip 文件DictionaryServiceDemo.zip
中,包括以下文件:
-
build.xml
-
DictionaryDemo
-
build.xml
-
build
-
dist
DictionaryDemo.jar
-
src
-
dictionary
DictionaryDemo.java
-
-
-
DictionaryServiceProvider
-
build.xml
-
build
-
dist
DictionaryServiceProvider.jar
-
src
-
dictionary
-
DictionaryService.java
-
spi
Dictionary.java
-
-
-
-
ExtendedDictionary
-
build.xml
-
build
-
dist
ExtendedDictionary.jar
-
src
-
dictionary
ExtendedDictionary.java
-
META-INF
-
services
dictionary.spi.Dictionary
-
-
-
-
GeneralDictionary
-
build.xml
-
build
-
dist
GeneralDictionary.jar
-
src
-
dictionary
GeneralDictionary.java
-
META-INF
-
services
dictionary.spi.Dictionary
-
-
-
注意:build
目录包含与src
目录中的 Java 源文件相同级别的编译后的类文件。
运行DictionaryServiceDemo
示例
因为 zip 文件DictionaryServiceDemo.zip
包含编译后的类文件,您可以将此文件解压缩到计算机上,并按照以下步骤运行示例而无需编译:
-
下载并解压缩示例代码:将文件
DictionaryServiceDemo.zip
下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo
中。 -
将当前目录更改为
C:\DictionaryServiceDemo\DictionaryDemo
,并按照步骤运行客户端进行操作。
编译和运行DictionaryServiceDemo
示例
DictionaryServiceDemo
示例包含 Apache Ant 构建文件,全部命名为build.xml
。以下步骤展示了如何使用 Apache Ant 编译、构建和运行DictionaryServiceDemo
示例:
-
安装 Apache Ant:前往以下链接下载并安装 Apache Ant:
[
ant.apache.org/](http://ant.apache.org/)
确保包含 Apache Ant 可执行文件的目录在您的
PATH
环境变量中,以便您可以从任何目录运行它。此外,请确保您的 JDK 的bin
目录,其中包含java
和javac
可执行文件(对于 Microsoft Windows 为java.exe
和javac.exe
),在您的PATH
环境变量中。有关设置PATH
环境变量的信息,请参阅 PATH and CLASSPATH。 -
下载并解压缩示例代码:将文件
DictionaryServiceDemo.zip
下载并解压缩到计算机上。这些步骤假定您已将此文件的内容解压缩到目录C:\DictionaryServiceDemo
中。 -
编译代码:将当前目录更改为
C:\DictionaryServiceDemo
,并运行以下命令:ant compile-all
此命令编译了
DictionaryDemo
、DictionaryServiceProvider
、ExtendedDictionary
和GeneralDictionary
目录中包含的src
目录中的源代码,并将生成的class
文件放入相应的build
目录中。 -
将编译后的 Java 文件打包成 JAR 文件:确保当前目录为
C:\DictionaryServiceDemo
,然后运行以下命令:ant jar
此命令创建以下 JAR 文件:
-
DictionaryDemo/dist/DictionaryDemo.jar
-
DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
-
GeneralDictionary/dist/GeneralDictionary.jar
-
ExtendedDictionary/dist/ExtendedDictionary.jar
-
-
运行示例:确保包含
java
可执行文件的目录在您的PATH
环境变量中。有关更多信息,请参阅 PATH 和 CLASSPATH。将当前目录更改为
C:\DictionaryServiceDemo\DictionaryDemo
,然后运行以下命令:ant run
该示例打印以下内容:
book: a set of written or printed pages, usually bound with a protective cover
编辑者:一个编辑文档的人
xml:一种经常用于 Web 服务等的文档标准
REST:一种用于创建、读取、更新和删除数据的架构风格,试图使用 HTTP 协议的常见词汇;表述性状态转移
理解 DictionaryServiceDemo 示例
以下步骤向您展示如何重新创建文件DictionaryServiceDemo.zip
的内容。这些步骤向您展示示例的工作原理以及如何运行它。
1. 定义服务提供者接口
DictionaryServiceDemo
示例定义了一个 SPI,即Dictionary.java
接口。它只包含一个方法:
package dictionary.spi;
public interface Dictionary {
public String getDefinition(String word);
}
该示例将编译后的类文件存储在DictionaryServiceProvider/build
目录中。
2. 定义检索服务提供者实现的服务
DictionaryService.java
类加载并访问可用的Dictionary
服务提供者,代表字典服务客户端:
package dictionary;
import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
public class DictionaryService {
private static DictionaryService service;
private ServiceLoader<Dictionary> loader;
private DictionaryService() {
loader = ServiceLoader.load(Dictionary.class);
}
public static synchronized DictionaryService getInstance() {
if (service == null) {
service = new DictionaryService();
}
return service;
}
public String getDefinition(String word) {
String definition = null;
try {
Iterator<Dictionary> dictionaries = loader.iterator();
while (definition == null && dictionaries.hasNext()) {
Dictionary d = dictionaries.next();
definition = d.getDefinition(word);
}
} catch (ServiceConfigurationError serviceError) {
definition = null;
serviceError.printStackTrace();
}
return definition;
}
}
该示例将编译后的类文件存储在DictionaryServiceProvider/build
目录中。
DictionaryService
类实现了单例设计模式。这意味着DictionaryService
类只会创建一个实例。有关更多信息,请参阅单例设计模式部分。
DictionaryService
类是字典服务客户端使用任何已安装的Dictionary
服务提供者的入口点。使用ServiceLoader.load
方法来检索私有静态成员DictionaryService.service
,单例服务入口点。然后应用程序可以调用getDefinition
方法,该方法遍历可用的Dictionary
提供者,直到找到目标词。如果没有Dictionary
实例包含指定词的定义,则getDefinition
方法返回 null。
字典服务使用ServiceLoader.load
方法来查找目标类。SPI 由接口dictionary.spi.Dictionary
定义,因此示例使用这个类作为 load 方法的参数。默认的 load 方法使用默认类加载器搜索应用程序类路径。
然而,这个方法的重载版本允许您指定自定义的类加载器。这使您能够进行更复杂的类搜索。一个特别热情的程序员可能会创建一个ClassLoader
实例,可以在运行时添加包含提供者 JAR 的应用程序特定子目录中进行搜索。结果是一个应用程序不需要重新启动就可以访问新的提供者类。
当这个类的加载器存在后,您可以使用它的迭代器方法来访问和使用它找到的每个提供者。getDefinition
方法使用Dictionary
迭代器来遍历提供者,直到找到指定词的定义。迭代器方法缓存Dictionary
实例,因此连续调用需要很少的额外处理时间。如果自上次调用以来已经投入使用新的提供者,则迭代器方法将它们添加到列表中。
DictionaryDemo.java
类使用这个服务。要使用该服务,应用程序获取一个DictionaryService
实例并调用getDefinition
方法。如果有定义可用,应用程序将打印出来。如果没有定义可用,应用程序将打印一条消息,说明没有可用的字典包含这个词。
单例设计模式
设计模式是软件设计中常见问题的一般解决方案。思路是将解决方案转化为代码,并且该代码可以应用在不同的情况下。单例模式描述了一种技术,确保只创建一个类的实例。本质上,该技术采取以下方法:不要让类外部的任何人创建对象的实例。
例如,DictionaryService
类实现了单例模式如下:
-
将
DictionaryService
构造函数声明为private
,这样除了DictionaryService
之外的所有其他类都无法创建它的实例。 -
将
DictionaryService
成员变量service
声明为static
,这确保只存在一个DictionaryService
实例。 -
定义了
getInstance
方法,使其他类可以受控地访问DictionaryService
成员变量service
。
3. 实现服务提供程序
要提供此服务,您必须创建一个Dictionary.java
的实现。为了保持简单,创建一个定义了几个词的通用词典。您可以使用数据库、一组属性文件或任何其他技术来实现词典。展示提供程序模式的最简单方法是在单个文件中包含所有单词和定义。
以下代码展示了Dictionary
SPI 的一个实现,GeneralDictionary.java
类。请注意,它提供了一个无参数构造函数,并实现了 SPI 定义的getDefinition
方法。
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class GeneralDictionary implements Dictionary {
private SortedMap<String, String> map;
public GeneralDictionary() {
map = new TreeMap<String, String>();
map.put(
"book",
"a set of written or printed pages, usually bound with " +
"a protective cover");
map.put(
"editor",
"a person who edits");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
示例将编译后的类文件存储在目录GeneralDictionary/build
中。注意:在类GeneralDictionary
之前,您必须编译类dictionary.DictionaryService
和dictionary.spi.Dictionary
。
此示例的GeneralDictionary
提供程序仅定义了两个词:book和editor。显然,一个更可用的词典将提供一个更实质的通用词汇列表。
为了演示多个提供程序如何实现相同的 SPI,以下代码展示了另一个可能的提供程序。ExtendedDictionary.java
服务提供程序是一个包含大多数软件开发人员熟悉的技术术语的扩展词典。
package dictionary;
import dictionary.spi.Dictionary;
import java.util.SortedMap;
import java.util.TreeMap;
public class ExtendedDictionary implements Dictionary {
private SortedMap<String, String> map;
public ExtendedDictionary() {
map = new TreeMap<String, String>();
map.put(
"xml",
"a document standard often used in web services, among other " +
"things");
map.put(
"REST",
"an architecture style for creating, reading, updating, " +
"and deleting data that attempts to use the common " +
"vocabulary of the HTTP protocol; Representational State " +
"Transfer");
}
@Override
public String getDefinition(String word) {
return map.get(word);
}
}
示例将编译后的类文件存储在目录ExtendedDictionary/build
中。注意:在类ExtendedDictionary
之前,您必须编译类dictionary.DictionaryService
和dictionary.spi.Dictionary
。
很容易想象客户使用完整的Dictionary
提供程序集来满足他们自己的特殊需求。服务加载器 API 使他们能够根据需要或偏好向其应用程序添加新的词典。由于底层的文字处理应用程序是可扩展的,因此客户无需编写额外的代码即可使用新的提供程序。
4. 注册服务提供程序
要注册您的服务提供者,需要创建一个提供者配置文件,该文件存储在服务提供者的 JAR 文件的 META-INF/services
目录中。配置文件的名称是服务提供者的完全限定类名,其中名称的每个组件由句点(.
)分隔,嵌套类由美元符号($
)分隔。
提供者配置文件包含您的服务提供者的完全限定类名,每个名称占一行。该文件必须使用 UTF-8 编码。此外,您可以通过在注释行前面加上井号(#
)来在文件中包含注释。
例如,要注册服务提供者 GeneralDictionary
,创建一个名为 dictionary.spi.Dictionary
的文本文件。该文件包含一行:
dictionary.GeneralDictionary
类似地,要注册服务提供者 ExtendedDictionary
,创建一个名为 dictionary.spi.Dictionary
的文本文件。该文件包含一行:
dictionary.ExtendedDictionary
5. 创建使用服务和服务提供者的客户端
因为开发完整的文字处理器应用程序是一项重大工作,本教程提供了一个更简单的应用程序,该应用程序使用 DictionaryService
和 Dictionary
SPI。DictionaryDemo
示例从类路径上的任何 Dictionary
提供者中搜索单词 book、editor、xml 和 REST,并检索它们的定义。
以下是 DictionaryDemo
示例。它从 DictionaryService
实例请求目标单词的定义,然后将请求传递给已知的 Dictionary
提供者。
package dictionary;
import dictionary.DictionaryService;
public class DictionaryDemo {
public static void main(String[] args) {
DictionaryService dictionary = DictionaryService.getInstance();
System.out.println(DictionaryDemo.lookup(dictionary, "book"));
System.out.println(DictionaryDemo.lookup(dictionary, "editor"));
System.out.println(DictionaryDemo.lookup(dictionary, "xml"));
System.out.println(DictionaryDemo.lookup(dictionary, "REST"));
}
public static String lookup(DictionaryService dictionary, String word) {
String outputString = word + ": ";
String definition = dictionary.getDefinition(word);
if (definition == null) {
return outputString + "Cannot find definition for this word.";
} else {
return outputString + definition;
}
}
}
该示例将编译后的类文件存储在目录 DictionaryDemo/build
中。注意:在运行 DictionaryDemo
类之前,必须先编译类 dictionary.DictionaryService
和 dictionary.spi.Dictionary
。
6. 将服务提供者、服务和服务客户端打包到 JAR 文件中
请参阅课程 在 JAR 文件中打包程序 了解如何创建 JAR 文件的信息。
在 JAR 文件中打包服务提供者
要打包 GeneralDictionary
服务提供者,创建一个名为 GeneralDictionary/dist/GeneralDictionary.jar
的 JAR 文件,其中包含此服务提供者的编译类文件和以下目录结构中的配置文件:
-
META-INF
-
服务
dictionary.spi.Dictionary
-
-
dictionary
GeneralDictionary.class
类似地,要打包 ExtendedDictionary
服务提供者,创建一个名为 ExtendedDictionary/dist/ExtendedDictionary.jar
的 JAR 文件,其中包含此服务提供者的编译类文件和以下目录结构中的配置文件:
-
META-INF
-
服务
dictionary.spi.Dictionary
-
-
dictionary
ExtendedDictionary.class
注意,提供者配置文件必须位于 JAR 文件中的META-INF/services
目录中。
将 Dictionary SPI 和 Dictionary Service 打包成一个 JAR 文件
创建一个名为DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
的 JAR 文件,其中包含以下文件:
-
dictionary
-
DictionaryService.class
-
spi
Dictionary.class
-
将客户端打包成一个 JAR 文件
创建一个名为DictionaryDemo/dist/DictionaryDemo.jar
的 JAR 文件,其中包含以下文件:
-
dictionary
DictionaryDemo.class
7. 运行客户端
以下命令运行带有GeneralDictionary
服务提供者的DictionaryDemo
示例:
Linux 和 Solaris:
java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../GeneralDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo
Windows:
java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\GeneralDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo
使用此命令时,假设以下情况:
-
当前目录是
DictionaryDemo
。 -
存在以下 JAR 文件:
-
DictionaryDemo/dist/DictionaryDemo.jar
: 包含DictionaryDemo
类 -
DictionaryServiceProvider/dist/DictionaryServiceProvider.jar
: 包含Dictionary
SPI 和DictionaryService
类 -
GeneralDictionary/dist/GeneralDictionary.jar
: 包含GeneralDictionary
服务提供者和配置文件
-
该命令打印以下内容:
book: a set of written or printed pages, usually bound with a protective cover
editor: a person who edits
xml: Cannot find definition for this word.
REST: Cannot find definition for this word.
假设你运行以下命令并且ExtendedDictionary/dist/ExtendedDictionary.jar
存在:
Linux 和 Solaris:
java -Djava.ext.dirs=../DictionaryServiceProvider/dist:../ExtendedDictionary/dist -cp dist/DictionaryDemo.jar dictionary.DictionaryDemo
Windows:
java -Djava.ext.dirs=..\DictionaryServiceProvider\dist;..\ExtendedDictionary\dist -cp dist\DictionaryDemo.jar dictionary.DictionaryDemo
该命令打印以下内容:
book: Cannot find definition for this word.
editor: Cannot find definition for this word.
xml: a document standard often used in web services, among other things
REST: an architecture style for creating, reading, updating, and deleting data that attempts to use the common vocabulary of the HTTP protocol; Representational State Transfer
ServiceLoader 类
java.util.ServiceLoader
类帮助你查找、加载和使用服务提供者。它在应用程序的类路径或运行时环境的扩展目录中搜索服务提供者。它加载它们并使你的应用程序能够使用提供者的 API。如果你将新的提供者添加到类路径或运行时扩展目录中,ServiceLoader
类会找到它们。如果你的应用程序知道提供者接口,它可以找到并使用该接口的不同实现。你可以使用接口的第一个可加载实例或遍历所有可用接口。
ServiceLoader
类是 final 的,这意味着你不能将其作为子类或覆盖其加载算法。例如,你不能改变其算法以从不同位置搜索服务。
从ServiceLoader
类的角度来看,所有服务都具有单一类型,通常是单一接口或抽象类。提供者本身包含一个或多个具体类,这些类扩展了服务类型,具有特定于其目的的实现。ServiceLoader
类要求单个公开的提供者类型具有默认构造函数,不需要参数。这使得ServiceLoader
类可以轻松实例化它找到的服务提供者。
提供者是按需定位和实例化的。服务加载器维护了已加载的提供者的缓存。加载器的iterator
方法的每次调用都会返回一个迭代器,首先按实例化顺序产生缓存中的所有元素。然后,服务加载器会定位和实例化任何新的提供者,依次将每个提供者添加到缓存中。您可以使用reload
方法清除提供者缓存。
要为特定类创建加载器,请将类本身提供给load
或loadInstalled
方法。您可以使用默认类加载器或提供自己的ClassLoader
子类。
loadInstalled
方法搜索已安装的运行时提供者的扩展目录。默认的扩展位置是您运行时环境的jre/lib/ext
目录。您应该仅将扩展位置用于知名的、可信任的提供者,因为此位置将成为所有应用程序的类路径的一部分。在本文中,提供者不使用扩展目录,而是依赖于特定于应用程序的类路径。
ServiceLoader
API 的局限性
ServiceLoader
API 很有用,但也有局限性。例如,不可能从ServiceLoader
类派生类,因此无法修改其行为。您可以使用自定义的ClassLoader
子类来更改类的查找方式,但ServiceLoader
本身无法扩展。此外,当前的ServiceLoader
类无法告诉您的应用程序运行时何时有新的提供者可用。此外,您无法向加载器添加更改侦听器,以查找新提供者是否放置在特定于应用程序的扩展目录中。
公共的ServiceLoader
API 在 Java SE 6 中可用。虽然加载器服务早在 JDK 1.3 时就存在,但 API 是私有的,只对内部 Java 运行时代码可用。
摘要
可扩展的应用程序提供了可以由服务提供者扩展的服务点。创建可扩展应用程序的最简单方法是使用ServiceLoader
,该工具适用于 Java SE 6 及更高版本。使用这个类,您可以将提供者实现添加到应用程序类路径中,以提供新功能。ServiceLoader
类是 final 的,因此您无法修改其功能。
课程:使扩展安全
原文:
docs.oracle.com/javase/tutorial/ext/security/index.html
现在您已经了解如何使用扩展,您可能想知道扩展具有哪些安全权限。例如,如果您正在开发一个涉及文件 I/O 的扩展,您需要了解如何为读写文件授予适当的权限。相反,如果您考虑使用他人开发的扩展,您将希望清楚地了解扩展具有哪些安全权限,以及如何在需要时更改这些权限。
本课程向您展示了 Java™ 平台的安全架构如何处理扩展。您将了解到如何查看授予扩展软件的权限,并学会通过一些简单的步骤修改扩展权限。此外,您还将学习如何封装扩展中的包,以限制对代码指定部分的访问。
本课程分为两个部分:
为扩展设置权限
本节包含一些示例,展示了扩展必须满足哪些条件才能被授予权限执行安全敏感操作。
封装扩展中的包
您可以选择在扩展 JAR 文件中封装包作为额外的安全措施。如果一个包被封装,这意味着该包中定义的所有类必须来自一个单独的 JAR 文件。本节将向您展示如何修改扩展清单以封装扩展包。
附加文档
您将在本课程的适当位置找到与安全相关的链接和参考文档。
要获取有关安全的完整信息,您可以参考以下内容:
-
Java SE 中的安全功能教程(在本教程中)
-
安全指南
为扩展设置权限
原文:
docs.oracle.com/javase/tutorial/ext/security/policy.html
如果有安全管理器生效,那么必须满足以下条件,以使任何软件,包括扩展软件,能够执行安全敏感操作:
-
扩展中的安全敏感代码必须包装在
PrivilegedAction
对象中。 -
安全管理器实施的安全策略必须授予扩展适当的权限。默认情况下,安装的扩展被授予所有安全权限,就好像它们是核心平台 API 的一部分。安全策略授予的权限仅适用于包装在
PrivilegedAction
实例中的代码。
让我们更详细地看一下这些条件,附带一些示例。
使用PrivilegedAction
类
假设你想要修改上一课程示例中扩展示例中的RectangleArea
类,将矩形面积写入文件而不是标准输出。然而,向文件写入是一个涉及安全的操作,因此如果你的软件将在安全管理器下运行,你需要将你的代码标记为特权代码。你需要执行两个步骤来实现这一点:
-
你需要将执行安全敏感操作的代码放在类型为
java.security.PrivilegedAction
的对象的run
方法中。 -
你必须将该
PrivilegedAction
对象作为参数传递给java.security.AccessController
的doPrivileged
方法。
如果我们将这些准则应用于RectangleArea
类,我们的类定义将如下所示:
import java.io.*;
import java.security.*;
public final class RectangleArea {
public static void
writeArea(final java.awt.Rectangle r) {
AccessController.
doPrivileged(new PrivilegedAction() {
public Object run() {
try {
int area = r.width * r.height;
String userHome = System.getProperty("user.home");
FileWriter fw = new FileWriter( userHome + File.separator
+ "test" + File.separator + "area.txt");
fw.write("The rectangle's area is " + area);
fw.flush();
fw.close();
} catch(IOException ioe) {
System.err.println(ioe);
}
return null;
}
});
}
}
这个类中的唯一方法writeArea
计算矩形的面积,并将面积写入名为area.txt
的文件中,该文件位于用户主目录下的test
目录中。
处理输出文件的安全敏感语句必须放在新的PrivilegedAction
实例的run
方法中。(注意,run
要求返回一个Object
实例。返回的对象可以是null
。)然后将新的PrivilegedAction
实例作为参数传递给AccessController.doPrivileged
的调用。
有关使用doPrivileged
的更多信息,请参阅JDK™文档中的特权块 API。
以这种方式将安全敏感代码包装在PrivilegedAction
对象中是使扩展能够执行安全敏感操作的第一个要求。第二个要求是:让安全管理器授予特权代码适当的权限。
使用安全策略指定权限
运行时生效的安全策略由策略文件指定。默认策略由 JRE 软件中的lib/security/java.policy
文件设置。
策略文件通过grant条目为软件分配安全权限。策略文件可以包含任意数量的 grant 条目。默认策略文件为安装的扩展程序具有以下 grant 条目:
grant codeBase "file:${{java.ext.dirs}}/*" {
permission java.security.AllPermission;
};
此条目指定由file:${{java.ext.dirs}}/*
指定的目录中的文件将被授予名为java.security.AllPermission
的权限。 (请注意,从 Java 6 开始,java.ext.dirs
指的是类似classpath
的目录路径,每个目录都可以容纳安装的扩展。)不难猜到java.security.AllPermission
授予安装的扩展程序可以授予的所有安全权限。
默认情况下,安装的扩展程序没有安全限制。只要安全敏感代码包含在作为doPrivileged
调用参数传递的PrivilegedAction
实例中,扩展软件就可以执行安全敏感操作,就好像没有安装安全管理器一样。
要限制授予扩展程序的权限,您需要修改策略文件。要拒绝所有扩展程序的所有权限,您可以简单地删除上述 grant 条目。
并非所有权限都像默认授予的java.security.AllPermission
那样全面。删除默认 grant 条目后,您可以为特定权限输入新的 grant 条目,包括:
-
java.awt.**AWTPermission**
-
java.io.**FilePermission**
-
java.net.**NetPermission**
-
java.util.**PropertyPermission**
-
java.lang.reflect.**ReflectPermission**
-
java.lang.**RuntimePermission**
-
java.security.**SecurityPermission**
-
java.io.**SerializablePermission**
-
java.net.**SocketPermission**
JDK 中的权限文档提供了关于每个权限的详细信息。让我们看看使用 RectangleArea 作为扩展所需的权限。
RectangleArea.writeArea
方法需要两个权限:一个用于确定用户主目录的路径,另一个用于写入文件。假设RectangleArea
类打包在文件area.jar
中,您可以通过向策略文件添加以下条目来授予写入权限:
grant codeBase "file:${java.home}/lib/ext/area.jar" {
permission java.io.PropertyPermission "user.home",
"read";
permission java.io.FilePermission
"${user.home}${/}test${/}*", "write";
};
此条目的codeBase "file:${java.home}/lib/ext/area.jar"
部分保证此条目指定的权限仅适用于area.jar
。 java.io.PropertyPermission
允许访问属性。 第一个参数"user.home"
指定属性的名称,第二个参数"read"
表示可以读取该属性。(另一个选择是"write"
。)
java.io.FilePermission
允许访问文件。第一个参数"${user.home}${/}test${/}*"
表示area.jar
被授予访问用户主目录中test
目录中所有文件的权限。(请注意${/}
是一个平台无关的文件分隔符。)第二个参数表示被授予的文件访问权限仅限于写入。(第二个参数的其他选择是"read"
、"delete"
和"execute"
。)
签署扩展程序
你可以使用策略文件对扩展程序授予的权限施加额外限制,要求它们必须由受信任的实体签名。(有关签署和验证 JAR 文件的审查,请参阅本教程中的签署 JAR 文件课程。)
为了允许在授予权限时与扩展程序或其他软件一起进行签名验证,策略文件必须包含一个keystore 条目。密钥库条目指定用于验证的密钥库。密钥库条目的形式为
keystore "*keystore_url*";
URL keystore_url可以是绝对或相对的。如果是相对的,URL 是相对于策略文件的位置。例如,要使用keytool
使用的默认密钥库,将此条目添加到java.policy
中
keystore "file://${user.home}/.keystore";
要指示必须签署扩展程序才能被授予安全权限,您可以使用signedBy
字段。例如,以下条目指示只有当扩展程序area.jar
由密钥库中的别名为 Robert 和 Rita 的用户签名时,才授予列出的权限:
grant signedBy "Robert,Rita",
codeBase "file:${java.home}/lib/ext/area.jar" {
permission java.io.PropertyPermission
"user.home", "read";
permission java.io.FilePermission
"${user.home}${/}test${/}*", "write";
};
如果codeBase
字段被省略,如下所示的"grant",则权限将授予任何由"Robert"或"Rita"签名的软件,包括已安装或下载的扩展程序:
grant signedBy "Robert,Rita" {
permission java.io.FilePermission "*", "write";
};
有关策略文件格式的更多详细信息,请参阅 JDK 文档中的安全体系结构规范第 3.3.1 节。
在扩展中封存包
原文:
docs.oracle.com/javase/tutorial/ext/security/sealing.html
您可以选择性地封存扩展 JAR 文件中的包作为额外的安全措施。如果一个包被封存,那么该包中定义的所有类必须来自一个单独的 JAR 文件。
没有封存,一个“敌对”的程序可以创建一个类并将其定义为您的扩展包的成员。然后,敌对软件将自由访问您的扩展包的包保护成员。
在扩展中封存包与封存任何 JAR 打包的类没有区别。要封存您的扩展包,您必须向包含您的扩展的 JAR 文件的清单中添加Sealed
头部。您可以通过将Sealed
头部与包的Name
头部关联来封存单个包。在存档中未与单个包关联的Sealed
头部表示所有包都被封存。这样的“全局”Sealed
头部将被与单个包关联的任何Sealed
头部覆盖。与Sealed
头部关联的值要么是true
,要么是false
。
例子
让我们看几个示例清单文件。在这些示例中,假设 JAR 文件包含这些包:
com/myCompany/package_1/
com/myCompany/package_2/
com/myCompany/package_3/
com/myCompany/package_4/
假设您想要封存所有包。您可以通过简单地向清单中添加一个存档级别的Sealed
头部来实现:
Manifest-Version: 1.0
Sealed: true
任何具有这个清单的 JAR 文件中的所有包都将被封存。
如果您只想封存com.myCompany.package_3
,您可以通过这个清单来实现:
Manifest-Version: 1.0
Name: com/myCompany/package_3/
Sealed: true
在这个例子中,唯一的Sealed
头部是与包com.myCompany.package_3
的Name
头部相关联的,因此只有该包被封存。(Sealed
头部与Name
头部相关联,因为它们之间没有空行。)
最后一个例子,假设您想要封存所有包除了com.myCompany.package_2
。您可以通过这样的清单来实现:
Manifest-Version: 1.0
Sealed: true
Name: com/myCompany/package_2/
Sealed: false
在这个例子中,存档级别的Sealed: true
头部表示 JAR 文件中的所有包都将被封存。然而,清单中还有一个与包com.myCompany.package_2
相关联的Sealed: false
头部,该头部覆盖了该包的存档级别封存。因此,这个清单将导致所有包都被封存,除了com.myCompany.package_2
。
路径:反射 API
原文:
docs.oracle.com/javase/tutorial/reflect/index.html
反射的用途
反射通常被需要能够检查或修改在 Java 虚拟机中运行的应用程序的运行时行为的程序所使用。这是一个相对高级的特性,应该只由对语言基础知识有很好掌握的开发人员使用。在这种情况下,反射是一种强大的技术,可以使应用程序执行原本不可能的操作。
可扩展性特性
应用程序可以通过使用完全限定名称创建可扩展性对象的实例来利用外部、用户定义的类。
类浏览器和可视化开发环境
类浏览器需要能够枚举类的成员。可视化开发环境可以从反射中可用的类型信息中受益,帮助开发人员编写正确的代码。
调试器和测试工具
调试器需要能够检查类的私有成员。测试工具可以利用反射系统地调用类中定义的可发现的一组 API,以确保测试套件中的代码覆盖率较高。
反射的缺点
反射很强大,但不应该被滥用。如果可以在不使用反射的情况下执行操作,则最好避免使用它。在通过反射访问代码时应牢记以下问题。
性能开销
因为反射涉及动态解析的类型,某些 Java 虚拟机优化无法执行。因此,反射操作比非反射操作性能较慢,并且应该避免在性能敏感应用程序中频繁调用的代码段中使用。
安全限制
反射需要运行时权限,当在安全管理器下运行时可能不存在。这是一个重要的考虑因素,对于必须在受限制的安全上下文中运行的代码,比如在 Applet 中。
内部信息的暴露
由于反射允许代码执行在非反射代码中非法的操作,比如访问private
字段和方法,使用反射可能导致意想不到的副作用,可能使代码失效并破坏可移植性。反射代码打破了抽象,因此可能会随着平台升级而改变行为。
路径课程
本路径涵盖了通过反射访问和操作类、字段、方法和构造函数的常见用法。每个课程包含代码示例、提示和故障排除信息。
本课程展示了获取Class
对象的各种方法,并使用它来检查类的属性,包括其声明和内容。
本课程描述了如何使用 Reflection API 查找类的字段、方法和构造函数。提供了设置和获取字段值、调用方法以及使用特定构造函数创建对象实例的示例。
这节课介绍了两种特殊类型的类:在运行时生成的数组和定义唯一命名对象实例的enum
类型。示例代码展示了如何检索数组的组件类型以及如何使用数组或enum
类型设置和获取字段。
注意:
本教程中的示例旨在用于实验 Reflection API。因此,异常处理与在生产代码中使用的方式不同。特别是,在生产代码中,不建议将对用户可见的堆栈跟踪信息输出。
课程:类
原文:
docs.oracle.com/javase/tutorial/reflect/class/index.html
每种类型都是引用类型或基本类型。类、枚举和数组(它们都继承自java.lang.Object
)以及接口都是引用类型。引用类型的示例包括java.lang.String
、所有基本类型的包装类,如java.lang.Double
、接口java.io.Serializable
和枚举javax.swing.SortOrder
。基本类型有一组固定的类型:boolean
、byte
、short
、int
、long
、char
、float
和double
。
对于每种类型的对象,Java 虚拟机实例化一个不可变的java.lang.Class
实例,该实例提供了用于检查对象的运行时属性的方法,包括其成员和类型信息。Class
还提供了创建新类和对象的能力。最重要的是,它是所有反射 API 的入口点。本课程涵盖了涉及类的最常用的反射操作:
-
检索类对象描述了获取
Class
的方法。 -
检查类修饰符和类型展示了如何访问类声明信息。
-
发现类成员说明了如何列出类中的构造函数、字段、方法和嵌套类。
-
故障排除描述了在使用
Class
时遇到的常见错误。
检索类对象
原文:
docs.oracle.com/javase/tutorial/reflect/class/classNew.html
所有反射操作的入口点是java.lang.Class
。除了java.lang.reflect.ReflectPermission
之外,java.lang.reflect
中的类都没有公共构造函数。要访问这些类,需要在Class
上调用适当的方法。有几种方法可以获取Class
,具体取决于代码是否可以访问对象、类的名称、类型或现有的Class
。
Object.getClass()
如果对象的实例可用,则获取其Class
的最简单方法是调用Object.getClass()
。当然,这仅适用于所有继承自Object
的引用类型。以下是一些示例。
Class c = "foo".getClass();
返回String
的Class
。
Class c = System.console().getClass();
与虚拟机关联的唯一控制台由static
方法System.console()
返回。getClass()
返回的值是对应于java.io.Console
的Class
。
enum E { A, B }
Class c = A.getClass();
A
是枚举E
的一个实例;因此getClass()
返回对应于枚举类型E
的Class
。
byte[] bytes = new byte[1024];
Class c = bytes.getClass();
由于数组是Objects
,因此也可以在数组实例上调用getClass()
。返回的Class
对应于具有组件类型byte
的数组。
import java.util.HashSet;
import java.util.Set;
Set<String> s = new HashSet<String>();
Class c = s.getClass();
在这种情况下,java.util.Set
是一个指向类型为java.util.HashSet
的对象的接口。getClass()
返回的值是与java.util.HashSet
对应的类。
.class 语法
如果类型可用但没有实例,则可以通过在类型名称后附加".class"
来获得一个Class
。这也是获取原始类型对应的Class
的最简单方式。
boolean b;
Class c = b.getClass(); // compile-time error
Class c = boolean.class; // correct
请注意,语句boolean.getClass()
会产生编译时错误,因为boolean
是原始类型,不能被解引用。.class
语法返回与类型boolean
对应的Class
。
Class c = java.io.PrintStream.class;
变量c
将是与类型java.io.PrintStream
对应的Class
。
Class c = int[][][].class;
.class
语法可用于检索与给定类型的多维数组对应的Class
。
Class.forName()
如果类的完全限定名称可用,则可以使用静态方法Class.forName()
获取相应的Class
。这不能用于原始类型。数组类名称的语法由Class.getName()
描述。此语法适用于引用和原始类型。
Class c = Class.forName("com.duke.MyLocaleServiceProvider");
此语句将根据给定的完全限定名称创建一个类。
Class cDoubleArray = Class.forName("[D");
Class cStringArray = Class.forName("[[Ljava.lang.String;");
变量cDoubleArray
将包含与原始类型double
的数组对应的Class
(即与double[].class
相同)。变量cStringArray
将包含与String
的二维数组对应的Class
(即与String[][].class
相同)。
原始类型包装器的 TYPE 字段
.class
语法是获取原始类型的 Class
更方便且更常用的方式;然而还有另一种获取 Class
的方法。每种原始类型和 void
在 java.lang
中都有一个包装类,用于将原始类型装箱为引用类型。每个包装类都包含一个名为 TYPE
的字段,该字段等于被包装的原始类型的 Class
。
Class c = Double.TYPE;
存在一个类 java.lang.Double
用于包装原始类型 double
,每当需要一个 Object
时。Double.TYPE
的值与 double.class
相同。
Class c = Void.TYPE;
Void.TYPE
与 void.class
相同。
返回类的方法
有几个反射 API 返回类,但只有在已经直接或间接地获取了 Class
后才能访问这些类。
Class.getSuperclass()
返回给定类的超类。
Class c = javax.swing.JButton.class.getSuperclass();
javax.swing.JButton
的超类是 javax.swing.AbstractButton
。
Class.getClasses()
返回类的所有公共类、接口和枚举,包括继承的成员。
Class<?>[] c = Character.class.getClasses();
Character
包含两个成员类 Character.Subset
和 Character.UnicodeBlock
。
Class.getDeclaredClasses()
返回在此类中显式声明的所有类、接口和枚举。
Class<?>[] c = Character.class.getDeclaredClasses();
Character
包含两个公共成员类 Character.Subset
和 Character.UnicodeBlock
以及一个私有类 Character.CharacterCache
。
Class.getDeclaringClass()
。
java.lang.reflect.Field.getDeclaringClass()
。
java.lang.reflect.Method.getDeclaringClass()
。
java.lang.reflect.Constructor.getDeclaringClass()
。
返回声明这些成员的Class
。匿名类声明不会有声明类,但会有封闭类。
import java.lang.reflect.Field;
Field f = System.class.getField("out");
Class c = f.getDeclaringClass();
字段out
在System
中声明。
public class MyClass {
static Object o = new Object() {
public void m() {}
};
static Class<c> = o.getClass().getEnclosingClass();
}
由o
定义的匿名类的声明类为null
。
Class.getEnclosingClass()
。
返回类的直接封闭类。
Class c = Thread.State.class().getEnclosingClass();
枚举Thread.State
的封闭类为Thread
。
public class MyClass {
static Object o = new Object() {
public void m() {}
};
static Class<c> = o.getClass().getEnclosingClass();
}
由o
定义的匿名类由MyClass
封闭。
检查类的修饰符和类型
原文:
docs.oracle.com/javase/tutorial/reflect/class/classModifiers.html
一个类可以用一个或多个修饰符声明,这些修饰符会影响其运行时行为:
-
访问修饰符:
public
,protected
和private
-
要求覆盖的修饰符:
abstract
-
限制为一个实例的修饰符:
static
-
禁止值修改的修饰符:
final
-
强制严格浮点行为的修饰符:
strictfp
-
注解
并非所有修饰符都允许用于所有类,例如接口不能是final
,枚举不能是abstract
。java.lang.reflect.Modifier
包含了所有可能的修饰符声明。它还包含可用于解码由Class.getModifiers()
返回的修饰符集合的方法。
ClassDeclarationSpy
示例展示了如何获取类的声明组件,包括修饰符、泛型类型参数、实现的接口和继承路径。由于Class
实现了java.lang.reflect.AnnotatedElement
接口,因此也可以查询运行时注解。
import java.lang.annotation.Annotation;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import static java.lang.System.out;
public class ClassDeclarationSpy {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
out.format("Class:%n %s%n%n", c.getCanonicalName());
out.format("Modifiers:%n %s%n%n",
Modifier.toString(c.getModifiers()));
out.format("Type Parameters:%n");
TypeVariable[] tv = c.getTypeParameters();
if (tv.length != 0) {
out.format(" ");
for (TypeVariable t : tv)
out.format("%s ", t.getName());
out.format("%n%n");
} else {
out.format(" -- No Type Parameters --%n%n");
}
out.format("Implemented Interfaces:%n");
Type[] intfs = c.getGenericInterfaces();
if (intfs.length != 0) {
for (Type intf : intfs)
out.format(" %s%n", intf.toString());
out.format("%n");
} else {
out.format(" -- No Implemented Interfaces --%n%n");
}
out.format("Inheritance Path:%n");
List<Class> l = new ArrayList<Class>();
printAncestor(c, l);
if (l.size() != 0) {
for (Class<?> cl : l)
out.format(" %s%n", cl.getCanonicalName());
out.format("%n");
} else {
out.format(" -- No Super Classes --%n%n");
}
out.format("Annotations:%n");
Annotation[] ann = c.getAnnotations();
if (ann.length != 0) {
for (Annotation a : ann)
out.format(" %s%n", a.toString());
out.format("%n");
} else {
out.format(" -- No Annotations --%n%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void printAncestor(Class<?> c, List<Class> l) {
Class<?> ancestor = c.getSuperclass();
if (ancestor != null) {
l.add(ancestor);
printAncestor(ancestor, l);
}
}
}
以下是一些输出示例。用户输入用斜体表示。
$ *java ClassDeclarationSpy java.util.concurrent.ConcurrentNavigableMap*
Class:
java.util.concurrent.ConcurrentNavigableMap
Modifiers:
public abstract interface
Type Parameters:
K V
Implemented Interfaces:
java.util.concurrent.ConcurrentMap<K, V>
java.util.NavigableMap<K, V>
Inheritance Path:
-- No Super Classes --
Annotations:
-- No Annotations --
这是源代码中java.util.concurrent.ConcurrentNavigableMap
的实际声明:
public interface ConcurrentNavigableMap<K,V>
extends ConcurrentMap<K,V>, NavigableMap<K,V>
请注意,由于这是一个接口,它隐式地是abstract
的。编译器为每个接口添加这个修饰符。此外,此声明包含两个泛型类型参数,K
和V
。示例代码仅打印这些参数的名称,但可以使用java.lang.reflect.TypeVariable
中的方法检索有关它们的其他信息。接口也可以像上面显示的那样实现其他接口。
$ *java ClassDeclarationSpy "[Ljava.lang.String;"*
Class:
java.lang.String[]
Modifiers:
public abstract final
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
interface java.lang.Cloneable
interface java.io.Serializable
Inheritance Path:
java.lang.Object
Annotations:
-- No Annotations --
由于数组是运行时对象,所有类型信息都由 Java 虚拟机定义。特别是,数组实现了Cloneable
和java.io.Serializable
,它们的直接超类始终是Object
。
$ *java ClassDeclarationSpy java.io.InterruptedIOException*
Class:
java.io.InterruptedIOException
Modifiers:
public
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
-- No Implemented Interfaces --
Inheritance Path:
java.io.IOException
java.lang.Exception
java.lang.Throwable
java.lang.Object
Annotations:
-- No Annotations --
从继承路径可以推断出java.io.InterruptedIOException
是一个受检异常,因为RuntimeException
不存在。
$ *java ClassDeclarationSpy java.security.Identity*
Class:
java.security.Identity
Modifiers:
public abstract
Type Parameters:
-- No Type Parameters --
Implemented Interfaces:
interface java.security.Principal
interface java.io.Serializable
Inheritance Path:
java.lang.Object
Annotations:
@java.lang.Deprecated()
此输出显示java.security.Identity
,一个已弃用的 API,具有注解java.lang.Deprecated
。这可能被反射代码用于检测已弃用的 API。
注意: 并非所有注解都可以通过反射获得。只有那些具有java.lang.annotation.RetentionPolicy
为RUNTIME
的注解是可访问的。在语言中预定义的三个注解@Deprecated
,@Override
,和@SuppressWarnings
中,只有@Deprecated
在运行时可用。
发现类成员
原文:
docs.oracle.com/javase/tutorial/reflect/class/classMembers.html
Class
提供了两类方法来访问字段、方法和构造函数:列举这些成员的方法和搜索特定成员的方法。此外,还有用于访问直接在类上声明的成员的方法,以及搜索超接口和超类以查找继承成员的方法。以下表格总结了所有定位成员的方法及其特性。
定位字段的类方法
Class API |
成员列表? | 继承成员? | 私有成员? |
---|---|---|---|
getDeclaredField() |
否 | 否 | 是 |
getField() |
否 | 是 | 否 |
getDeclaredFields() |
是 | 否 | 是 |
getFields() |
是 | 是 | 否 |
定位方法的类方法
Class API |
成员列表? | 继承成员? | 私有成员? |
---|---|---|---|
getDeclaredMethod() |
否 | 否 | 是 |
getMethod() |
否 | 是 | 否 |
getDeclaredMethods() |
是 | 否 | 是 |
getMethods() |
是 | 是 | 否 |
定位构造函数的类方法
Class API |
成员列表? | 继承成员? | 私有成员? |
---|---|---|---|
getDeclaredConstructor() |
否 | N/A¹ | 是 |
getConstructor() |
否 | N/A¹ | 否 |
getDeclaredConstructors() |
是 | N/A¹ | 是 |
getConstructors() |
是 | N/A¹ | 否 |
¹ 构造函数不会被继承。
给定一个类名和感兴趣的成员指示,ClassSpy
示例使用get*s()
方法来确定所有公共元素的列表,包括任何继承的元素。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Member;
import static java.lang.System.out;
enum ClassMember { CONSTRUCTOR, FIELD, METHOD, CLASS, ALL }
public class ClassSpy {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
out.format("Class:%n %s%n%n", c.getCanonicalName());
Package p = c.getPackage();
out.format("Package:%n %s%n%n",
(p != null ? p.getName() : "-- No Package --"));
for (int i = 1; i < args.length; i++) {
switch (ClassMember.valueOf(args[i])) {
case CONSTRUCTOR:
printMembers(c.getConstructors(), "Constructor");
break;
case FIELD:
printMembers(c.getFields(), "Fields");
break;
case METHOD:
printMembers(c.getMethods(), "Methods");
break;
case CLASS:
printClasses(c);
break;
case ALL:
printMembers(c.getConstructors(), "Constuctors");
printMembers(c.getFields(), "Fields");
printMembers(c.getMethods(), "Methods");
printClasses(c);
break;
default:
assert false;
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void printMembers(Member[] mbrs, String s) {
out.format("%s:%n", s);
for (Member mbr : mbrs) {
if (mbr instanceof Field)
out.format(" %s%n", ((Field)mbr).toGenericString());
else if (mbr instanceof Constructor)
out.format(" %s%n", ((Constructor)mbr).toGenericString());
else if (mbr instanceof Method)
out.format(" %s%n", ((Method)mbr).toGenericString());
}
if (mbrs.length == 0)
out.format(" -- No %s --%n", s);
out.format("%n");
}
private static void printClasses(Class<?> c) {
out.format("Classes:%n");
Class<?>[] clss = c.getClasses();
for (Class<?> cls : clss)
out.format(" %s%n", cls.getCanonicalName());
if (clss.length == 0)
out.format(" -- No member interfaces, classes, or enums --%n");
out.format("%n");
}
}
这个例子相对紧凑;然而,printMembers()
方法略显笨拙,因为java.lang.reflect.Member
接口自反射最早的实现以来就存在,当引入泛型时,它无法被修改以包含更有用的getGenericString()
方法。唯一的替代方法是像所示那样进行测试和转换,用printConstructors()
、printFields()
和printMethods()
替换此方法,或者满足于相对简洁的Member.getName()
的结果。
输出示例及其解释如下。用户输入用斜体表示。
$ *java ClassSpy java.lang.ClassCastException CONSTRUCTOR*
Class:
java.lang.ClassCastException
Package:
java.lang
Constructor:
public java.lang.ClassCastException()
public java.lang.ClassCastException(java.lang.String)
由于构造函数不会被继承,因此在直接超类RuntimeException
和其他超类中定义的异常链接机制构造函数(具有Throwable
参数)将不会被找到。
$ *java ClassSpy java.nio.channels.ReadableByteChannel METHOD*
Class:
java.nio.channels.ReadableByteChannel
Package:
java.nio.channels
Methods:
public abstract int java.nio.channels.ReadableByteChannel.read
(java.nio.ByteBuffer) throws java.io.IOException
public abstract void java.nio.channels.Channel.close() throws
java.io.IOException
public abstract boolean java.nio.channels.Channel.isOpen()
接口java.nio.channels.ReadableByteChannel
定义了read()
。其余方法是从超级接口继承的。可以通过将get*s()
替换为getDeclared*s()
来轻松修改此代码,仅列出实际在类中声明的方法。
$ *java ClassSpy ClassMember FIELD METHOD*
Class:
ClassMember
Package:
-- No Package --
Fields:
public static final ClassMember ClassMember.CONSTRUCTOR
public static final ClassMember ClassMember.FIELD
public static final ClassMember ClassMember.METHOD
public static final ClassMember ClassMember.CLASS
public static final ClassMember ClassMember.ALL
Methods:
public static ClassMember ClassMember.valueOf(java.lang.String)
public static ClassMember[] ClassMember.values()
public final int java.lang.Enum.hashCode()
public final int java.lang.Enum.compareTo(E)
public int java.lang.Enum.compareTo(java.lang.Object)
public final java.lang.String java.lang.Enum.name()
public final boolean java.lang.Enum.equals(java.lang.Object)
public java.lang.String java.lang.Enum.toString()
public static <T> T java.lang.Enum.valueOf
(java.lang.Class<T>,java.lang.String)
public final java.lang.Class<E> java.lang.Enum.getDeclaringClass()
public final int java.lang.Enum.ordinal()
public final native java.lang.Class<?> java.lang.Object.getClass()
public final native void java.lang.Object.wait(long) throws
java.lang.InterruptedException
public final void java.lang.Object.wait(long,int) throws
java.lang.InterruptedException
public final void java.lang.Object.wait() hrows java.lang.InterruptedException
public final native void java.lang.Object.notify()
public final native void java.lang.Object.notifyAll()
在这些结果的字段部分,枚举常量被列出。虽然这些在技术上是字段,但将它们与其他字段区分开可能是有用的。这个例子可以修改为使用java.lang.reflect.Field.isEnumConstant()
来实现这一目的。在本教程的后续部分检查枚举中的EnumSpy
示例包含了一个可能的实现。
在输出的方法部分中,观察到方法名称包含声明类的名称。因此,toString()
方法是由Enum
实现的,而不是从Object
继承的。可以通过使用Field.getDeclaringClass()
来修改代码,使这一点更明显。以下片段展示了潜在解决方案的一部分。
if (mbr instanceof Field) {
Field f = (Field)mbr;
out.format(" %s%n", f.toGenericString());
out.format(" -- declared in: %s%n", f.getDeclaringClass());
}
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/class/classTrouble.html
以下示例展示了在反射类时可能遇到的典型错误。
编译器警告:"注意:...使用了未经检查或不安全的操作"
当调用方法时,会检查参数值的类型并可能进行转换。ClassWarning
调用getMethod()
会导致典型的未经检查的转换警告:
import java.lang.reflect.Method;
public class ClassWarning {
void m() {
try {
Class c = ClassWarning.class;
Method m = c.getMethod("m"); // warning
// production code should handle this exception more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
}
}
}
$ *javac ClassWarning.java*
Note: ClassWarning.java uses unchecked or unsafe operations.
Note: Recompile with -Xlint:unchecked for details.
$ *javac -Xlint:unchecked ClassWarning.java*
ClassWarning.java:6: warning: [unchecked] unchecked call to getMethod
(String,Class<?>...) as a member of the raw type Class
Method m = c.getMethod("m"); // warning
^
1 warning
许多库方法已经使用了泛型声明,包括Class
中的几个方法。由于c
声明为原始类型(没有类型参数),并且getMethod()
的相应参数是参数化类型,因此会发生未经检查的转换。编译器需要生成警告。(参见Java 语言规范,Java SE 7 版,章节未经检查的转换和方法调用转换。)
有两种可能的解决方案。更可取的是修改c
的声明以包含适当的通用类型。在这种情况下,声明应为:
Class<?> c = warn.getClass();
或者,可以在有问题的语句之前使用预定义的注释@SuppressWarnings
来明确抑制警告。
Class c = ClassWarning.class;
@SuppressWarnings("unchecked")
Method m = c.getMethod("m");
// warning gone
提示: 作为一个一般原则,不应忽略警告,因为它们可能表明存在错误。应适当使用参数化声明。如果不可能(也许是因为应用程序必须与库供应商的代码交互),则可以使用@SuppressWarnings
对有问题的行进行注释。
当构造函数不可访问时会出现 InstantiationException
Class.newInstance()
如果尝试创建一个类的新实例且零参数构造函数不可见,则会抛出InstantiationException
。ClassTrouble
示例展示了生成的堆栈跟踪。
class Cls {
private Cls() {}
}
public class ClassTrouble {
public static void main(String... args) {
try {
Class<?> c = Class.forName("Cls");
c.newInstance(); // InstantiationException
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
$ *java ClassTrouble*
java.lang.IllegalAccessException: Class ClassTrouble can not access a member of
class Cls with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.Class.newInstance0(Class.java:349)
at java.lang.Class.newInstance(Class.java:308)
at ClassTrouble.main(ClassTrouble.java:9)
Class.newInstance()
的行为非常类似于new
关键字,并且会因为与new
相同的原因而失败。在反射中的典型解决方案是利用java.lang.reflect.AccessibleObject
类,该类提供了抑制访问控制检查的能力;然而,这种方法不起作用,因为java.lang.Class
不扩展AccessibleObject
。唯一的解决方案是修改代码以使用Constructor.newInstance()
,该方法确实扩展了AccessibleObject
。
提示: 一般来说,最好使用Constructor.newInstance()
,原因在创建新类实例部分的成员课程中有描述。
使用Constructor.newInstance()
可能会出现潜在问题的其他示例,可以在构造函数故障排除部分的成员课程中找到。
课程:成员
原文:
docs.oracle.com/javase/tutorial/reflect/member/index.html
反射定义了一个接口java.lang.reflect.Member
,该接口由java.lang.reflect.Field
、java.lang.reflect.Method
和java.lang.reflect.Constructor
实现。这些对象将在本课程中讨论。对于每个成员,本课程将描述相关的 API 以检索声明和类型信息,成员特有的任何操作(例如,设置字段的值或调用方法),以及常见的错误。每个概念将通过代码示例和相关输出进行说明,这些输出近似一些预期的反射用法。
注意: 根据Java 语言规范,Java SE 7 版,类的成员是类主体的继承组件,包括字段、方法、嵌套类、接口和枚举类型。由于构造函数不会被继承,因此它们不是成员。这与java.lang.reflect.Member
的实现类有所不同。
字段
字段具有类型和值。java.lang.reflect.Field
类提供了用于访问类型信息以及在给定对象上设置和获取字段值的方法。
-
获取字段类型 描述了如何获取字段的声明类型和泛型类型
-
检索和解析字段修饰符 展示了如何获取字段声明的部分,如
public
或transient
-
获取和设置字段值 说明了如何访问字段的值
-
故障排除 描述了可能导致混淆的一些常见编码错误
方法
方法具有返回值、参数,并可能抛出异常。java.lang.reflect.Method
类提供了用于获取参数和返回值的类型信息的方法。它还可以用于在给定对象上调用方法。
-
获取方法类型信息 展示了如何枚举类中声明的方法并获取类型信息
-
获取方法参数的名称 展示了如何检索方法或构造函数的参数的名称和其他信息
-
检索和解析方法修饰符描述了如何访问和解码与方法相关的修饰符和其他信息
-
调用方法说明了如何执行一个方法并获得其返回值
-
故障排除涵盖了在查找或调用方法时遇到的常见错误
构造函数
构造函数的反射 API 在java.lang.reflect.Constructor
中定义,与方法的 API 类似,但有两个主要例外:首先,构造函数没有返回值;其次,调用构造函数会为给定类创建一个新的对象实例。
-
查找构造函数说明了如何检索具有特定参数的构造函数
-
检索和解析构造函数修饰符展示了如何获取构造函数声明的修饰符以及有关构造函数的其他信息
-
创建新的类实例展示了如何通过调用其构造函数来实例化一个对象的实例
-
故障排除描述了在查找或调用构造函数时可能遇到的常见错误
字段
原文:
docs.oracle.com/javase/tutorial/reflect/member/field.html
一个字段是一个具有关联值的类、接口或枚举。java.lang.reflect.Field
类中的方法可以检索有关字段的信息,比如它的名称、类型、修饰符和注解。(检查类修饰符和类型章节中的类课程描述了如何检索注解。)还有一些方法可以实现对字段值的动态访问和修改。这些任务在以下章节中介绍:
-
获取字段类型描述了如何获取字段的声明类型和泛型类型
-
检索和解析字段修饰符展示了如何获取字段声明的部分,比如
public
或transient
-
获取和设置字段值说明了如何访问字段值
-
故障排除描述了可能导致混淆的一些常见编码错误
当编写一个应用程序,比如一个类浏览器时,找出哪些字段属于特定类可能会很有用。通过调用Class.getFields()
来识别类的字段。getFields()
方法返回一个包含每个可访问的公共字段的Field
对象数组。
如果一个公共字段是以下任一成员,则可以访问它:
-
这个类
-
这个类的一个超类
-
由这个类实现的接口
-
由这个类实现的接口扩展的一个接口
一个字段可以是一个类(实例)字段,比如java.io.Reader.lock
,一个静态字段,比如java.lang.Integer.MAX_VALUE
,或一个枚举常量,比如java.lang.Thread.State.WAITING
。
获取字段类型
原文:
docs.oracle.com/javase/tutorial/reflect/member/fieldTypes.html
字段可以是原始类型或引用类型。有八种原始类型:boolean
、byte
、short
、int
、long
、char
、float
和 double
。引用类型是任何直接或间接是 java.lang.Object
的子类,包括接口、数组和枚举类型。
FieldSpy
示例根据完全限定的二进制类名和字段名打印字段的类型和泛型类型。
import java.lang.reflect.Field;
import java.util.List;
public class FieldSpy<T> {
public boolean[][] b = {{ false, false }, { true, true } };
public String name = "Alice";
public List<Integer> list;
public T val;
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Field f = c.getField(args[1]);
System.out.format("Type: %s%n", f.getType());
System.out.format("GenericType: %s%n", f.getGenericType());
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
获取此类中三个公共字段(b
、name
和参数化类型 list
)的类型的示例输出如下。用户输入以斜体表示。
$ *java FieldSpy FieldSpy b*
Type: class [[Z
GenericType: class [[Z
$ *java FieldSpy FieldSpy name*
Type: class java.lang.String
GenericType: class java.lang.String
$ *java FieldSpy FieldSpy list*
Type: interface java.util.List
GenericType: java.util.List<java.lang.Integer>
$ *java FieldSpy FieldSpy val*
Type: class java.lang.Object
GenericType: T
字段 b
的类型是布尔值的二维数组。类型名称的语法在 Class.getName()
中有描述。
字段 val
的类型报告为 java.lang.Object
,因为泛型是通过类型擦除实现的,在编译期间删除了关于泛型类型的所有信息。因此,T
被替换为类型变量的上界,在本例中为 java.lang.Object
。
Field.getGenericType()
如果存在,将查阅类文件中的 Signature 属性。如果该属性不可用,则退而求其次使用 Field.getType()
,这个方法在引入泛型之后并没有改变。反射中其他以 getGeneric*Foo*
命名的方法,对于某个 Foo 值的实现方式类似。
检索和解析字段修饰符
原文:
docs.oracle.com/javase/tutorial/reflect/member/fieldModifiers.html
有几个修饰符可能是字段声明的一部分:
-
访问修饰符:
public
、protected
和private
-
控制运行时行为的字段特定修饰符:
transient
和volatile
-
限制为一个实例的修饰符:
static
-
禁止值修改的修饰符:
final
-
注解
方法Field.getModifiers()
可用于返回表示字段的声明修饰符集合的整数。该整数中表示修饰符的位在java.lang.reflect.Modifier
中定义。
FieldModifierSpy
示例演示了如何搜索具有给定修饰符的字段。它还通过调用Field.isSynthetic()
和Field.isEnumCostant()
确定所定位的字段是合成的(编译器生成的)还是枚举常量。
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import static java.lang.System.out;
enum Spy { BLACK , WHITE }
public class FieldModifierSpy {
volatile int share;
int instance;
class Inner {}
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
int searchMods = 0x0;
for (int i = 1; i < args.length; i++) {
searchMods |= modifierFromString(args[i]);
}
Field[] flds = c.getDeclaredFields();
out.format("Fields in Class '%s' containing modifiers: %s%n",
c.getName(),
Modifier.toString(searchMods));
boolean found = false;
for (Field f : flds) {
int foundMods = f.getModifiers();
// Require all of the requested modifiers to be present
if ((foundMods & searchMods) == searchMods) {
out.format("%-8s [ synthetic=%-5b enum_constant=%-5b ]%n",
f.getName(), f.isSynthetic(),
f.isEnumConstant());
found = true;
}
}
if (!found) {
out.format("No matching fields%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static int modifierFromString(String s) {
int m = 0x0;
if ("public".equals(s)) m |= Modifier.PUBLIC;
else if ("protected".equals(s)) m |= Modifier.PROTECTED;
else if ("private".equals(s)) m |= Modifier.PRIVATE;
else if ("static".equals(s)) m |= Modifier.STATIC;
else if ("final".equals(s)) m |= Modifier.FINAL;
else if ("transient".equals(s)) m |= Modifier.TRANSIENT;
else if ("volatile".equals(s)) m |= Modifier.VOLATILE;
return m;
}
}
示例输出如下:
$ *java FieldModifierSpy FieldModifierSpy volatile*
Fields in Class 'FieldModifierSpy' containing modifiers: volatile
share [ synthetic=false enum_constant=false ]
$ *java FieldModifierSpy Spy public*
Fields in Class 'Spy' containing modifiers: public
BLACK [ synthetic=false enum_constant=true ]
WHITE [ synthetic=false enum_constant=true ]
$ *java FieldModifierSpy FieldModifierSpy\$Inner final*
Fields in Class 'FieldModifierSpy$Inner' containing modifiers: final
this$0 [ synthetic=true enum_constant=false ]
$ *java FieldModifierSpy Spy private static final*
Fields in Class 'Spy' containing modifiers: private static final
$VALUES [ synthetic=true enum_constant=false ]
请注意,有些字段即使在原始代码中未声明也会被报告。这是因为编译器会生成一些合成字段,这些字段在运行时是必需的。为了测试一个字段是否是合成的,示例调用Field.isSynthetic()
。合成字段的集合是依赖于编译器的;然而,常用的字段包括用于内部类(即非静态成员类)引用最外层封闭类的this$0
和用于枚举实现隐式定义的静态方法values()
的$VALUES
。合成类成员的名称未指定,可能在所有编译器实现或版本中不同。这些和其他合成字段将包含在Class.getDeclaredFields()
返回的数组中,但不会被Class.getField()
识别,因为合成成员通常不是public
。
因为Field
实现了接口java.lang.reflect.AnnotatedElement
,所以可以使用java.lang.annotation.RetentionPolicy.RUNTIME
来检索任何运行时注解。有关获取注解的示例,请参见检查类修饰符和类型部分。
获取和设置字段值
原文:
docs.oracle.com/javase/tutorial/reflect/member/fieldValues.html
给定一个类的实例,可以使用反射来设置该类中字段的值。通常只在特殊情况下进行此操作,当通常方式无法设置值时。由于这种访问通常违反了类的设计意图,应该谨慎使用。
Book
类演示了如何设置长整型、数组和枚举字段类型的值。获取和设置其他基本类型的方法在Field
中有描述。
import java.lang.reflect.Field;
import java.util.Arrays;
import static java.lang.System.out;
enum Tweedle { DEE, DUM }
public class Book {
public long chapters = 0;
public String[] characters = { "Alice", "White Rabbit" };
public Tweedle twin = Tweedle.DEE;
public static void main(String... args) {
Book book = new Book();
String fmt = "%6S: %-12s = %s%n";
try {
Class<?> c = book.getClass();
Field chap = c.getDeclaredField("chapters");
out.format(fmt, "before", "chapters", book.chapters);
chap.setLong(book, 12);
out.format(fmt, "after", "chapters", chap.getLong(book));
Field chars = c.getDeclaredField("characters");
out.format(fmt, "before", "characters",
Arrays.asList(book.characters));
String[] newChars = { "Queen", "King" };
chars.set(book, newChars);
out.format(fmt, "after", "characters",
Arrays.asList(book.characters));
Field t = c.getDeclaredField("twin");
out.format(fmt, "before", "twin", book.twin);
t.set(book, Tweedle.DUM);
out.format(fmt, "after", "twin", t.get(book));
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
这是相应的输出:
$ *java Book*
BEFORE: chapters = 0
AFTER: chapters = 12
BEFORE: characters = [Alice, White Rabbit]
AFTER: characters = [Queen, King]
BEFORE: twin = DEE
AFTER: twin = DUM
注意: 通过反射设置字段的值会带来一定的性能开销,因为必须执行各种操作,如验证访问权限。从运行时的角度来看,效果是相同的,操作与直接在类代码中更改值一样原子。
使用反射可能导致一些运行时优化丢失。例如,以下代码很可能会被 Java 虚拟机优化:
int x = 1;
x = 2;
x = 3;
使用Field.set*()
的等效代码可能不会。
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/member/fieldTrouble.html
开发人员遇到的一些常见问题及其解释和解决方法如下。
由于不可转换类型而引发的IllegalArgumentException
FieldTrouble
示例将生成一个IllegalArgumentException
。调用Field.setInt()
来设置一个引用类型为Integer
的字段,其值为原始类型。在非反射等效的Integer val = 42
中,编译器会将原始类型42
转换(或装箱)为引用类型new Integer(42)
,以便其类型检查接受该语句。在使用反射时,类型检查仅在运行时发生,因此没有机会对值进行装箱。
import java.lang.reflect.Field;
public class FieldTrouble {
public Integer val;
public static void main(String... args) {
FieldTrouble ft = new FieldTrouble();
try {
Class<?> c = ft.getClass();
Field f = c.getDeclaredField("val");
f.setInt(ft, 42); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java FieldTrouble*
Exception in thread "main" java.lang.IllegalArgumentException: Can not set
java.lang.Object field FieldTrouble.val to (long)42
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:146)
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:174)
at sun.reflect.UnsafeObjectFieldAccessorImpl.setLong
(UnsafeObjectFieldAccessorImpl.java:102)
at java.lang.reflect.Field.setLong(Field.java:831)
at FieldTrouble.main(FieldTrouble.java:11)
要消除此异常,问题行应替换为以下调用Field.set(Object obj, Object value)
:
f.set(ft, new Integer(43));
提示: 当使用反射设置或获取字段时,编译器没有机会执行装箱。它只能转换由Class.isAssignableFrom()
规范描述的相关类型。该示例预计会失败,因为在此测试中isAssignableFrom()
将返回false
,可用于编程验证特定转换是否可能:
Integer.class.isAssignableFrom(int.class) == false
同样,在反射中也不可能自动将原始类型转换为引用类型。
int.class.isAssignableFrom(Integer.class) == false
针对非公共字段的NoSuchFieldException
机智的读者可能会注意到,如果之前展示的FieldSpy
示例用于获取非公共字段的信息,它将失败:
$ *java FieldSpy java.lang.String count*
java.lang.NoSuchFieldException: count
at java.lang.Class.getField(Class.java:1519)
at FieldSpy.main(FieldSpy.java:12)
提示: Class.getField()
和 Class.getFields()
方法返回由Class
对象表示的类、枚举或接口的公共成员字段。要检索在Class
中声明的所有字段(但不是继承的字段),请使用Class.getDeclaredFields()
方法。
修改最终字段时的IllegalAccessException
如果尝试获取或设置private
或其他不可访问字段的值,或者设置final
字段的值(无论其访问修饰符如何),可能会抛出IllegalAccessException
。
FieldTroubleToo
示例展示了尝试设置 final 字段时产生的堆栈跟踪类型。
import java.lang.reflect.Field;
public class FieldTroubleToo {
public final boolean b = true;
public static void main(String... args) {
FieldTroubleToo ft = new FieldTroubleToo();
try {
Class<?> c = ft.getClass();
Field f = c.getDeclaredField("b");
// f.setAccessible(true); // solution
f.setBoolean(ft, Boolean.FALSE); // IllegalAccessException
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalArgumentException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java FieldTroubleToo*
java.lang.IllegalAccessException: Can not set final boolean field
FieldTroubleToo.b to (boolean)false
at sun.reflect.UnsafeFieldAccessorImpl.
throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:55)
at sun.reflect.UnsafeFieldAccessorImpl.
throwFinalFieldIllegalAccessException(UnsafeFieldAccessorImpl.java:63)
at sun.reflect.UnsafeQualifiedBooleanFieldAccessorImpl.setBoolean
(UnsafeQualifiedBooleanFieldAccessorImpl.java:78)
at java.lang.reflect.Field.setBoolean(Field.java:686)
at FieldTroubleToo.main(FieldTroubleToo.java:12)
提示:存在访问限制,阻止在类初始化后设置final
字段。但是,Field
声明为扩展AccessibleObject
,从而提供了抑制此检查的能力。
如果AccessibleObject.setAccessible()
成功,那么对该字段值的后续操作将不会因此问题而失败。这可能会产生意想不到的副作用;例如,有时候原始值将继续被应用程序的某些部分使用,即使该值已被修改。只有在安全上下文允许的情况下,AccessibleObject.setAccessible()
才会成功。
方法
原文:
docs.oracle.com/javase/tutorial/reflect/member/method.html
方法 包含可调用的可执行代码。方法是继承的,在非反射代码中,编译器强制执行重载、覆盖和隐藏等行为。相比之下,反射代码使得方法选择可以限制在特定类中而不考虑其超类。可以访问超类方法,但可以确定它们的声明类;这在没有反射的情况下是不可能通过编程方式发现的,这是许多微妙错误的根源。
java.lang.reflect.Method
类提供了访问有关方法修饰符、返回类型、参数、注解和抛出异常的信息的 API。它还可以用于调用方法。以下部分涵盖了这些主题:
-
获取方法类型信息展示了如何枚举在类中声明的方法并获取类型信息
-
获取方法参数的名称展示了如何检索方法或构造函数的参数的名称和其他信息
-
检索和解析方法修饰符描述了如何访问和解码与方法相关的修饰符和其他信息
-
调用方法演示了如何执行一个方法并获取其返回值
-
故障排除 涵盖了在查找或调用方法时遇到的常见错误
获取方法类型信息
原文:
docs.oracle.com/javase/tutorial/reflect/member/methodType.html
方法声明包括名称、修饰符、参数、返回类型和可抛出异常列表。java.lang.reflect.Method
类提供了获取这些信息的方法。
MethodSpy
示例演示了如何枚举给定类中声明的所有方法,并检索给定名称的所有方法的返回、参数和异常类型。
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class MethodSpy {
private static final String fmt = "%24s: %s%n";
// for the morbidly curious
<E extends RuntimeException> void genericThrow() throws E {}
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
if (!m.getName().equals(args[1])) {
continue;
}
out.format("%s%n", m.toGenericString());
out.format(fmt, "ReturnType", m.getReturnType());
out.format(fmt, "GenericReturnType", m.getGenericReturnType());
Class<?>[] pType = m.getParameterTypes();
Type[] gpType = m.getGenericParameterTypes();
for (int i = 0; i < pType.length; i++) {
out.format(fmt,"ParameterType", pType[i]);
out.format(fmt,"GenericParameterType", gpType[i]);
}
Class<?>[] xType = m.getExceptionTypes();
Type[] gxType = m.getGenericExceptionTypes();
for (int i = 0; i < xType.length; i++) {
out.format(fmt,"ExceptionType", xType[i]);
out.format(fmt,"GenericExceptionType", gxType[i]);
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
这是Class.getConstructor()
的输出,这是一个具有参数化类型和可变数量参数的方法的示例。
$ *java MethodSpy java.lang.Class getConstructor*
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
(java.lang.Class<?>[]) throws java.lang.NoSuchMethodException,
java.lang.SecurityException
ReturnType: class java.lang.reflect.Constructor
GenericReturnType: java.lang.reflect.Constructor<T>
ParameterType: class [Ljava.lang.Class;
GenericParameterType: java.lang.Class<?>[]
ExceptionType: class java.lang.NoSuchMethodException
GenericExceptionType: class java.lang.NoSuchMethodException
ExceptionType: class java.lang.SecurityException
GenericExceptionType: class java.lang.SecurityException
这是源代码中方法的实际声明:
public Constructor<T> getConstructor(Class<?>... parameterTypes)
首先注意返回和参数类型是通用的。如果存在类文件中的签名属性,Method.getGenericReturnType()
将会查看它。如果属性不可用,则会回退到未更改的Method.getReturnType()
,这是在引入泛型之前没有更改的。其他以反射中某个值Foo为名称的getGeneric*Foo*()
方法实现方式类似。
接下来,请注意最后(也是唯一的)参数parameterType
是可变参数(具有可变数量的参数)类型为java.lang.Class
。它被表示为java.lang.Class
类型的单维数组。这可以通过调用Method.isVarArgs()
来区分明确为java.lang.Class
数组的参数。Method.get*Types()
返回值的语法在Class.getName()
中描述。
以下示例说明了具有通用返回类型的方法。
$ *java MethodSpy java.lang.Class cast*
public T java.lang.Class.cast(java.lang.Object)
ReturnType: class java.lang.Object
GenericReturnType: T
ParameterType: class java.lang.Object
GenericParameterType: class java.lang.Object
方法Class.cast()
的通用返回类型报告为java.lang.Object
,因为泛型是通过类型擦除实现的,在编译期间删除了有关泛型类型的所有信息。T
的擦除由Class
的声明定义:
public final class Class<T> implements ...
因此,T
被类型变量的上界替换,即java.lang.Object
。
最后一个示例说明了具有多个重载的方法的输出。
$ *java MethodSpy java.io.PrintStream format*
public java.io.PrintStream java.io.PrintStream.format
(java.util.Locale,java.lang.String,java.lang.Object[])
ReturnType: class java.io.PrintStream
GenericReturnType: class java.io.PrintStream
ParameterType: class java.util.Locale
GenericParameterType: class java.util.Locale
ParameterType: class java.lang.String
GenericParameterType: class java.lang.String
ParameterType: class [Ljava.lang.Object;
GenericParameterType: class [Ljava.lang.Object;
public java.io.PrintStream java.io.PrintStream.format
(java.lang.String,java.lang.Object[])
ReturnType: class java.io.PrintStream
GenericReturnType: class java.io.PrintStream
ParameterType: class java.lang.String
GenericParameterType: class java.lang.String
ParameterType: class [Ljava.lang.Object;
GenericParameterType: class [Ljava.lang.Object;
如果发现同一方法名的多个重载,它们都会被Class.getDeclaredMethods()
返回。由于format()
有两个重载(一个带有Locale
,一个没有),MethodSpy
都会显示出来。
注意: Method.getGenericExceptionTypes()
的存在是因为实际上可以声明一个带有泛型异常类型的方法。然而,这很少被使用,因为无法捕获泛型异常类型。
获取方法参数的名称
原文:
docs.oracle.com/javase/tutorial/reflect/member/methodparameterreflection.html
您可以使用方法java.lang.reflect.Executable.getParameters
获取任何方法或构造函数的形式参数的名称。(类Method
和Constructor
扩展了类Executable
,因此继承了方法Executable.getParameters
。)但是,默认情况下,.class
文件不会存储形式参数名称。这是因为许多生成和消费类文件的工具可能不希望.class
文件包含参数名称的更大的静态和动态占用空间。特别是,这些工具将不得不处理更大的.class
文件,并且 Java 虚拟机(JVM)将使用更多内存。此外,一些参数名称,如secret
或password
,可能会暴露有关安全敏感方法的信息。
要在特定的.class
文件中存储形式参数名称,并使反射 API 能够检索形式参数名称,请使用javac
编译器的-parameters
选项编译源文件。
MethodParameterSpy
示例演示了如何检索给定类的所有构造函数和方法的形式参数的名称。该示例还打印有关每个参数的其他信息。
以下命令打印类ExampleMethods
的构造函数和方法的形式参数名称。注意:记得使用-parameters
编译器选项编译示例ExampleMethods
:
*java MethodParameterSpy ExampleMethods*
此命令打印以下内容:
Number of constructors: 1
Constructor #1
public ExampleMethods()
Number of declared constructors: 1
Declared constructor #1
public ExampleMethods()
Number of methods: 4
Method #1
public boolean ExampleMethods.simpleMethod(java.lang.String,int)
Return type: boolean
Generic return type: boolean
Parameter class: class java.lang.String
Parameter name: stringParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: int
Parameter name: intParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #2
public int ExampleMethods.varArgsMethod(java.lang.String...)
Return type: int
Generic return type: int
Parameter class: class [Ljava.lang.String;
Parameter name: manyStrings
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #3
public boolean ExampleMethods.methodWithList(java.util.List<java.lang.String>)
Return type: boolean
Generic return type: boolean
Parameter class: interface java.util.List
Parameter name: listParam
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Method #4
public <T> void ExampleMethods.genericMethod(T[],java.util.Collection<T>)
Return type: void
Generic return type: void
Parameter class: class [Ljava.lang.Object;
Parameter name: a
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
Parameter class: interface java.util.Collection
Parameter name: c
Modifiers: 0
Is implicit?: false
Is name present?: true
Is synthetic?: false
MethodParameterSpy
示例使用Parameter
类中的以下方法:
-
getType
: 返回标识参数声明类型的Class
对象。 -
getName
: 返回参数的名称。如果参数的名称存在,则此方法返回.class
文件提供的名称。否则,此方法将合成一个形式为arg*N*
的名称,其中*N*
是声明参数的方法描述符中的参数索引。例如,假设您编译了类
ExampleMethods
而没有指定-parameters
编译器选项。示例MethodParameterSpy
将为方法ExampleMethods.simpleMethod
打印以下内容:public boolean ExampleMethods.simpleMethod(java.lang.String,int) Return type: boolean Generic return type: boolean Parameter class: class java.lang.String Parameter name: arg0 Modifiers: 0 Is implicit?: false Is name present?: false Is synthetic?: false Parameter class: int Parameter name: arg1 Modifiers: 0 Is implicit?: false Is name present?: false Is synthetic?: false
-
getModifiers
:返回一个整数,表示形式参数具有的各种特征。如果适用于形式参数,则此值是以下值的总和:值(十进制) 值(十六进制) 描述 16 0x0010 形式参数声明为 final
4096 0x1000 形式参数是合成的。或者,您可以调用方法 isSynthetic
。32768 0x8000 参数在源代码中是隐式声明的。或者,您可以调用方法 isImplicit
-
isImplicit
:如果此参数在源代码中是隐式声明的,则返回true
。有关更多信息,请参阅隐式和合成参数部分。 -
isNamePresent
:如果参数在.class
文件中具有名称,则返回true
。 -
isSynthetic
:如果此参数在源代码中既不是隐式声明也不是显式声明,则返回true
。有关更多信息,请参阅隐式和合成参数部分。
隐式和合成参数
如果源代码中未明确编写某些构造,则某些构造将被隐式声明。例如,ExampleMethods
示例不包含构造函数。它将隐式声明一个默认构造函数。MethodParameterSpy
示例打印有关ExampleMethods
隐式声明构造函数的信息:
Number of declared constructors: 1
public ExampleMethods()
请考虑来自MethodParameterExamples
的以下摘录:
public class MethodParameterExamples {
public class InnerClass { }
}
类InnerClass
是一个非静态嵌套类或内部类。内部类也会隐式声明一个构造函数。但是,此构造函数将包含一个参数。当 Java 编译器编译InnerClass
时,它会创建一个代表以下代码的.class
文件:
public class MethodParameterExamples {
public class InnerClass {
final MethodParameterExamples parent;
InnerClass(final MethodParameterExamples this$0) {
parent = this$0;
}
}
}
InnerClass
构造函数包含一个参数,其类型是包含InnerClass
的类,即MethodParameterExamples
。因此,示例MethodParameterExamples
打印如下内容:
public MethodParameterExamples$InnerClass(MethodParameterExamples)
Parameter class: class MethodParameterExamples
Parameter name: this$0
Modifiers: 32784
Is implicit?: true
Is name present?: true
Is synthetic?: false
因为类InnerClass
的构造函数是隐式声明的,所以它的参数也是隐式的。
注意:
-
Java 编译器为内部类的构造函数创建一个形式参数,以便编译器能够从创建表达式传递一个引用(表示立即封闭实例)到成员类的构造函数。
-
值 32784 表示
InnerClass
构造函数的参数既是 final(16)又是隐式的(32768)。 -
Java 编程语言允许在变量名中使用美元符号(
$
);然而,按照惯例,在变量名中不使用美元符号。
Java 编译器生成的构造如果不对应于源代码中显式或隐式声明的构造,则标记为合成的,除非它们是类初始化方法。合成的构造是编译器生成的在不同实现之间变化的工件。考虑以下摘录来自MethodParameterExamples
:
public class MethodParameterExamples {
enum Colors {
RED, WHITE;
}
}
当 Java 编译器遇到enum
构造时,它会创建几个与.class
文件结构兼容且提供enum
构造所期望功能的方法。例如,Java 编译器会为代表以下代码的enum
构造Colors
创建一个.class
文件:
final class Colors extends java.lang.Enum<Colors> {
public final static Colors RED = new Colors("RED", 0);
public final static Colors BLUE = new Colors("WHITE", 1);
private final static values = new Colors[]{ RED, BLUE };
private Colors(String name, int ordinal) {
super(name, ordinal);
}
public static Colors[] values(){
return values;
}
public static Colors valueOf(String name){
return (Colors)java.lang.Enum.valueOf(Colors.class, name);
}
}
Java 编译器为这个enum
构造创建了三个构造函数和方法:Colors(String name, int ordinal)
、Colors[] values()
和Colors valueOf(String name)
。方法values
和valueOf
是隐式声明的。因此,它们的形式参数名称也是隐式声明的。
enum
构造函数Colors(String name, int ordinal)
是一个默认构造函数,它是隐式声明的。然而,这个构造函数的形式参数(name
和ordinal
)并没有隐式声明。因为这些形式参数既没有显式声明也没有隐式声明,它们是合成的。(enum
构造函数的默认构造函数的形式参数不是隐式声明的,因为不同的编译器可能对这个构造函数的形式参数形式有不同的规定;另一个 Java 编译器可能为其指定不同的形式参数。当编译器编译使用enum
常量的表达式时,它们仅依赖于enum
构造的公共静态字段,这些字段是隐式声明的,而不依赖于它们的构造函数或这些常量是如何初始化的。)
因此,示例MethodParameterExample
关于enum
构造Colors
打印如下内容:
enum Colors:
Number of constructors: 0
Number of declared constructors: 1
Declared constructor #1
private MethodParameterExamples$Colors()
Parameter class: class java.lang.String
Parameter name: $enum$name
Modifiers: 4096
Is implicit?: false
Is name present?: true
Is synthetic?: true
Parameter class: int
Parameter name: $enum$ordinal
Modifiers: 4096
Is implicit?: false
Is name present?: true
Is synthetic?: true
Number of methods: 2
Method #1
public static MethodParameterExamples$Colors[]
MethodParameterExamples$Colors.values()
Return type: class [LMethodParameterExamples$Colors;
Generic return type: class [LMethodParameterExamples$Colors;
Method #2
public static MethodParameterExamples$Colors
MethodParameterExamples$Colors.valueOf(java.lang.String)
Return type: class MethodParameterExamples$Colors
Generic return type: class MethodParameterExamples$Colors
Parameter class: class java.lang.String
Parameter name: name
Modifiers: 32768
Is implicit?: true
Is name present?: true
Is synthetic?: false
有关隐式声明的构造的更多信息,请参考Java 语言规范,包括在反射 API 中出现为隐式的参数。
检索和解析方法修饰符
原文:
docs.oracle.com/javase/tutorial/reflect/member/methodModifiers.html
方法声明中可能包含的几个修饰符:
-
访问修饰符:
public
、protected
和private
-
限制为一个实例的修饰符:
static
-
禁止值修改的修饰符:
final
-
要求覆盖的修饰符:
abstract
-
防止重入的修饰符:
synchronized
-
表示在另一种编程语言中实现的修饰符:
native
-
强制严格浮点行为的修饰符:
strictfp
-
注解
MethodModifierSpy
示例列出了具有给定名称的方法的修饰符。它还显示方法是否是合成的(编译器生成的)、可变参数的,或者是桥接方法(编译器生成的以支持泛型接口)。
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import static java.lang.System.out;
public class MethodModifierSpy {
private static int count;
private static synchronized void inc() { count++; }
private static synchronized int cnt() { return count; }
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
if (!m.getName().equals(args[1])) {
continue;
}
out.format("%s%n", m.toGenericString());
out.format(" Modifiers: %s%n",
Modifier.toString(m.getModifiers()));
out.format(" [ synthetic=%-5b var_args=%-5b bridge=%-5b ]%n",
m.isSynthetic(), m.isVarArgs(), m.isBridge());
inc();
}
out.format("%d matching overload%s found%n", cnt(),
(cnt() == 1 ? "" : "s"));
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
MethodModifierSpy
产生的输出示例如下。
$ *java MethodModifierSpy java.lang.Object wait*
public final void java.lang.Object.wait() throws java.lang.InterruptedException
Modifiers: public final
[ synthetic=false var_args=false bridge=false ]
public final void java.lang.Object.wait(long,int)
throws java.lang.InterruptedException
Modifiers: public final
[ synthetic=false var_args=false bridge=false ]
public final native void java.lang.Object.wait(long)
throws java.lang.InterruptedException
Modifiers: public final native
[ synthetic=false var_args=false bridge=false ]
3 matching overloads found
$ *java MethodModifierSpy java.lang.StrictMath toRadians*
public static double java.lang.StrictMath.toRadians(double)
Modifiers: public static strictfp
[ synthetic=false var_args=false bridge=false ]
1 matching overload found
$ *java MethodModifierSpy MethodModifierSpy inc*
private synchronized void MethodModifierSpy.inc()
Modifiers: private synchronized
[ synthetic=false var_args=false bridge=false ]
1 matching overload found
$ *java MethodModifierSpy java.lang.Class getConstructor*
public java.lang.reflect.Constructor<T> java.lang.Class.getConstructor
(java.lang.Class<T>[]) throws java.lang.NoSuchMethodException,
java.lang.SecurityException
Modifiers: public transient
[ synthetic=false var_args=true bridge=false ]
1 matching overload found
$ *java MethodModifierSpy java.lang.String compareTo*
public int java.lang.String.compareTo(java.lang.String)
Modifiers: public
[ synthetic=false var_args=false bridge=false ]
public int java.lang.String.compareTo(java.lang.Object)
Modifiers: public volatile
[ synthetic=true var_args=false bridge=true ]
2 matching overloads found
请注意,Method.isVarArgs()
对于Class.getConstructor()
返回true
。这表明方法声明如下:
public Constructor<T> getConstructor(Class<?>... parameterTypes)
不要这样:
public Constructor<T> getConstructor(Class<?> [] parameterTypes)
请注意,String.compareTo()
的输出包含两种方法。在String.java
中声明的方法:
public int compareTo(String anotherString);
和第二个合成或编译器生成的桥接方法。这是因为String
实现了参数化接口Comparable
。在类型擦除期间,继承方法Comparable.compareTo()
的参数类型从java.lang.Object
更改为java.lang.String
。由于Comparable
和String
中的compareTo
方法的参数类型在擦除后不再匹配,因此无法进行覆盖。在所有其他情况下,这将产生编译时错误,因为接口未实现。桥接方法的添加避免了这个问题。
Method
实现了java.lang.reflect.AnnotatedElement
。因此,任何具有java.lang.annotation.RetentionPolicy.RUNTIME
的运行时注解都可以被检索。有关获取注解的示例,请参见检查类修饰符和类型部分。
调用方法
原文:
docs.oracle.com/javase/tutorial/reflect/member/methodInvocation.html
反射提供了一种在类上调用方法的方式。通常,只有在非反射代码中无法将类的实例强制转换为所需类型时才需要这样做。方法是使用java.lang.reflect.Method.invoke()
来调用的。第一个参数是要调用该特定方法的对象实例。(如果方法是static
,第一个参数应为null
。)后续参数是方法的参数。如果底层方法抛出异常,它将被java.lang.reflect.InvocationTargetException
包装。可以使用异常链接机制的InvocationTargetException.getCause()
方法检索方法的原始异常。
查找和调用具有特定声明的方法
考虑一个测试套件,它使用反射来调用给定类中的私有测试方法。Deet
示例搜索类中以字符串"test
"开头,具有布尔返回类型和单个Locale
参数的public
方法。然后调用每个匹配的方法。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.Locale;
import static java.lang.System.out;
import static java.lang.System.err;
public class Deet<T> {
private boolean testDeet(Locale l) {
// getISO3Language() may throw a MissingResourceException
out.format("Locale = %s, ISO Language Code = %s%n", l.getDisplayName(), l.getISO3Language());
return true;
}
private int testFoo(Locale l) { return 0; }
private boolean testBar() { return true; }
public static void main(String... args) {
if (args.length != 4) {
err.format("Usage: java Deet <classname> <langauge> <country> <variant>%n");
return;
}
try {
Class<?> c = Class.forName(args[0]);
Object t = c.newInstance();
Method[] allMethods = c.getDeclaredMethods();
for (Method m : allMethods) {
String mname = m.getName();
if (!mname.startsWith("test")
|| (m.getGenericReturnType() != boolean.class)) {
continue;
}
Type[] pType = m.getGenericParameterTypes();
if ((pType.length != 1)
|| Locale.class.isAssignableFrom(pType[0].getClass())) {
continue;
}
out.format("invoking %s()%n", mname);
try {
m.setAccessible(true);
Object o = m.invoke(t, new Locale(args[1], args[2], args[3]));
out.format("%s() returned %b%n", mname, (Boolean) o);
// Handle any exceptions thrown by method to be invoked.
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
err.format("invocation of %s failed: %s%n",
mname, cause.getMessage());
}
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
Deet
调用getDeclaredMethods()
,它将返回类中明确声明的所有方法。此外,使用Class.isAssignableFrom()
来确定定位方法的参数是否与所需调用兼容。从技术上讲,代码可以测试以下语句是否为true
,因为Locale
是final
的:
Locale.class == pType[0].getClass()
然而,Class.isAssignableFrom()
更通用。
$ *java Deet Deet ja JP JP*
invoking testDeet()
Locale = Japanese (Japan,JP),
ISO Language Code = jpn
testDeet() returned true
$ *java Deet Deet xx XX XX*
invoking testDeet()
invocation of testDeet failed:
Couldn't find 3-letter language code for xx
首先,请注意只有testDeet()
符合代码强制执行的声明限制。接下来,当testDeet()
传递无效参数时,它会抛出一个未经检查的java.util.MissingResourceException
。在反射中,对于已检查和未检查的异常处理没有区别。它们都被包装在一个InvocationTargetException
中。
调用具有可变数量参数的方法
Method.invoke()
可用于向方法传递可变数量的参数。要理解的关键概念是,可变参数的方法被实现为如果可变参数被打包在一个数组中。
InvokeMain
示例演示了如何调用任何类中的main()
入口点,并在运行时传递一组参数。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
public class InvokeMain {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Class[] argTypes = new Class[] { String[].class };
Method main = c.getDeclaredMethod("main", argTypes);
String[] mainArgs = Arrays.copyOfRange(args, 1, args.length);
System.out.format("invoking %s.main()%n", c.getName());
main.invoke(null, (Object)mainArgs);
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
首先,为了找到main()
方法,代码会搜索一个名为"main"的类,该类有一个参数,参数是一个String
数组。由于main()
是static
的,null
是传递给Method.invoke()
的第一个参数。第二个参数是要传递的参数数组。
$ *java InvokeMain Deet Deet ja JP JP*
invoking Deet.main()
invoking testDeet()
Locale = Japanese (Japan,JP),
ISO Language Code = jpn
testDeet() returned true
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/member/methodTrouble.html
本节包含开发人员在使用反射定位、调用或获取方法时可能遇到的问题示例。
由于类型擦除导致的 NoSuchMethodException
MethodTrouble
示例说明了当代码在类中搜索特定方法时未考虑类型擦除时会发生什么。
import java.lang.reflect.Method;
public class MethodTrouble<T> {
public void lookup(T t) {}
public void find(Integer i) {}
public static void main(String... args) {
try {
String mName = args[0];
Class cArg = Class.forName(args[1]);
Class<?> c = (new MethodTrouble<Integer>()).getClass();
Method m = c.getMethod(mName, cArg);
System.out.format("Found:%n %s%n", m.toGenericString());
// production code should handle these exceptions more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
$ *java MethodTrouble lookup java.lang.Integer*
java.lang.NoSuchMethodException: MethodTrouble.lookup(java.lang.Integer)
at java.lang.Class.getMethod(Class.java:1605)
at MethodTrouble.main(MethodTrouble.java:12)
$ *java MethodTrouble lookup java.lang.Object*
Found:
public void MethodTrouble.lookup(T)
当方法声明具有泛型参数类型时,编译器将使用其上界替换泛型类型,在本例中,T
的上界为Object
。因此,当代码搜索lookup(Integer)
时,尽管MethodTrouble
的实例是如下创建的,但不会找到方法:
Class<?> c = (new MethodTrouble<Integer>()).getClass();
搜索lookup(Object)
成功,如预期。
$ *java MethodTrouble find java.lang.Integer*
Found:
public void MethodTrouble.find(java.lang.Integer)
$ *java MethodTrouble find java.lang.Object*
java.lang.NoSuchMethodException: MethodTrouble.find(java.lang.Object)
at java.lang.Class.getMethod(Class.java:1605)
at MethodTrouble.main(MethodTrouble.java:12)
在这种情况下,find()
没有泛型参数,因此getMethod()
搜索的参数类型必须完全匹配。
提示: 在搜索方法时,始终传递参数化类型的上界。
调用方法时的 IllegalAccessException
如果尝试调用private
或其他不可访问方法,则会抛出IllegalAccessException
。
MethodTroubleAgain
示例展示了尝试在另一个类中调用私有方法导致的典型堆栈跟踪。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
class AnotherClass {
private void m() {}
}
public class MethodTroubleAgain {
public static void main(String... args) {
AnotherClass ac = new AnotherClass();
try {
Class<?> c = ac.getClass();
Method m = c.getDeclaredMethod("m");
// m.setAccessible(true); // solution
Object o = m.invoke(ac); // IllegalAccessException
// production code should handle these exceptions more gracefully
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
异常抛出的堆栈跟踪如下。
$ *java MethodTroubleAgain*
java.lang.IllegalAccessException: Class MethodTroubleAgain can not access a
member of class AnotherClass with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.reflect.Method.invoke(Method.java:588)
at MethodTroubleAgain.main(MethodTroubleAgain.java:15)
提示: 存在访问限制,阻止对通常无法通过直接调用访问的方法进行反射调用。(包括但不限于在另一个类中的private
方法和在另一个私有类中的公共方法。)但是,Method
被声明为扩展AccessibleObject
,通过AccessibleObject.setAccessible()
提供了抑制此检查的能力。如果成功,则随后对该方法对象的调用不会因此问题而失败。
从 Method.invoke()中抛出的 IllegalArgumentException
Method.invoke()
已经被改造成为可变参数方法。这是一个巨大的便利,但可能会导致意外行为。MethodTroubleToo
示例展示了Method.invoke()
可能产生混乱结果的各种方式。
import java.lang.reflect.Method;
public class MethodTroubleToo {
public void ping() { System.out.format("PONG!%n"); }
public static void main(String... args) {
try {
MethodTroubleToo mtt = new MethodTroubleToo();
Method m = MethodTroubleToo.class.getMethod("ping");
switch(Integer.parseInt(args[0])) {
case 0:
m.invoke(mtt); // works
break;
case 1:
m.invoke(mtt, null); // works (expect compiler warning)
break;
case 2:
Object arg2 = null;
m.invoke(mtt, arg2); // IllegalArgumentException
break;
case 3:
m.invoke(mtt, new Object[0]); // works
break;
case 4:
Object arg4 = new Object[0];
m.invoke(mtt, arg4); // IllegalArgumentException
break;
default:
System.out.format("Test not found%n");
}
// production code should handle these exceptions more gracefully
} catch (Exception x) {
x.printStackTrace();
}
}
}
$ *java MethodTroubleToo 0*
PONG!
由于Method.invoke()
的所有参数都是可选的,除了第一个,当要调用的方法没有参数时,可以省略它们。
$ *java MethodTroubleToo 1*
PONG!
在这种情况下,代码生成这个编译器警告,因为null
是模棱两可的。
$ *javac MethodTroubleToo.java*
MethodTroubleToo.java:16: warning: non-varargs call of varargs method with
inexact argument type for last parameter;
m.invoke(mtt, null); // works (expect compiler warning)
^
cast to Object for a varargs call
cast to Object[] for a non-varargs call and to suppress this warning
1 warning
不可能确定null
代表空参数数组还是第一个参数为null
。
$ *java MethodTroubleToo 2*
java.lang.IllegalArgumentException: wrong number of arguments
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke
(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at MethodTroubleToo.main(MethodTroubleToo.java:21)
尽管参数为null
,但这会失败,因为类型是Object
,而ping()
不需要任何参数。
$ *java MethodTroubleToo 3*
PONG!
这能够成功是因为new Object[0]
创建了一个空数组,对于可变参数方法来说,这等同于不传递任何可选参数。
$ *java MethodTroubleToo 4*
java.lang.IllegalArgumentException: wrong number of arguments
at sun.reflect.NativeMethodAccessorImpl.invoke0
(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke
(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke
(DelegatingMethodAccessorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:597)
at MethodTroubleToo.main(MethodTroubleToo.java:28)
与前面的例子不同,如果空数组存储在一个Object
中,那么它将被视为一个Object
。这与案例 2 失败的原因相同,ping()
不期望有参数。
提示:当声明一个方法foo(Object... o)
时,编译器会将传递给foo()
的所有参数放入一个Object
类型的数组中。foo()
的实现与声明为foo(Object[] o)
时相同。理解这一点可能有助于避免上面所示问题的类型。
调用方法失败时的 InvocationTargetException
InvocationTargetException
包装了调用方法对象时产生的所有异常(已检查和未检查)。MethodTroubleReturns
示例展示了如何检索被调用方法抛出的原始异常。
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class MethodTroubleReturns {
private void drinkMe(int liters) {
if (liters < 0)
throw new IllegalArgumentException("I can't drink a negative amount of liquid");
}
public static void main(String... args) {
try {
MethodTroubleReturns mtr = new MethodTroubleReturns();
Class<?> c = mtr.getClass();
Method m = c.getDeclaredMethod("drinkMe", int.class);
m.invoke(mtr, -1);
// production code should handle these exceptions more gracefully
} catch (InvocationTargetException x) {
Throwable cause = x.getCause();
System.err.format("drinkMe() failed: %s%n", cause.getMessage());
} catch (Exception x) {
x.printStackTrace();
}
}
}
$ *java MethodTroubleReturns*
drinkMe() failed: I can't drink a negative amount of liquid
提示:如果抛出InvocationTargetException
,则方法已被调用。诊断问题的方法与直接调用方法并通过getCause()
检索到的异常相同。此异常并不表示反射包或其使用存在问题。
构造函数
原文:
docs.oracle.com/javase/tutorial/reflect/member/ctor.html
构造函数 用于创建一个属于类的实例的对象。通常在调用方法或访问字段之前执行初始化类所需的操作。构造函数不会被继承。
与方法类似,反射提供了 API 来发现和检索类的构造函数,并获取声明信息,如修饰符、参数、注解和抛出的异常。还可以使用指定的构造函数创建类的新实例。在处理构造函数时使用的关键类是Class
和 java.lang.reflect.Constructor
。涵盖了涉及构造函数的常见操作的以下部分:
-
查找构造函数 说明了如何检索具有特定参数的构造函数
-
检索和解析构造函数修饰符 展示了如何获取构造函数声明的修饰符以及有关构造函数的其他信息
-
创建新的类实例 展示了如何通过调用其构造函数实例化对象的实例
-
故障排除 描述了在查找或调用构造函数时可能遇到的常见错误
查找构造函数
原文:
docs.oracle.com/javase/tutorial/reflect/member/ctorLocation.html
构造函数声明包括名称、修饰符、参数和可抛出异常列表。java.lang.reflect.Constructor
类提供了获取这些信息的方法。
ConstructorSift
示例演示了如何搜索一个类的声明构造函数中具有给定类型参数的构造函数。
import java.lang.reflect.Constructor;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ConstructorSift {
public static void main(String... args) {
try {
Class<?> cArg = Class.forName(args[1]);
Class<?> c = Class.forName(args[0]);
Constructor[] allConstructors = c.getDeclaredConstructors();
for (Constructor ctor : allConstructors) {
Class<?>[] pType = ctor.getParameterTypes();
for (int i = 0; i < pType.length; i++) {
if (pType[i].equals(cArg)) {
out.format("%s%n", ctor.toGenericString());
Type[] gpType = ctor.getGenericParameterTypes();
for (int j = 0; j < gpType.length; j++) {
char ch = (pType[j].equals(cArg) ? '*' : ' ');
out.format("%7c%s[%d]: %s%n", ch,
"GenericParameterType", j, gpType[j]);
}
break;
}
}
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
Method.getGenericParameterTypes()
将在类文件中的签名属性中查找(如果存在)。如果属性不可用,则会回退到Method.getParameterType()
,这个方法在引入泛型之前并未更改。其他以getGeneric*Foo*()
命名的反射方法也是类似实现的。Method.get*Types()
返回值的语法在Class.getName()
中有描述。
这里是所有在java.util.Formatter
中具有Locale
参数的构造函数的输出。
$ *java ConstructorSift java.util.Formatter java.util.Locale*
public
java.util.Formatter(java.io.OutputStream,java.lang.String,java.util.Locale)
throws java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.io.OutputStream
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.String,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.lang.String
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
public java.util.Formatter(java.lang.Appendable,java.util.Locale)
GenericParameterType[0]: interface java.lang.Appendable
*GenericParameterType[1]: class java.util.Locale
public java.util.Formatter(java.util.Locale)
*GenericParameterType[0]: class java.util.Locale
public java.util.Formatter(java.io.File,java.lang.String,java.util.Locale)
throws java.io.FileNotFoundException,java.io.UnsupportedEncodingException
GenericParameterType[0]: class java.io.File
GenericParameterType[1]: class java.lang.String
*GenericParameterType[2]: class java.util.Locale
下一个示例输出演示了如何在String
中搜索类型为char[]
的参数。
$ *java ConstructorSift java.lang.String "[C"*
java.lang.String(int,int,char[])
GenericParameterType[0]: int
GenericParameterType[1]: int
*GenericParameterType[2]: class [C
public java.lang.String(char[],int,int)
*GenericParameterType[0]: class [C
GenericParameterType[1]: int
GenericParameterType[2]: int
public java.lang.String(char[])
*GenericParameterType[0]: class [C
表达接受Class.forName()
的引用和基本类型数组的语法在Class.getName()
中有描述。请注意,第一个列出的构造函数是包私有
的,而不是public
的。它被返回是因为示例代码使用了Class.getDeclaredConstructors()
而不是Class.getConstructors()
,后者只返回public
构造函数。
这个示例表明,搜索可变参数的参数(具有可变数量的参数)需要使用数组语法:
$ *java ConstructorSift java.lang.ProcessBuilder "[Ljava.lang.String;"*
public java.lang.ProcessBuilder(java.lang.String[])
*GenericParameterType[0]: class [Ljava.lang.String;
这是源代码中ProcessBuilder
构造函数的实际声明:
public ProcessBuilder(String... command)
参数表示为类型为java.lang.String
的单维数组。可以通过调用Constructor.isVarArgs()
来区分明确为java.lang.String
数组的参数。
最后一个示例报告了已声明具有泛型参数类型的构造函数的输出:
$ *java ConstructorSift java.util.HashMap java.util.Map*
public java.util.HashMap(java.util.Map<? extends K, ? extends V>)
*GenericParameterType[0]: java.util.Map<? extends K, ? extends V>
与方法类似,可以以类似的方式检索构造函数的异常类型。有关更多详细信息,请参见MethodSpy
示例中描述的获取方法类型信息部分。
检索和解析构造函数修饰符
原文:
docs.oracle.com/javase/tutorial/reflect/member/ctorModifiers.html
由于构造函数在语言中的作用,比方法更少的修饰符是有意义的:
-
访问修饰符:
public
,protected
和private
-
注解
ConstructorAccess
示例在给定类中搜索具有指定访问修饰符的构造函数。它还显示构造函数是否是合成的(由编译器生成)或具有可变参数。
import java.lang.reflect.Constructor;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ConstructorAccess {
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
Constructor[] allConstructors = c.getDeclaredConstructors();
for (Constructor ctor : allConstructors) {
int searchMod = modifierFromString(args[1]);
int mods = accessModifiers(ctor.getModifiers());
if (searchMod == mods) {
out.format("%s%n", ctor.toGenericString());
out.format(" [ synthetic=%-5b var_args=%-5b ]%n",
ctor.isSynthetic(), ctor.isVarArgs());
}
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static int accessModifiers(int m) {
return m & (Modifier.PUBLIC | Modifier.PRIVATE | Modifier.PROTECTED);
}
private static int modifierFromString(String s) {
if ("public".equals(s)) return Modifier.PUBLIC;
else if ("protected".equals(s)) return Modifier.PROTECTED;
else if ("private".equals(s)) return Modifier.PRIVATE;
else if ("package-private".equals(s)) return 0;
else return -1;
}
}
没有明确对应于“包私有”访问权限的Modifier
常量,因此需要检查所有三个访问修饰符的缺失来识别包私有构造函数。
此输出显示了java.io.File
中的私有构造函数:
$ *java ConstructorAccess java.io.File private*
private java.io.File(java.lang.String,int)
[ synthetic=false var_args=false ]
private java.io.File(java.lang.String,java.io.File)
[ synthetic=false var_args=false ]
合成构造函数很少见;但是SyntheticConstructor
示例说明了可能发生这种情况的典型情况:
public class SyntheticConstructor {
private SyntheticConstructor() {}
class Inner {
// Compiler will generate a synthetic constructor since
// SyntheticConstructor() is private.
Inner() { new SyntheticConstructor(); }
}
}
$ *java ConstructorAccess SyntheticConstructor package-private*
SyntheticConstructor(SyntheticConstructor$1)
[ synthetic=true var_args=false ]
由于内部类的构造函数引用了封闭类的私有构造函数,编译器必须生成一个包私有构造函数。参数类型SyntheticConstructor$1
是任意的,取决于编译器的实现。依赖于任何合成或非公共类成员存在的代码可能不具有可移植性。
构造函数实现了java.lang.reflect.AnnotatedElement
,提供了用于检索运行时注解的方法,使用java.lang.annotation.RetentionPolicy.RUNTIME
。有关获取注解的示例,请参见检查类修饰符和类型部分。
创建新的类实例
原文:
docs.oracle.com/javase/tutorial/reflect/member/ctorInstance.html
创建类实例的两种反射方法:java.lang.reflect.Constructor.newInstance()
和 Class.newInstance()
。前者更受青睐,因此在这些示例中使用,原因如下:
-
Class.newInstance()
只能调用零参数构造函数,而Constructor.newInstance()
可以调用任何构造函数,无论参数个数如何。 -
无论构造函数抛出的是已检查异常还是未检查异常,
Class.newInstance()
都会抛出该异常。Constructor.newInstance()
总是用InvocationTargetException
包装抛出的异常。 -
Class.newInstance()
要求构造函数可见;Constructor.newInstance()
在某些情况下可以调用private
构造函数。
有时可能希望从仅在构造后设置的对象中检索内部状态。考虑一个场景,需要获取java.io.Console
使用的内部字符集。(Console
字符集存储在私有字段中,并且不一定与java.nio.charset.Charset.defaultCharset()
返回的 Java 虚拟机默认字符集相同)。ConsoleCharset
示例展示了如何实现这一点:
import java.io.Console;
import java.nio.charset.Charset;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
public class ConsoleCharset {
public static void main(String... args) {
Constructor[] ctors = Console.class.getDeclaredConstructors();
Constructor ctor = null;
for (int i = 0; i < ctors.length; i++) {
ctor = ctors[i];
if (ctor.getGenericParameterTypes().length == 0)
break;
}
try {
ctor.setAccessible(true);
Console c = (Console)ctor.newInstance();
Field f = c.getClass().getDeclaredField("cs");
f.setAccessible(true);
out.format("Console charset : %s%n", f.get(c));
out.format("Charset.defaultCharset(): %s%n",
Charset.defaultCharset());
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
注意:
如果构造函数没有参数且已经可访问,则Class.newInstance()
才会成功。否则,需要像上面的示例一样使用Constructor.newInstance()
。
UNIX 系统的示例输出:
$ *java ConsoleCharset*
Console charset : ISO-8859-1
Charset.defaultCharset() : ISO-8859-1
Windows 系统的示例输出:
C:\> *java ConsoleCharset*
Console charset : IBM437
Charset.defaultCharset() : windows-1252
另一个常见的 Constructor.newInstance()
应用是调用需要参数的构造函数。RestoreAliases
示例找到一个特定的单参数构造函数并调用它:
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import static java.lang.System.out;
class EmailAliases {
private Set<String> aliases;
private EmailAliases(HashMap<String, String> h) {
aliases = h.keySet();
}
public void printKeys() {
out.format("Mail keys:%n");
for (String k : aliases)
out.format(" %s%n", k);
}
}
public class RestoreAliases {
private static Map<String, String> defaultAliases = new HashMap<String, String>();
static {
defaultAliases.put("Duke", "duke@i-love-java");
defaultAliases.put("Fang", "fang@evil-jealous-twin");
}
public static void main(String... args) {
try {
Constructor ctor = EmailAliases.class.getDeclaredConstructor(HashMap.class);
ctor.setAccessible(true);
EmailAliases email = (EmailAliases)ctor.newInstance(defaultAliases);
email.printKeys();
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
}
}
}
这个示例使用 Class.getDeclaredConstructor()
来找到一个参数类型为 java.util.HashMap
的构造函数。请注意,只需传递 HashMap.class
就足够了,因为任何 get*Constructor()
方法的参数只需要类来确定类型。由于 类型擦除,以下表达式求值为 true
:
HashMap.class == defaultAliases.getClass()
然后,示例使用这个构造函数使用 Constructor.newInstance()
创建类的新实例。
$ *java RestoreAliases*
Mail keys:
Duke
Fang
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/member/ctorTrouble.html
开发人员在尝试通过反射调用构造函数时,有时会遇到以下问题。
由于缺少零参数构造函数而导致的 InstantiationException
ConstructorTrouble
示例说明了当代码尝试使用Class.newInstance()
创建类的新实例时,且没有可访问的零参数构造函数时会发生什么:
public class ConstructorTrouble {
private ConstructorTrouble(int i) {}
public static void main(String... args){
try {
Class<?> c = Class.forName("ConstructorTrouble");
Object o = c.newInstance(); // InstantiationException
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java ConstructorTrouble*
java.lang.InstantiationException: ConstructorTrouble
at java.lang.Class.newInstance0(Class.java:340)
at java.lang.Class.newInstance(Class.java:308)
at ConstructorTrouble.main(ConstructorTrouble.java:7)
提示:InstantiationException
可能发生的原因有很多。在这种情况下,问题在于具有int
参数的构造函数的存在阻止了编译器生成默认(或零参数)构造函数,并且代码中没有显式的零参数构造函数。请记住,Class.newInstance()
的行为非常类似于new
关键字,只要new
失败,它就会失败。
Class.newInstance() 抛出意外异常
ConstructorTroubleToo
示例展示了在Class.newInstance()
中出现的无法解决的问题。即,它传播构造函数抛出的任何异常(已检查或未检查)。
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.err;
public class ConstructorTroubleToo {
public ConstructorTroubleToo() {
throw new RuntimeException("exception in constructor");
}
public static void main(String... args) {
try {
Class<?> c = Class.forName("ConstructorTroubleToo");
// Method propagetes any exception thrown by the constructor
// (including checked exceptions).
if (args.length > 0 && args[0].equals("class")) {
Object o = c.newInstance();
} else {
Object o = c.getConstructor().newInstance();
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
err.format("%n%nCaught exception: %s%n", x.getCause());
}
}
}
$ *java ConstructorTroubleToo class*
Exception in thread "main" java.lang.RuntimeException: exception in constructor
at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:6)
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance
(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance
(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at java.lang.Class.newInstance0(Class.java:355)
at java.lang.Class.newInstance(Class.java:308)
at ConstructorTroubleToo.main(ConstructorTroubleToo.java:15)
这种情况是反射独有的。通常情况下,不可能编写忽略已检查异常的代码,因为这样的代码不会编译。可以通过使用Constructor.newInstance()
而不是Class.newInstance()
来包装构造函数抛出的任何异常。
$ *java ConstructorTroubleToo*
java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance
(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance
(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at ConstructorTroubleToo.main(ConstructorTroubleToo.java:17)
Caused by: java.lang.RuntimeException: exception in constructor
at ConstructorTroubleToo.<init>(ConstructorTroubleToo.java:6)
... 5 more
Caught exception: java.lang.RuntimeException: exception in constructor
如果抛出InvocationTargetException
,则表示方法已被调用。对问题的诊断与直接调用构造函数并抛出异常,然后通过InvocationTargetException.getCause()
检索到的异常相同。此异常并不表示反射包或其使用存在问题。
提示:最好使用Constructor.newInstance()
而不是Class.newInstance()
,因为前者的 API 允许检查和处理构造函数抛出的任意异常。
定位或调用正确构造函数的问题
ConstructorTroubleAgain
类展示了代码错误可能无法定位或调用预期构造函数的各种方式。
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
public class ConstructorTroubleAgain {
public ConstructorTroubleAgain() {}
public ConstructorTroubleAgain(Integer i) {}
public ConstructorTroubleAgain(Object o) {
out.format("Constructor passed Object%n");
}
public ConstructorTroubleAgain(String s) {
out.format("Constructor passed String%n");
}
public static void main(String... args){
String argType = (args.length == 0 ? "" : args[0]);
try {
Class<?> c = Class.forName("ConstructorTroubleAgain");
if ("".equals(argType)) {
// IllegalArgumentException: wrong number of arguments
Object o = c.getConstructor().newInstance("foo");
} else if ("int".equals(argType)) {
// NoSuchMethodException - looking for int, have Integer
Object o = c.getConstructor(int.class);
} else if ("Object".equals(argType)) {
// newInstance() does not perform method resolution
Object o = c.getConstructor(Object.class).newInstance("foo");
} else {
assert false;
}
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java ConstructorTroubleAgain*
Exception in thread "main" java.lang.IllegalArgumentException: wrong number of
arguments
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance
(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance
(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:23)
抛出IllegalArgumentException
是因为请求零参数构造函数并尝试传递参数。如果构造函数传递了错误类型的参数,也会抛出相同的异常。
$ *java ConstructorTroubleAgain int*
java.lang.NoSuchMethodException: ConstructorTroubleAgain.<init>(int)
at java.lang.Class.getConstructor0(Class.java:2706)
at java.lang.Class.getConstructor(Class.java:1657)
at ConstructorTroubleAgain.main(ConstructorTroubleAgain.java:26)
如果开发人员错误地认为反射会自动装箱或拆箱类型,则可能会出现此异常。装箱(将原始类型转换为引用类型)仅在编译期间发生。在反射中没有机会进行此操作,因此在定位构造函数时必须使用特定类型。
$ *java ConstructorTroubleAgain Object*
Constructor passed Object
在这里,可能期望调用接受String
参数的构造函数,因为使用了更具体的String
类型调用了newInstance()
。然而,为时已晚!找到的构造函数已经是接受Object
参数的构造函数。newInstance()
不会尝试进行方法解析;它只是在现有构造函数对象上操作。
提示: new
和Constructor.newInstance()
之间的一个重要区别是,new
执行方法参数类型检查、装箱和方法解析。在反射中,这些都不会发生,必须做出明确选择。
尝试调用不可访问构造函数时出现 IllegalAccessException
如果尝试调用私有或其他不可访问的构造函数,则可能会抛出IllegalAccessException
。ConstructorTroubleAccess
示例展示了产生的堆栈跟踪。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
class Deny {
private Deny() {
System.out.format("Deny constructor%n");
}
}
public class ConstructorTroubleAccess {
public static void main(String... args) {
try {
Constructor c = Deny.class.getDeclaredConstructor();
// c.setAccessible(true); // solution
c.newInstance();
// production code should handle these exceptions more gracefully
} catch (InvocationTargetException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java ConstructorTroubleAccess*
java.lang.IllegalAccessException: Class ConstructorTroubleAccess can not access
a member of class Deny with modifiers "private"
at sun.reflect.Reflection.ensureMemberAccess(Reflection.java:65)
at java.lang.reflect.Constructor.newInstance(Constructor.java:505)
at ConstructorTroubleAccess.main(ConstructorTroubleAccess.java:15)
提示: 存在访问限制,阻止通过直接调用无法访问的构造函数进行反射调用。(这包括但不限于在单独类中的私有构造函数和在单独私有类中的公共构造函数。)但是,Constructor
被声明为扩展AccessibleObject
,它提供了通过AccessibleObject.setAccessible()
来抑制此检查的能力。
课程:数组和枚举类型
原文:
docs.oracle.com/javase/tutorial/reflect/special/index.html
从 Java 虚拟机的角度看,数组和枚举类型(或枚举)只是类。许多 Class
中的方法可以用于它们。反射为数组和枚举提供了一些特定的 API。本课程使用一系列代码示例来描述如何区分这些对象与其他类,并对其进行操作。还将检查各种错误。
数组
数组有一个组件类型和一个长度(长度不是类型的一部分)。数组可以整体操作,也可以逐个组件操作。反射为后者提供了 java.lang.reflect.Array
类。
-
识别数组类型 描述了如何确定类成员是否是数组类型的字段
-
创建新数组 演示了如何创建具有简单和复杂组件类型的新数组实例
-
获取和设置数组及其组件 展示了如何访问数组类型的字段以及单独访问数组元素
-
故障排除 包括常见错误和编程误解
枚举类型
在反射代码中,枚举类型与普通类非常相似。Class.isEnum()
可以告诉一个 Class
是否表示一个 enum
。Class.getEnumConstants()
可以检索在枚举中定义的枚举常量。java.lang.reflect.Field.isEnumConstant()
表示一个字段是否是一个枚举类型。
-
检查枚举 演示了如何检索枚举的常量以及任何其他字段、构造函数和方法
-
使用枚举类型获取和设置字段 展示了如何设置和获取具有枚举常量值的字段
-
故障排除 描述了与枚举相关的常见错误
数组
原文:
docs.oracle.com/javase/tutorial/reflect/special/array.html
一个数组是引用类型的对象,包含固定数量的相同类型的组件;数组的长度是不可变的。创建数组的实例需要知道长度和组件类型。每个组件可以是原始类型(如byte
、int
或double
),引用类型(如String
、Object
或java.nio.CharBuffer
),或者是数组。多维数组实际上只是包含数组类型组件的数组。
数组在 Java 虚拟机中实现。数组上的唯一方法是从Object
继承的方法。数组的长度不是其类型的一部分;数组有一个length
字段,可以通过java.lang.reflect.Array.getLength()
访问。
反射提供了访问数组类型和数组组件类型、创建新数组以及检索和设置数组组件值的方法。以下各节包括对数组上常见操作的示例:
-
识别数组类型描述了如何确定类成员是否是数组类型的字段
-
创建新数组演示了如何创建具有简单和复杂组件类型的新数组实例
-
获取和设置数组及其组件展示了如何访问数组类型的字段以及单独访问数组元素
-
故障排除涵盖了常见错误和编程误解
所有这些操作都通过java.lang.reflect.Array
中的static
方法支持。
识别数组类型
原文:
docs.oracle.com/javase/tutorial/reflect/special/arrayComponents.html
可以通过调用Class.isArray()
来识别数组类型。要获取一个Class
,请使用本教程中检索类对象部分描述的方法之一。
ArrayFind
示例标识了命名类中的数组类型字段,并报告了每个字段的组件类型。
import java.lang.reflect.Field;
import java.lang.reflect.Type;
import static java.lang.System.out;
public class ArrayFind {
public static void main(String... args) {
boolean found = false;
try {
Class<?> cls = Class.forName(args[0]);
Field[] flds = cls.getDeclaredFields();
for (Field f : flds) {
Class<?> c = f.getType();
if (c.isArray()) {
found = true;
out.format("%s%n"
+ " Field: %s%n"
+ " Type: %s%n"
+ " Component Type: %s%n",
f, f.getName(), c, c.getComponentType());
}
}
if (!found) {
out.format("No array fields%n");
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
Class.get*Type()
返回值的语法在Class.getName()
中有描述。类型名称开头的'[
'字符的数量表示数组的维度(即嵌套的深度)。
输出示例如下。用户输入用斜体表示。一个原始类型为byte
的数组:
$*java ArrayFind java.nio.ByteBuffer*
final byte[] java.nio.ByteBuffer.hb
Field: hb
Type: class [B
Component Type: byte
一个引用类型为StackTraceElement
的数组:
$ *java ArrayFind java.lang.Throwable*
private java.lang.StackTraceElement[] java.lang.Throwable.stackTrace
Field: stackTrace
Type: class [Ljava.lang.StackTraceElement;
Component Type: class java.lang.StackTraceElement
predefined
是一个引用类型的一维数组java.awt.Cursor
,而cursorProperties
是一个引用类型的二维数组String
:
$ *java ArrayFind java.awt.Cursor*
protected static java.awt.Cursor[] java.awt.Cursor.predefined
Field: predefined
Type: class [Ljava.awt.Cursor;
Component Type: class java.awt.Cursor
static final java.lang.String[][] java.awt.Cursor.cursorProperties
Field: cursorProperties
Type: class [[Ljava.lang.String;
Component Type: class [Ljava.lang.String;
创建新数组
原文:
docs.oracle.com/javase/tutorial/reflect/special/arrayInstance.html
就像非反射代码一样,反射支持通过java.lang.reflect.Array.newInstance()
动态创建任意类型和维度的数组的能力。考虑ArrayCreator
,一个能够动态创建数组的基本解释器。将解析的语法如下:
fully_qualified_class_name variable_name[] =
{ val1, val2, val3, ... }
假设fully_qualified_class_name
代表一个具有接受单个String
参数的构造函数的类。数组的维度由提供的值的数量确定。以下示例将构造一个fully_qualified_class_name
数组的实例,并用val1
、val2
等给定的实例填充其值。(此示例假定熟悉Class.getConstructor()
和java.lang.reflect.Constructor.newInstance()
。有关Constructor
的反射 API 的讨论,请参阅本教程的 Creating New Class Instances 部分。)
import java.lang.reflect.Array;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Arrays;
import static java.lang.System.out;
public class ArrayCreator {
private static String s = "java.math.BigInteger bi[] = { 123, 234, 345 }";
private static Pattern p = Pattern.compile("^\\s*(\\S+)\\s*\\w+\\[\\].*\\{\\s*([^}]+)\\s*\\}");
public static void main(String... args) {
Matcher m = p.matcher(s);
if (m.find()) {
String cName = m.group(1);
String[] cVals = m.group(2).split("[\\s,]+");
int n = cVals.length;
try {
Class<?> c = Class.forName(cName);
Object o = Array.newInstance(c, n);
for (int i = 0; i < n; i++) {
String v = cVals[i];
Constructor ctor = c.getConstructor(String.class);
Object val = ctor.newInstance(v);
Array.set(o, i, val);
}
Object[] oo = (Object[])o;
out.format("%s[] = %s%n", cName, Arrays.toString(oo));
// production code should handle these exceptions more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
} catch (NoSuchMethodException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InstantiationException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
}
$ *java ArrayCreator*
java.math.BigInteger [] = [123, 234, 345]
上面的示例展示了可能希望通过反射创建数组的一种情况;即如果组件类型直到运行时才知道。在这种情况下,代码使用Class.forName()
获取所需组件类型的类,然后调用特定的构造函数来初始化数组的每个组件,然后设置相应的数组值。
获取和设置数组及其组件
原文:
docs.oracle.com/javase/tutorial/reflect/special/arraySetGet.html
就像在非反射代码中一样,可以整体设置或逐个组件设置或检索数组字段。要一次设置整个数组,请使用java.lang.reflect.Field.set(Object obj, Object value)
。要检索整个数组,请使用Field.get(Object)
。可以使用java.lang.reflect.Array
中的方法来设置或检索单个组件。
Array
提供了形式为set*Foo*()
和get*Foo*()
的方法,用于设置和获取任何原始类型的组件。例如,可以使用Array.setInt(Object array, int index, int value)
设置int
数组的组件,并可以使用Array.getInt(Object array, int index)
检索它。
这些方法支持自动扩宽数据类型。因此,Array.getShort()
可以用于设置int
数组的值,因为一个 16 位的short
可以被扩宽为 32 位的int
而不会丢失数据;另一方面,在int
数组上调用Array.setLong()
将导致抛出IllegalArgumentException
,因为 64 位的long
不能被缩小为 32 位的int
而不丢失信息。无论传递的实际值是否能够准确表示为目标数据类型,这都是正确的。Java 语言规范,Java SE 7 版,章节Widening Primitive Conversion和Narrowing Primitive Conversion包含了对扩宽和缩窄转换的完整讨论。
引用类型数组(包括数组的数组)的组件使用Array.set(Object array, int index, int value)
和Array.get(Object array, int index)
进行设置和检索。
设置类型为数组的字段
GrowBufferedReader
示例演示了如何替换类型为数组的字段的值。在这种情况下,代码将java.io.BufferedReader
的后备数组替换为更大的数组。(这假设原始BufferedReader
的创建在不可修改的代码中;否则,可以简单地使用接受输入缓冲区大小的替代构造函数BufferedReader(java.io.Reader in, int size)
。)
import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Field;
import java.util.Arrays;
import static java.lang.System.out;
public class GrowBufferedReader {
private static final int srcBufSize = 10 * 1024;
private static char[] src = new char[srcBufSize];
static {
src[srcBufSize - 1] = 'x';
}
private static CharArrayReader car = new CharArrayReader(src);
public static void main(String... args) {
try {
BufferedReader br = new BufferedReader(car);
Class<?> c = br.getClass();
Field f = c.getDeclaredField("cb");
// cb is a private field
f.setAccessible(true);
char[] cbVal = char[].class.cast(f.get(br));
char[] newVal = Arrays.copyOf(cbVal, cbVal.length * 2);
if (args.length > 0 && args[0].equals("grow"))
f.set(br, newVal);
for (int i = 0; i < srcBufSize; i++)
br.read();
// see if the new backing array is being used
if (newVal[srcBufSize - 1] == src[srcBufSize - 1])
out.format("Using new backing array, size=%d%n", newVal.length);
else
out.format("Using original backing array, size=%d%n", cbVal.length);
// production code should handle these exceptions more gracefully
} catch (FileNotFoundException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (IOException x) {
x.printStackTrace();
}
}
}
$ *java GrowBufferedReader grow*
Using new backing array, size=16384
$ *java GrowBufferedReader*
Using original backing array, size=8192
请注意,上述示例使用了数组实用方法java.util.Arrays.copyOf)
。java.util.Arrays
包含许多在操作数组时方便的方法。
访问多维数组的元素
多维数组简单来说就是嵌套数组。二维数组是数组的数组。三维数组是二维数组的数组,依此类推。CreateMatrix
示例演示了如何使用反射创建和初始化多维数组。
import java.lang.reflect.Array;
import static java.lang.System.out;
public class CreateMatrix {
public static void main(String... args) {
Object matrix = Array.newInstance(int.class, 2, 2);
Object row0 = Array.get(matrix, 0);
Object row1 = Array.get(matrix, 1);
Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
for (int i = 0; i < 2; i++)
for (int j = 0; j < 2; j++)
out.format("matrix[%d][%d] = %d%n", i, j, ((int[][])matrix)[i][j]);
}
}
$ *java CreateMatrix*
matrix[0][0] = 1
matrix[0][1] = 2
matrix[1][0] = 3
matrix[1][1] = 4
通过使用以下代码片段也可以获得相同的结果:
Object matrix = Array.newInstance(int.class, 2);
Object row0 = Array.newInstance(int.class, 2);
Object row1 = Array.newInstance(int.class, 2);
Array.setInt(row0, 0, 1);
Array.setInt(row0, 1, 2);
Array.setInt(row1, 0, 3);
Array.setInt(row1, 1, 4);
Array.set(matrix, 0, row0);
Array.set(matrix, 1, row1);
可变参数Array.newInstance(Class<?> componentType, int... dimensions)
提供了一个方便的方式来创建多维数组,但组件仍然需要使用多维数组是嵌套数组的原则进行初始化。(反射不提供用于此目的的多个索引get
/set
方法。)
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/special/arrayTrouble.html
以下示例展示了在操作数组时可能发生的典型错误。
由于不可转换的类型导致IllegalArgumentException
ArrayTroubleAgain
示例将生成一个IllegalArgumentException
。调用Array.setInt()
来设置一个Integer
类型的组件,其值为基本类型int
。在非反射等效的ary[0] = 1
中,编译器会将值1
转换(或装箱)为引用类型new Integer(1)
,以便其类型检查接受该语句。在使用反射时,类型检查仅在运行时发生,因此没有机会将值装箱。
import java.lang.reflect.Array;
import static java.lang.System.err;
public class ArrayTroubleAgain {
public static void main(String... args) {
Integer[] ary = new Integer[2];
try {
Array.setInt(ary, 0, 1); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (IllegalArgumentException x) {
err.format("Unable to box%n");
} catch (ArrayIndexOutOfBoundsException x) {
x.printStackTrace();
}
}
}
$ *java ArrayTroubleAgain*
Unable to box
要消除此异常,有问题的行应该被以下调用替换Array.set(Object array, int index, Object value)
:
Array.set(ary, 0, new Integer(1));
提示: 当使用反射设置或获取数组组件时,编译器无法执行装箱。它只能转换与Class.isAssignableFrom()
规范描述的相关类型。该示例预计会失败,因为isAssignableFrom()
在此测试中将返回false
,可以用程序验证特定转换是否可能:
Integer.class.isAssignableFrom(int.class) == false
同样,在反射中从基本类型到引用类型的自动转换也是不可能的。
int.class.isAssignableFrom(Integer.class) == false
对空数组的ArrayIndexOutOfBoundsException
ArrayTrouble
示例说明了如果尝试访问长度为零的数组元素将会发生的错误:
import java.lang.reflect.Array;
import static java.lang.System.out;
public class ArrayTrouble {
public static void main(String... args) {
Object o = Array.newInstance(int.class, 0);
int[] i = (int[])o;
int[] j = new int[0];
out.format("i.length = %d, j.length = %d, args.length = %d%n",
i.length, j.length, args.length);
Array.getInt(o, 0); // ArrayIndexOutOfBoundsException
}
}
$ *java ArrayTrouble*
i.length = 0, j.length = 0, args.length = 0
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException
at java.lang.reflect.Array.getInt(Native Method)
at ArrayTrouble.main(ArrayTrouble.java:11)
提示: 可以有没有元素的数组(空数组)。在常见代码中只有少数情况下会看到它们,但它们可能会在反射中无意中出现。当然,无法设置/获取空数组的值,因为会抛出ArrayIndexOutOfBoundsException
。
如果尝试缩小范围会导致IllegalArgumentException
ArrayTroubleToo
示例包含的代码会失败,因为它尝试执行一个可能会丢失数据的操作:
import java.lang.reflect.Array;
import static java.lang.System.out;
public class ArrayTroubleToo {
public static void main(String... args) {
Object o = new int[2];
Array.setShort(o, 0, (short)2); // widening, succeeds
Array.setLong(o, 1, 2L); // narrowing, fails
}
}
$ *java ArrayTroubleToo*
Exception in thread "main" java.lang.IllegalArgumentException: argument type
mismatch
at java.lang.reflect.Array.setLong(Native Method)
at ArrayTroubleToo.main(ArrayTroubleToo.java:9)
提示: Array.set*()
和 Array.get*()
方法将执行自动扩展转换,但如果尝试进行缩小转换,则会抛出 IllegalArgumentException
。有关扩展和缩小转换的完整讨论,请参阅Java 语言规范,Java SE 7 版,分别查看Widening Primitive Conversion和Narrowing Primitive Conversion部分。
枚举类型
原文:
docs.oracle.com/javase/tutorial/reflect/special/enum.html
枚举是一种语言构造,用于定义类型安全的枚举,当需要固定一组命名值时可以使用。所有枚举隐式扩展 java.lang.Enum
。枚举可以包含一个或多个枚举常量,这些常量定义了枚举类型的唯一实例。枚举声明定义了一个枚举类型,与类非常相似,可以具有字段、方法和构造函数等成员(有一些限制)。
由于枚举是类,反射不需要定义一个显式的java.lang.reflect.Enum
类。枚举特定的反射 API 只有 Class.isEnum()
、Class.getEnumConstants()
和 java.lang.reflect.Field.isEnumConstant()
。涉及枚举的大多数反射操作与任何其他类或成员相同。例如,枚举常量被实现为枚举上的public static final
字段。以下部分展示了如何在枚举中使用 Class
和 java.lang.reflect.Field
。
-
检查枚举 说明了如何检索枚举的常量以及任何其他字段、构造函数和方法
-
使用枚举类型获取和设置字段 展示了如何使用枚举常量值设置和获取字段
-
故障排除描述了与枚举相关的常见错误
有关枚举的介绍,请参阅 枚举类型 课程。
检查枚举
原文:
docs.oracle.com/javase/tutorial/reflect/special/enumMembers.html
反射提供了三个特定于枚举的 API:
Class.isEnum()
表示此类是否表示枚举类型
Class.getEnumConstants()
检索由枚举定义的枚举常量列表,按照它们声明的顺序
java.lang.reflect.Field.isEnumConstant()
表示此字段是否表示枚举类型的元素
有时需要动态检索枚举常量的列表;在非反射代码中,可以通过在枚举上调用隐式声明的静态方法 values()
来实现这一点。 如果枚举类型的实例不可用,则获取可能值列表的唯一方法是调用 Class.getEnumConstants()
,因为无法实例化枚举类型。
给定完全限定名称,EnumConstants
示例显示如何使用 Class.getEnumConstants()
检索枚举中常量的有序列表。
import java.util.Arrays;
import static java.lang.System.out;
enum Eon { HADEAN, ARCHAEAN, PROTEROZOIC, PHANEROZOIC }
public class EnumConstants {
public static void main(String... args) {
try {
Class<?> c = (args.length == 0 ? Eon.class : Class.forName(args[0]));
out.format("Enum name: %s%nEnum constants: %s%n",
c.getName(), Arrays.asList(c.getEnumConstants()));
if (c == Eon.class)
out.format(" Eon.values(): %s%n",
Arrays.asList(Eon.values()));
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
}
输出示例如下。 用户输入以斜体显示。
$ *java EnumConstants java.lang.annotation.RetentionPolicy*
Enum name: java.lang.annotation.RetentionPolicy
Enum constants: [SOURCE, CLASS, RUNTIME]
$ *java EnumConstants java.util.concurrent.TimeUnit*
Enum name: java.util.concurrent.TimeUnit
Enum constants: [NANOSECONDS, MICROSECONDS,
MILLISECONDS, SECONDS,
MINUTES, HOURS, DAYS]
该示例还表明,通过调用 Class.getEnumConstants()
返回的值与在枚举类型上调用 values()
返回的值相同。
$ *java EnumConstants*
Enum name: Eon
Enum constants: [HADEAN, ARCHAEAN,
PROTEROZOIC, PHANEROZOIC]
Eon.values(): [HADEAN, ARCHAEAN,
PROTEROZOIC, PHANEROZOIC]
由于枚举是类,可以使用本教程中描述的字段、方法和构造函数部分中描述的相同反射 API 获取其他信息。 EnumSpy
代码示例说明了如何使用这些 API 获取有关枚举声明的其他信息。 该示例使用 Class.isEnum()
来限制要检查的类集。 它还使用 Field.isEnumConstant()
来区分枚举声明中的枚举常量和其他字段(并非所有字段都是枚举常量)。
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Member;
import java.util.List;
import java.util.ArrayList;
import static java.lang.System.out;
public class EnumSpy {
private static final String fmt = " %11s: %s %s%n";
public static void main(String... args) {
try {
Class<?> c = Class.forName(args[0]);
if (!c.isEnum()) {
out.format("%s is not an enum type%n", c);
return;
}
out.format("Class: %s%n", c);
Field[] flds = c.getDeclaredFields();
List<Field> cst = new ArrayList<Field>(); // enum constants
List<Field> mbr = new ArrayList<Field>(); // member fields
for (Field f : flds) {
if (f.isEnumConstant())
cst.add(f);
else
mbr.add(f);
}
if (!cst.isEmpty())
print(cst, "Constant");
if (!mbr.isEmpty())
print(mbr, "Field");
Constructor[] ctors = c.getDeclaredConstructors();
for (Constructor ctor : ctors) {
out.format(fmt, "Constructor", ctor.toGenericString(),
synthetic(ctor));
}
Method[] mths = c.getDeclaredMethods();
for (Method m : mths) {
out.format(fmt, "Method", m.toGenericString(),
synthetic(m));
}
// production code should handle this exception more gracefully
} catch (ClassNotFoundException x) {
x.printStackTrace();
}
}
private static void print(List<Field> lst, String s) {
for (Field f : lst) {
out.format(fmt, s, f.toGenericString(), synthetic(f));
}
}
private static String synthetic(Member m) {
return (m.isSynthetic() ? "[ synthetic ]" : "");
}
}
$ *java EnumSpy java.lang.annotation.RetentionPolicy*
Class: class java.lang.annotation.RetentionPolicy
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.SOURCE
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.CLASS
Constant: public static final java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.RUNTIME
Field: private static final java.lang.annotation.RetentionPolicy[]
java.lang.annotation.RetentionPolicy. [ synthetic ]
Constructor: private java.lang.annotation.RetentionPolicy()
Method: public static java.lang.annotation.RetentionPolicy[]
java.lang.annotation.RetentionPolicy.values()
Method: public static java.lang.annotation.RetentionPolicy
java.lang.annotation.RetentionPolicy.valueOf(java.lang.String)
输出显示,java.lang.annotation.RetentionPolicy
的声明仅包含三个枚举常量。枚举常量暴露为public static final
字段。字段、构造函数和方法是由编译器生成的。$VALUES
字段与values()
方法的实现有关。
注意:出于各种原因,包括支持枚举类型的演变,枚举常量的声明顺序很重要。Class.getFields()
和Class.getDeclaredFields()
不能保证返回值的顺序与声明源代码中的顺序匹配。如果应用程序需要排序,请使用Class.getEnumConstants()
。
对于java.util.concurrent.TimeUnit
的输出显示,更复杂的枚举是可能的。这个类包括几个方法以及额外声明为static final
的字段,这些字段不是枚举常量。
$ java EnumSpy java.util.concurrent.TimeUnit
Class: class java.util.concurrent.TimeUnit
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.NANOSECONDS
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.MICROSECONDS
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.MILLISECONDS
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.SECONDS
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.MINUTES
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.HOURS
Constant: public static final java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.DAYS
Field: static final long java.util.concurrent.TimeUnit.C0
Field: static final long java.util.concurrent.TimeUnit.C1
Field: static final long java.util.concurrent.TimeUnit.C2
Field: static final long java.util.concurrent.TimeUnit.C3
Field: static final long java.util.concurrent.TimeUnit.C4
Field: static final long java.util.concurrent.TimeUnit.C5
Field: static final long java.util.concurrent.TimeUnit.C6
Field: static final long java.util.concurrent.TimeUnit.MAX
Field: private static final java.util.concurrent.TimeUnit[]
java.util.concurrent.TimeUnit. [ synthetic ]
Constructor: private java.util.concurrent.TimeUnit()
Constructor: java.util.concurrent.TimeUnit
(java.lang.String,int,java.util.concurrent.TimeUnit)
[ synthetic ]
Method: public static java.util.concurrent.TimeUnit
java.util.concurrent.TimeUnit.valueOf(java.lang.String)
Method: public static java.util.concurrent.TimeUnit[]
java.util.concurrent.TimeUnit.values()
Method: public void java.util.concurrent.TimeUnit.sleep(long)
throws java.lang.InterruptedException
Method: public long java.util.concurrent.TimeUnit.toNanos(long)
Method: public long java.util.concurrent.TimeUnit.convert
(long,java.util.concurrent.TimeUnit)
Method: abstract int java.util.concurrent.TimeUnit.excessNanos
(long,long)
Method: public void java.util.concurrent.TimeUnit.timedJoin
(java.lang.Thread,long) throws java.lang.InterruptedException
Method: public void java.util.concurrent.TimeUnit.timedWait
(java.lang.Object,long) throws java.lang.InterruptedException
Method: public long java.util.concurrent.TimeUnit.toDays(long)
Method: public long java.util.concurrent.TimeUnit.toHours(long)
Method: public long java.util.concurrent.TimeUnit.toMicros(long)
Method: public long java.util.concurrent.TimeUnit.toMillis(long)
Method: public long java.util.concurrent.TimeUnit.toMinutes(long)
Method: public long java.util.concurrent.TimeUnit.toSeconds(long)
Method: static long java.util.concurrent.TimeUnit.x(long,long,long)
使用枚举类型获取和设置字段
原文:
docs.oracle.com/javase/tutorial/reflect/special/enumSetGet.html
存储枚举的字段与任何其他引用类型一样设置和检索,使用Field.set()
和Field.get()
。有关访问字段的更多信息,请参阅本教程的 Fields 部分。
考虑一个需要在服务器应用程序中动态修改跟踪级别的应用程序,通常在运行时不允许此更改。假设服务器对象的实例可用。SetTrace
示例展示了代码如何将枚举的String
表示转换为枚举类型,并检索和设置存储枚举的字段的值。
import java.lang.reflect.Field;
import static java.lang.System.out;
enum TraceLevel { OFF, LOW, MEDIUM, HIGH, DEBUG }
class MyServer {
private TraceLevel level = TraceLevel.OFF;
}
public class SetTrace {
public static void main(String... args) {
TraceLevel newLevel = TraceLevel.valueOf(args[0]);
try {
MyServer svr = new MyServer();
Class<?> c = svr.getClass();
Field f = c.getDeclaredField("level");
f.setAccessible(true);
TraceLevel oldLevel = (TraceLevel)f.get(svr);
out.format("Original trace level: %s%n", oldLevel);
if (oldLevel != newLevel) {
f.set(svr, newLevel);
out.format(" New trace level: %s%n", f.get(svr));
}
// production code should handle these exceptions more gracefully
} catch (IllegalArgumentException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (NoSuchFieldException x) {
x.printStackTrace();
}
}
}
由于枚举常量是单例,可以使用==
和!=
运算符来比较相同类型的枚举常量。
$ *java SetTrace OFF*
Original trace level: OFF
$ *java SetTrace DEBUG*
Original trace level: OFF
New trace level: DEBUG
故障排除
原文:
docs.oracle.com/javase/tutorial/reflect/special/enumTrouble.html
以下示例展示了在使用枚举类型时可能遇到的问题。
尝试实例化枚举类型时出现 IllegalArgumentException
正如前面提到的,实例化枚举类型是被禁止的。EnumTrouble
示例尝试这样做。
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import static java.lang.System.out;
enum Charge {
POSITIVE, NEGATIVE, NEUTRAL;
Charge() {
out.format("under construction%n");
}
}
public class EnumTrouble {
public static void main(String... args) {
try {
Class<?> c = Charge.class;
Constructor[] ctors = c.getDeclaredConstructors();
for (Constructor ctor : ctors) {
out.format("Constructor: %s%n", ctor.toGenericString());
ctor.setAccessible(true);
ctor.newInstance();
}
// production code should handle these exceptions more gracefully
} catch (InstantiationException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
} catch (InvocationTargetException x) {
x.printStackTrace();
}
}
}
$ *java EnumTrouble*
Constructor: private Charge()
Exception in thread "main" java.lang.IllegalArgumentException: Cannot
reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:511)
at EnumTrouble.main(EnumTrouble.java:22)
提示: 明确实例化枚举是一种编译时错误,因为这将阻止定义的枚举常量保持唯一。这种限制也在反射代码中执行。试图使用默认构造函数实例化类的代码应该首先调用Class.isEnum()
来确定该类是否为枚举。
设置具有不兼容枚举类型的字段时出现 IllegalArgumentException
存储枚举的字段应该设置为适当的枚举类型。(实际上,任何类型的字段都必须设置为兼容的类型。)EnumTroubleToo
示例会产生预期的错误。
import java.lang.reflect.Field;
enum E0 { A, B }
enum E1 { A, B }
class ETest {
private E0 fld = E0.A;
}
public class EnumTroubleToo {
public static void main(String... args) {
try {
ETest test = new ETest();
Field f = test.getClass().getDeclaredField("fld");
f.setAccessible(true);
f.set(test, E1.A); // IllegalArgumentException
// production code should handle these exceptions more gracefully
} catch (NoSuchFieldException x) {
x.printStackTrace();
} catch (IllegalAccessException x) {
x.printStackTrace();
}
}
}
$ *java EnumTroubleToo*
Exception in thread "main" java.lang.IllegalArgumentException: Can not set E0
field ETest.fld to E1
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:146)
at sun.reflect.UnsafeFieldAccessorImpl.throwSetIllegalArgumentException
(UnsafeFieldAccessorImpl.java:150)
at sun.reflect.UnsafeObjectFieldAccessorImpl.set
(UnsafeObjectFieldAccessorImpl.java:63)
at java.lang.reflect.Field.set(Field.java:657)
at EnumTroubleToo.main(EnumTroubleToo.java:16)
提示: 严格来说,将类型为X
的字段设置为类型为Y
的值只有在以下语句成立时才能成功:
X.class.isAssignableFrom(Y.class) == true
代码可以修改以执行以下测试,以验证类型是否兼容:
if (f.getType().isAssignableFrom(E0.class))
// compatible
else
// expect IllegalArgumentException
教程:自定义网络
原文:
docs.oracle.com/javase/tutorial/networking/index.html
Java 平台备受推崇,部分原因是其适用于编写使用和与互联网资源以及万维网进行交互的程序。事实上,兼容 Java 的浏览器极大地利用了 Java 平台的这种能力,在互联网上传输和运行小程序。
本教程将引导您了解编写可在互联网上使用的 Java 应用程序和小程序的复杂性。
有两个部分。第一部分描述了 Java 平台的网络功能,您可能已经在不知不觉中使用网络。第二部分简要概述了网络,使您熟悉在阅读如何使用 URL、套接字和数据报之前应了解的术语和概念。
讨论了您的 Java 程序如何使用 URL 来访问互联网上的信息。URL(统一资源定位符)是互联网上资源的地址。您的 Java 程序可以使用 URL 来连接并检索网络上的信息。本课程提供了 URL 的更完整定义,并向您展示如何创建和解析 URL,如何打开与 URL 的连接,以及如何从该连接读取和写入。
解释了如何使用套接字,以便您的程序可以与网络上的其他程序通信。套接字是运行在网络上的两个程序之间的双向通信链路的一个端点。本课程向您展示了客户端如何连接到标准服务器 Echo 服务器,并通过套接字与其通信。然后,它详细介绍了一个完整客户端/服务器示例的细节,展示了如何实现客户端和服务器端的客户端/服务器对。
逐步带您完成一个使用数据报进行通信的简单客户端/服务器示例。然后,它挑战您使用多播套接字重新编写示例。
解释了为什么您可能想要访问网络接口参数以及如何做到这一点。它提供了如何列出分配给计算机的所有 IP 地址以及其他有用信息的示例,例如接口是否正在运行。
讨论了如何使用 cookies 在客户端和服务器之间创建会话,以及如何在 HTTP URL 连接中利用 cookies。
安全考虑:
请注意,网络通信需经当前安全管理器批准。安全管理器 描述了安全管理器是什么以及它如何影响您的应用程序。有关 JDK 提供的安全功能的一般信息,请参考 Java SE 中的安全功能。
下面课程中的示例程序涵盖了 URL、套接字和数据报,它们都是独立的应用程序,默认情况下没有安全管理器。如果将这些应用程序转换为小程序,根据运行它们的浏览器或查看器的不同,它们可能无法在网络上通信。请参阅 小程序的功能和限制 以获取有关小程序所受安全限制的信息。
课程:网络概述
原文:
docs.oracle.com/javase/tutorial/networking/overview/index.html
在接下来几节的示例之前,您应该对一些网络基础知识有所了解。此外,为了增强您的信心,我们还包括了一个回顾您可能已经了解的关于在 Java 中进行网络操作的部分。
您可能已经了解的关于在 Java 中进行网络操作的内容
如果您已经在本教程的其他部分工作过,那么您可能已经通过互联网加载了一个小程序,并且可能已经将网络中的图像加载到通过网络运行的小程序中。所有这些都是使用网络,而您已经知道如何做。本页突出了在本教程的其他部分和课程中涵盖的网络功能,这些功能您可能已经熟悉,并提供链接到讨论这些功能的页面。
网络基础知识
您将学习关于 TCP、UDP、套接字、数据报和端口的知识,以充分利用本教程中剩余课程的内容。如果您已经熟悉这些概念,可以随意跳过本节。
你可能已经了解的关于 Java 网络的知识
原文:
docs.oracle.com/javase/tutorial/networking/overview/alreadyknow.html
“网络”这个词让许多程序员心生恐惧。不要害怕!在 Java 环境中提供的网络功能非常容易使用。事实上,你可能已经在不知不觉中使用网络了!
从网络加载小程序
如果你有一个支持 Java 的浏览器,你肯定已经执行了许多小程序。你运行的小程序是通过 HTML 文件中的特殊标签引用的 - <APPLET>
标签。小程序可以位于任何地方,无论是在本地计算机上还是在互联网上的某个地方。小程序的位置对用户完全不可见。然而,小程序的位置被编码在<APPLET>
标签中。浏览器解码这些信息,找到小程序并运行它。如果小程序位于你自己的机器之外的某台机器上,浏览器必须在运行之前下载小程序。
这是你从 Java 开发环境访问互联网的最高级别。其他人已经花时间编写了一个浏览器,完成了连接到网络和从中获取数据的所有繁重工作,从而使你能够从世界各地运行小程序。
更多信息:
“Hello World!”应用程序展示了如何编写你的第一个小程序并运行它。
Java 小程序系列描述了如何从头到尾编写 Java 小程序。
从 URL 加载图像
如果你尝试编写自己的 Java 小程序和应用程序,你可能已经遇到了 java.net 包中的一个类叫做 URL。这个类代表统一资源定位符,是网络上某些资源的地址。你的小程序和应用程序可以使用 URL 来引用甚至连接到网络上的资源。例如,要从网络加载图像,你的 Java 程序必须首先创建一个包含图像地址的 URL。
这是你可以与互联网互动的下一个最高级别 - 你的 Java 程序获取它想要的东西的地址,为其创建一个 URL,然后使用 Java 开发环境中的某个现有函数来连接到网络并检索资源。
更多信息:
如何使用图标展示了当你有图像的 URL 时如何将其加载到你的 Java 程序中(无论是小程序还是应用程序)。在加载图像之前,你必须创建一个包含资源地址的 URL 对象。
处理 URL,这一系列中的下一课,提供了关于 URL 的完整讨论,包括你的程序如何连接到它们以及如何从连接中读取和写入数据。
网络基础知识
原文:
docs.oracle.com/javase/tutorial/networking/overview/networking.html
在互联网上运行的计算机使用传输控制协议(TCP)或用户数据报协议(UDP)相互通信,如此图所示:
当你编写通过网络进行通信的 Java 程序时,你是在应用层编程。通常情况下,你不需要关心 TCP 和 UDP 层。相反,你可以使用 java.net
包中的类。这些类提供了系统独立的网络通信。然而,为了决定你的程序应该使用哪些 Java 类,你需要了解 TCP 和 UDP 的区别。
TCP
当两个应用程序想要可靠地相互通信时,它们建立连接并在该连接上来回发送数据。这类似于打电话。如果你想和肯塔基州的比阿特丽斯阿姨通话,当你拨打她的电话号码并她接听时,就建立了连接。你通过电话线互相交流数据。像电话公司一样,TCP 保证从连接的一端发送的数据实际上到达另一端,并且按照发送顺序。否则,会报告错误。
TCP 为需要可靠通信的应用程序提供了一对一的通道。超文本传输协议(HTTP)、文件传输协议(FTP)和 Telnet 都是需要可靠通信通道的应用程序的示例。在网络上传输和接收数据的顺序对于这些应用程序的成功至关重要。当使用 HTTP 从 URL 读取数据时,数据必须按照发送顺序接收。否则,你会得到一个混乱的 HTML 文件、一个损坏的 zip 文件或其他无效信息。
定义:
TCP(传输控制协议)是一种基于连接的协议,它在两台计算机之间提供可靠的数据流。
UDP
UDP 协议提供了两个应用程序之间不保证通信的功能。UDP 不像 TCP 那样基于连接。相反,它从一个应用程序发送独立的数据包(称为数据报)到另一个应用程序。发送数据报就像通过邮政服务发送信件一样:交付顺序不重要,也不保证,每个消息都是独立的。
定义:
UDP(用户数据报协议)是一种协议,它发送独立的数据包(称为数据报)从一台计算机到另一台计算机,不保证到达。UDP 不像 TCP 那样基于连接。
对于许多应用程序,可靠性的保证对于信息从连接的一端传输到另一端的成功至关重要。然而,其他形式的通信并不需要如此严格的标准。事实上,它们可能会因为额外的开销或可靠连接可能使服务无效而变慢。
举个例子,考虑一个时钟服务器,当被请求时向其客户端发送当前时间。如果客户端错过了一个数据包,重新发送它并没有意义,因为当客户端第二次接收到时,时间将不正确。如果客户端发出两个请求并且接收到服务器发送的数据包顺序不对,也没关系,因为客户端可以发现数据包顺序不对并发出另一个请求。在这种情况下,TCP 的可靠性是不必要的,因为它会导致性能下降,可能会影响服务的实用性。
另一个不需要可靠通道保证的服务的例子是 ping 命令。ping 命令的目的是测试网络上两个程序之间的通信。事实上,ping 需要知道丢失或顺序错乱的数据包,以确定连接的好坏。可靠通道会使这项服务完全无效。
UDP 协议提供了在网络上两个应用程序之间不保证通信的功能。UDP 不像 TCP 那样基于连接。相反,它从一个应用程序发送独立的数据包到另一个应用程序。发送数据报就像通过邮件服务发送信件一样:交付顺序不重要,也不被保证,每个消息都独立于其他消息。
注意:
许多防火墙和路由器已经配置为不允许 UDP 数据包。如果您在防火墙外部连接到服务时遇到问题,或者客户端无法连接到您的服务,请询问系统管理员是否允许 UDP。
理解端口
一般来说,计算机与网络之间有一个物理连接。所有发送给特定计算机的数据都通过这个连接到达。然而,这些数据可能是针对计算机上运行的不同应用程序的。那么计算机如何知道将数据转发给哪个应用程序呢?通过端口。
通过互联网传输的数据附带有标识计算机和目标端口的寻址信息。计算机通过其 32 位 IP 地址进行标识,IP 使用它将数据传递到网络上的正确计算机。端口由一个 16 位数字进行标识,TCP 和 UDP 使用它将数据传递给正确的应用程序。
在基于连接的通信(如 TCP)中,服务器应用程序将套接字绑定到特定端口号。这样做的效果是向系统注册服务器以接收所有发送到该端口的数据。然后客户端可以在服务器的端口处与服务器会合,如下图所示:
定义:
TCP 和 UDP 协议使用端口将传入的数据映射到计算机上运行的特定进程。
在基于数据报的通信(如 UDP)中,数据报包含其目的地的端口号,UDP 将数据包路由到适当的应用程序,如下图所示:
端口号范围从 0 到 65,535,因为端口由 16 位数字表示。端口号从 0 到 1023 是受限制的;它们保留供 HTTP 和 FTP 等众所周知的服务以及其他系统服务使用。这些端口被称为众所周知的端口。您的应用程序不应尝试绑定到它们。
JDK 中的网络类
通过java.net
中的类,Java 程序可以使用 TCP 或 UDP 在互联网上进行通信。URL
、URLConnection
、Socket
和ServerSocket
类都使用 TCP 在网络上进行通信。DatagramPacket
、DatagramSocket
和MulticastSocket
类用于 UDP。
教程:使用 URL
原文:
docs.oracle.com/javase/tutorial/networking/urls/index.html
URL 是统一资源定位符的缩写。它是指向互联网上资源的引用(地址)。您向您喜爱的 Web 浏览器提供 URL,以便它可以定位互联网上的文件,就像您向邮局提供地址以便它可以找到您的通信对象一样。
与互联网交互的 Java 程序还可以使用 URL 来找到他们希望访问的互联网资源。Java 程序可以使用java.net
包中的一个名为URL
的类来表示 URL 地址。
术语说明:
术语URL可能会产生歧义。它可以指互联网地址或 Java 程序中的URL
对象。在需要明确 URL 含义的情况下,本文使用"URL 地址"来指代互联网地址,使用"URL
对象"来指代程序中URL
类的实例。
什么是 URL?
URL 采用描述如何在互联网上找到资源的字符串形式。URL 有两个主要组成部分:访问资源所需的协议和资源的位置。
创建 URL
在您的 Java 程序中,您可以创建一个表示 URL 地址的 URL 对象。URL 对象始终指向绝对 URL,但可以从绝对 URL、相对 URL 或 URL 组件构造。
解析 URL
过去需要解析 URL 以查找主机名、文件名和其他信息的日子已经一去不复返了。有了有效的 URL 对象,您可以调用其任何访问器方法,从 URL 中获取所有这些信息,而无需进行任何字符串解析!
直接从 URL 读取
本节展示了您的 Java 程序如何使用openStream()
方法从 URL 中读取信息。
连接到 URL
如果您想要的不仅仅是从 URL 中读取信息,您可以通过在 URL 上调用openConnection()
来连接到它。openConnection()
方法返回一个 URLConnection 对象,您可以用它进行更一般的与 URL 通信,如从中读取、向其中写入或查询其内容和其他信息。
从 URLConnection 读取和写入
一些 URL(例如许多与 cgi-bin 脚本相关的 URL)允许您(甚至要求您)向 URL 写入信息。例如,搜索脚本可能要求在执行搜索之前向 URL 写入详细的查询数据。本节展示了如何向 URL 写入信息以及如何获取结果。
什么是 URL?
原文:
docs.oracle.com/javase/tutorial/networking/urls/definition.html
如果您一直在浏览网络,您无疑听说过 URL 这个术语,并且已经使用 URL 从网络访问 HTML 页面。
尽管不完全准确,但通常最容易将 URL 视为全球网络上文件的名称,因为大多数 URL 指向网络上某台机器上的文件。但请记住,URL 也可以指向网络上的其他资源,例如数据库查询和命令输出。
定义:
URL 是统一资源定位符的缩写,是指向互联网上资源的引用(地址)。
URL 有两个主要组成部分:
-
协议标识符:对于 URL
http://example.com
,协议标识符是http
。 -
资源名称:对于 URL
http://example.com
,资源名称是example.com
。
请注意,协议标识符和资源名称之间用冒号和两个正斜杠分隔。协议标识符指示要用于获取资源的协议的名称。此示例使用超文本传输协议(HTTP),通常用于提供超文本文档。HTTP 只是用于访问网络上不同类型资源的许多不同协议之一。其他协议包括文件传输协议(FTP)、Gopher、文件和新闻。
资源名称是资源的完整地址。资源名称的格式完全取决于所使用的协议,但对于许多协议,包括 HTTP,资源名称包含以下一个或多个组件:
主机名
存储资源的机器名称。
文件名
机器上文件的路径名。
端口号
要连接的端口号(通常是可选的)。
参考
指向资源内命名锚点的参考,通常标识文件内的特定位置(通常是可选的)。
对于许多协议,主机名和文件名是必需的,而端口号和参考是可选的。例如,HTTP URL 的资源名称必须指定网络上的服务器(主机名)和该机器上文档的路径(文件名);它还可以指定端口号和参考。
创建 URL
原文:
docs.oracle.com/javase/tutorial/networking/urls/creatingUrls.html
创建 URL
对象的最简单方法是从表示 URL 地址的人类可读形式的 String
创建。这通常是另一个人将用于 URL 的形式。在您的 Java 程序中,您可以使用包含此文本的 String
来创建 URL
对象:
URL myURL = new URL("http://example.com/");
上面创建的 URL
对象表示一个绝对 URL。绝对 URL 包含到达所需资源的所有必要信息。您还可以从相对 URL地址创建 URL
对象。
创建相对于另一个的 URL
相对 URL 只包含足够的信息,以便相对于(或在另一个 URL 的上下文中)到达资源。
相对 URL 规范通常在 HTML 文件中使用。例如,假设您编写了一个名为 JoesHomePage.html
的 HTML 文件。在此页面中,有指向其他页面 PicturesOfMe.html
和 MyKids.html
的链接,这些页面位于与 JoesHomePage.html
相同的机器和目录中。从 JoesHomePage.html
到 PicturesOfMe.html
和 MyKids.html
的链接可以像文件名一样指定,如下所示:
<a href="PicturesOfMe.html">Pictures of Me</a>
<a href="MyKids.html">Pictures of My Kids</a>
这些 URL 地址是相对 URL。也就是说,这些 URL 是相对于包含它们的文件——JoesHomePage.html
来指定的。
在您的 Java 程序中,您可以从相对 URL 规范创建一个 URL
对象。例如,假设您在站点 example.com
知道两个 URL:
http://example.com/pages/page1.html
http://example.com/pages/page2.html
您可以相对于它们的共同基本 URL http://example.com/pages/
创建这些页面的 URL
对象,如下所示:
URL myURL = new URL("http://example.com/pages/");
URL page1URL = new URL(myURL, "page1.html");
URL page2URL = new URL(myURL, "page2.html");
此代码片段使用了一个 URL
构造函数,让您可以从另一个 URL
对象(基础)和相对 URL 规范创建一个 URL
对象。此构造函数的一般形式是:
URL(URL *baseURL*, String *relativeURL*)
第一个参数是指定新 URL
的基础的 URL
对象。第二个参数是指定相对于基础的资源名称的其余部分的 String
。如果 baseURL
为 null,则此构造函数将 relativeURL
视为绝对 URL 规范。相反,如果 relativeURL
是绝对 URL 规范,则构造函数将忽略 baseURL
。
这个构造函数也适用于为文件中的命名锚点(也称为引用)创建 URL
对象。例如,假设 page1.html
文件在文件底部有一个名为 BOTTOM
的命名锚点。您可以使用相对 URL 构造函数为其创建一个 URL
对象,如下所示:
URL page1BottomURL = new URL(page1URL,"#BOTTOM");
其他 URL 构造函数
URL
类提供了另外两个用于创建 URL
对象的构造函数。当您使用具有主机名、文件名、端口号和 URL 资源名称部分中的引用组件的 URL(如 HTTP URL)时,这些构造函数很有用。当您没有包含完整 URL 规范的 String,但您知道 URL 的各个组件时,这两个构造函数很有用。
例如,假设您设计一个类似文件浏览面板的网络浏览面板,允许用户选择协议、主机名、端口号和文件名。您可以从面板的组件构造一个URL
。第一个构造函数从协议、主机名和文件名创建一个URL
对象。以下代码片段创建一个URL
到example.com
站点的page1.html
文件:
new URL("http", "example.com", "/pages/page1.html");
这等同于
new URL("http://example.com/pages/page1.html");
第一个参数是协议,第二个是主机名,最后一个是文件的路径名。请注意,文件名包含一个斜杠在开头。这表示文件名是从主机的根目录指定的。
最终的URL
构造函数将端口号添加到前一个构造函数中使用的参数列表中:
URL gamelan = new URL("http", "example.com", 80, "pages/page1.html");
这将为以下 URL 创建一个URL
对象:
http://example.com:80/pages/page1.html
如果使用这些构造函数之一构造URL
对象,您可以通过使用URL
对象的toString
方法或等效的toExternalForm
方法获得包含完整 URL 地址的String
。
具有特殊字符的 URL 地址
一些 URL 地址包含特殊字符,例如空格字符。就像这样:
http://example.com/hello world/
在将这些字符传递给 URL 构造函数之前,它们需要进行编码以使其合法。
URL url = new URL("http://example.com/hello%20world");
在这个例子中编码特殊字符很容易,因为只有一个字符需要编码,但是对于具有多个这些字符的 URL 地址,或者在编写代码时不确定需要访问哪些 URL 地址时,您可以使用java.net.URI
类的多参数构造函数来自动处理编码。
URI uri = new URI("http", "example.com", "/hello world/", "");
然后将 URI 转换为 URL。
URL url = uri.toURL();
MalformedURLException
四个URL
构造函数中的每一个如果构造函数的参数引用null
或未知协议,则会抛出MalformedURLException
。通常,您希望通过将 URL 构造函数语句嵌入到try
/catch
对中来捕获和处理此异常,就像这样:
try {
URL myURL = new URL(...);
}
catch (MalformedURLException e) {
// *exception handler code here*
// ...
}
有关处理异常的信息,请参阅异常。
注意:
URL
是“一次写入”对象。一旦创建了URL
对象,就无法更改其任何属性(协议、主机名、文件名或端口号)。
解析 URL
原文:
docs.oracle.com/javase/tutorial/networking/urls/urlInfo.html
URL
类提供了几种方法,让您可以查询URL
对象。您可以使用这些访问器方法从 URL 中获取协议、权限、主机名、端口号、路径、查询、文件名和引用:
getProtocol
返回 URL 的协议标识符组件。
getAuthority
返回 URL 的权限组件。
getHost
返回 URL 的主机名组件。
getPort
返回 URL 的端口号组件。getPort
方法返回一个整数,即端口号。如果端口未设置,getPort
返回-1。
getPath
返回此 URL 的路径组件。
getQuery
返回此 URL 的查询组件。
getFile
返回 URL 的文件名组件。getFile
方法返回与getPath
相同的内容,再加上getQuery
的值(如果有的话)。
getRef
返回 URL 的引用组件。
注意:
请记住,并非所有 URL 地址都包含这些组件。URL 类提供这些方法是因为 HTTP URL 包含这些组件,可能是最常用的 URL。URL 类在某种程度上是以 HTTP 为中心的。
您可以使用这些get*XXX*
方法获取有关 URL 的信息,而不管您使用哪种构造函数创建 URL 对象。
URL 类以及这些访问器方法使您永远不必再解析 URL!只需给定任何 URL 的字符串规范,创建一个新的 URL 对象,并调用所需信息的任何访问器方法即可。这个小例子程序从字符串规范创建一个 URL,然后使用 URL 对象的访问器方法解析 URL:
import java.net.*;
import java.io.*;
public class ParseURL {
public static void main(String[] args) throws Exception {
URL aURL = new URL("http://example.com:80/docs/books/tutorial"
+ "/index.html?name=networking#DOWNLOADING");
System.out.println("protocol = " + aURL.getProtocol());
System.out.println("authority = " + aURL.getAuthority());
System.out.println("host = " + aURL.getHost());
System.out.println("port = " + aURL.getPort());
System.out.println("path = " + aURL.getPath());
System.out.println("query = " + aURL.getQuery());
System.out.println("filename = " + aURL.getFile());
System.out.println("ref = " + aURL.getRef());
}
}
下面是程序显示的输出:
protocol = http
authority = example.com:80
host = example.com
port = 80
path = /docs/books/tutorial/index.html
query = name=networking
filename = /docs/books/tutorial/index.html?name=networking
ref = DOWNLOADING
直接从 URL 读取
原文:
docs.oracle.com/javase/tutorial/networking/urls/readingURL.html
成功创建URL
后,您可以调用URL
的openStream()
方法来获取一个流,从中可以读取 URL 的内容。openStream()
方法返回一个java.io.InputStream
对象,因此从 URL 读取就像从输入流读取一样简单。
以下的小型 Java 程序使用openStream()
来在 URL http://www.oracle.com/
上获取输入流。然后在输入流上打开一个BufferedReader
,并从BufferedReader
读取,从而从 URL 读取。所有读取的内容都被复制到标准输出流:
import java.net.*;
import java.io.*;
public class URLReader {
public static void main(String[] args) throws Exception {
URL oracle = new URL("http://www.oracle.com/");
BufferedReader in = new BufferedReader(
new InputStreamReader(oracle.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
}
}
当您运行该程序时,您应该在命令窗口中看到从http://www.oracle.com/
位置的 HTML 文件中滚动显示的 HTML 命令和文本内容。或者,程序可能会挂起,或者您可能会看到异常堆栈跟踪。如果出现后两种情况中的任何一种,您可能需要设置代理主机以便程序可以找到 Oracle 服务器。
连接到 URL
原文:
docs.oracle.com/javase/tutorial/networking/urls/connecting.html
成功创建URL
对象后,您可以调用URL
对象的openConnection
方法获取一个URLConnection
对象,或者其协议特定的子类之一,例如java.net.HttpURLConnection
您可以使用此URLConnection
对象在连接之前设置参数和一般请求属性。仅当调用URLConnection.connect
方法时才会启动与 URL 表示的远程对象的连接。这样做时,您正在初始化您的 Java 程序与网络上的 URL 之间的通信链接。例如,以下代码打开到example.com
站点的连接:
try {
URL myURL = new URL("http://example.com/");
URLConnection myURLConnection = myURL.openConnection();
myURLConnection.connect();
}
catch (MalformedURLException e) {
// new URL() failed
// ...
}
catch (IOException e) {
// openConnection() failed
// ...
}
每次调用此 URL 的协议处理程序的openConnection
方法都会创建一个新的URLConnection
对象。
您并不总是需要显式调用connect
方法来启动连接。依赖于连接的操作,如getInputStream
、getOutputStream
等,将在必要时隐式执行连接。
现在您已成功连接到您的 URL,您可以使用URLConnection
对象执行诸如从连接中读取或写入的操作。接下来的部分将向您展示如何操作。
从 URLConnection 读取和写入
原文:
docs.oracle.com/javase/tutorial/networking/urls/readingWriting.html
URLConnection
类包含许多方法,让您可以通过网络与 URL 通信。URLConnection
是一个以 HTTP 为中心的类;也就是说,当您使用 HTTP URL 时,其中许多方法才有用。然而,大多数 URL 协议允许您从连接中读取和写入。本节描述了这两个功能。
从 URLConnection 读取
以下程序执行与直接从 URL 读取中显示的URLReader
程序相同的功能。
然而,与直接从 URL 获取输入流不同,此程序显式检索URLConnection
对象并从连接获取输入流。通过调用getInputStream
隐式打开连接。然后,像URLReader
一样,此程序在输入流上创建一个BufferedReader
并从中读取。粗体语句突出了此示例与先前示例之间的区别:
import java.net.*;
import java.io.*;
public class URLConnectionReader {
public static void main(String[] args) throws Exception {
URL oracle = new URL("http://www.oracle.com/");
URLConnection yc = oracle.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(
yc.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
System.out.println(inputLine);
in.close();
}
}
该程序的输出与直接从 URL 打开流的程序的输出相同。您可以使用任一方式从 URL 读取。但是,与直接从 URL 读取不同,从URLConnection
读取可能更有用。这是因为您可以同时将URLConnection
对象用于其他任务(如向 URL 写入)。
再次,如果程序挂起或出现错误消息,您可能需要设置代理主机,以便程序可以找到 Oracle 服务器。
向 URLConnection 写入
许多 HTML 页面包含表单——文本字段和其他 GUI 对象,让您输入要发送到服务器的数据。在输入所需信息并通过单击按钮启动查询后,您的 Web 浏览器通过网络将数据写入 URL。在另一端,服务器接收数据,处理数据,然后向您发送响应,通常以新的 HTML 页面形式。
这些 HTML 表单中的许多使用 HTTP POST 方法将数据发送到服务器。因此,向 URL 写入通常称为向 URL 发布。服务器识别 POST 请求并读取从客户端发送的数据。
要使 Java 程序与服务器端进程交互,它只需能够写入 URL,从而向服务器提供数据。它可以通过以下步骤实现:
-
创建一个
URL
。 -
检索
URLConnection
对象。 -
在
URLConnection
上设置输出功能。 -
打开到资源的连接。
-
从连接获取输出流。
-
写入输出流。
-
关闭输出流。
这里有一个名为ReverseServlet
的小servlet
(或者如果你更喜欢一个 cgi-bin 脚本)。您可以使用这个 servlet 来测试以下示例程序。
运行在容器中的 servlet 从其 InputStream 读取,反转字符串,并将其写入其 OutputStream。servlet 需要形式为string=string_to_reverse
的输入,其中string_to_reverse
是您想要以相反顺序显示其字符的字符串。
这是一个通过URLConnection
在网络上运行ReverseServlet
的示例程序:
import java.io.*;
import java.net.*;
public class Reverse {
public static void main(String[] args) throws Exception {
if (args.length != 2) {
System.err.println("Usage: java Reverse "
+ "http://<location of your servlet/script>"
+ " string_to_reverse");
System.exit(1);
}
String stringToReverse = URLEncoder.encode(args[1], "UTF-8");
URL url = new URL(args[0]);
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(
connection.getOutputStream());
out.write("string=" + stringToReverse);
out.close();
BufferedReader in = new BufferedReader(
new InputStreamReader(
connection.getInputStream()));
String decodedString;
while ((decodedString = in.readLine()) != null) {
System.out.println(decodedString);
}
in.close();
}
}
让我们来检查程序,看看它是如何工作的。首先,程序处理其命令行参数:
if (args.length != 2) {
System.err.println("Usage: java Reverse "
+ "http://*<location of your servlet/script>*"
+ " string_to_reverse");
System.exit(1);
}
String stringToReverse = URLEncoder.encode(args[1], "UTF-8");
这些语句确保用户向程序提供两个且仅有两个命令行参数。命令行参数是ReverseServlet
的位置和将被反转的字符串。它可能包含空格或其他非字母数字字符。这些字符必须进行编码,因为字符串在传输到服务器时会被处理。URLEncoder
类的方法对字符进行编码。
接下来,程序创建URL
对象,并设置连接以便可以向其写入:
URL url = new URL(args[0]);
URLConnection connection = url.openConnection();
connection.setDoOutput(true);
程序然后在连接上创建一个输出流,并在其上打开一个OutputStreamWriter
:
OutputStreamWriter out = new OutputStreamWriter(connection.getOutputStream());
如果 URL 不支持输出,getOutputStream
方法会抛出UnknownServiceException
。如果 URL 支持输出,则此方法返回一个连接到服务器端 URL 的输入流的输出流 —— 客户端的输出是服务器的输入。
接下来,程序向输出流写入所需的信息并关闭流:
out.write("string=" + stringToReverse);
out.close();
这段代码使用write
方法向输出流写入数据。因此你可以看到,向 URL 写入数据就像向流写入数据一样简单。客户端输出流中写入的数据是服务器端 servlet 的输入。Reverse
程序通过在要反转的编码字符串前添加string=
来构造脚本所需的输入形式。
servlet 读取您写入的信息,对字符串值执行反转操作,然后将其发送回给您。现在您需要读取服务器发送回的字符串。Reverse
程序这样做:
BufferedReader in = new BufferedReader(
new InputStreamReader(
connection.getInputStream()));
String decodedString;
while ((decodedString = in.readLine()) != null) {
System.out.println(decodedString);
}
in.close();
如果您的ReverseServlet
位于http://example.com/servlet/ReverseServlet
,那么当您使用Reverse
程序运行时
http://example.com/servlet/ReverseServlet "Reverse Me"
作为参数(包括双引号),你应该看到这个输出:
Reverse Me
reversed is:
eM esreveR
教程:关于套接字的一切
原文:
docs.oracle.com/javase/tutorial/networking/sockets/index.html
URL
和URLConnection
提供了一个相对高级的机制,用于访问互联网上的资源。有时,您的程序需要更低级别的网络通信,例如,当您想编写一个客户端-服务器应用程序时。
在客户端-服务器应用程序中,服务器提供某些服务,比如处理数据库查询或发送当前股票价格。客户端使用服务器提供的服务,要么将数据库查询结果显示给用户,要么向投资者提供股票购买建议。客户端和服务器之间发生的通信必须是可靠的。也就是说,不能丢失任何数据,并且必须按照服务器发送的顺序到达客户端。
TCP 提供了一个可靠的点对点通信通道,用于互联网上的客户端-服务器应用程序之间的通信。要通过 TCP 进行通信,客户端程序和服务器程序要建立连接。每个程序将套接字绑定到连接的端点。为了通信,客户端和服务器分别从绑定到连接的套接字读取和写入。
什么是套接字?
套接字是网络上运行的两个程序之间的双向通信链路的一个端点。Socket 类用于表示客户端程序和服务器程序之间的连接。java.net 包提供了两个类——Socket 和 ServerSocket——分别实现连接的客户端部分和服务器端部分。
从套接字读取和写入
本页包含一个小例子,演示了客户端程序如何从套接字读取和写入。
编写客户端/服务器对
前一页展示了一个示例,演示了如何编写一个与现有服务器通过 Socket 对象交互的客户端程序。本页将向您展示如何编写一个实现连接的另一端的程序——服务器程序。
什么是套接字?
原文:
docs.oracle.com/javase/tutorial/networking/sockets/definition.html
通常,服务器在特定计算机上运行,并具有绑定到特定端口号的套接字。服务器只是等待,监听套接字,等待客户端发出连接请求。
在客户端:客户端知道服务器正在运行的机器的主机名和服务器正在监听的端口号。为了发出连接请求,客户端尝试在服务器的机器和端口上与服务器会合。客户端还需要向服务器标识自己,因此它绑定到一个本地端口号,在此连接期间将使用该端口号。这通常由系统分配。
如果一切顺利,服务器接受连接。接受后,服务器会获得一个新的套接字,绑定到相同的本地端口,并且其远程端点设置为客户端的地址和端口。它需要一个新的套接字,以便在继续监听原始套接字以接受连接请求的同时,满足已连接客户端的需求。
在客户端,如果连接被接受,套接字将成功创建,客户端可以使用该套接字与服务器通信。
客户端和服务器现在可以通过写入或从套接字读取来进行通信。
定义:
套接字是网络上两个运行程序之间的双向通信链路的一个端点。套接字绑定到一个端口号,以便 TCP 层可以识别数据要发送到的应用程序。
一个端点是 IP 地址和端口号的组合。每个 TCP 连接可以通过其两个端点唯一标识。这样,您可以在主机和服务器之间建立多个连接。
Java 平台中的java.net
包提供了一个类Socket
,它实现了您的 Java 程序与网络上另一个程序之间的双向连接的一侧。Socket
类位于一个平台相关的实现之上,隐藏了任何特定系统的细节,使您的 Java 程序能够以与平台无关的方式在网络上通信,而不是依赖本机代码。
此外,java.net
包含ServerSocket
类,它实现了服务器可以用来监听并接受客户端连接的套接字。本课程将向您展示如何使用Socket
和ServerSocket
类。
如果你想连接到网络,URL
类及其相关类(URLConnection
,URLEncoder
)可能比套接字类更合适。实际上,URL 是与网络连接的相对高级的方式,并且在其底层实现中使用了套接字。有关通过 URL 连接到网络的信息,请参阅使用 URL。
从套接字读取和写入
译文:
docs.oracle.com/javase/tutorial/networking/sockets/readingWriting.html
让我们看一个简单的示例,说明程序如何使用Socket
类与服务器程序建立连接,然后客户端如何通过套接字向服务器发送数据并从服务器接收数据。
该示例程序实现了一个客户端,EchoClient
,连接到一个回显服务器。回显服务器从其客户端接收数据并将其回显。示例EchoServer
实现了一个回显服务器。(或者,客户端可以连接到支持Echo Protocol的任何主机。)
EchoClient
示例创建一个套接字,从而连接到回显服务器。它从标准输入流中读取用户输入,然后通过将文本写入套接字将该文本转发到回显服务器。服务器通过套接字将输入回显到客户端。客户端程序读取并显示从服务器传回的数据。
请注意,EchoClient
示例既向其套接字写入数据,也从其套接字读取数据,从而向回显服务器发送数据并接收数据。
让我们逐步走过程序并研究有趣的部分。在EchoClient
示例中的try
-with-resources 语句中,以下语句至关重要。这些行建立了客户端和服务器之间的套接字连接,并在套接字上打开了一个PrintWriter
和一个BufferedReader
:
String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);
try (
Socket echoSocket = new Socket(hostName, portNumber); // 1st statement
PrintWriter out = // 2nd statement
new PrintWriter(echoSocket.getOutputStream(), true);
BufferedReader in = // 3rd statement
new BufferedReader(
new InputStreamReader(echoSocket.getInputStream()));
BufferedReader stdIn = // 4th statement
new BufferedReader(
new InputStreamReader(System.in))
)
try
-with 资源语句中的第一条语句创建了一个新的Socket
对象,并命名为echoSocket
。此处使用的Socket
构造函数需要要连接的计算机的名称和端口号。示例程序使用第一个命令行参数作为计算机的名称(主机名),第二个命令行参数作为端口号。当您在计算机上运行此程序时,请确保您使用的主机名是要连接的计算机的完全限定 IP 名称。例如,如果您的回显服务器在计算机echoserver.example.com
上运行,并且正在侦听端口号 7,则如果您想将EchoServer
示例用作您的回显服务器,请先从计算机echoserver.example.com
运行以下命令:
java EchoServer 7
后续,使用以下命令运行EchoClient
示例:
java EchoClient echoserver.example.com 7
try
-with 资源语句中的第二个语句获取套接字的输出流,并在其上打开名为out
的PrintWriter
。类似地,第三个语句获取套接字的输入流,并在其上打开名为in
的BufferedReader
。该示例使用读取器和写入器,以便可以通过套接字写入 Unicode 字符。如果您还不熟悉 Java 平台的 I/O 类,请阅读基本 I/O。
程序的下一个有趣部分是while
循环。该循环从标准输入流中逐行读取数据,使用在try
-with 资源语句中创建的BufferedReader
对象stdIn
。然后,该循环立即将该行数据通过写入到与套接字连接的PrintWriter
发送到服务器:
String userInput;
while ((userInput = stdIn.readLine()) != null) {
out.println(userInput);
System.out.println("echo: " + in.readLine());
}
while
循环中的最后一个语句从与套接字连接的BufferedReader
中读取一行信息。readLine
方法会等待服务器将信息回显给EchoClient
。当readline
返回时,EchoClient
将信息打印到标准输出。
while
循环会一直持续,直到用户输入结束符号。也就是说,EchoClient
示例从用户那里读取输入,将其发送到 Echo 服务器,从服务器获取响应,并显示它,直到达到输入结束。 (您可以通过按下Ctrl-C键来输入结束符号。)然后while
循环终止,并且 Java 运行时会自动关闭与套接字和标准输入流连接的读取器和写入器,并关闭与服务器的套接字连接。Java 运行时会自动关闭这些资源,因为它们是在try
-with 资源语句中创建的。Java 运行时会按照它们创建的相反顺序关闭这些资源。(这很好,因为连接到套接字的流应该在套接字本身关闭之前关闭。)
该客户端程序非常简单直接,因为回显服务器实现了一个简单的协议。客户端向服务器发送文本,服务器将其回显。当您的客户端程序与更复杂的服务器(如 HTTP 服务器)通信时,您的客户端程序也会更复杂。但是,基本原理与本程序中的相同:
-
打开一个套接字。
-
打开一个输入流和输出流到套接字。
-
根据服务器的协议从流中读取和写入数据。
-
关闭流。
-
关闭套接字。
仅步骤 3 因客户端而异,取决于服务器。其他步骤基本保持不变。
编写套接字的服务器端
原文:
docs.oracle.com/javase/tutorial/networking/sockets/clientServer.html
本部分向您展示如何编写一个服务器和与之配套的客户端。客户端/服务器对中的服务器提供叩叩笑话。叩叩笑话深受儿童喜爱,通常是糟糕双关语的载体。它们的形式如下:
服务器:"叩叩!"
客户端:"谁在那里?"
服务器:"迪克斯特尔。"
客户端:"迪克斯特尔是谁?"
服务器:"迪克斯特尔大厅挂满冬青树枝。"
客户端:"呻吟。"
该示例由两个独立运行的 Java 程序组成:客户端程序和服务器程序。客户端程序由一个类KnockKnockClient
实现,与前一部分的EchoClient
示例非常相似。服务器程序由两个类实现:KnockKnockServer
和KnockKnockProtocol
。KnockKnockServer
类似于EchoServer
,包含服务器程序的main
方法,并负责监听端口、建立连接以及读取和写入套接字。类KnockKnockProtocol
提供笑话。它跟踪当前笑话、当前状态(发送叩叩、发送提示等),并根据当前状态返回笑话的各个文本部分。该对象实现了协议——客户端和服务器约定用于通信的语言。
以下部分详细介绍了客户端和服务器中每个类的代码,然后演示如何运行它们。
叩叩笑话服务器
本部分将介绍实现叩叩笑话服务器程序KnockKnockServer
的代码。
服务器程序首先通过创建一个新的ServerSocket
对象来监听特定端口(请参见以下代码段中的粗体语句)。运行此服务器时,请选择一个尚未用于其他服务的端口。例如,以下命令启动服务器程序KnockKnockServer
,使其监听端口 4444:
java KnockKnockServer 4444
服务器程序在try
-with-resources 语句中创建ServerSocket
对象:
int portNumber = Integer.parseInt(args[0]);
try (
ServerSocket serverSocket = new ServerSocket(portNumber);
Socket clientSocket = serverSocket.accept();
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
ServerSocket
是java.net
类,提供了客户端/服务器套接字连接的服务器端的系统独立实现。如果ServerSocket
的构造函数无法监听指定端口(例如,端口已被使用),则会抛出异常。在这种情况下,KnockKnockServer
别无选择,只能退出。
如果服务器成功绑定到其端口,则ServerSocket
对象成功创建,服务器继续下一步——接受来自客户端的连接(try
-with-resources 语句中的下一条语句):
clientSocket = serverSocket.accept();
accept
方法会等待,直到客户端在此服务器的主机和端口上启动并请求连接。(假设您在名为knockknockserver.example.com
的计算机上运行了服务器程序KnockKnockServer
。)在这个例子中,服务器正在运行在第一个命令行参数指定的端口号上。当请求连接并成功建立连接时,accept 方法会返回一个新的Socket
对象,该对象绑定到相同的本地端口,并将其远程地址和远程端口设置为客户端的地址和端口。服务器可以通过这个新的Socket
与客户端通信,并继续监听原始ServerSocket
上的客户端连接请求。这个程序的特定版本不会监听更多的客户端连接请求。然而,在支持多个客户端中提供了程序的修改版本。
在服务器成功与客户端建立连接后,使用此代码与客户端通信:
try (
// ...
PrintWriter out =
new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(clientSocket.getInputStream()));
) {
String inputLine, outputLine;
// Initiate conversation with client
KnockKnockProtocol kkp = new KnockKnockProtocol();
outputLine = kkp.processInput(null);
out.println(outputLine);
while ((inputLine = in.readLine()) != null) {
outputLine = kkp.processInput(inputLine);
out.println(outputLine);
if (outputLine.equals("Bye."))
break;
}
该代码执行以下操作:
-
获取套接字的输入和输出流,并在其上打开读取器和写入器。
-
通过向套接字写入来与客户端启动通信(以粗体显示)。
-
通过读取和写入套接字(
while
循环)与客户端通信。
第 1 步已经很熟悉。第 2 步以粗体显示,并值得一些评论。上面代码段中的粗体语句启动了与客户端的对话。代码创建了一个KnockKnockProtocol
对象,该对象跟踪当前笑话,笑话中的当前状态等。
创建KnockKnockProtocol
后,代码调用KnockKnockProtocol
的processInput
方法,以获取服务器发送给客户端的第一条消息。在这个例子中,服务器首先说的是“敲门!”。接下来,服务器将信息写入连接到客户端套接字的PrintWriter
,从而将消息发送给客户端。
第 3 步在while
循环中编码。只要客户端和服务器仍有话要说,服务器就会从套接字中读取并写入,来回发送消息。
服务器通过一个"敲门!敲门!"的方式开始对话,因此服务器必须等待客户端说"谁在那里?"。因此,while
循环在从输入流读取时进行迭代。readLine
方法会等待客户端通过写入内容到其输出流(服务器的输入流)来做出响应。当客户端做出响应时,服务器将客户端的响应传递给KnockKnockProtocol
对象,并要求KnockKnockProtocol
对象提供合适的回复。服务器立即通过连接到套接字的输出流将回复发送给客户端,使用println
方法。如果从KnockKnockServer
对象生成的服务器响应是"再见。",这表示客户端不想再听笑话了,循环结束。
Java 运行时会自动关闭输入和输出流、客户端套接字和服务器套接字,因为它们是在try
-with-resources 语句中创建的。
敲门协议
KnockKnockProtocol
类实现了客户端和服务器用于通信的协议。该类跟踪客户端和服务器在对话中的位置,并为客户端的陈述提供服务器的响应。KnockKnockProtocol
对象包含所有笑话的文本,并确保客户端对服务器的陈述给出正确的回应。当服务器说"敲门!敲门!"时,客户端说"谁在那里?"才是正确的回应。
所有的客户端/服务器对都必须有一些协议来进行通信;否则,来回传递的数据将毫无意义。你自己的客户端和服务器使用的协议完全取决于它们完成任务所需的通信。
敲门客户端
KnockKnockClient
类实现了与KnockKnockServer
通信的客户端程序。KnockKnockClient
基于前一节中的EchoClient
程序从套接字读取和写入,应该对您来说有些熟悉。但我们仍然会查看程序并了解客户端中发生的事情,以及与服务器中发生的情况相比较。
当您启动客户端程序时,服务器应该已经在运行并监听端口,等待客户端请求连接。因此,客户端程序的第一步是打开一个连接到指定主机名和端口上运行的服务器的套接字:
String hostName = args[0];
int portNumber = Integer.parseInt(args[1]);
try (
Socket kkSocket = new Socket(hostName, portNumber);
PrintWriter out = new PrintWriter(kkSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(
new InputStreamReader(kkSocket.getInputStream()));
)
在创建套接字时,KnockKnockClient
示例使用第一个命令行参数的主机名,即运行服务器程序KnockKnockServer
的网络计算机的名称。
KnockKnockClient
示例在创建套接字时使用第二个命令行参数作为端口号。这是一个远程端口号——服务器计算机上的端口号——是KnockKnockServer
正在监听的端口。例如,以下命令在运行KnockKnockServer
程序的计算机上以knockknockserver.example.com
作为服务器程序KnockKnockServer
的计算机名称,4444 作为远程端口号运行KnockKnockClient
示例:
java KnockKnockClient knockknockserver.example.com 4444
客户端的套接字绑定到任何可用的本地端口——客户端计算机上的端口。请记住,服务器也会获得一个新的套接字。如果你使用前面示例中的命令行参数运行KnockKnockClient
示例,那么这个套接字将绑定到你运行KnockKnockClient
示例的计算机上的本地端口号 4444。服务器的套接字和客户端的套接字已连接。
接下来是实现客户端和服务器之间通信的while
循环。服务器先发言,所以客户端必须先听。客户端通过从连接到套接字的输入流中读取来实现这一点。如果服务器发言,它会说“再见”,然后客户端退出循环。否则,客户端将文本显示到标准输出,然后从用户那里读取响应,用户通过标准输入键入。用户键入回车后,客户端通过连接到套接字的输出流将文本发送到服务器。
while ((fromServer = in.readLine()) != null) {
System.out.println("Server: " + fromServer);
if (fromServer.equals("Bye."))
break;
fromUser = stdIn.readLine();
if (fromUser != null) {
System.out.println("Client: " + fromUser);
out.println(fromUser);
}
}
当服务器询问客户端是否想听另一个笑话时,通信结束,客户端说不想听,服务器说“再见”。
客户端会自动关闭其输入和输出流以及套接字,因为它们是在try
-with-resources 语句中创建的。
运行程序
你必须先启动服务器程序。为此,请使用 Java 解释器运行服务器程序,就像运行任何其他 Java 应用程序一样。指定服务器程序监听的端口号作为命令行参数:
java KnockKnockServer 4444
接下来,运行客户端程序。请注意,你可以在网络上的任何计算机上运行客户端;它不必在与服务器相同的计算机上运行。指定运行KnockKnockServer
服务器程序的计算机的主机名和端口号作为命令行参数:
java KnockKnockClient knockknockserver.example.com 4444
如果你太快了,可能会在服务器有机会初始化并开始监听端口之前启动客户端。如果发生这种情况,你将会看到客户端的堆栈跟踪。如果发生这种情况,只需重新启动客户端。
如果在第一个客户端连接到服务器时尝试启动第二个客户端,第二个客户端将会挂起。下一节,支持多个客户端,讨论了支持多个客户端的问题。
当客户端和服务器成功建立连接时,你将在屏幕上看到以下文本显示:
Server: Knock! Knock!
现在,你必须回复:
Who's there?
客户端会回显你输入的内容并将文本发送到服务器。服务器会回复其中一个众多“敲敲”笑话的第一行。现在你的屏幕应该显示如下内容(你输入的文本用粗体表示):
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
现在,你回复:
Turnip who?
再次,客户端会回显你输入的内容并将文本发送到服务器。服务器会回复一个笑话的结尾。现在你的屏幕应该显示如下内容:
Server: Knock! Knock!
Who's there?
Client: Who's there?
Server: Turnip
Turnip who?
Client: Turnip who?
Server: Turnip the heat, it's cold in here! Want another? (y/n)
如果你想听另一个笑话,输入y;如果不想,输入n。如果输入y,服务器会重新开始“敲敲”;如果输入n,服务器会说“再见”,导致客户端和服务器都退出。
如果在任何时候你打错字,KnockKnockServer
对象会捕捉到并回复类似于这样的消息:
Server: You're supposed to say "Who's there?"!
然后服务器会重新开始讲笑话:
Server: Try again. Knock! Knock!
注意,KnockKnockProtocol
对象对拼写和标点符号很在意,但对大小写不在意。
支持多个客户端
为了保持KnockKnockServer
示例简单,我们设计它来监听并处理单个连接请求。然而,多个客户端请求可以进入同一端口,因此也进入同一个ServerSocket
。客户端连接请求在端口处排队,因此服务器必须按顺序接受连接。然而,服务器可以通过使用线程同时为它们提供服务——每个客户端连接一个线程。
这样一个服务器的基本逻辑流程是这样的:
while (true) {
accept a connection;
create a thread to deal with the client;
}
线程根据需要从客户端连接读取和写入。
试一试:
修改KnockKnockServer
以便能够同时为多个客户端提供服务。我们的解决方案由两个类组成:KKMultiServer
和 KKMultiServerThread
。KKMultiServer
永远循环,监听ServerSocket
上的客户端连接请求。当有请求进来时,KKMultiServer
接受连接,创建一个新的KKMultiServerThread
对象来处理它,将从 accept 返回的套接字传递给它,并启动线程。然后服务器继续监听连接请求。KKMultiServerThread
对象通过读取和写入套接字与客户端通信。运行新的“敲敲”服务器KKMultiServer
,然后依次运行几个客户端。
教训:关于数据报的一切
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/index.html
您编写的一些用于在网络上通信的应用程序不需要 TCP 提供的可靠的点对点通道。相反,您的应用程序可能会从一种通信模式中受益,该模式传递独立的信息包,其到达和到达顺序不受保证。
UDP 协议提供了一种网络通信模式,应用程序可以向彼此发送数据包,称为数据报。数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。java.net
包中的DatagramPacket
和DatagramSocket
类使用 UDP 实现了系统独立的数据报通信。
什么是数据报?
数据报是通过网络发送的独立、自包含的消息,其到达、到达时间和内容均不受保证。
编写数据报客户端和服务器
本节将通过一个示例引导您,其中包含两个使用数据报进行通信的 Java 程序。服务器端是一个引用服务器,它监听其DatagramSocket
并在客户端请求时向客户端发送引用。客户端是一个简单的程序,只需向服务器发出请求。
向多个接收方广播
本节修改引用服务器,使其不再在请求时向单个客户端发送引用,而是每分钟向所有正在侦听的客户端广播一条引用。客户端程序必须相应地进行修改。
注意:
许多防火墙和路由器被配置为不允许 UDP 数据包。如果您在防火墙外部连接到服务时遇到问题,或者客户端无法连接到您的服务,请向系统管理员询问是否允许 UDP。
什么是数据报?
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/definition.html
客户端和服务器通过可靠的通道(如 TCP 套接字)进行通信,它们之间有一个专用的点对点通道,或者至少有这种幻觉。为了通信,它们建立连接,传输数据,然后关闭连接。所有通过通道发送的数据都按照发送顺序接收。这由通道保证。
相反,通过数据报通信的应用程序发送和接收完全独立的信息包。这些客户端和服务器没有也不需要专用的点对点通道。数据报的传递到目的地不被保证。它们的到达顺序也不被保证。
定义:
一个数据报是独立的、自包含的消息,通过网络发送,其到达、到达时间和内容都不被保证。
java.net
包含三个类,帮助你编写使用数据报在网络上传输和接收数据包的 Java 程序:DatagramSocket
、DatagramPacket
和MulticastSocket
。一个应用程序可以通过DatagramSocket
发送和接收DatagramPacket
。此外,DatagramPacket
可以广播到多个接收者,所有接收者都监听MulticastSocket
。
编写数据报客户端和服务器
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/clientServer.html
本节中展示的示例由两个应用程序组成:客户端和服务器。服务器持续接收数据报包,每个数据报包都表示客户端请求报价。当服务器接收到数据报时,它会通过数据报包将一行“此刻引用”发送回客户端。
本示例中的客户端应用程序非常简单。它向服务器发送一个数据报包,指示客户端希望接收此刻引用。然后客户端等待服务器发送数据报包作为响应。
两个类实现了服务器应用程序:QuoteServer
和 QuoteServerThread
。一个类实现了客户端应用程序:QuoteClient
。
让我们从包含服务器应用程序的main
方法的类开始研究这些类。使用服务器端应用程序包含QuoteClient
类的小程序版本。
引用服务器类
在这里完整显示的QuoteServer
类包含一个方法:引用服务器应用程序的main
方法。main
方法只是创建一个新的QuoteServerThread
对象并启动它:
import java.io.*;
public class QuoteServer {
public static void main(String[] args) throws IOException {
new QuoteServerThread().start();
}
}
QuoteServerThread
类实现了引用服务器的主要逻辑。
QuoteServerThread
类
创建时,QuoteServerThread
在端口 4445 上创建了一个DatagramSocket
(任意选择)。这是服务器与所有客户端通信的DatagramSocket
。
public QuoteServerThread() throws IOException {
this("QuoteServer");
}
public QuoteServerThread(String name) throws IOException {
super(name);
socket = new DatagramSocket(4445);
try {
in = new BufferedReader(new FileReader("one-liners.txt"));
}
catch (FileNotFoundException e){
System.err.println("Couldn't open quote file. Serving time instead.");
}
}
请记住,某些端口专用于已知服务,您不能使用它们。如果指定一个正在使用的端口,DatagramSocket
的创建将失败。
构造函数还在名为one-liners.txt
的文件上打开了一个BufferedReader
,其中包含引用列表。文件中的每个引用都在单独的一行上。
现在来看QuoteServerThread
的有趣部分:它的run
方法。run
方法覆盖了Thread
类中的run
方法,并为线程提供了实现。有关线程的信息,请参见定义和启动线程。
run
方法包含一个while
循环,只要文件中还有更多引用,循环就会继续。在循环的每次迭代中,线程会等待一个DatagramPacket
通过DatagramSocket
到达。数据包表示客户端的请求。作为对客户端请求的响应,QuoteServerThread
从文件中获取引用,将其放入DatagramPacket
中,并通过DatagramSocket
将其发送到请求的客户端。
让我们首先看一下接收客户端请求的部分:
byte[] buf = new byte[256];
DatagramPacket packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
第一条语句创建了一个字节数组,然后用它创建了一个DatagramPacket
。DatagramPacket
将用于从套接字接收数据报,因为使用了创建它的构造函数。这个构造函数只需要两个参数:一个包含客户端特定数据的字节数组和字节数组的长度。在构造要通过DatagramSocket
发送的DatagramPacket
时,还必须提供数据包目的地的互联网地址和端口号。当我们讨论服务器如何响应客户端请求时,你会看到这一点。
前一个代码片段中的最后一条语句从套接字接收一个数据报(从客户端接收的信息被复制到数据包中)。receive
方法会一直等待,直到接收到一个数据包。如果没有接收到数据包,服务器将不会有进一步的进展,只会等待。
现在假设,服务器收到了客户端请求报价的请求。现在服务器必须做出响应。run
方法中的这部分代码构建了响应:
String dString = null;
if (in == null)
dString = new Date().toString();
else
dString = getNextQuote();
buf = dString.getBytes();
如果由于某种原因未打开报价文件,则in
等于 null。如果是这种情况,报价服务器将提供当天的时间。否则,报价服务器从已打开的文件中获取下一个报价。最后,代码将字符串转换为字节数组。
现在,run
方法使用以下代码向客户端通过DatagramSocket
发送响应:
InetAddress address = packet.getAddress();
int port = packet.getPort();
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
这段代码中的前两条语句分别从客户端接收的数据报中获取了互联网地址和端口号。互联网地址和端口号指示了数据报包的来源。这是服务器必须发送响应的地方。在这个例子中,数据报包的字节数组不包含相关信息。根据报包的到达本身就表示了来自互联网地址和端口号所指示的客户端的请求。
第三条语句创建了一个新的DatagramPacket
对象,用于通过数据报套接字发送数据报消息。你可以通过创建它的构造函数来判断新的DatagramPacket
是用于通过套接字发送数据的。这个构造函数需要四个参数。前两个参数与用于创建接收数据报的构造函数相同:包含从发送方到接收方的消息的字节数组和该数组的长度。接下来的两个参数不同:一个互联网地址和一个端口号。这两个参数是数据报包的目的地的完整地址,必须由数据报的发送方提供。最后一行代码将DatagramPacket
发送出去。
当服务器从报价文件中读取了所有报价后,while
循环终止,run
方法进行清理:
socket.close();
QuoteClient 类
QuoteClient
类实现了QuoteServer
的客户端应用程序。此应用程序向QuoteServer
发送请求,等待响应,并在接收到响应时将其显示到标准输出。让我们详细看看代码。
QuoteClient
类包含一个方法,即客户端应用程序的main
方法。main
方法的顶部声明了几个局部变量供其使用:
int port;
InetAddress address;
DatagramSocket socket = null;
DatagramPacket packet;
byte[] sendBuf = new byte[256];
首先,main
方法处理用于调用QuoteClient
应用程序的命令行参数:
if (args.length != 1) {
System.out.println("Usage: java QuoteClient <hostname>");
return;
}
QuoteClient
应用程序需要一个命令行参数:运行QuoteServer
的机器的名称。
接下来,main
方法创建一个DatagramSocket
:
DatagramSocket socket = new DatagramSocket();
客户端使用一个不需要端口号的构造函数。此构造函数只是将DatagramSocket
绑定到任何可用的本地端口。客户端绑定到的端口号并不重要,因为DatagramPacket
包含寻址信息。服务器从DatagramPacket
中获取端口号并将其响应发送到该端口。
接下来,QuoteClient
程序向服务器发送一个请求:
byte[] buf = new byte[256];
InetAddress address = InetAddress.getByName(args[0]);
DatagramPacket packet = new DatagramPacket(buf, buf.length,
address, 4445);
socket.send(packet);
代码段获取命令行上命名的主机的 Internet 地址(假设服务器运行的机器的名称)。然后使用此InetAddress
和端口号 4445(服务器用于创建其DatagramSocket
的端口号)创建一个发送到该 Internet 地址和端口号的DatagramPacket
。因此,DatagramPacket
将被传递到引用服务器。
请注意,代码创建了一个带有空字节数组的DatagramPacket
。字节数组为空,因为此数据报包仅仅是向服务器请求信息。服务器发送响应所需的所有信息—地址和端口号—自动包含在数据包中。
接下来,客户端从服务器获取响应并显示出来:
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData(), 0, packet.getLength());
System.out.println("Quote of the Moment: " + received);
要从服务器获取响应,客户端创建一个“接收”数据包,并使用DatagramSocket
的接收方法从服务器接收回复。接收方法会等待直到一个发送给客户端的数据包通过套接字传输过来。请注意,如果服务器的回复某种原因丢失,客户端将永远等待,因为数据报模型没有保证策略。通常,客户端会设置一个计时器,以便不会永远等待回复;如果没有收到回复,计时器会触发,客户端会重新发送。
当客户端从服务器接收到回复时,客户端使用getData
方法从数据包中检索数据。然后客户端将数据转换为字符串并显示出来。
运行服务器和客户端
在成功编译服务器和客户端程序之后,您可以运行它们。必须先运行服务器程序。只需使用 Java 解释器并指定QuoteServer
类名。
一旦服务器启动,你可以运行客户端程序。记得用一个命令行参数来运行客户端程序:QuoteServer
运行的主机名。
当客户端发送请求并从服务器接收到响应后,你应该看到类似于以下输出:
Quote of the Moment:
Good programming is 99% sweat and 1% coffee.
向多个接收者广播
原文:
docs.oracle.com/javase/tutorial/networking/datagrams/broadcasting.html
除了DatagramSocket
,它让程序之间可以发送数据包,java.net 还包括一个名为MulticastSocket
的类。这种类型的套接字用于客户端监听服务器广播给多个客户端的数据包。
让我们重写引用服务器,使其可以向多个接收者广播DatagramPacket
。而不是向请求的特定客户端发送引用,新服务器现在需要定期广播引用。客户端需要进行修改,以便被动地监听引用,并在MulticastSocket
上这样做。
这个示例由三个类组成,这三个类是前一个示例中三个类的修改版本:MulticastServer
,MulticastServerThread
,和MulticastClient
。本讨论重点介绍了这些类的有趣部分。
这是服务器主程序的新版本。这段代码与之前版本QuoteServer
的不同之处用粗体显示出来:
import java.io.*;
public class MulticastServer {
public static void main(String[] args) throws IOException {
new MulticastServerThread().start();
}
}
基本上,服务器得到了一个新的名称,并创建了一个MulticastServerThread
而不是QuoteServerThread
。现在让我们看看MulticastServerThread
,其中包含服务器的核心。这是它的类声明:
public class MulticastServerThread extends QuoteServerThread {
// ...
}
我们将这个类作为QuoteServerThread
的子类,以便它可以使用构造函数,并继承一些成员变量和getNextQuote
方法。回想一下,QuoteServerThread
创建了一个绑定到端口 4445 的DatagramSocket
并打开引用文件。在这个示例中,DatagramSocket
的端口号实际上并不重要,因为客户端从不向服务器发送任何内容。
MulticastServerThread
中唯一显式实现的方法是其run
方法。这个run
方法与QuoteServerThread
中的方法的不同之处用粗体显示出来:
public void run() {
while (moreQuotes) {
try {
byte[] buf = new byte[256];
// don't wait for request...just send a quote
String dString = null;
if (in == null)
dString = new Date().toString();
else
dString = getNextQuote();
buf = dString.getBytes();
InetAddress group = InetAddress.getByName("203.0.113.0");
DatagramPacket packet;
packet = new DatagramPacket(buf, buf.length, group, 4446);
socket.send(packet);
try {
sleep((long)Math.random() * FIVE_SECONDS);
}
catch (InterruptedException e) { }
}
catch (IOException e) {
e.printStackTrace();
moreQuotes = false;
}
}
socket.close();
}
有趣的变化是如何构造DatagramPacket
,特别是用于构造DatagramPacket
的InetAddress
和端口。回想一下,前一个示例从客户端发送到服务器的数据包中检索了InetAddress
和端口号。这是因为服务器需要直接回复给客户端。现在,服务器需要向多个客户端发送数据。因此,这次InetAddress
和端口号都是硬编码的。
硬编码的端口号是 4446(客户端必须将MulticastSocket
绑定到此端口)。DatagramPacket
的硬编码InetAddress
是"203.0.113.0",是一个组标识符(而不是单个客户端所在机器的互联网地址)。这个特定地址是从保留给此目的的地址中任意选择的。
以这种方式创建的DatagramPacket
将发送到所有监听端口号为 4446 且属于"203.0.113.0"组的客户端。
要监听端口号 4446,新创建的客户端程序只需使用该端口号创建其MulticastSocket
。要成为"203.0.113.0"组的成员,客户端调用MulticastSocket
的joinGroup
方法,并提供标识该组的InetAddress
。现在,客户端已设置好接收发送到指定端口和组的DatagramPacket
。以下是新客户端程序中相关的代码(还对其进行了重写,以 passively 接收引用而不是主动请求)。粗体语句是与MulticastSocket
交互的语句:
MulticastSocket socket = new MulticastSocket(4446);
InetAddress group = InetAddress.getByName("203.0.113.0");
socket.joinGroup(group);
DatagramPacket packet;
for (int i = 0; i < 5; i++) {
byte[] buf = new byte[256];
packet = new DatagramPacket(buf, buf.length);
socket.receive(packet);
String received = new String(packet.getData());
System.out.println("Quote of the Moment: " + received);
}
socket.leaveGroup(group);
socket.close();
注意,服务器使用DatagramSocket
广播客户端接收的DatagramPacket
,而客户端使用MulticastSocket
。或者,它可以使用MulticastSocket
。服务器用于发送DatagramPacket
的套接字并不重要。在广播数据包时重要的是DatagramPacket
中包含的寻址信息,以及客户端用于监听的套接字
试试这个:
运行MulticastServer
和几个客户端。观察客户端如何都收到相同的引用。
课程:程序化访问网络参数
原文:
docs.oracle.com/javase/tutorial/networking/nifs/index.html
系统通常会同时运行多个活动网络连接,比如有线以太网,802.11 b/g
(无线)和蓝牙。一些应用程序可能需要访问这些信息,以便在特定连接上执行特定的网络活动。
java.net.NetworkInterface
类提供了访问这些信息的途径。
本课程将指导您了解此类的一些常见用法,并提供列出机器上所有网络接口及其 IP 地址和状态的示例。
什么是网络接口?
本页面描述了一个网络接口,并解释了为什么您可能想要使用它。
检索网络接口
本页面包含一个示例,演示了客户端程序如何检索机器上的所有网络接口。
列出网络接口地址
本页面向您展示如何列出分配给机器上所有网络接口的 IP 地址。
网络接口参数
本页面向您展示如何确定网络接口是否正在运行,或者网络接口是环回接口、点对点接口还是虚拟接口。您还可以了解如何确定接口是否支持多播。
什么是网络接口?
原文:
docs.oracle.com/javase/tutorial/networking/nifs/definition.html
网络接口是计算机与私有或公共网络之间的连接点。网络接口通常是网络接口卡(NIC),但不一定要有物理形式。相反,网络接口可以在软件中实现。例如,环回接口(IPv4 的127.0.0.1
和 IPv6 的::1
)不是物理设备,而是模拟网络接口的软件部分。环回接口通常用于测试环境。
java.net.NetworkInterface
类代表两种类型的接口。
NetworkInterface
对于具有多个 NIC 的多宿主系统非常有用。使用NetworkInterface
,您可以指定要用于特定网络活动的 NIC。
例如,假设您有一台配置了两个 NIC 的机器,并且您想向服务器发送数据。您可以这样创建一个套接字:
Socket soc = new java.net.Socket();
soc.connect(new InetSocketAddress(address, port));
发送数据时,系统会确定使用哪个接口。但是,如果您有偏好或需要指定要使用的 NIC,您可以查询系统以找到适当的接口并在您想要使用的接口上找到一个地址。当您创建套接字并将其绑定到该地址时,系统将使用相关联的接口。例如:
NetworkInterface nif = NetworkInterface.getByName("bge0");
Enumeration<InetAddress> nifAddresses = nif.getInetAddresses();
Socket soc = new java.net.Socket();
soc.bind(new InetSocketAddress(nifAddresses.nextElement(), 0));
soc.connect(new InetSocketAddress(address, port));
您还可以使用NetworkInterface
来识别要加入多播组的本地接口。例如:
NetworkInterface nif = NetworkInterface.getByName("bge0");
MulticastSocket ms = new MulticastSocket();
ms.joinGroup(new InetSocketAddress(hostname, port), nif);
NetworkInterface
可以与 Java API 一起以许多其他方式使用,超出了此处描述的两种用途。
检索网络接口
原文:
docs.oracle.com/javase/tutorial/networking/nifs/retrieving.html
NetworkInterface
类没有公共构造函数。因此,您不能只使用new
运算符创建此类的新实例。相反,提供了以下静态方法,以便您可以从系统中检索接口详细信息:getByInetAddress()
、getByName()
和 getNetworkInterfaces()
。前两种方法用于当您已经知道特定接口的 IP 地址或名称时。第三种方法,getNetworkInterfaces()
返回计算机上所有接口的完整列表。
网络接口可以按层次结构组织。NetworkInterface
类包括两个方法,getParent()
和 getSubInterfaces()
,这些方法与网络接口层次结构相关。getParent()
方法返回接口的父NetworkInterface
。如果网络接口是子接口,则getParent()
返回一个非空值。getSubInterfaces()
方法返回网络接口的所有子接口。
以下示例程序列出了计算机上所有网络接口和子接口(如果存在)的名称。
import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;
public class ListNIFs
{
public static void main(String args[]) throws SocketException {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface netIf : Collections.list(nets)) {
out.printf("Display name: %s\n", netIf.getDisplayName());
out.printf("Name: %s\n", netIf.getName());
displaySubInterfaces(netIf);
out.printf("\n");
}
}
static void displaySubInterfaces(NetworkInterface netIf) throws SocketException {
Enumeration<NetworkInterface> subIfs = netIf.getSubInterfaces();
for (NetworkInterface subIf : Collections.list(subIfs)) {
out.printf("\tSub Interface Display name: %s\n", subIf.getDisplayName());
out.printf("\tSub Interface Name: %s\n", subIf.getName());
}
}
}
以下是示例程序的样本输出:
Display name: bge0
Name: bge0
Sub Interface Display name: bge0:3
Sub Interface Name: bge0:3
Sub Interface Display name: bge0:2
Sub Interface Name: bge0:2
Sub Interface Display name: bge0:1
Sub Interface Name: bge0:1
Display name: lo0
Name: lo0
列出网络接口地址
原文:
docs.oracle.com/javase/tutorial/networking/nifs/listing.html
从网络接口获取的最有用的信息之一是分配给它的 IP 地址列表。您可以通过使用两种方法之一从NetworkInterface
实例中获取此信息。第一种方法getInetAddresses()
返回一个InetAddress
的Enumeration
。另一种方法getInterfaceAddresses()
返回一个java.net.InterfaceAddress
实例的列表。当您需要有关接口地址的更多信息时,可以使用此方法。例如,当地址是 IPv4 地址时,您可能需要有关子网掩码和广播地址的附加信息,以及在 IPv6 地址的情况下的网络前缀长度。
以下示例程序列出了计算机上所有网络接口及其地址:
import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;
public class ListNets {
public static void main(String args[]) throws SocketException {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface netint : Collections.list(nets))
displayInterfaceInformation(netint);
}
static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
out.printf("Display name: %s\n", netint.getDisplayName());
out.printf("Name: %s\n", netint.getName());
Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
for (InetAddress inetAddress : Collections.list(inetAddresses)) {
out.printf("InetAddress: %s\n", inetAddress);
}
out.printf("\n");
}
}
以下是示例程序的输出样本:
Display name: TCP Loopback interface
Name: lo
InetAddress: /127.0.0.1
Display name: Wireless Network Connection
Name: eth0
InetAddress: /192.0.2.0
网络接口参数
原文:
docs.oracle.com/javase/tutorial/networking/nifs/parameters.html
你可以访问有关网络接口的网络参数,除了分配给它的名称和 IP 地址之外。
你可以使用isUP()
方法来发现网络接口是否处于“up”(即运行)状态。以下方法指示网络接口类型:
-
isLoopback()
指示网络接口是否为环回接口。 -
isPointToPoint()
指示接口是否为点对点接口。 -
isVirtual()
指示接口是否为虚拟接口。
supportsMulticast()
方法指示网络接口是否支持多播。getHardwareAddress()
方法在可用时返回网络接口的物理硬件地址,通常称为 MAC 地址。getMTU()
方法返回最大传输单元(MTU),即最大数据包大小。
以下示例在列出网络接口地址的基础上,通过添加本页描述的额外网络参数进行了扩展:
import java.io.*;
import java.net.*;
import java.util.*;
import static java.lang.System.out;
public class ListNetsEx {
public static void main(String args[]) throws SocketException {
Enumeration<NetworkInterface> nets = NetworkInterface.getNetworkInterfaces();
for (NetworkInterface netint : Collections.list(nets))
displayInterfaceInformation(netint);
}
static void displayInterfaceInformation(NetworkInterface netint) throws SocketException {
out.printf("Display name: %s\n", netint.getDisplayName());
out.printf("Name: %s\n", netint.getName());
Enumeration<InetAddress> inetAddresses = netint.getInetAddresses();
for (InetAddress inetAddress : Collections.list(inetAddresses)) {
out.printf("InetAddress: %s\n", inetAddress);
}
out.printf("Up? %s\n", netint.isUp());
out.printf("Loopback? %s\n", netint.isLoopback());
out.printf("PointToPoint? %s\n", netint.isPointToPoint());
out.printf("Supports multicast? %s\n", netint.supportsMulticast());
out.printf("Virtual? %s\n", netint.isVirtual());
out.printf("Hardware address: %s\n",
Arrays.toString(netint.getHardwareAddress()));
out.printf("MTU: %s\n", netint.getMTU());
out.printf("\n");
}
}
以下是示例程序的输出样本:
Display name: bge0
Name: bge0
InetAddress: /fe80:0:0:0:203:baff:fef2:e99d%2
InetAddress: /129.156.225.59
Up? true
Loopback? false
PointToPoint? false
Supports multicast? false
Virtual? false
Hardware address: [0, 3, 4, 5, 6, 7]
MTU: 1500
Display name: lo0
Name: lo0
InetAddress: /0:0:0:0:0:0:0:1%1
InetAddress: /127.0.0.1
Up? true
Loopback? true
PointToPoint? false
Supports multicast? false
Virtual? false
Hardware address: null
MTU: 8232
课程:处理 Cookies
原文:
docs.oracle.com/javase/tutorial/networking/cookies/index.html
虽然您可能已经熟悉 cookies,但您可能不知道如何在您的 Java 应用程序中充分利用它们。本课程将引导您了解 cookies 的概念,并解释如何设置 cookie 处理程序,以便您的 HTTP URL 连接将使用它。
Java SE 为此功能提供了一个主要类,java.net.CookieHandler
,以及以下支持类和接口:java.net.CookieManager
,java.net.CookiePolicy
,java.net.CookieStore
,和 java.net.HttpCookie
。
使用 Cookies 进行 HTTP 状态管理
本页面描述了 cookies 并解释了它们如何用于提供会话。
CookieHandler 回调机制
当您访问网站时,本页面解释了 cookie 处理程序是如何被调用以及如何设置 cookie 处理程序。
默认 CookieManager
Java SE 提供了一个默认的 cookie 处理程序实现,在大多数情况下都足够使用,并且高度可定制。
自定义 CookieManager
这里有一些如何自定义 cookie 策略并编写自己的 cookie 存储的示例。
使用 Cookies 进行 HTTP 状态管理
原文:
docs.oracle.com/javase/tutorial/networking/cookies/definition.html
HTTP 状态管理机制指定了一种通过 HTTP 请求和响应创建有状态会话的方式。
通常,HTTP 请求/响应对是相互独立的。然而,状态管理机制使得可以交换状态信息的客户端和服务器将这些对放入更大的上下文中,这被称为会话。用于创建和维护会话的状态信息被称为cookie。
一个 cookie 是可以存储在浏览器缓存中的数据片段。如果您访问一个网站然后再次访问它,cookie 数据可以用来识别您为回访者。Cookies 使得状态信息,比如在线购物车,可以被记住。一个 cookie 可以是短期的,保存数据一个网页会话,也就是直到您关闭浏览器,或者一个 cookie 可以是长期的,保存数据一周或一年。
关于 HTTP 状态管理的更多信息,请参阅RFC 2965: HTTP 状态管理机制。
CookieHandler 回调机制
原文:
docs.oracle.com/javase/tutorial/networking/cookies/cookiehandler.html
Java SE 中通过 java.net.CookieHandler
类实现了 HTTP 状态管理。CookieHandler
对象提供了一个回调机制,在 HTTP 协议处理程序中提供 HTTP 状态管理策略的实现。也就是说,使用 HTTP 协议的 URL,例如 new URL("http://example.com")
,将使用 HTTP 协议处理程序。如果设置了 CookieHandler
对象,此协议处理程序将回调到 CookieHandler
对象以处理状态管理。
CookieHandler
类是一个抽象类,具有两对相关方法。第一对方法,getDefault()
和 setDefault(cookieHandler)
,是静态方法,允许您发现当前安装的处理程序并安装自己的处理程序。
没有安装默认处理程序,安装处理程序是在系统范围内进行的。对于在安全环境中运行的应用程序,即已安装了安全管理器的应用程序,您必须具有特殊权限才能获取和设置处理程序。有关更多信息,请参阅 java.net.CookieHandler.getDefault
。
第二对相关方法,put(uri, responseHeaders)
和 get(uri, requestHeaders)
,允许您为指定的 URI 在响应/请求头中设置和获取所有适用的 cookie 到 cookie 缓存中。这些方法是抽象的,CookieHandler
的具体实现必须提供实现。
Java Web Start 和 Java Plug-in 都安装了默认的 CookieHandler
。但是,如果您正在运行一个独立的应用程序并希望启用 HTTP 状态管理,您必须设置一个系统范围的处理程序。本课程的下两页将向您展示如何操作。
默认 CookieManager
原文:
docs.oracle.com/javase/tutorial/networking/cookies/cookiemanager.html
java.net.CookieManager
提供了CookieHandler
的具体实现,对于大多数用户来说,足以处理 HTTP 状态管理。CookieManager
将 cookie 的存储与接受、拒绝的策略分开。CookieManager
通过java.net.CookieStore
和java.net.CookiePolicy
进行初始化。CookieStore
管理 cookie 的存储。CookiePolicy
根据策略决定是否接受或拒绝 cookie。
以下代码显示了如何创建并设置系统范围的CookieManager
:
java.net.CookieManager cm = new java.net.CookieManager();
java.net.CookieHandler.setDefault(cm);
第一行调用默认的CookieManager
构造函数创建实例。第二行调用CookieHandler
的静态setDefault
方法来设置系统范围的处理程序。
默认的CookieManager
构造函数创建一个具有默认 cookie 存储和接受策略的新CookieManager
实例。CookieStore
是存储任何接受的 HTTP cookie 的地方。如果在创建时未指定,CookieManager
实例将使用内部的内存实现。这种实现不是持久的,只存在于 Java 虚拟机的生命周期内。需要持久存储的用户必须实现自己的存储。
CookieManager
使用的默认 cookie 策略是CookiePolicy.ACCEPT_ORIGINAL_SERVER
,只接受来自原始服务器的 cookie。因此,服务器的Set-Cookie
响应必须设置“domain”属性,并且必须与 URL 中的主机域匹配。有关更多信息,请参阅java.net.HttpCookie.domainMatches
。需要不同策略的用户必须实现CookiePolicy
接口,并将其传递给CookieManager
构造函数,或者通过使用setCookiePolicy(cookiePolicy)
方法将其设置为已构造的CookieManager
实例。
在从 cookie 存储中检索 cookie 时,CookieManager
还执行来自RFC 2965第 3.3.4 节的路径匹配规则。因此,cookie 还必须设置其“path”属性,以便在从 cookie 存储中检索 cookie 之前应用路径匹配规则。
总之,CookieManager
提供了处理 cookie 的框架,并为CookieStore
提供了良好的默认实现。通过允许您设置自己的CookieStore
、CookiePolicy
或两者,CookieManager
具有高度的可定制性。
自定义 CookieManager
原文:
docs.oracle.com/javase/tutorial/networking/cookies/custom.html
CookieManager
类的两个方面可以定制,即 CookiePolicy
和 CookieStore
。
CookiePolicy
为方便起见,CookiePolicy
定义了以下预定义的接受 cookie 的策略:
-
CookiePolicy.ACCEPT_ORIGINAL_SERVER
仅接受来自原始服务器的 cookie。 -
CookiePolicy.ACCEPT_ALL
接受所有 cookie。 -
CookiePolicy.ACCEPT_NONE
不接受任何 cookie。 -
你还可以通过实现
CookiePolicy
的shouldAccept
方法来定义自己的 cookie 策略。然后,通过将其传递给多参数CookieManager
构造函数或调用setCookiePolicy(cookiePolicy)
方法来使用此CookiePolicy
更改已经存在的 cookie 管理器。以下是一个拒绝来自黑名单域的 cookie,然后应用
CookiePolicy.ACCEPT_ORIGINAL_SERVER
策略的示例:import java.net.*; public class BlacklistCookiePolicy implements CookiePolicy { String[] blacklist; public BlacklistCookiePolicy(String[] list) { blacklist = list; } public boolean shouldAccept(URI uri, HttpCookie cookie) { String host; try { host = InetAddress.getByName(uri.getHost()).getCanonicalHostName(); } catch (UnknownHostException e) { host = uri.getHost(); } for (int i = 0; i<blacklist.length; i++) { if (HttpCookie.domainMatches(blacklist[i], host)) { return false; } } return CookiePolicy.ACCEPT_ORIGINAL_SERVER.shouldAccept(uri, cookie); } }
当你创建一个
BlacklistCookiePolicy
实例时,你需要传递一个表示不希望接受来自的域的字符串数组。然后,将此BlacklistCookiePolicy
实例设置为CookieManager
的 cookie 策略。例如:String[] list = new String[]{ ".example.com" }; CookieManager cm = new CookieManager(null, new BlacklistCookiePolicy(list)); CookieHandler.setDefault(cm);
此示例代码将不接受来自以下主机的 cookie:
host.example.com domain.example.com
然而,此示例代码将接受来自以下主机的 cookie:
example.com example.org myhost.example.org
CookieStore
CookieStore
是一个代表存储 cookie 的接口。CookieManager
为每个 HTTP 响应添加 cookie 到CookieStore
,并为每个 HTTP 请求从CookieStore
检索 cookie。你可以实现此接口以提供自己的
CookieStore
并在创建时将其传递给CookieManager
。在创建CookieManager
实例后无法设置CookieStore
。但是,你可以通过调用CookieManager.getCookieStore()
来获取对 cookie 存储的引用。这样做很有用,因为它使你能够利用 Java SE 提供的默认内存中CookieStore
实现,并补充其功能。例如,你可能想创建一个持久性 cookie 存储,以便保存 cookie,即使 Java 虚拟机重新启动也可以使用。你的实现方式类似于以下方式:
-
之前保存的任何 cookie 都将被读取。
-
在运行时,cookie 被存储和检索到内存中。
-
在退出之前,cookie 被写入持久存储。
以下是此 cookie 存储的不完整示例。此示例展示了如何利用 Java SE 默认的内存中 cookie 存储,并如何扩展其功能。
import java.net.*; import java.util.*; public class PersistentCookieStore implements CookieStore, Runnable { CookieStore store; public PersistentCookieStore() { // get the default in memory cookie store store = new CookieManager().getCookieStore(); // todo: read in cookies from persistant storage // and add them store // add a shutdown hook to write out the in memory cookies Runtime.getRuntime().addShutdownHook(new Thread(this)); } public void run() { // todo: write cookies in store to persistent storage } public void add(URI uri, HttpCookie cookie) { store.add(uri, cookie); } public List<HttpCookie> get(URI uri) { return store.get(uri); } public List<HttpCookie> getCookies() { return store.getCookies(); } public List<URI> getURIs() { return store.getURIs(); } public boolean remove(URI uri, HttpCookie cookie) { return store.remove(uri, cookie); } public boolean removeAll() { return store.removeAll(); } }
-
Trail: Bonus
原文:
docs.oracle.com/javase/tutorial/extra/index.html
,引入于 J2SE 5.0,是对类型系统的期待已久的增强。它允许类型或方法在各种类型的对象上操作,同时提供编译时类型安全性。它为集合框架添加了编译时类型安全性,并消除了强制转换的繁琐工作。
教你关于 J2SE™版本 1.4 的一个强大功能,允许程序员暂停窗口系统,以便直接在屏幕上进行绘图。编写游戏或其他图形密集型应用程序的程序员应该阅读这个教训。
提供了关于培训资源的信息,并列出了教程中涵盖潜在考试主题的部分。
课程:泛型
原文:
docs.oracle.com/javase/tutorial/extra/generics/index.html
作者:吉拉德·布拉查
J2SE 5.0 中引入的这一期待已久的类型系统增强允许类型或方法在提供编译时类型安全性的同时操作各种类型的对象。它为集合框架添加了编译时类型安全性,并消除了强制类型转换的繁琐工作。
介绍
定义简单的泛型
泛型和子类型化
通配符
泛型方法
与旧代码的互操作
详细信息
类文字作为运行时类型标记
更多有趣的通配符
将旧代码转换为使用泛型
致谢
介绍
原文:
docs.oracle.com/javase/tutorial/extra/generics/intro.html
JDK 5.0 引入了 Java 编程语言的几个新扩展之一就是泛型。
本教程是关于泛型的介绍。您可能熟悉其他语言中类似构造的概念,尤其是 C++的模板。如果是这样,您会发现有相似之处,也有重要的区别。如果您对其他地方类似的构造不熟悉,那就更好了;您可以从头开始,无需消除任何误解。
泛型允许您对类型进行抽象。最常见的例子是容器类型,比如在 Collections 层次结构中的类型。
这是一个典型的使用方式:
List myIntList = new LinkedList(); // 1
myIntList.add(new Integer(0)); // 2
Integer x = (Integer) myIntList.iterator().next(); // 3
第 3 行的强制类型转换略显烦人。通常,程序员知道特定列表中放入了什么类型的数据。然而,强制类型转换是必不可少的。编译器只能保证迭代器返回一个Object
。为了确保将其分配给类型为Integer
的变量是类型安全的,需要进行强制类型转换。
当然,强制类型转换不仅引入了混乱。它还引入了运行时错误的可能性,因为程序员可能会犯错。
如果程序员能够真正表达他们的意图,并将列表标记为仅包含特定数据类型,会怎么样?这就是泛型背后的核心思想。以下是使用泛型给出的程序片段的一个版本:
List<Integer>
myIntList = new LinkedList<Integer>(); // 1'
myIntList.add(new Integer(0)); // 2'
Integer x = myIntList.iterator().next(); // 3'
注意变量myIntList
的类型声明。它指定这不仅仅是一个任意的List
,而是一个Integer
的List
,写作List<Integer>
。我们说List
是一个接受类型参数的泛型接口--在这种情况下是Integer
。我们在创建列表对象时也指定了类型参数。
注意,第 3 行的强制类型转换已经消失了。
现在,您可能会认为我们所做的只是将混乱的地方移动了一下。在第 3 行进行Integer
的强制类型转换,我们在第 1 行将Integer
作为类型参数。然而,这里有一个非常大的区别。编译器现在可以在编译时检查程序的类型正确性。当我们说myIntList
声明为类型List<Integer>
时,这告诉我们关于变量myIntList
的一些信息,无论何时何地使用它,编译器都会保证这一点。相比之下,强制类型转换告诉我们程序员认为在代码中的某个单一点上是正确的。
整体效果,尤其是在大型程序中,是提高了可读性和健壮性。
定义简单的通用类型
原文:
docs.oracle.com/javase/tutorial/extra/generics/simple.html
这里是包java.util
中接口List
和Iterator
的定义的一个小节选:
public interface List <E> {
void add(E x);
Iterator<E> iterator();
}
public interface Iterator<E> {
E next();
boolean hasNext();
}
这段代码应该都很熟悉,除了尖括号中的内容。那些是接口List
和Iterator
的形式类型参数的声明。
类型参数可以在通用声明中的几乎任何地方使用,就像你会使用普通类型一样(尽管有一些重要的限制;请参阅细则部分)。
在介绍中,我们看到了List
的调用,比如List<Integer>
。在调用(通常称为参数化类型)中,所有形式类型参数(在本例中为E
)的所有出现都被实际类型参数(在本例中为Integer
)替换。
你可能会想象List<Integer>
代表了List
的一个版本,其中E
已经被Integer
统一替换:
public interface IntegerList {
void add(Integer x);
Iterator<Integer> iterator();
}
这种直觉可能有所帮助,但也是误导的。
这很有帮助,因为参数化类型List<Integer>
确实有看起来像这个展开的方法。
这是误导的,因为通用的声明实际上从未以这种方式展开。代码中没有多个副本——不在源代码中,也不在二进制代码中,也不在磁盘上,也不在内存中。如果您是 C++程序员,您会明白这与 C++模板非常不同。
通用类型声明只编译一次,并转换为单个类文件,就像普通类或接口声明一样。
类型参数类似于方法或构造函数中使用的普通参数。就像方法有描述其操作的值种类的形式值参数一样,通用声明有形式类型参数。当调用方法时,实际参数被替换为形式参数,并且方法体被评估。当调用通用声明时,实际类型参数被替换为形式类型参数。
关于命名约定的说明。我们建议您为形式类型参数使用简洁(如果可能的话是单个字符)但富有启发性的名称。最好避免在这些名称中使用小写字符,这样可以轻松区分形式类型参数和普通类和接口。许多容器类型使用E
,表示元素,就像上面的例子一样。我们将在后面的例子中看到一些额外的约定。
泛型和子类型
原文:
docs.oracle.com/javase/tutorial/extra/generics/subtype.html
让我们测试一下你对泛型的理解。以下代码片段是否合法?
List<String> ls = new ArrayList<String>(); // 1
List<Object> lo = ls; // 2
第 1 行肯定是合法的。问题的棘手部分在于第 2 行。这归结为一个问题:String
的List
是否是Object
的List
。大多数人本能地回答:“当然!”
好吧,看看接下来的几行:
lo.add(new Object()); // 3
String s = ls.get(0); // 4: Attempts to assign an Object to a String!
在这里,我们给ls
和lo
取了别名。通过别名lo
访问String
的列表ls
,我们可以向其中插入任意对象。结果ls
不再只包含String
,当我们尝试从中取出东西时,会得到一个不愉快的惊喜。
Java 编译器当然会阻止这种情况发生。第 2 行将导致编译时错误。
一般来说,如果Foo
是Bar
的子类型(子类或子接口),而G
是某个泛型类型声明,那么G<Foo>
不是G<Bar>
的子类型。这可能是你需要了解的关于泛型最困难的事情,因为它违背了我们根深蒂固的直觉。
我们不应该假设集合不会改变。我们的直觉可能会让我们认为这些东西是不可变的。
例如,如果机动车管理部门向人口普查局提供驾驶员名单,这似乎是合理的。我们认为List<Driver>
是List<Person>
,假设Driver
是Person
的子类型。实际上,传递的是驾驶员注册表的副本。否则,人口普查局可能会将不是驾驶员的新人加入列表,从而破坏了机动车管理部门的记录。
为了应对这种情况,考虑更灵活的泛型类型是很有用的。到目前为止,我们看到的规则相当严格。
通配符
原文:
docs.oracle.com/javase/tutorial/extra/generics/wildcards.html
考虑编写一个打印集合中所有元素的例程的问题。以下是你可能在语言的旧版本(即 5.0 版本之前)中编写的方式:
void printCollection(Collection c) {
Iterator i = c.iterator();
for (k = 0; k < c.size(); k++) {
System.out.println(i.next());
}
}
这是一个使用泛型(和新的for
循环语法)编写的天真尝试:
void printCollection(Collection<Object> c) {
for (Object e : c) {
System.out.println(e);
}
}
问题在于这个新版本比旧版本要不实用得多。而旧代码可以使用任何类型的集合作为参数调用,新代码只接受Collection<Object>
,正如我们刚刚证明的,这不是所有种类集合的超类型!
那么所有种类的集合的超类型是什么呢?它被写作Collection<?>
(读作"未知类型的集合"),即元素类型匹配任何内容的集合。它被称为通配符类型,原因显而易见。我们可以这样写:
void printCollection(Collection<?> c) {
for (Object e : c) {
System.out.println(e);
}
}
现在,我们可以使用任何类型的集合调用它。请注意,在printCollection()
内部,我们仍然可以从c
中读取元素并将它们赋予类型Object
。这总是安全的,因为无论集合的实际类型是什么,它都包含对象。但是向其中添加任意对象是不安全的:
Collection<?> c = new ArrayList<String>();
c.add(new Object()); // Compile time error
由于我们不知道c
的元素类型代表什么,我们无法向其添加对象。add()
方法接受类型为E
的参数,即集合的元素类型。当实际类型参数为?
时,它代表某种未知类型。我们传递给add
的任何参数都必须是这种未知类型的子类型。由于我们不知道那种类型是什么,我们无法传递任何内容。唯一的例外是null
,它是每种类型的成员。
另一方面,对于给定的List<?>
,我们可以调用get()
并利用结果。结果类型是一个未知类型,但我们始终知道它是一个对象。因此,将get()
的结果分配给类型为Object
的变量或将其作为期望类型为Object
的参数传递是安全的。
有界通配符
考虑一个简单的绘图应用程序,可以绘制矩形和圆形等形状。为了在程序中表示这些形状,你可以定义一个类层次结构,如下所示:
public abstract class Shape {
public abstract void draw(Canvas c);
}
public class Circle extends Shape {
private int x, y, radius;
public void draw(Canvas c) {
...
}
}
public class Rectangle extends Shape {
private int x, y, width, height;
public void draw(Canvas c) {
...
}
}
这些类可以在画布上绘制:
public class Canvas {
public void draw(Shape s) {
s.draw(this);
}
}
任何绘图通常会包含许多形状。假设它们被表示为一个列表,那么在Canvas
中有一个绘制它们所有的方法会很方便:
public void drawAll(List<Shape> shapes) {
for (Shape s: shapes) {
s.draw(this);
}
}
现在,类型规则表明drawAll()
只能在Shape
的列表上调用:例如,不能在List<Circle>
上调用。这很不幸,因为该方法所做的只是从列表中读取形状,所以它同样可以在List<Circle>
上调用。我们真正想要的是该方法接受任何种类的形状列表:
public void drawAll(List<? extends Shape> shapes) {
...
}
这里有一个很小但非常重要的区别:我们用List<? **extends** Shape>
替换了类型List<Shape>
。现在drawAll()
将接受任何Shape
的子类列表,因此我们现在可以在List<Circle>
上调用它。
List<? **extends** Shape>
是有界通配符的一个例子。?
代表一个未知类型,就像我们之前看到的通配符一样。然而,在这种情况下,我们知道这个未知类型实际上是Shape
的子类型。(注意:它可以是Shape
本身,或者某个子类;它不一定要直接扩展Shape
。)我们说Shape
是通配符的上界。
使用通配符灵活性的代价通常是很高的。这个代价是在方法体中写入shapes
现在是非法的。例如,下面的写法是不允许的:
public void addRectangle(List<? extends Shape> shapes) {
// *Compile-time error!*
shapes.add(0, new Rectangle());
}
你应该能够弄清楚为什么上面的代码是不允许的。shapes.add()
的第二个参数的类型是? **extends** Shape
-- 一个未知的Shape
子类型。由于我们不知道它的类型,我们也不知道它是否是Rectangle
的超类型;它可能是也可能不是这样的超类型,因此在这里传递Rectangle
是不安全的。
有界通配符正是处理 DMV 将其数据传递给人口普查局的例子所需的。我们的例子假设数据由从名称(表示为字符串)到人员(由Person
或其子类型,如Driver
表示的引用类型)的映射表示。Map<K,V>
是一个接受两个类型参数的泛型类型的例子,表示映射的键和值。
再次注意正式类型参数的命名约定--K
代表键,V
代表值。
public class Census {
public static void addRegistry(Map<String, ? extends Person> registry) {
}
...
Map<String, Driver> allDrivers = ... ;
Census.addRegistry(allDrivers);
通用方法
原文:
docs.oracle.com/javase/tutorial/extra/generics/methods.html
考虑编写一个方法,该方法接受一个对象数组和一个集合,并将数组中的所有对象放入集合中。以下是第一次尝试:
static void fromArrayToCollection(Object[] a, Collection<?> c) {
for (Object o : a) {
c.add(o); // *compile-time error*
}
}
到目前为止,您已经学会了避免初学者的错误,即尝试将Collection<Object>
用作集合参数的类型。您可能已经意识到,使用Collection<?>
也行不通。请记住,您不能将对象随意放入未知类型的集合中。
处理这些问题的方法是使用通用方法。就像类型声明一样,方法声明也可以是通用的即,由一个或多个类型参数参数化。
static <T> void fromArrayToCollection(T[] a, Collection<T> c) {
for (T o : a) {
c.add(o); // *Correct*
}
}
我们可以使用任何元素类型为数组元素类型的超类型的任何类型的集合调用此方法。
Object[] oa = new Object[100];
Collection<Object> co = new ArrayList<Object>();
// *T inferred to be Object*
fromArrayToCollection(oa, co);
String[] sa = new String[100];
Collection<String> cs = new ArrayList<String>();
// *T inferred to be String*
fromArrayToCollection(sa, cs);
// *T inferred to be Object*
fromArrayToCollection(sa, co);
Integer[] ia = new Integer[100];
Float[] fa = new Float[100];
Number[] na = new Number[100];
Collection<Number> cn = new ArrayList<Number>();
// *T inferred to be Number*
fromArrayToCollection(ia, cn);
// *T inferred to be Number*
fromArrayToCollection(fa, cn);
// *T inferred to be Number*
fromArrayToCollection(na, cn);
// *T inferred to be Object*
fromArrayToCollection(na, co);
// *compile-time error*
fromArrayToCollection(na, cs);
请注意,我们不必向通用方法传递实际类型参数。编译器根据实际参数的类型为我们推断类型参数。它通常会推断使调用类型正确的最具体的类型参数。
一个问题是:何时应该使用通用方法,何时应该使用通配符类型?为了理解答案,让我们来看看Collection
库中的一些方法。
interface Collection<E> {
public boolean containsAll(Collection<?> c);
public boolean addAll(Collection<? extends E> c);
}
我们可以在这里使用通用方法:
interface Collection<E> {
public <T> boolean containsAll(Collection<T> c);
public <T extends E> boolean addAll(Collection<T> c);
// *Hey, type variables can have bounds too!*
}
然而,在containsAll
和addAll
中,类型参数T
仅使用一次。返回类型不依赖于类型参数,方法的任何其他参数也不依赖于类型参数(在这种情况下,只有一个参数)。这告诉我们,类型参数用于多态性;它的唯一效果是允许在不同的调用站点使用各种实际参数类型。如果是这种情况,应该使用通配符。通配符旨在支持灵活的子类型化,这正是我们要表达的。
通用方法允许使用类型参数来表示方法的一个或多个参数以及/或其返回类型之间的依赖关系。如果没有这样的依赖关系,则不应使用通用方法。
可以同时使用通用方法和通配符。以下是Collections.copy()
方法的使用方法:
class Collections {
public static <T> void copy(List<T> dest, List<? extends T> src) {
...
}
注意两个参数类型之间的依赖关系。从源列表src
复制的任何对象必须可分配给目标列表dst
的元素类型T
。因此,src
的元素类型可以是T
的任何子类型我们不关心是哪种类型。copy
的签名使用类型参数表达依赖关系,但对第二个参数的元素类型使用通配符。
我们可以以另一种方式编写此方法的签名,而根本不使用通配符:
class Collections {
public static <T, S extends T> void copy(List<T> dest, List<S> src) {
...
}
这是可以的,但是第一个类型参数既用于dst
的类型,也用于第二个类型参数S
的边界,而S
本身只在src
的类型中使用了一次没有其他地方依赖于它。这表明我们可以用通配符替换S
。使用通配符比声明显式类型参数更清晰、更简洁,因此在可能的情况下应优先使用通配符。
通配符还有一个优点,就是它们可以在方法签名之外使用,比如字段、局部变量和数组的类型。这里是一个例子。
回到我们的形状绘制问题,假设我们想要保留绘制请求的历史记录。我们可以在Shape
类内部的静态变量中维护历史记录,并让drawAll()
将其传入的参数存储到历史字段中。
static List<List<? extends Shape>>
history = new ArrayList<List<? extends Shape>>();
public void drawAll(List<? extends Shape> shapes) {
history.addLast(shapes);
for (Shape s: shapes) {
s.draw(this);
}
}
最后,让我们再次注意一下用于类型参数的命名约定。我们在没有更具体的类型来区分时使用T
表示类型。这在泛型方法中经常发生。如果有多个类型参数,我们可能会使用字母来区分T
在字母表中的邻居,比如S
。如果泛型方法出现在泛型类中,最好避免在方法和类的类型参数中使用相同的名称,以避免混淆。嵌套泛型类也适用相同的规则。
与遗留代码互操作
原文:
docs.oracle.com/javase/tutorial/extra/generics/legacy.html
到目前为止,我们所有的例子都假设了一个理想化的世界,在这个世界中,每个人都在使用支持泛型的最新版本的 Java 编程语言。
然而,现实情况并非如此。数百万行代码是用早期版本的语言编写的,它们不会一夜之间全部转换。
稍后,在将遗留代码转换为使用泛型部分,我们将解决将旧代码转换为使用泛型的问题。在本节中,我们将专注于一个更简单的问题:如何使遗留代码和通用代码互操作?这个问题有两个部分:在通用代码中使用遗留代码和在遗留代码中使用通用代码。
在通用代码中使用遗留代码
如何在使用自己的代码时使用旧代码,同时仍享受泛型的好处?
举个例子,假设你想使用包 com.Example.widgets
。Example.com 的人们推出了一个用于库存控制的系统,其亮点如下所示:
package com.Example.widgets;
public interface Part {...}
public class Inventory {
/**
* Adds a new Assembly to the inventory database.
* The assembly is given the name name, and
* consists of a set parts specified by parts.
* All elements of the collection parts
* must support the Part interface.
**/
public static void addAssembly(String name, Collection parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// *Returns a collection of Parts*
Collection getParts();
}
现在,你想添加新代码,使用上面的 API。最好确保你总是使用正确的参数调用 addAssembly()
- 也就是说,你传入的集合确实是 Part
的 Collection
。当然,泛型正是为此而设计的:
package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(String[] args) {
Collection<Part> c = new ArrayList<Part>();
c.add(new Guillotine()) ;
c.add(new Blade());
Inventory.addAssembly("thingee", c);
Collection<Part> k = Inventory.getAssembly("thingee").getParts();
}
}
当我们调用 addAssembly
时,它期望第二个参数是 Collection
类型。实际参数是 Collection<Part>
类型。这样也能工作,但为什么呢?毕竟,大多数集合不包含 Part
对象,因此一般情况下,编译器无法知道 Collection
类型引用的是什么类型的集合。
在适当的通用代码中,Collection
总是伴随着一个类型参数。当像 Collection
这样的通用类型在没有类型参数的情况下使用时,它被称为原始类型。
大多数人的第一反应是 Collection
真的意味着 Collection<Object>
。然而,正如我们之前看到的,将 Collection<Part>
传递到需要 Collection<Object>
的地方是不安全的。更准确地说,类型 Collection
表示某种未知类型的集合,就像 Collection<?>
一样。
但等等,这也不对!考虑一下对 getParts()
的调用,它返回一个 Collection
。然后将其赋给 k
,它是一个 Collection<Part>
。如果调用的结果是 Collection<?>
,那么赋值就会出错。
实际上,这种赋值是合法的,但会生成一个未经检查的警告。这个警告是必要的,因为事实上编译器无法保证其正确性。我们无法检查 getAssembly()
中的遗留代码,以确保返回的确实是一个 Part
集合。代码中使用的类型是 Collection
,可以合法地向这样的集合中插入各种对象。
那么,这不应该是一个错误吗?从理论上讲,是的;但从实际上讲,如果通用代码要调用遗留代码,这必须被允许。这取决于你,程序员,要确信在这种情况下,赋值是安全的,因为getAssembly()
的契约规定它返回一个Part
的集合,即使类型签名没有显示这一点。
因此,原始类型非常类似于通配符类型,但它们的类型检查不那么严格。这是一个故意的设计决定,允许泛型与现有的遗留代码进行交互。
从通用代码调用遗留代码本质上是危险的;一旦将通用代码与非通用遗留代码混合,通常提供的所有通用类型系统的安全保证都将无效。然而,你仍然比根本不使用泛型要好。至少你知道你这边的代码是一致的。
目前,非泛型代码比泛型代码要多得多,而且不可避免地会出现必须混合使用它们的情况。
如果你发现必须混合使用遗留代码和通用代码,请密切关注未经检查的警告。仔细考虑如何证明引发警告的代码的安全性。
如果你仍然犯了错误,导致警告的代码确实不安全,会发生什么?让我们看看这种情况。在这个过程中,我们将深入了解编译器的工作原理。
擦除和转换
public String loophole(Integer x) {
List<String> ys = new LinkedList<String>();
List xs = ys;
xs.add(x); // *Compile-time unchecked warning*
return ys.iterator().next();
}
在这里,我们给字符串列表和普通旧列表取了别名。我们将一个Integer
插入列表,并尝试提取一个String
。这显然是错误的。如果我们忽略警告并尝试执行此代码,它将在我们尝试使用错误类型的地方失败。在运行时,此代码的行为如下:
public String loophole(Integer x) {
List ys = new LinkedList;
List xs = ys;
xs.add(x);
return(String) ys.iterator().next(); // *run time error*
}
当我们从列表中提取一个元素,并尝试将其强制转换为String
以将其视为字符串时,我们将收到ClassCastException
。与loophole()
的泛型版本发生的事情完全相同。
这是因为,泛型是由 Java 编译器实现的一种称为擦除的前端转换。你(几乎)可以将其视为源到源的转换,其中loophole()
的泛型版本转换为非泛型版本。
因此,即使存在未经检查的警告,Java 虚拟机的类型安全性和完整性也永远不会受到威胁。
基本上,擦除会消除(或擦除)所有泛型类型信息。所有尖括号之间的类型信息都被丢弃,因此,例如,像List<String>
这样的参数化类型被转换为List
。所有类型变量的剩余用法都被替换为类型变量的上界(通常为Object
)。并且,每当生成的代码不符合类型时,都会插入到适当类型的强制转换,就像loophole
的最后一行一样。
擦除的全部细节超出了本教程的范围,但我们刚刚给出的简单描述并不离谱。了解一些关于这个是很有好处的,特别是如果您想要做一些更复杂的事情,比如将现有 API 转换为使用泛型(参见将旧代码转换为使用泛型部分),或者只是想了解为什么事情是这样的。
在旧代码中使用泛型代码
现在让我们考虑相反的情况。想象一下,Example.com 选择将他们的 API 转换为使用泛型,但是一些客户端还没有这样做。现在代码看起来像这样:
package com.Example.widgets;
public interface Part {
...
}
public class Inventory {
/**
* Adds a new Assembly to the inventory database.
* The assembly is given the name name, and
* consists of a set parts specified by parts.
* All elements of the collection parts
* must support the Part interface.
**/
public static void addAssembly(String name, Collection<Part> parts) {...}
public static Assembly getAssembly(String name) {...}
}
public interface Assembly {
// *Returns a collection of Parts*
Collection<Part> getParts();
}
客户端代码如下:
package com.mycompany.inventory;
import com.Example.widgets.*;
public class Blade implements Part {
...
}
public class Guillotine implements Part {
}
public class Main {
public static void main(String[] args) {
Collection c = new ArrayList();
c.add(new Guillotine()) ;
c.add(new Blade());
// *1: unchecked warning*
Inventory.addAssembly("thingee", c);
Collection k = Inventory.getAssembly("thingee").getParts();
}
}
客户端代码是在引入泛型之前编写的,但它使用了com.Example.widgets
包和集合库,两者都使用了泛型类型。客户端代码中所有泛型类型声明的使用都是原始类型。
第 1 行生成了一个未经检查的警告,因为一个原始Collection
被传递到一个期望Part
集合的Collection
位置,编译器无法确保原始Collection
确实是Part
集合。
作为一种替代方案,您可以使用源 1.4 标志编译客户端代码,确保不会生成任何警告。然而,在这种情况下,您将无法使用 JDK 5.0 引入的任何新语言特性。
细则
译文:
docs.oracle.com/javase/tutorial/extra/generics/fineprint.html
一个泛型类被所有调用共享
以下代码片段打印什么?
List <String> l1 = new ArrayList<String>();
List<Integer> l2 = new ArrayList<Integer>();
System.out.println(l1.getClass() == l2.getClass());
你可能会倾向于说false
,但你会错。它打印true
,因为泛型类的所有实例具有相同的运行时类,而不考虑它们的实际类型参数。
实际上,使类成为泛型的是它对所有可能的类型参数具有相同行为的事实;同一个类可以被视为具有许多不同的类型。
因此,类的静态变量和方法也被所有实例共享。这就是为什么在静态方法或初始化程序中引用类型声明的类型参数,或在静态变量的声明或初始化程序中引用类型参数是非法的原因。
强制转换和 InstanceOf
泛型类被所有实例共享的事实的另一个含义是,通常没有意义询问一个实例是否是泛型类型的特定调用的实例:
Collection cs = new ArrayList<String>();
// *Illegal.*
if (cs instanceof Collection<String>) { ... }
同样,像这样的强制转换
// *Unchecked warning,*
Collection<String> cstr = (Collection<String>) cs;
给出一个未经检查的警告,因为这不是运行时系统会为你检查的内容。
类型变量也是如此
// *Unchecked warning.*
<T> T badCast(T t, Object o) {
return (T) o;
}
类型变量在运行时不存在。这意味着它们在时间和空间上都没有性能开销,这很好。不幸的是,这也意味着你不能可靠地在强制转换中使用它们。
数组
除非是(无界)通配符类型,否则数组对象的组件类型可能不是类型变量或参数化类型。你可以声明元素类型为类型变量或参数化类型的数组类型,但不能声明数组对象。
这确实很烦人。这个限制是必要的,以避免出现这样的情况:
// *Not really allowed.*
List<String>[] lsa = new List<String>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Unsound, but passes run time store check*
oa[1] = li;
// *Run-time error: ClassCastException.*
String s = lsa[1].get(0);
如果允许参数化类型的数组,前面的例子将在没有任何未经检查警告的情况下编译,并在运行时失败。我们将类型安全作为泛型的主要设计目标。特别是,该语言被设计为保证如果整个应用程序使用javac -source 1.5
编译时没有未经检查的警告,那么它是类型安全的。
然而,你仍然可以使用通配符数组。前面代码的以下变体放弃了使用数组对象和元素类型为参数化的数组类型。因此,我们必须显式转换才能从数组中获取String
。
// *OK, array of unbounded wildcard type.*
List<?>[] lsa = new List<?>[10];
Object o = lsa;
Object[] oa = (Object[]) o;
List<Integer> li = new ArrayList<Integer>();
li.add(new Integer(3));
// *Correct.*
oa[1] = li;
// *Run time error, but cast is explicit.*
String s = (String) lsa[1].get(0);
在下一个变体中,会导致编译时错误,我们避免创建元素类型为参数化的数组对象,但仍然使用具有参数化元素类型的数组类型。
// *Error.*
List<String>[] lsa = new List<?>[10];
同样,尝试创建元素类型为类型变量的数组对象会导致编译时错误:
<T> T[] makeArray(T t) {
return new T[100]; // *Error.*
}
由于类型变量在运行时不存在,无法确定实际的数组类型。
解决这类限制的方法是使用类字面量作为运行时类型标记,如下一节所述,类字面量作为运行时类型标记。
类字面量作为运行时类型标记
原文:
docs.oracle.com/javase/tutorial/extra/generics/literals.html
JDK 5.0 中的一个变化是类java.lang.Class
是泛型的。这是一个有趣的例子,使用泛型性来做除了容器类之外的事情。
现在Class
有一个类型参数T
,你可能会问,T
代表什么?它代表Class
对象所代表的类型。
例如,String.class
的类型是Class<String>
,Serializable.class
的类型是Class<Serializable>
。这可以用来提高反射代码的类型安全性。
特别是,由于Class
中的newInstance()
方法现在返回一个T
,在通过反射创建对象时可以获得更精确的类型。
例如,假设你需要编写一个实用方法,执行数据库查询,给定一个 SQL 字符串,并返回与该查询匹配的数据库中的对象集合。
一种方法是显式传递一个工厂对象,在调用点编写代码如下:
interface Factory<T> { T make();}
public <T> Collection<T> select(Factory<T> factory, String statement) {
Collection<T> result = new ArrayList<T>();
/* *Run sql query using jdbc* */
for (/* *Iterate over jdbc results.* */) {
T item = factory.make();
/* *Use reflection and set all of item's
* fields from sql results.*
*/
result.add(item);
}
return result;
}
你可以这样调用
select(new Factory<EmpInfo>(){
public EmpInfo make() {
return new EmpInfo();
}}, "selection string");
或者你可以声明一个类EmpInfoFactory
来支持Factory
接口
class EmpInfoFactory implements Factory<EmpInfo> {
...
public EmpInfo make() {
return new EmpInfo();
}
}
并调用它
select(getMyEmpInfoFactory(), "selection string");
这种解决方案的缺点是需要:
-
使用冗长的匿名工厂类在调用点,或者
-
为每种使用的类型声明一个工厂类,并在调用点传递一个工厂实例,这有点不自然。
将类字面量作为工厂对象是很自然的,然后可以通过反射来使用。今天(没有泛型的情况下)代码可能会这样写:
Collection emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static Collection select(Class c, String sqlStatement) {
Collection result = new ArrayList();
/* *Run sql query using jdbc.* */
for (/* *Iterate over jdbc results.* */ ) {
Object item = c.newInstance();
/* *Use reflection and set all of item's
* fields from sql results.*
*/
result.add(item);
}
return result;
}
然而,这不会给我们提供我们想要的精确类型的集合。现在Class
是泛型的,我们可以改为写如下代码:
Collection<EmpInfo>
emps = sqlUtility.select(EmpInfo.class, "select * from emps");
...
public static <T> Collection<T> select(Class<T> c, String sqlStatement) {
Collection<T> result = new ArrayList<T>();
/* *Run sql query using jdbc.* */
for (/* *Iterate over jdbc results.* */ ) {
T item = c.newInstance();
/* *Use reflection and set all of item's
* fields from sql results.*
*/
result.add(item);
}
return result;
}
上面的代码以类型安全的方式给出了我们想要的精确类型的集合。
使用类字面量作为运行时类型标记的技术是一个非常有用的技巧。这是一个在新的用于操作注解的 API 中广泛使用的习语。
通配符更有趣
原文:
docs.oracle.com/javase/tutorial/extra/generics/morefun.html
在本节中,我们将考虑通配符的一些更高级用法。我们已经看到了几个示例,在从数据结构中读取时有界通配符是有用的。现在考虑相反的情况,一个只写数据结构。接口Sink
是这种类型的一个简单示例。
interface Sink<T> {
flush(T t);
}
我们可以想象使用它,如下面的代码所示。方法writeAll()
旨在将集合coll
的所有元素刷新到接收器snk
中,并返回最后一个刷新的元素。
public static <T> T writeAll(Collection<T> coll, Sink<T> snk) {
T last;
for (T t : coll) {
last = t;
snk.flush(last);
}
return last;
}
...
Sink<Object> s;
Collection<String> cs;
String str = writeAll(cs, s); // *Illegal call.*
如所写,对writeAll()
的调用是非法的,因为无法推断出有效的类型参数;String
和Object
都不适合T
的类型,因为Collection
元素和Sink
元素必须是相同类型。
我们可以通过修改writeAll()
的签名来修复此错误,如下所示,使用通配符。
public static <T> T writeAll(Collection<? extends T>, Sink<T>) {...}
...
// *Call is OK, but wrong return type.*
String str = writeAll(cs, s);
现在调用是合法的,但赋值是错误的,因为推断的返回类型是Object
,因为T
与s
的元素类型匹配,而s
是Object
。
解决方案是使用我们尚未看到的一种有界通配符形式:带有下界的通配符。语法? **super** T
表示一个未知类型,它是T
的超类型(或T
本身;请记住超类型关系是自反的)。这是我们一直在使用的有界通配符的对偶,我们使用? **extends** T
来表示一个未知类型,它是T
的子类型。
public static <T> T writeAll(Collection<T> coll, Sink<? super T> snk) {
...
}
String str = writeAll(cs, s); // *Yes!*
使用这种语法,调用是合法的,并且推断的类型是String
,如所需。
现在让我们转向一个更现实的例子。java.util.TreeSet<E>
表示一个按顺序排列的类型为E
的元素树。构造TreeSet
的一种方法是将Comparator
对象传递给构造函数。该比较器将用于根据所需的顺序对TreeSet
的元素进行排序。
TreeSet(Comparator<E> c)
Comparator
接口本质上是:
interface Comparator<T> {
int compare(T fst, T snd);
}
假设我们想创建一个TreeSet<String>
并传入一个合适的比较器,我们需要传递一个可以比较String
的Comparator
。这可以通过Comparator<String>
来完成,但Comparator<Object>
同样有效。但是,我们将无法在Comparator<Object>
上调用上面给出的构造函数。我们可以使用下界通配符来获得所需的灵活性:
TreeSet(Comparator<? super E> c)
此代码允许使用任何适用的比较器。
作为使用下界通配符的最后一个示例,让我们看看方法Collections.max()
,它返回作为参数传递给它的集合中的最大元素。现在,为了让max()
起作用,传入的集合的所有元素都必须实现Comparable
。此外,它们都必须可以相互比较。
对这种方法签名的泛型化的第一次尝试产生了:
public static <T extends Comparable<T>> T max(Collection<T> coll)
换句话说,该方法接受一个可与自身比较的某种类型T
的集合,并返回该类型的一个元素。然而,这段代码实际上过于限制性。要了解原因,考虑一种可与任意对象进行比较的类型:
class Foo implements Comparable<Object> {
...
}
Collection<Foo> cf = ... ;
Collections.max(cf); // *Should work.*
cf
的每个元素都可以与cf
中的其他元素进行比较,因为每个这样的元素都是Foo
,而Foo
可以与任何对象进行比较,特别是与另一个Foo
进行比较。然而,使用上面的签名,我们发现该调用被拒绝了。推断的类型必须是Foo
,但Foo
并没有实现Comparable<Foo>
。
T
与自身比较并不是必需的。所需的是T
与其超类型之一进行比较。这给了我们:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
请注意,Collections.max()
的实际签名更为复杂。我们将在下一节将遗留代码转换为使用泛型中回到这一点。这种推理几乎适用于任何旨在适用于任意类型的Comparable
的用法:您总是希望使用Comparable<? **super** T>
。
一般来说,如果您的 API 只使用类型参数T
作为参数,那么它的使用应该利用下界通配符(? **super** T
)。相反,如果 API 只返回T
,那么使用上界通配符(? **extends** T
)将为客户端提供更大的灵活性。
通配符捕获
现在应该很清楚了,鉴于:
Set<?> unknownSet = new HashSet<String>();
...
/* Add an element t to a Set s. */
public static <T> void addToSet(Set<T> s, T t) {
...
}
下面的调用是非法的。
addToSet(unknownSet, "abc"); // *Illegal.*
实际传递的集合是字符串集合并不重要;重要的是作为参数传递的表达式是未知类型的集合,不能保证是字符串集合,或者是特定类型的集合。
现在,考虑以下代码:
class Collections {
...
<T> public static Set<T> unmodifiableSet(Set<T> set) {
...
}
}
...
Set<?> s = Collections.unmodifiableSet(unknownSet); // *This works! Why?*
看起来这不应该被允许;然而,看着这个具体的调用,允许它是完全安全的。毕竟,unmodifiableSet()
对于任何类型的Set
都有效,无论其元素类型是什么。
由于这种情况相对频繁出现,有一条特殊规则允许在非常具体的情况下使用这样的代码,其中可以证明代码是安全的。这条规则称为通配符捕获,允许编译器将通配符的未知类型推断为泛型方法的类型参数。
将遗留代码转换为使用泛型
原文:
docs.oracle.com/javase/tutorial/extra/generics/convert.html
之前,我们展示了新代码和旧代码如何互操作。现在,是时候看看更难的问题,即"泛型化"旧代码。
如果决定将旧代码转换为使用泛型,需要仔细考虑如何修改 API。
您需要确保泛型 API 不会过于限制性;它必须继续支持 API 的原始契约。再次考虑一些java.util.Collection
的示例。预泛型 API 看起来像:
interface Collection {
public boolean containsAll(Collection c);
public boolean addAll(Collection c);
}
一个天真的泛型尝试可能是以下内容:
interface Collection<E> {
public boolean containsAll(Collection<E> c);
public boolean addAll(Collection<E> c);
}
虽然这肯定是类型安全的,但它并不符合 API 的原始契约。containsAll()
方法适用于任何类型的传入集合。只有当传入集合确实只包含E
的实例时才会成功,但:
-
传入集合的静态类型可能不同,也许是因为调用者不知道传入的集合的确切类型,或者是因为它是一个
Collection<S>
,其中S
是E
的子类型。 -
使用不同类型的集合调用
containsAll()
是完全合法的。该例程应该正常工作,返回false
。
在addAll()
的情况下,我们应该能够添加任何由E
的子类型的实例组成的集合。我们看到如何在泛型方法部分正确处理这种情况。
您还需要确保修改后的 API 与旧客户端保持二进制兼容性。这意味着 API 的擦除必须与原始的未泛型化 API 相同。在大多数情况下,这是自然而然的,但也有一些微妙的情况。我们将检查我们遇到的最微妙的情况之一,即方法Collections.max()
。正如我们在通配符的更多乐趣部分看到的,max()
的一个合理签名是:
public static <T extends Comparable<? super T>>
T max(Collection<T> coll)
这很好,除了这个签名的擦除是:
public static Comparable max(Collection coll)
这与max()
的原始签名不同:
public static Object max(Collection coll)
当然可以为max()
指定这个签名,但没有这样做,所有调用Collections.max()
的旧二进制类文件都依赖于返回Object
的签名。
我们可以通过在形式类型参数T
的边界中明确指定一个超类来强制擦除不同。
public static <T extends Object & Comparable<? super T>>
T max(Collection<T> coll)
这是给类型参数多重边界的一个示例,使用语法T1 & T2 ... & Tn
。具有多重边界的类型变量被认为是边界中列出的所有类型的子类型。当使用多重边界时,边界中提到的第一个类型被用作类型变量的擦除。
最后,我们应该记住max
只从其输入集合中读取,因此适用于T
的任何子类型的集合。
这将我们带到 JDK 中实际使用的签名:
public static <T extends Object & Comparable<? super T>>
T max(Collection<? extends T> coll)
在实践中很少会出现这么复杂的情况,但是专家库设计者在转换现有 API 时应该准备好仔细思考。
另一个需要注意的问题是协变返回,即在子类中细化方法的返回类型。在旧的 API 中不应该利用这个特性。为了理解为什么,让我们看一个例子。
假设你原始的 API 形式为:
public class Foo {
// *Factory. Should create an instance of*
// *whatever class it is declared in.*
public Foo create() {
...
}
}
public class Bar extends Foo {
// *Actually creates a Bar.*
public Foo create() {
...
}
}
利用协变返回,你将其修改为:
public class Foo {
// *Factory. Should create an instance of*
// *whatever class it is declared in.*
public Foo create() {
...
}
}
public class Bar extends Foo {
// *Actually creates a Bar.*
public Bar create() {
...
}
}
现在,假设你的代码的第三方客户写了以下内容:
public class Baz extends Bar {
// *Actually creates a Baz.*
public Foo create() {
...
}
}
Java 虚拟机不直接支持具有不同返回类型的方法重写。这个特性由编译器支持。因此,除非重新编译类Baz
,否则它将无法正确重写Bar
的create()
方法。此外,Baz
将需要修改,因为代码将被拒绝——Baz
中的create()
的返回类型不是Bar
中create()
的返回类型的子类型。
致谢
原文:
docs.oracle.com/javase/tutorial/extra/generics/acknowledgements.html
Erik Ernst, Christian Plesner Hansen, Jeff Norton, Mads Torgersen, Peter von der Ahe 和 Philip Wadler 为本教程贡献了材料。
感谢 David Biesack, Bruce Chapman, David Flanagan, Neal Gafter, Orjan Petersson, Scott Seligman, Yoshiki Shibata 和 Kresten Krab Thorup 对本教程早期版本提供的宝贵反馈。如果有遗漏的人,请谅解。
教程:全屏独占模式 API
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/index.html
作者:迈克尔·马塔克
您想在 Java 开发环境中使用高性能图形吗?您一直想编写游戏,但是您的图像移动得不够快吗?您的幻灯片程序因为无法控制用户的显示分辨率而无法正常工作吗?如果您一直在问这些问题,那么在 1.4 版本中引入的全屏独占模式 API 可能是您正在寻找的东西。
全屏独占模式
全屏独占模式是一个强大的新功能,它使您能够暂停窗口系统,以便直接在屏幕上进行绘制。
显示模式
本节描述了如何选择和设置显示模式。还讨论了为什么首先要设置显示模式。
被动渲染 vs. 主动渲染
本节讨论了被动渲染和主动渲染的优点。例如,在主事件循环中使用paint
方法绘制是被动的,而在自己的线程中渲染是主动的。还列出了主动渲染的技巧。
双缓冲和页面翻转
本节解释了双缓冲并介绍了页面翻转,这是全屏独占模式中可用的双缓冲技术。
BufferStrategy 和 BufferCapabilities
本节介绍了java.awt.image.BufferStrategy
类,该类允许您在不知道使用的缓冲区数量或显示它们的技术的情况下绘制到表面和组件。本节还回顾了java.awt.BufferCapabilities
类,该类可以帮助您确定图形设备的功能。
示例
本页面列出了几个全屏独占模式示例。
全屏独占模式
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/exclusivemode.html
使用微软的 DirectX API 的程序员可能已经熟悉全屏独占模式。其他程序员可能对这个概念有些陌生。无论哪种情况,全屏独占模式是 J2SE™ 1.4 版本的一个强大功能,允许程序员暂停窗口系统,以便直接向屏幕绘制。
这是与许多传统 GUI 程序有些微的范式转变。在传统的 Java GUI 程序中,AWT 负责从操作系统通过事件分发线程传播绘制事件,并在适当时调用 AWT 的Component.paint
方法。在全屏独占应用程序中,绘制通常由程序自身主动完成。此外,传统的 GUI 应用程序受限于用户选择的屏幕位深和大小。在全屏独占应用程序中,程序可以控制屏幕的位深和大小(显示模式)。最后,许多更高级的技术,如翻页(下文讨论)和立体缓冲(利用为每只眼睛使用单独一组帧的系统)在某些平台上要求应用程序首先处于全屏独占模式。
硬件加速图像基础知识
要理解全屏独占模式 API,您需要了解一些关于硬件加速图像的基本原理。VolatileImage
接口封装了一个表面,该表面可能会或可能不会利用硬件加速。这些表面可能会因操作系统的要求而失去其硬件加速或内存(因此称为“易失性”)。有关易失性图像的更多信息,请参阅VolatileImage
教程(即将推出)。
全屏独占模式通过java.awt.GraphicsDevice
对象处理。要获取所有可用屏幕图形设备的列表(在单个或多监视器系统中),可以在本地java.awt.GraphicsEnvironment
上调用getScreenDevices
方法;对于默认(主要)屏幕(在单监视器系统中唯一的屏幕),可以调用getDefaultScreenDevice
方法。
一旦获得了图形设备,可以调用以下方法之一:
-
public boolean isFullScreenSupported()
此方法返回全屏独占模式是否可用。在不支持全屏独占模式的系统上,最好在固定大小的窗口模式下运行应用程序,而不是设置全屏窗口。
-
public void setFullScreenWindow(Window w)
给定一个窗口,该方法使用该窗口进入全屏独占模式。如果全屏独占模式不可用,则窗口将定位在(0,0)并调整大小以适应屏幕。使用带有
null
参数的方法退出全屏独占模式。
编程提示
以下是关于使用全屏独占模式进行编程的一些建议:
-
在进入全屏独占模式之前检查
isFullScreenSupported
。如果不支持,性能可能会降低。 -
进入和退出全屏模式时,使用
try...finally
子句更加健壮。这不仅是良好的编码实践,还可以防止程序停留在全屏独占模式中的时间超过应有的时间:GraphicsDevice myDevice; Window myWindow; try { myDevice.setFullScreenWindow(myWindow); ... } finally { myDevice.setFullScreenWindow(null); }
-
大多数全屏独占应用程序更适合使用无装饰窗口。使用
setUndecorated
方法在框架或对话框中关闭装饰。 -
全屏独占应用程序不应该是可调整大小的,因为调整全屏应用程序的大小可能会导致不可预测的(或可能危险的)行为。
-
出于安全原因,在应用程序中使用全屏独占模式时,用户必须授予
fullScreenExclusive
权限。
显示模式
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/displaymode.html
一旦应用程序处于全屏独占模式,它可能能够利用主动设置显示模式。显示模式(java.awt.DisplayMode
)由尺寸(以像素为单位的监视器宽度和高度)、位深度(每像素的位数)和刷新率(监视器更新自身的频率)组成。一些操作系统允许您同时使用多个位深度,此时特殊值BIT_DEPTH_MULTI
用于位深度的值。此外,一些操作系统可能无法控制刷新率(或者您可能不关心刷新率设置)。在这种情况下,特殊值REFRESH_RATE_UNKNOWN
用于刷新率值。
如何设置显示模式
要获取当前显示模式,只需在图形设备上调用getDisplayMode
方法。要获取所有可能的显示模式列表,请调用getDisplayModes
方法。无论您是否处于全屏独占模式,都可以随时调用getDisplayMode
和getDisplayModes
。
在尝试更改显示模式之前,您应该首先调用isDisplayChangeSupported
方法。如果此方法返回false
,则操作系统不支持更改显示模式。
只有在全屏独占模式下才能更改显示模式。要更改显示模式,请使用所需的显示模式调用setDisplayMode
方法。如果显示模式不可用,不支持显示模式更改,或者您未在全屏独占模式下运行,则会抛出运行时异常。
更改显示模式的原因
设置显示模式的主要原因是性能。如果应用程序选择显示的图像与屏幕具有相同的位深度,应用程序可以运行得更快。此外,如果您可以依赖显示器具有特定尺寸,那么绘制到该显示器会简单得多,因为您不必根据用户设置的显示方式缩放或放大物体。
编程提示
以下是选择和设置显示模式的一些建议:
-
在尝试更改图形设备上的显示模式之前,请检查
isDisplayChangeSupported
方法返回的值。 -
在尝试更改显示模式之前,请确保您处于全屏独占模式。
-
与使用全屏模式一样,设置显示模式在
try...finally
子句中更加健壮:GraphicsDevice myDevice; Window myWindow; DisplayMode newDisplayMode; DisplayMode oldDisplayMode = myDevice.getDisplayMode(); try { myDevice.setFullScreenWindow(myWindow); myDevice.setDisplayMode(newDisplayMode); ... } finally { myDevice.setDisplayMode(oldDisplayMode); myDevice.setFullScreenWindow(null); }
-
在为应用程序选择显示模式时,您可能希望保留首选显示模式列表,然后从可用显示模式列表中选择最佳显示模式。
-
作为备用方案,如果您想要的显示模式不可用,您可能希望以固定大小的窗口模式运行。
被动渲染 vs. 主动渲染
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/rendering.html
正如之前提到的,大多数全屏应用程序通常在绘制时更好地处于控制状态。在传统的窗口化 GUI 应用程序中,绘制的时间通常由操作系统处理。在窗口化环境中,这是完全合理的。窗口化应用程序不知道用户何时会移动、调整大小、暴露或被另一个窗口覆盖,直到实际发生。在 Java GUI 应用程序中,操作系统向 AWT 传递一个绘画事件,AWT 找出需要绘制的内容,创建一个带有适当裁剪区域的java.awt.Graphics
对象,然后使用该Graphics
对象调用paint
方法:
// Traditional GUI Application paint method:
// This can be called at any time, usually
// from the event dispatch thread
public void paint(Graphics g) {
// Use g to draw my Component
}
这有时被称为被动渲染。可以想象,这样的系统会产生很多开销,让许多性能敏感的 AWT 和 Swing 程序员感到恼火。
在全屏独占模式下,您不再需要担心窗口被调整大小、移动、暴露或遮挡(除非您忽略了我关于关闭调整大小的建议)。相反,应用程序窗口直接绘制到屏幕上(主动渲染)。这样做可以简化绘制过程,因为您永远不需要担心绘画事件。事实上,在全屏独占模式下,由操作系统传递的绘画事件甚至可能在不适当或不可预测的时间传递。
在全屏独占模式下,不要依赖paint
方法,绘制代码通常更适合在渲染循环中完成:
public void myRenderingLoop() {
while (!done) {
Graphics myGraphics = getPaintGraphics();
// Draw as appropriate using myGraphics
myGraphics.dispose();
}
}
这样的渲染循环可以在任何线程中完成,可以是自己的辅助线程,也可以作为主应用程序线程的一部分。
编程提示
一些关于使用主动渲染的提示:
-
不要将绘制代码放在
paint
方法中。您永远不知道该方法何时会被调用!相反,使用另一个方法名,比如render(Graphics g)
,在窗口模式下可以从paint
方法中调用,或者在渲染循环中使用自己的图形调用。 -
使用
setIgnoreRepaint
方法在应用程序窗口和组件上关闭所有从操作系统完全分派的绘画事件,因为这些事件可能在不适当的时间调用,或者更糟糕的是,调用paint
,这可能导致 AWT 事件线程和您的渲染循环之间的竞争条件。 -
将绘制代码与渲染循环分开,以便在全屏独占和窗口模式下都能完全运行。
-
优化您的渲染,以便您不会始终在屏幕上绘制所有内容(除非您使用翻页或双缓冲,下面将讨论)。
-
不要依赖
update
或repaint
方法来传递绘画事件。 -
不要使用重量级组件,因为这些组件仍会产生涉及 AWT 和平台窗口系统的开销。
-
如果您使用轻量级组件,比如 Swing 组件,您可能需要稍微调整它们,以便它们使用您的
Graphics
进行绘制,而不是直接调用paint
方法。请随意从您的渲染循环直接调用 Swing 方法,如paintComponents
、paintComponent
、paintBorder
和paintChildren
。 -
如果您只想要一个简单的全屏 Swing 或 AWT 应用程序,可以随意使用被动渲染,但请记住,在全屏独占模式下,绘制事件可能有些不可靠或不必要。此外,如果您使用被动渲染,您将无法使用更高级的技术,如翻页。最后,请务必小心避免死锁,如果您决定同时使用主动和被动渲染—这种方法并不推荐。
双缓冲和页面翻转
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/doublebuf.html
假设您必须逐像素或逐行在屏幕上绘制整个图片。如果您直接将这样的东西绘制到屏幕上(使用Graphics.drawLine
),您可能会发现需要一些时间。您甚至可能会注意到您的图片是如何绘制的可见痕迹。与观看以这种方式和速度绘制事物不同,大多数程序员使用一种称为双缓冲的技术。
在 Java 应用程序中,双缓冲的传统概念相当简单:创建一个屏幕外图像,使用图像的图形对象绘制到该图像,然后,在一步中,使用目标窗口的图形对象和屏幕外图像调用drawImage
。您可能已经注意到,Swing 在其许多组件中使用了这种技术,通常默认启用,使用setDoubleBuffered
方法。
屏幕表面通常被称为主表面,用于双缓冲的屏幕外图像通常被称为后备缓冲区。将内容从一个表面复制到另一个表面的行为通常被称为块线传输,或blitting(blt 通常发音为"blit",不应与 BLT 三明治混淆)。
主表面通常通过任何显示组件的图形对象进行操作;在全屏模式下,使用全屏窗口的图形进行任何操作是对屏幕内存的直接操作。因此,您可以利用全屏独占模式中可能由于窗口系统的开销而无法使用的其他功能。在全屏独占模式中仅可用的一种技术是一种称为页面翻转的双缓冲形式。
页面翻转
许多显卡都有视频指针的概念,它只是视频内存中的一个地址。这个指针告诉显卡在下一个刷新周期期间要显示的视频内容在哪里。在一些显卡和一些操作系统中,这个指针甚至可以通过编程方式进行操作。假设你在视频内存中创建了一个与屏幕精确宽度、高度和位深度相同的后备缓冲区,然后像使用双缓冲区一样向该缓冲区绘制。现在想象一下,如果不像双缓冲区那样将图像传输到屏幕上,而是简单地将视频指针更改为你的后备缓冲区,那会发生什么。在下一次刷新期间,显卡现在将使用你的图像来显示。这种切换称为翻页,与基于 blt 的双缓冲区相比,性能提升在于只需要在内存中移动一个指针,而不是将整个内容从一个缓冲区复制到另一个缓冲区。
当发生页面翻转时,旧后备缓冲区的指针现在指向主表面,旧主表面的指针现在指向后备缓冲区内存。这会自动为下一个绘制操作做好准备。
有时在翻页链中设置多个后备缓冲区是有利的。当绘制所花费的时间大于显示器的刷新率时,这是特别有用的。翻页链简单地是两个或更多后备缓冲区(有时称为中间缓冲区)加上主表面(有时称为三重缓冲、四重缓冲等)。在翻页链中,下一个可用的后备缓冲区变为主表面,依此类推,一直到用于绘制的最后一个后备缓冲区。
双缓冲和翻页的好处
如果你的性能指标只是双缓冲或翻页发生的速度与直接渲染相比,你可能会感到失望。你可能会发现,直接渲染的数字远远超过双缓冲的数字,而双缓冲的数字远远超过翻页的数字。这些技术都是用于改善感知性能,在图形应用程序中比数值性能更重要。
双缓冲主要用于消除可见的绘制,这可以使应用程序看起来业余、迟缓或闪烁。翻页主要用于消除撕裂,这是一种在绘制到屏幕上比显示器的刷新率更快时发生的分裂效果。更平滑的绘制意味着更好的感知性能和更好的用户体验。
BufferStrategy 和 BufferCapabilities
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/bufferstrategy.html
BufferStrategy
在 Java 2 标准版中,您无需担心视频指针或视频内存就可以充分利用双缓冲或翻页。新的类java.awt.image.BufferStrategy
已经添加,以便处理绘制到表面和组件的通用方式,无论使用的缓冲区数量或显示它们的技术。
缓冲策略为您提供了两种通用的绘图方法:getDrawGraphics
和 show
。当您想开始绘图时,获取一个绘图对象并使用它。当您完成绘图并希望将信息呈现到屏幕上时,请调用 show
。这两种方法被设计得相当优雅,可以很好地适应渲染循环:
BufferStrategy myStrategy;
while (!done) {
Graphics g = myStrategy.getDrawGraphics();
render(g);
g.dispose();
myStrategy.show();
}
缓冲策略还设置了帮助您监视VolatileImage
问题。在全屏独占模式下,VolatileImage
问题尤为重要,因为窗口系统有时会收回它给您的视频内存。一个重要的例子是当用户在 Windows 中按下ALT+TAB
组合键时,突然您的全屏程序在后台运行,您的视频内存就会丢失。您可以调用contentsLost
方法来查看是否发生了这种情况。同样,当窗口系统将内存还给您时,您可以使用contentsRestored
方法来查找。
BufferCapabilities
如前所述,不同的操作系统,甚至同一操作系统上的不同显卡,都有不同的可用技术。这些能力被暴露出来,以便您可以为应用程序选择最佳技术。
类java.awt.BufferCapabilities
封装了这些功能。每个缓冲策略都由其缓冲能力控制,因此为您的应用程序选择正确的缓冲能力非常关键。要找出哪些功能可用,请从您的图形设备上可用的GraphicsConfiguration
对象调用getBufferCapabilities
方法。
Java 2 标准版 1.4 版中提供的功能有:
-
isPageFlipping
此功能返回此图形配置上是否可用硬件翻页。
-
isFullScreenRequired
此功能返回在尝试硬件翻页之前是否需要全屏独占模式。
-
isMultiBufferAvailable
此功能返回硬件中是否可用多缓冲(两个或更多后备缓冲区加上主表面)。
-
getFlipContents
此功能返回用于执行硬件翻页的技术提示。这很重要,因为在使用的技术不同的情况下,
show
后的后备缓冲区内容也会有所不同。返回的值可以为 null(如果isPageFlipping
返回false
)或以下值之一。只要isPageFlipping
方法返回 true,就可以为缓冲策略指定任何值,尽管性能会根据可用功能而变化。 -
FlipContents.COPIED
此值意味着后备缓冲区的内容已复制到主表面。"翻页"可能是作为硬件位块传输执行的,这意味着硬件双缓冲可能是通过位块传输而不是真正的翻页完成的。这在理论上应该更快,或者至少与从
VolatileImage
到主表面的位块传输一样快,尽管实际效果可能有所不同。翻页后,后备缓冲区的内容与主表面相同。 -
FlipContents.BACKGROUND
此值意味着后备缓冲区的内容已用背景颜色清除。可能已执行真正的翻页或位块传输。
-
FlipContents.PRIOR
此值意味着后备缓冲区的内容现在是旧主表面的内容,反之亦然。通常,此值表示真正的翻页发生,尽管这并不是保证,再次强调,此操作的效果可能有所不同。
-
FlipContents.UNKNOWN
此值意味着翻页后的后备缓冲区内容未定义。您可能需要尝试找出哪种技术最适合您(或者您可能不在乎),并且您肯定需要每次绘制时自己设置后备缓冲区的内容。
要为组件创建缓冲策略,请调用createBufferStrategy
方法,提供所需的缓冲区数量(此数字包括主表面)。如果需要任何特定的缓冲技术,请提供适当的BufferCapabilities
对象。请注意,当您使用此方法的版本时,必须在您的选择不可用时捕获AWTException
。还要注意,这些方法仅适用于Canvas
和Window
。
一旦为组件创建了特定的缓冲策略,您可以使用getBufferStrategy
方法对其进行操作。请注意,此方法也仅适用于画布和窗口。
编程提示
关于使用缓冲能力和缓冲策略的一些建议:
-
在
try...finally
子句中更加稳健地获取、使用和释放图形对象:BufferStrategy myStrategy; while (!done) { Graphics g; try { g = myStrategy.getDrawGraphics(); render(g); } finally { g.dispose(); } myStrategy.show(); }
-
在使用缓冲策略之前,请检查可用的功能。
-
为了获得最佳效果,请在全屏独占窗口上创建您的缓冲策略。在使用翻页之前,请确保检查
isFullScreenRequired
和isPageFlipping
功能。 -
不要对性能做任何假设。根据需要调整你的代码,但要记住不同的操作系统和显卡具有不同的功能。对你的应用程序进行性能分析!
-
你可能想要对你的组件进行子类化,以重写
createBufferStrategy
方法。使用一个适合你的应用程序的策略选择算法。FlipBufferStrategy
和BltBufferStrategy
内部类是受保护的,可以被子类化。 -
不要忘记你可能会丢失你的绘图表面!在绘制之前一定要检查
contentsLost
和contentsRestored
。所有已丢失的缓冲区在恢复时都必须重新绘制。 -
如果在 Swing 应用程序中使用缓冲策略进行双缓冲,你可能希望关闭 Swing 组件的双缓冲,因为它们已经是双缓冲的。视频内存有一定的价值,应仅在绝对必要时使用。
-
使用多个后备缓冲区可能会导致浪费。当绘制时间超过执行
show
的时间时,多缓冲仅有用。对你的应用程序进行性能分析!
示例
原文:
docs.oracle.com/javase/tutorial/extra/fullscreen/example.html
CapabilitiesTest
展示了在运行该程序的机器上可用的不同缓冲能力。
DisplayModeTest
展示了一个使用被动渲染的 Swing 应用程序。如果全屏独占模式可用,它将进入全屏独占模式。如果允许显示模式更改,它允许您在显示模式之间切换。
MultiBufferTest
进入全屏模式,并通过活动渲染循环使用多缓冲。
课程:准备 Java 程序员语言认证
原文:
docs.oracle.com/javase/tutorial/extra/certification/index.html
Oracle 为 Java SE 8 程序员提供了两条认证路径:
-
Oracle 认证 Java SE 8 程序员(Oracle Certified Associate, Java SE 8 Programmer)
-
Oracle 认证 Java SE 8 专业程序员(Oracle Certified Professional, Java SE 8 Programmer)
想要学习 Java 语言的高级指南,请参阅 Java 教程学习路径。
教程:Java 命名和目录接口
原文:
docs.oracle.com/javase/tutorial/jndi/index.html
本教程描述了 JNDI™(Java 命名和目录接口)是一个用于访问目录和命名服务的 API。在这里,您将了解基本的命名和目录服务以及如何使用 JNDI 编写简单的应用程序来使用这些服务。最流行的目录服务 LDAP 被用来演示使用 JNDI 访问目录服务。
从这里开始,了解命名和目录概念的概述。
为您提供了 JNDI、其架构和打包的概述。
描述了设置运行本教程中描述的示例以及任何其他 JNDI 应用程序所需的环境的说明和步骤。
描述了各种命名和目录操作,并通过许多示例演示了使用 JNDI 访问命名/目录服务。
针对 LDAP 用户的专门课程。它讨论了将 JNDI 建模为 LDAP API、如何执行 LDAP 认证、SSL 和在生产环境中管理连接等内容。
展示了如何将您的应用程序与目录集成,以便您可以将 Java 对象存储到目录中并从目录中检索。
介绍了 JDK 5.0 和 JDK 6 中可用的 JNDI 和 LDAP 服务提供程序中的功能。
注意: 本教程基于位于 docs.oracle.com/javase/jndi/tutorial/
的独立 JNDI 教程。最后更新于 Java 2 SDK,标准版,v 1.4.2,全面介绍了 JNDI,但不再受支持。本教程从独立教程中摘录了基础知识,并包括了在 Java 平台标准版 5.0 和 6 发行版中添加的功能。
旧版 JNDI 教程被保留为 docs.oracle.com 上的存档。
教训:命名和目录概念
原文:
docs.oracle.com/javase/tutorial/jndi/concepts/index.html
命名概念
在任何计算系统中的一个基本设施是命名服务——通过名称与对象关联并根据其名称找到对象的方法。在几乎任何计算机程序或系统中使用时,您总是在命名一个对象或另一个对象。例如,当您使用电子邮件系统时,必须提供收件人的名称。要访问计算机中的文件,必须提供其名称。命名服务允许您根据名称查找对象。
命名服务的主要功能是将人们友好的名称映射到对象,例如地址、标识符或通常由计算机程序使用的对象。
例如,Internet 域名系统(DNS)将机器名称映射到 IP 地址:
www.example.com ==> 192.0.2.5
文件系统将文件名映射到程序可以使用的文件引用,以访问文件的内容。
c:\bin\autoexec.bat ==> File Reference
这两个示例也说明了命名服务存在的广泛规模范围——从在互联网上命名对象到在本地文件系统上命名文件。
名称
要在命名系统中查找对象,您需要提供对象的名称。命名系统确定名称必须遵循的语法。这种语法有时被称为命名系统的命名约定。名称由组件组成。名称的表示包括一个组件分隔符,标记名称的组件。
命名系统 | 组件分隔符 | 名称 |
---|---|---|
UNIX 文件系统 | "/" | /usr/hello |
DNS | "." | sales.Wiz.COM |
LDAP | "," 和 "=" | cn=Rosanna Lee, o=Sun, c=US |
UNIX 文件系统的命名约定是,文件的命名来自于相对于文件系统根目录的路径,路径中的每个组件从左到右使用斜杠字符("/")分隔。例如,UNIX 的路径名 /usr/hello
,命名了位于文件系统根目录中的文件目录usr
中的文件hello
。
DNS 命名约定要求 DNS 名称中的组件从右到左排序,并用点字符(".")分隔。因此,DNS 名称sales.Wiz.COM
命名了一个名为sales
的 DNS 条目,相对于 DNS 条目Wiz.COM
。而 DNS 条目Wiz.COM
又命名了一个名为Wiz
的条目,位于COM
条目中。
轻量级目录访问协议(LDAP)命名约定按从右到左的顺序排列组件,以逗号字符(“,”)分隔。因此,LDAP 名称cn=Rosanna Lee, o=Sun, c=US
指定了一个 LDAP 条目cn=Rosanna Lee
,相对于条目o=Sun
,后者又相对于c=us
。LDAP 还有一个规则,即名称的每个组件必须是一个名称/值对,名称和值之间用等号字符(“=”)分隔。
绑定
名称与对象的关联称为绑定。文件名绑定到文件。
DNS 包含将机器名称映射到 IP 地址的绑定。LDAP 名称绑定到 LDAP 条目。
参考和地址
根据命名服务的不同,一些对象无法直接存储在命名服务中;也就是说,不能将对象的副本放入命名服务中。相反,它们必须通过引用存储;也就是说,将指向对象的指针或引用放入命名服务中。引用表示如何访问对象的信息。通常,它是一种紧凑的表示形式,可用于与对象通信,而对象本身可能包含更多状态信息。使用引用,您可以联系对象并获取有关对象的更多信息。
例如,飞机对象可能包含飞机的乘客和机组人员列表,其飞行计划,燃料和仪表状态,以及其航班号和起飞时间。相比之下,飞机对象引用可能只包含其航班号和起飞时间。引用是关于飞机对象信息的更紧凑表示,并可用于获取附加信息。例如,文件对象使用文件引用访问。另外,打印机对象可能包含打印机的状态,如当前队列和纸盒中的纸张量。另一方面,打印机对象引用可能只包含有关如何到达打印机的信息,如其打印服务器名称和打印协议。
尽管一般来说引用可以包含任意信息,但将其内容称为地址(或通信终点)是有用的:关于如何访问对象的具体信息。
为简单起见,本教程在不需要区分两者时使用“对象”来指代对象和对象引用。
上下文
上下文是一组名称到对象的绑定。每个上下文都有一个相关的命名约定。上下文始终提供一个查找(解析)操作,返回对象,通常还提供绑定名称、解绑名称和列出绑定名称等操作。一个上下文对象中的名称可以绑定到另一个具有相同命名约定的上下文对象(称为子上下文)。
UNIX 文件系统中的文件目录,例如/usr
,代表一个上下文。相对于另一个文件目录命名的文件目录代表一个子上下文(UNIX 用户将其称为子目录)。也就是说,在文件目录/usr/bin
中,目录bin
是usr
的子上下文。DNS 域,例如COM
,代表一个上下文。相对于另一个 DNS 域命名的 DNS 域代表一个子上下文。对于 DNS 域Sun.COM
,DNS 域Sun
是COM
的子上下文。
最后,LDAP 条目,例如c=us
,代表一个上下文。相对于另一个 LDAP 条目命名的 LDAP 条目代表一个子上下文。对于 LDAP 条目o=sun,c=us
,条目o=sun
是c=us
的子上下文。
命名系统和命名空间
命名系统是同一类型上下文的连接集合(它们具有相同的命名约定),并提供一组共同的操作。
实现 DNS 的系统是一个命名系统。使用 LDAP 进行通信的系统也是一个命名系统。
命名系统为其客户提供命名服务,用于执行与命名相关的操作。命名服务通过其自己的接口访问。DNS 提供将机器名称映射到 IP 地址的命名服务。LDAP 提供将 LDAP 名称映射到 LDAP 条目的命名服务。文件系统提供将文件名映射到文件和目录的命名服务。
命名空间是命名系统中所有可能名称的集合。UNIX 文件系统具有一个命名空间,其中包含该文件系统中所有文件和目录的名称。DNS 命名空间包含 DNS 域和条目的名称。LDAP 命名空间包含 LDAP 条目的名称。
目录概念
原文:
docs.oracle.com/javase/tutorial/jndi/concepts/directory.html
许多命名服务都扩展了目录服务。目录服务将名称与对象关联,并将这些对象与属性关联起来。
目录服务 = 命名服务 + 包含属性的对象
你不仅可以通过名称查找对象,还可以获取对象的属性或根据其属性搜索对象。
一个例子是电话公司的目录服务。它将订户的姓名映射到他的地址和电话号码。计算机的目录服务非常类似于电话公司的目录服务,因为两者都可以用来存储诸如电话号码和地址之类的信息。然而,计算机的目录服务更加强大,因为它可以在线使用,并且可以用来存储各种信息,这些信息可以被用户、程序甚至计算机本身和其他计算机利用。
目录对象表示计算环境中的一个对象。例如,目录对象可以用来表示打印机、人员、计算机或网络。目录对象包含描述其所代表对象的属性。
属性
目录对象可以具有属性。例如,打印机可以由具有其速度、分辨率和颜色等属性的目录对象表示。用户可以由具有用户的电子邮件地址、各种电话号码、邮寄地址和计算机帐户信息等属性的目录对象表示。
属性具有属性标识符和一组属性值。属性标识符是一个标记,独立于其值标识属性。例如,两个不同的计算机帐户可能具有一个"mail"
属性;"mail"
是属性标识符。属性值是属性的内容。例如,电子邮件地址可能有:
Attribute Identifier : Attribute Value
mail john.smith@example.com
目录和目录服务
目录是一组连接的目录对象。目录服务是提供用于在目录中创建、添加、删除和修改与对象关联的属性的操作的服务。该服务通过其自己的接口访问。
可能有许多目录服务的示例。
网络信息服务(NIS)
NIS 是 UNIX 操作系统上可用的目录服务,用于存储与机器、网络、打印机和用户相关的系统信息。
Oracle 目录服务器
Oracle 目录服务器是一个基于互联网标准LDAP的通用目录服务。
搜索服务
你可以通过向目录服务提供其名称来查找目录对象。另外,许多目录,比如基于 LDAP 的目录,支持搜索的概念。当你搜索时,你可以提供一个查询,其中包含一个逻辑表达式,你可以在其中指定对象必须具有的属性。这个查询被称为搜索过滤器。这种搜索方式有时被称为反向查找或基于内容的搜索。目录服务会搜索并返回满足搜索过滤器的对象。
例如,你可以查询目录服务以查找:
-
所有具有属性
"age"
大于 40 岁的用户。 -
所有 IP 地址以"192.113.50"开头的机器。
结合命名和目录服务
目录通常将它们的对象按层次结构排列。例如,LDAP 将所有目录对象排列在一棵树中,称为目录信息树(DIT)。在 DIT 中,一个组织对象,例如,可能包含可能又包含人员对象的组对象。当目录对象以这种方式排列时,它们除了作为属性容器外还扮演了命名上下文的角色。
课程:JNDI 概述
原文:
docs.oracle.com/javase/tutorial/jndi/overview/index.html
Java 命名和目录接口(JNDI)是一个提供命名和目录功能的应用程序编程接口(API),用于使用 Java™ 编程语言编写的应用程序。它被定义为独立于任何特定目录服务实现。因此,各种目录 - 新的、新兴的和已部署的都可以以一种通用的方式访问。
架构
JNDI 架构包括 API 和服务提供者接口(SPI)。Java 应用程序使用 JNDI API 访问各种命名和目录服务。SPI 使各种命名和目录服务能够透明地插入,从而允许使用 JNDI API 的 Java 应用程序访问它们的服务。请参见下图:
打包
JNDI 包含在 Java SE 平台中。要使用 JNDI,您必须具有 JNDI 类和一个或多个服务提供者。JDK 包含以下命名/目录服务的服务提供者:
-
轻量级目录访问协议(LDAP)
-
公共对象请求代理体系结构(CORBA)公共对象服务(COS)名称服务
-
Java 远程方法调用(RMI)注册表
-
域名服务(DNS)
其他服务提供者可以从JNDI 页面下载或从其他供应商获取。
JNDI 分为五个包:
-
javax.naming
-
javax.naming.directory
-
javax.naming.ldap
-
javax.naming.event
-
javax.naming.spi
课程的下一部分简要描述了 JNDI 包。
命名包
原文:
docs.oracle.com/javase/tutorial/jndi/overview/naming.html
javax.naming
包含用于访问命名服务的类和接口。
上下文
javax.naming
包定义了一个Context
接口,这是查找、绑定/解绑、重命名对象以及创建和销毁子上下文的核心接口。
查找
最常用的操作是lookup()
。您提供lookup()
要查找的对象的名称,它将返回绑定到该名称的对象。
绑定
listBindings()
返回一个名称到对象绑定的枚举。绑定是一个包含绑定对象的名称、对象类的名称和对象本身的元组。
列表
list()
类似于listBindings()
,只是它返回一个包含对象名称和对象类名称的名称枚举。list()
对于诸如浏览器之类的应用程序很有用,这些应用程序希望发现上下文中绑定的对象的信息,但不需要所有实际对象。虽然listBindings()
提供了相同的信息,但它可能是一个更昂贵的操作。
名称
Name
是表示通用名称的接口——一个有序的零个或多个组件的序列。命名系统使用此接口来定义遵循其约定的名称,如命名和目录概念课程中所述。
引用
对象以不同方式存储在命名和目录服务中。引用可能是对象的非常紧凑的表示。
JNDI 定义了Reference
类来表示引用。引用包含有关如何构造对象副本的信息。JNDI 将尝试将从目录查找的引用转换为它们所代表的 Java 对象,以便 JNDI 客户端产生存储在目录中的是 Java 对象的错觉。
初始上下文
在 JNDI 中,所有命名和目录操作都是相对于上下文执行的。没有绝对的根。因此,JNDI 定义了一个InitialContext
,它提供了命名和目录操作的起始点。一旦您有了初始上下文,您可以使用它来查找其他上下文和对象。
异常
JNDI 定义了一个类层次结构,用于在执行命名和目录操作过程中可能抛出的异常。这个类层次结构的根是NamingException
。对于对特定异常感兴趣的程序,可以捕获异常的相应子类。否则,它们应该捕获NamingException
。
目录和 LDAP 包
原文:
docs.oracle.com/javase/tutorial/jndi/overview/dir.html
目录包
javax.naming.directory
包扩展了javax.naming
包,提供了访问目录服务以及命名服务的功能。该包允许应用程序检索存储在目录中的对象相关的属性,并使用指定的属性搜索对象。
目录上下文
DirContext
接口代表一个目录上下文。DirContext
还通过扩展Context
接口来充当命名上下文。这意味着任何目录对象也可以提供命名上下文。它定义了用于检查和更新与目录条目关联的属性的方法。
属性
您可以使用getAttributes()
方法检索与目录条目关联的属性(您提供名称)。使用modifyAttributes()
方法修改属性。您可以使用此操作添加、替换或删除属性和/或属性值。
搜索
DirContext
包含用于执行基于内容的目录搜索的方法。在最简单和最常见的用法中,应用程序指定一组可能具有特定值的属性,并将此属性集提交给search()
方法。其他重载形式的search()
支持更复杂的搜索过滤器。
LDAP 包
javax.naming.ldap
包含了用于使用特定于LDAP v3的功能的类和接口,这些功能不在更通用的javax.naming.directory
包中。事实上,大多数使用 LDAP 的 JNDI 应用程序会发现javax.naming.directory
包已经足够,并且根本不需要使用javax.naming.ldap
包。这个包主要是为那些需要使用“扩展”操作、控件或未经请求的通知的应用程序而设计的。
“扩展”操作
除了指定了搜索和修改等明确定义的操作之外,LDAP v3 (RFC 2251)还指定了在 LDAP 客户端和服务器之间传输尚未定义操作的方法。这些操作被称为“扩展”操作。一个“扩展”操作可以由标准组织(如互联网工程任务组)或供应商定义。
控件
LDAP v3允许任何请求或响应通过尚未定义的修饰符进行增强,称为控件。与请求一起发送的控件是请求控件,与响应一起发送的控件是响应控件。控件可以由标准组织(如 IETF)或供应商定义。请求控件和响应控件不一定是成对的,也就是说,并非每个发送的请求控件都需要有相应的响应控件,反之亦然。
未经请求的通知
除了客户端和服务器之间的正常请求/响应交互方式之外,LDAP v3还指定了未经请求的通知--这些消息是从服务器异步发送给客户端的,而不是响应于任何客户端请求。
LDAP 上下文
LdapContext
接口代表了执行“扩展”操作、发送请求控件和接收响应控件的上下文。如何使用这些功能的示例在 JNDI 教程的控件和扩展课程中有描述。
事件和服务提供者包
原文:
docs.oracle.com/javase/tutorial/jndi/overview/event.html
事件包
javax.naming.event
包含用于支持命名和目录服务中事件通知的类和接口。事件通知在事件通知指南中有详细描述。
事件
一个NamingEvent
代表由命名/目录服务生成的事件。事件包含一个类型,用于标识事件类型。例如,事件类型被分类为影响命名空间的事件,如“对象添加”,以及不影响命名空间的事件,如“对象更改”。
监听器
一个NamingListener
是一个监听NamingEvent
的对象。每种事件类型的类别都有相应类型的NamingListener
。例如,一个NamespaceChangeListener
代表一个对命名空间更改事件感兴趣的监听器,而一个ObjectChangeListener
代表一个对对象更改事件感兴趣的监听器。
要接收事件通知,监听器必须注册到EventContext
或EventDirContext
中。一旦注册,当命名/目录服务中发生相应更改时,监听器将接收事件通知。有关事件通知的详细信息可以在JNDI 教程中找到。
服务提供者包
javax.naming.spi
包提供了不同命名/目录服务提供者的开发者可以开发和连接其实现的手段,以便从使用 JNDI 的应用程序中访问相应服务。
插件架构
javax.naming.spi
包允许动态地插入不同的实现。这些实现包括初始上下文和可以从初始上下文访问的上下文。
Java 对象支持
javax.naming.spi
包支持 lookup 及其相关方法的实现者返回对于 Java 程序员而言自然且直观的 Java 对象。例如,如果你从目录中查找打印机名称,那么你很可能期望得到一个打印机对象来进行操作。这种支持以 object factories 的形式提供。
这个包还提供了支持进行相反操作的功能。也就是说,Context.bind()
及其相关方法的实现者可以接受 Java 对象,并将这些对象存储在底层命名/目录服务可接受的格式中。这种支持以 state factories 的形式提供。
多个命名系统(联邦)
JNDI 操作允许应用程序提供跨多个命名系统的名称。在完成操作的过程中,一个服务提供者可能需要与另一个服务提供者交互,例如将操作传递给下一个命名系统继续进行。这个包提供了不同提供者合作完成 JNDI 操作的支持。
有关服务提供者机制的详细信息可以在JNDI 教程中找到。
课程:软件设置
原文:
docs.oracle.com/javase/tutorial/jndi/software/index.html
所需软件
以下是您需要的软件/系统列表:
-
Java 平台软件
-
服务提供者软件
-
命名和目录服务器软件
Java 平台软件
JNDI 包含在 Java SE 平台中。
要运行小程序,请在 Microsoft Edge 上使用 IE 模式。请参阅Microsoft Edge + Internet Explorer 模式:入门指南。
服务提供者软件
JNDI API 是用于访问任何命名或目录服务的通用 API。通过在 JNDI 下插入服务提供者,可以实现对命名或目录服务的实际访问。有关 JNDI 架构和服务提供者角色的概述,请参阅 JNDI 概述课程。
服务提供者是将 JNDI API 映射到对命名或目录服务器的实际调用的软件。通常,服务提供者的角色与命名/目录服务器的角色不同。在客户端/服务器软件术语中,JNDI 和服务提供者是客户端(称为JNDI 客户端),而命名/目录服务器是服务器。
客户端和服务器可以以许多方式进行交互。在一种常见的方式中,它们使用网络协议,以便客户端和服务器可以在网络环境中独立存在。服务器通常支持许多不同的客户端,不仅限于 JNDI 客户端,只要客户端符合指定的协议。JNDI 不规定 JNDI 客户端和服务器之间的任何特定交互方式。例如,在一个极端情况下,客户端和服务器可以是同一实体。
您需要获取将要使用的服务提供者的类。例如,如果您计划使用 JNDI 访问 LDAP 目录服务器,则需要 LDAP 服务提供者的软件。
JDK 附带以下服务提供者:
-
轻量级目录访问协议(LDAP)
-
CORBA 公共对象服务命名(COS 命名)
-
RMI 注册表
-
域名服务(DNS)
如果您对其他提供者感兴趣,请查看JNDI 页面以获取下载信息。
本教程仅使用 LDAP 服务提供者。使用 LDAP 服务提供者时,您需要设置自己的服务器或访问现有服务器,如下所述。
命名和目录服务器软件
一旦您获得了服务提供商软件,您就需要设置或访问相应的命名/目录服务器。设置命名/目录服务器通常是网络系统管理员的工作。不同的供应商对其命名/目录服务器有不同的安装程序。有些在服务器安装之前需要特殊的机器权限。您应该查阅命名/目录服务器软件的安装说明。
在本教程中的目录示例中,您需要访问一个 LDAP 服务器。如果您想快速了解 LDAP 是什么,请查看这里。您可以使用您选择的任何符合 LDAP 标准的服务器。Oracle Directory Server 在许多平台上运行,包括 Windows,可在以下网址进行评估:Oracle Directory Server。
您也可以在下面下载一个免费的 LDAP 服务器:
-
389 目录服务器
-
Apache 目录服务器
-
OpenLDAP
一个公开访问的服务器位于:ldap://ldap.openldap.org 命名上下文:dc=OpenLDAP,dc=org
LDAP 设置
原文:
docs.oracle.com/javase/tutorial/jndi/software/content.html
下面是构建访问 LDAP 目录服务器的 Java 应用程序涉及的步骤。
-
安装 Java 平台软件。
-
获取目录服务器软件,如之前讨论过的。
-
配置目录服务器以使用所需的模式。在本教程中使用的示例需要在服务器上配置一个特殊的 schema。
-
使用所需的内容填充目录服务器。在本教程中使用的示例需要在服务器上填充一个特殊的内容。
-
编写一个 JNDI 应用程序来访问目录,编译并运行它以获得您想要的结果。JNDI 示例将在下一个课程中介绍。
前两个步骤在前一部分中已经涵盖。本课程的其余部分讨论了第三步和第四步的一部分。涉及编写 JNDI 应用程序的第五步在下一课程中介绍,展示如何编写 JNDI 应用程序来执行目录上的各种操作。
一旦您设置了目录,或者已经指示您的程序与现有目录通信,您可以在那里找到什么样的信息?
目录可以被视为由名称到对象绑定组成。也就是说,目录中的每个对象都有一个对应的名称。您可以通过查找其名称来检索目录中的对象。
目录中还存储着属性。目录中的对象除了有一个名称外,还有一组可选的属性。您可以向目录查询对象的属性,也可以要求它搜索具有特定属性的对象。
步骤 3:目录模式
模式指定目录可能包含的对象类型。本教程使用条目填充目录,其中一些条目需要特殊的模式定义。为了容纳这些条目,您必须首先在服务器中关闭模式检查,或者将附带本教程的模式文件添加到服务器中。这两项任务通常由目录服务器的管理员执行。
本教程附带两个必须安装的模式文件:
-
Java 对象的模式
-
CORBA 对象的模式
这些文件的格式是一种可能不能直接复制粘贴到服务器配置文件中的正式描述。具体来说,属性语法是根据RFC 2252描述的。
不同的目录服务器有不同的配置模式方式。本教程包括一些工具,用于在允许通过 LDAP 修改其模式的目录服务器上安装 Java 和 CORBA 模式。以下是工具可以执行的任务列表。
-
创建 Java 模式
-
创建 CORBA 模式
按照随附的README 文件
中的说明运行这些程序。
注意:Windows 活动目录。 Active Directory 通过使用内部格式管理其模式。要更新模式,您可以使用 Active Directory 管理控制台插件ADSIEdit
或CreateJavaSchema
实用程序,按照 Active Directory 的说明进行操作。
第 4 步:为本教程提供目录内容
在本教程的示例中,显示的结果反映了使用随附本教程的配置文件(tutorial.ldif
)设置 LDAP 目录的方式。如果您正在使用现有服务器或具有不同设置的服务器,则可能会看到不同的结果。在将配置文件(tutorial.ldif
)加载到目录服务器之前,您必须按照更新服务器模式的说明进行操作,或者如果您的 UNIX 系统上有ldapadd或ldapmodify命令,则可以使用它们。
例如,使用 ldapmodify,您可以执行以下操作(通过为主机名、管理员 DN(-D 选项)和密码插入适当的值):
ldapmodify -a -c -v -h hostname -p 389\
-D "cn=Administrator, cn=users, dc=xxx, dc=xxx"\
-w passwd -f tutorial.ldif
安装注意事项:访问控制。 不同的目录服务器以不同方式处理访问控制。本教程中的一些示例执行对目录的更新。此外,您安装教程的命名空间部分可能具有读取访问限制。因此,您需要采取特定于服务器的操作,使目录可读和/或可更新,以使这些示例正常工作。对于Oracle 目录服务器,请将sunds.aci.ldif
文件中建议的aci
条目添加到dn: o=JNDITutorial
条目中,以使整个目录可读和可更新。或者,您可以更改示例以对目录进行身份验证。如何执行此操作的详细信息在安全课程中有描述。
安装注意事项:命名空间设置。 tutorial.ldif
文件中的条目使用了"o=JNDITutorial"作为根命名上下文的区分名称(DN)。如果您尚未将目录服务器配置为具有"o=JNDITutorial"作为根命名上下文,则导入tutorial.ldif
的尝试将失败。解决此问题的最简单方法是将现有根命名上下文的 DN 添加到tutorial.ldif
文件中的每个"dn:"行中。例如,如果您的服务器已经具有根命名上下文"dc=imc,dc=org",则应更改该行
dn: o=JNDITutorial
到
dn: o=JNDITutorial, dc=imc, dc=org
对文件中以"dn:"开头的每一行进行更改。然后,在本教程的所有示例中,无论何时使用"o=JNDITutorial",请改用"o=JNDITutorial,dc=imc,dc=org"。
安装说明:文件格式。根据您使用的操作系统平台,您可能需要编辑tutorial.ldif
,以便其中包含该平台的正确换行符。例如,如果您发现tutorial.ldif
包含 Windows 风格的换行符(CRLF),而您要将此文件导入运行在 UNIX 平台上的目录服务器,则需要编辑文件并将 CRLF 替换为 LF。此问题的症状是目录服务器拒绝tutorial.ldif
中的所有条目。
安装说明:Windows Active Directory。
-
根命名上下文不会是“o=jnditutorial”。它将采用“dc=x,dc=y,dc=z”的形式。您需要遵循之前的命名空间设置说明。
-
通过使用 Active Directory 管理控制台插件
ADSIEdit
,为“inetOrgPerson”和“groupOfUniqueNames”对象类添加对象类和相关属性到 Active Directory 模式中。"groupOfUniqueNames"在RFC 2256中定义,"inetOrgPerson"在RFC 2798中定义。 -
教程中使用的一些层次关系在 Active Directory 中默认情况下是不允许的。要启用这些关系,请使用 Active Directory 管理控制台插件
ADSIEdit
添加它们。objectclass: organizationalUnit possible superiors: domainDNS inetOrgPerson organization organizationalPerson organizationalUnit person top objectclass: groupOfUniqueNames possible superiors: top objectclass: inetOrgPerson possible superiors: container organizationalPerson person top
-
从
tutorial.ldif
中的马克·吐温条目中删除两个“sn”属性中的一个。Active Directory 将“sn”定义为单值属性,与RFC 2256相悖。 -
使用
ldifde
命令行实用程序加载修改后的tutorial.ldif
文件。# ldifde -i -v -k -f tutorial.ldif
-
大多数示例假定目录已设置为允许未经身份验证的读取和更新访问。您的 Active Directory 设置可能不允许您这样做。请参阅访问控制安装说明。
-
有时读取条目会产生比教程中显示的更多属性,因为 Active Directory 通常会返回一些内部属性。
-
创建条目可能需要指定其他 Active Directory 特定属性或使用其他对象类。
Java 应用程序设置
原文:
docs.oracle.com/javase/tutorial/jndi/software/package.html
要在程序中使用 JNDI,您需要设置其编译和执行环境。
导入 JNDI 类
以下是 JNDI 包:
-
javax.naming
-
javax.naming.directory
-
javax.naming.event
-
javax.naming.ldap
-
javax.naming.spi
本教程中的示例使用来自前两个包的类和接口。您需要将这两个包导入到您的程序中,或者导入您使用的单个类和接口。以下两行导入了两个包javax.naming
和javax.naming.directory
中的所有类和接口。
import javax.naming.*;
import javax.naming.directory.*;
编译环境
要编译使用 JNDI 的程序,您需要访问 JNDI 类。 Java SE 6 已经包含了 JNDI 类,因此如果您正在使用它,您无需采取进一步的操作。
执行环境
要运行使用 JNDI 的程序,您需要访问 JNDI 类和程序使用的任何服务提供商的类。 Java Runtime Environment(JRE)6 已经包含了 LDAP、COS 命名、RMI 注册表和 DNS 的 JNDI 类和服务提供商。
如果您正在使用其他服务提供商,则需要下载并安装它们的存档文件到JAVA_HOME/jre/lib/ext
目录中,其中JAVA_HOME是包含 JRE 的目录。 JNDI 页面列出了一些服务提供商。您可以下载这些提供商或使用其他供应商的提供商。
课程:命名和目录操作
原文:
docs.oracle.com/javase/tutorial/jndi/ops/index.html
您可以使用 JNDI 执行命名操作,包括读取操作和更新命名空间的操作。本课程中描述了以下操作:
-
查找对象
-
列出上下文的内容
-
添加、覆盖和删除绑定
-
重命名对象
-
创建和销毁子上下文
配置
在执行任何命名或目录服务操作之前,您需要获取一个初始上下文--进入命名空间的起始点。这是因为所有命名和目录服务上的方法都是相对于某个上下文执行的。要获取初始上下文,您必须按照以下步骤进行。
-
选择要访问的相应服务的服务提供者。
-
指定初始上下文需要的任何配置。
-
调用
InitialContext
构造函数。
步骤 1:选择初始上下文的服务提供者
您可以通过创建一组环境属性(Hashtable
)并将服务提供者类的名称添加到其中来指定初始上下文要使用的服务提供者。环境属性在JNDI 教程中有详细描述。
如果您正在使用 JDK 中包含的 LDAP 服务提供者,则您的代码将如下所示。
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
要在 JDK 中指定文件系统服务提供者,您需要编写类似以下代码的代码。
Hashtable<String, Object> env = new Hashtable>String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
您还可以使用系统属性来指定要使用的服务提供者。查看JNDI 教程获取详细信息。
步骤 2:提供初始上下文所需的信息
不同目录的客户端可能需要各种信息来联系目录。例如,您可能需要指定服务器在哪台机器上运行以及需要什么信息来识别用户到目录。此类信息通过环境属性传递给服务提供者。JNDI 指定了一些通用的环境属性,服务提供者可以使用。您的服务提供者文档将详细说明这些属性所需的信息。
LDAP 提供程序要求程序指定 LDAP 服务器的位置,以及用户身份信息。为了提供这些信息,您需要编写如下代码。
env.put(Context.PROVIDER_URL, "ldap://ldap.wiz.com:389");
env.put(Context.SECURITY_PRINCIPAL, "joeuser");
env.put(Context.SECURITY_CREDENTIALS, "joepassword");
本教程使用 JDK 中的 LDAP 服务提供程序。示例假设在本地机器上的端口 389 上设置了一个服务器,根专有名称为"o=JNDITutorial"
,并且更新目录不需要认证。它们包括以下代码来设置环境。
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
如果你正在使用设置不同的目录,那么你需要相应地设置这些环境属性。你需要将"localhost"
替换为那台机器的名称。你可以运行这些示例代码来访问任何公共目录服务器或者运行在不同机器上的你自己的服务器。你需要将"localhost"
替换为那台机器的名称,并将o=JNDITutorial
替换为可用的命名上下文。
第三步:创建初始上下文
现在你已经准备好创建初始上下文了。为此,你需要将之前创建的环境属性传递给InitialContext
构造函数:
Context ctx = new InitialContext(env);
现在你已经有了一个指向Context
对象的引用,你可以开始访问命名服务。
要执行目录操作,你需要使用一个InitialDirContext
。为此,使用其中的一个构造函数:
DirContext ctx = new InitialDirContext(env);
这个语句返回一个用于执行目录操作的DirContext
对象的引用。
命名异常
原文:
docs.oracle.com/javase/tutorial/jndi/ops/exception.html
JNDI 包中的许多方法在需要指示无法执行请求的操作时会抛出NamingException
。通常,您会看到围绕可能引发NamingException
的方法的try/catch
包装器:
try {
Context ctx = new InitialContext();
Object obj = ctx.lookup("somename");
} catch (NamingException e) {
// Handle the error
System.err.println(e);
}
异常类层次结构
JNDI 具有丰富的异常层次结构,源自NamingException
类。异常的类名是自解释的,并在此处列出。
要特别处理NamingException
的特定子类,您需要单独捕获子类。例如,以下代码特别处理AuthenticationException
及其子类。
try {
Context ctx = new InitialContext();
Object obj = ctx.lookup("somename");
} catch (AuthenticationException e) {
// attempt to reacquire the authentication information
...
} catch (NamingException e) {
// Handle the error
System.err.println(e);
}
枚举
操作,如Context.list()
和DirContext.search()
返回一个NamingEnumeration
。在这些情况下,如果发生错误并且没有返回结果,则在调用方法时将抛出NamingException
或其适当的子类。如果发生错误但有一些结果需要返回,则会返回一个NamingEnumeration
,以便您可以获取这些结果。当所有结果耗尽时,调用NamingEnumeration.hasMore()
将导致抛出NamingException
(或其子类之一)以指示错误。此时,枚举将变为无效状态,不应在其上调用更多方法。
例如,如果执行search()
并指定要返回多少个答案的计数限制(n),则search()
将返回最多 n 个结果的枚举。如果结果数量超过 n,则当第 n+1 次调用NamingEnumeration.hasMore()
时,将抛出SizeLimitExceededException
。请参阅本课程的结果计数示例代码。
本教程中的示例
在本教程文本中嵌入的内联示例代码中,通常为了可读性而省略了try/catch
子句。通常,因为这里只显示代码片段,所以只包含直接用于说明概念的行。如果查看本教程附带的源文件,您将看到用于NamingException
的try/catch
子句的适当位置。
javax.naming 包中的异常可以在这里找到。
查找一个对象
原文:
docs.oracle.com/javase/tutorial/jndi/ops/lookup.html
要从命名服务中查找对象,请使用Context.lookup()
并传递要检索的对象的名称。假设在命名服务中有一个名为cn=Rosanna Lee,ou=People
的对象。要检索对象,您可以编写
Object obj = ctx.lookup("cn=Rosanna Lee,ou=People");
lookup()
返回的对象类型取决于底层命名系统和对象本身关联的数据。命名系统可以包含许多不同类型的对象,在系统的不同部分查找对象可能会产生不同类型的对象。在这个例子中,"cn=Rosanna Lee,ou=People"
恰好绑定到一个上下文对象(javax.naming.ldap.LdapContext
)。你可以将lookup()
的结果转换为目标类。
例如,以下代码查找对象"cn=Rosanna Lee,ou=People"
并将其转换为LdapContext
。
import javax.naming.ldap.LdapContext;
...
LdapContext ctx = (LdapContext) ctx.lookup("cn=Rosanna Lee,ou=People");
完整的示例在文件Lookup.java
中。
Java SE 6 中有两个新的静态方法可用于查找名称:
-
InitialContext.doLookup(Name name)
-
InitialContext.doLookup(String name)
这些方法提供了一种快捷的查找名称的方式,而无需实例化 InitialContext。
列出上下文
原文:
docs.oracle.com/javase/tutorial/jndi/ops/list.html
与Context.lookup()
一次获取一个对象不同,您可以通过一次操作列出整个上下文。有两种列出上下文的方法:一种返回绑定,另一种仅返回名称到对象类名对。
Context.List()方法
Context.list()
返回一个NameClassPair
的枚举。每个NameClassPair
包含对象的名称和类名。以下代码片段列出了"ou=People"
目录的内容(即在"ou=People"
目录中找到的文件和目录)。
NamingEnumeration list = ctx.list("ou=People");
while (list.hasMore()) {
NameClassPair nc = (NameClassPair)list.next();
System.out.println(nc);
}
运行这个示例
会产生以下输出。
# java List
cn=Jon Ruiz: javax.naming.directory.DirContext
cn=Scott Seligman: javax.naming.directory.DirContext
cn=Samuel Clemens: javax.naming.directory.DirContext
cn=Rosanna Lee: javax.naming.directory.DirContext
cn=Maxine Erlund: javax.naming.directory.DirContext
cn=Niels Bohr: javax.naming.directory.DirContext
cn=Uri Geller: javax.naming.directory.DirContext
cn=Colleen Sullivan: javax.naming.directory.DirContext
cn=Vinnie Ryan: javax.naming.directory.DirContext
cn=Rod Serling: javax.naming.directory.DirContext
cn=Jonathan Wood: javax.naming.directory.DirContext
cn=Aravindan Ranganathan: javax.naming.directory.DirContext
cn=Ian Anderson: javax.naming.directory.DirContext
cn=Lao Tzu: javax.naming.directory.DirContext
cn=Don Knuth: javax.naming.directory.DirContext
cn=Roger Waters: javax.naming.directory.DirContext
cn=Ben Dubin: javax.naming.directory.DirContext
cn=Spuds Mackenzie: javax.naming.directory.DirContext
cn=John Fowler: javax.naming.directory.DirContext
cn=Londo Mollari: javax.naming.directory.DirContext
cn=Ted Geisel: javax.naming.directory.DirContext
Context.listBindings()方法
Context.listBindings()
返回一个Binding
的枚举。Binding
是NameClassPair
的子类。一个绑定不仅包含对象的名称和类名,还包含对象本身。以下代码枚举了"ou=People"
上下文,打印出每个绑定的名称和对象。
NamingEnumeration bindings = ctx.listBindings("ou=People");
while (bindings.hasMore()) {
Binding bd = (Binding)bindings.next();
System.out.println(bd.getName() + ": " + bd.getObject());
}
运行这个示例
会产生以下输出。
# java ListBindings
cn=Jon Ruiz: com.sun.jndi.ldap.LdapCtx@1d4c61c
cn=Scott Seligman: com.sun.jndi.ldap.LdapCtx@1a626f
cn=Samuel Clemens: com.sun.jndi.ldap.LdapCtx@34a1fc
cn=Rosanna Lee: com.sun.jndi.ldap.LdapCtx@176c74b
cn=Maxine Erlund: com.sun.jndi.ldap.LdapCtx@11b9fb1
cn=Niels Bohr: com.sun.jndi.ldap.LdapCtx@913fe2
cn=Uri Geller: com.sun.jndi.ldap.LdapCtx@12558d6
cn=Colleen Sullivan: com.sun.jndi.ldap.LdapCtx@eb7859
cn=Vinnie Ryan: com.sun.jndi.ldap.LdapCtx@12a54f9
cn=Rod Serling: com.sun.jndi.ldap.LdapCtx@30e280
cn=Jonathan Wood: com.sun.jndi.ldap.LdapCtx@16672d6
cn=Aravindan Ranganathan: com.sun.jndi.ldap.LdapCtx@fd54d6
cn=Ian Anderson: com.sun.jndi.ldap.LdapCtx@1415de6
cn=Lao Tzu: com.sun.jndi.ldap.LdapCtx@7bd9f2
cn=Don Knuth: com.sun.jndi.ldap.LdapCtx@121cc40
cn=Roger Waters: com.sun.jndi.ldap.LdapCtx@443226
cn=Ben Dubin: com.sun.jndi.ldap.LdapCtx@1386000
cn=Spuds Mackenzie: com.sun.jndi.ldap.LdapCtx@26d4f1
cn=John Fowler: com.sun.jndi.ldap.LdapCtx@1662dc8
cn=Londo Mollari: com.sun.jndi.ldap.LdapCtx@147c5fc
cn=Ted Geisel: com.sun.jndi.ldap.LdapCtx@3eca90
终止 NamingEnumeration
一个NamingEnumeration
可以以自然、显式或意外的方式终止。
-
当
NamingEnumeration.hasMore()
返回false
时,枚举已完成并实际上被终止。 -
您可以通过调用
NamingEnumeration.close()
在枚举完成之前显式终止枚举。这样做会向底层实现提供一个提示,释放与枚举相关的任何资源。 -
如果
hasMore()
或next()
抛出NamingException
,则枚举实际上被终止。
无论枚举如何被终止,一旦终止,就不能再使用。在终止的枚举上调用方法会产生未定义的结果。
为什么有两种不同的列出方法?
list()
适用于浏览器样式的应用程序,只需显示上下文中对象的名称。例如,浏览器可能会列出上下文中的名称,并等待用户选择其中一个或几个名称以执行进一步操作。这类应用程序通常不需要访问上下文中的所有对象。
listBindings()
适用于需要对上下文中的对象进行批量操作的应用程序。例如,备份应用程序可能需要对文件目录中的所有对象执行“文件统计”操作。或者打印机管理程序可能希望重新启动建筑物中的所有打印机。为了执行这些操作,这些应用程序需要获取上下文中绑定的所有对象。因此,将对象作为枚举的一部分返回更为方便。
应用程序可以使用list()
或可能更昂贵的listBindings()
,具体取决于它所需的信息类型。
添加、替换或移除绑定
原文:
docs.oracle.com/javase/tutorial/jndi/ops/bind.html
Context
接口包含用于在上下文中添加、替换和移除绑定的方法。
添加绑定
Context.bind()
用于向上下文添加绑定。它接受对象的名称和要绑定的对象作为参数。
在继续之前: 本课程中的示例需要您对模式进行添加。您必须在 LDAP 服务器中关闭模式检查,或者将伴随本教程的模式
添加到服务器中。这两项任务通常由目录服务器的管理员执行。请参阅 LDAP 设置课程。
// Create the object to be bound
Fruit fruit = new Fruit("orange");
// Perform the bind
ctx.bind("cn=Favorite Fruit", fruit);
这个例子
创建一个 Fruit
类的对象,并将其绑定到上下文 ctx
中的名称 "cn=Favorite Fruit"
。如果随后在 ctx
中查找名称 "cn=Favorite Fruit"
,则会得到 fruit
对象。请注意,要编译 Fruit
类,您需要 FruitFactory
类。
如果您运行此示例两次,则第二次尝试将失败,并显示 NameAlreadyBoundException
。这是因为名称 "cn=Favorite Fruit"
已经绑定。要使第二次尝试成功,您必须使用 rebind()
。
添加或替换绑定
rebind()
用于添加或替换绑定。它接受与 bind()
相同的参数,但语义是如果名称已经绑定,则将解绑并绑定新给定的对象。
// Create the object to be bound
Fruit fruit = new Fruit("lemon");
// Perform the bind
ctx.rebind("cn=Favorite Fruit", fruit);
当你运行这个例子
时,它将替换由bind()
例子创建的绑定。
移除绑定
要移除绑定,您可以使用unbind()
。
// Remove the binding
ctx.unbind("cn=Favorite Fruit");
运行时,这个例子
将移除由bind()
或rebind()
例子创建的绑定。
重命名
原文:
docs.oracle.com/javase/tutorial/jndi/ops/rename.html
您可以使用Context.rename()
在上下文中重命名对象。
// Rename to Scott S
ctx.rename("cn=Scott Seligman", "cn=Scott S");
这个例子
将绑定到"cn=Scott Seligman"
的对象重命名为"cn=Scott S"
。在验证对象已重命名后,程序将其重新命名为原始名称("cn=Scott Seligman"
)。
// Rename back to Scott Seligman
ctx.rename("cn=Scott S", "cn=Scott Seligman");
欲了解更多关于 LDAP 条目重命名的示例,请查看 LDAP 用户的高级主题课程。
创建和销毁子上下文
原文:
docs.oracle.com/javase/tutorial/jndi/ops/create.html
Context
接口包含用于创建和销毁 子上下文 的方法,即绑定在同一类型的另一个上下文中的上下文。
此处描述的示例使用具有属性的对象,并在目录中创建一个子上下文。您可以使用这些DirContext
方法在将绑定或子上下文添加到命名空间时将属性与对象关联起来。例如,您可以创建一个Person
对象并将其绑定到命名空间,并同时关联有关该Person
对象的属性。命名等效项将没有属性。
createSubcontext()与 bind()的不同之处在于它创建一个新对象,即一个新的上下文,将其绑定到目录,而 bind()将给定对象绑定到目录。
创建上下文
要创建命名上下文,您需要向createSubcontext()
提供要创建的上下文的名称。要创建具有属性的上下文,您需要向DirContext.createSubcontext()
提供要创建的上下文的名称和其属性。
在继续之前: 本课程中的示例需要您对模式进行添加。您必须在 LDAP 服务器中关闭模式检查,或者将伴随本教程的模式
添加到服务器中。这两项任务通常由目录服务器的管理员执行。请参阅 LDAP 设置课程。
// Create attributes to be associated with the new context
Attributes attrs = new BasicAttributes(true); // case-ignore
Attribute objclass = new BasicAttribute("objectclass");
objclass.add("top");
objclass.add("organizationalUnit");
attrs.put(objclass);
// Create the context
Context result = ctx.createSubcontext("NewOu", attrs);
This example
创建了一个名为"ou=NewOu"
的新上下文,在上下文ctx
中有一个属性"objectclass"
,其值为"top"
和"organizationalUnit"
。
# java Create
ou=Groups: javax.naming.directory.DirContext
ou=People: javax.naming.directory.DirContext
ou=NewOu: javax.naming.directory.DirContext
This example
创建了一个名为"NewOu"
的新上下文,它是ctx
的子上下文。
销毁上下文
要销毁一个上下文,您需要向destroySubcontext()
提供要销毁的上下文的名称。
// Destroy the context
ctx.destroySubcontext("NewOu");
This example
在上下文ctx
中销毁了上下文"NewOu"
。
属性名称
原文:
docs.oracle.com/javase/tutorial/jndi/ops/attrnames.html
一个属性由一个属性标识符和一组属性值组成。属性标识符,也称为属性名称,是一个字符串,用于标识属性。属性值是属性的内容,其类型不限于字符串。当您想要为检索、搜索或修改指定特定属性时,您使用属性名称。名称也会被返回操作返回的属性(例如在目录中执行读取或搜索时)。
在使用属性名称时,您需要了解某些目录服务器功能,以免对结果感到惊讶。这些功能在下一小节中描述。
属性类型
在 LDAP 等目录中,属性的名称标识属性的类型,通常称为属性类型名称。例如,属性名称"cn"
也称为属性类型名称。属性的类型定义指定属性值应具有的语法,它是否可以具有多个值,以及在执行比较和排序操作时使用的相等性和排序规则。
属性子类化
一些目录实现支持属性子类化,其中服务器允许以其他属性类型定义属性类型。例如,"name"
属性可能是所有与名称相关属性的超类:"commonName"
可能是"name"
的子类。对于支持此功能的目录实现,请求"name"
属性可能返回"commonName"
属性。
当访问支持属性子类化的目录时,您必须注意服务器可能返回具有与您请求的名称不同的属性。为了最大程度减少这种情况发生的机会,请使用最派生的子类。
属性名称同义词
一些目录实现支持属性名称的同义词。例如,"cn"
可能是"commonName"
的同义词。因此,对"cn"
属性的请求可能返回"commonName"
属性。
当访问支持属性名称同义词的目录时,您必须注意服务器可能返回具有与您请求的名称不同的属性。为了防止这种情况发生,请使用规范属性名称而不是其同义词。规范属性名称是属性定义中使用的名称;同义词是在其定义中指向规范属性名称的名称。
语言偏好
LDAP v3 的扩展(RFC 2596)允许您在属性名称旁指定语言代码。这类似于属性子类化,一个属性名称可以表示多个不同的属性。例如,一个具有两种语言变体的"description"
属性:
description: software
description;lang-en: software products
description;lang-de: Softwareprodukte
请求"description"
属性将返回所有三个属性。
当访问支持此功能的目录时,您必须注意服务器可能返回与您请求的名称不同的属性。
读取属性
原文:
docs.oracle.com/javase/tutorial/jndi/ops/getattrs.html
要从目录中读取对象的属性,请使用DirContext.getAttributes()
并传递您想要属性的对象的名称。假设命名服务中的一个对象的名称为"cn=Ted Geisel, ou=People"
。要检索此对象的属性,您需要类似于以下的code
:
Attributes answer = ctx.getAttributes("cn=Ted Geisel, ou=People");
您可以按照以下方式打印此答案的内容。
for (NamingEnumeration ae = answer.getAll(); ae.hasMore();) {
Attribute attr = (Attribute)ae.next();
System.out.println("attribute: " + attr.getID());
/* Print each value */
for (NamingEnumeration e = attr.getAll(); e.hasMore();
System.out.println("value: " + e.next()))
;
}
这将产生以下输出。
# java GetattrsAll
attribute: sn
value: Geisel
attribute: objectclass
value: top
value: person
value: organizationalPerson
value: inetOrgPerson
attribute: jpegphoto
value: [B@1dacd78b
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: facsimiletelephonenumber
value: +1 408 555 2329
attribute: telephonenumber
value: +1 408 555 5252
attribute: cn
value: Ted Geisel
返回选定的属性
要读取属性的选择性子集,您需要提供一个字符串数组,这些字符串是您想要检索的属性的属性标识符。
// Specify the ids of the attributes to return
String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"};
// Get the attributes requested
Attributes answer = ctx.getAttributes("cn=Ted Geisel, ou=People", attrIDs);
此示例
请求对象"cn=Ted Geisel, ou=People"
的"sn"
、"telephonenumber"
、"golfhandicap"
和"mail"
属性。此对象除了"golfhandicap"
属性外,其余属性都存在,因此答案中返回了三个属性。以下是示例的输出。
# java Getattrs
attribute: sn
value: Geisel
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: telephonenumber
value: +1 408 555 5252
修改属性
原文:
docs.oracle.com/javase/tutorial/jndi/ops/modattrs.html
DirContext
接口包含了修改目录中对象的属性和属性值的方法。
使用修改列表
修改对象属性的一种方法是提供修改请求的列表(ModificationItem
)。每个ModificationItem
包含一个数字常量,指示要进行的修改类型,以及描述要进行的修改的Attribute
。以下是三种修改类型:
-
添加属性
-
替换属性
-
删除属性
修改按列表中出现的顺序应用。要么执行所有修改,要么一个也不执行。
以下代码创建了一个修改列表。它用值"geisel@wizards.com"替换了"mail"
属性的值,向"telephonenumber"
属性添加了一个额外的值,并删除了"jpegphoto"
属性。
// Specify the changes to make
ModificationItem[] mods = new ModificationItem[3];
// Replace the "mail" attribute with a new value
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("mail", "geisel@wizards.com"));
// Add an additional value to "telephonenumber"
mods[1] = new ModificationItem(DirContext.ADD_ATTRIBUTE,
new BasicAttribute("telephonenumber", "+1 555 555 5555"));
// Remove the "jpegphoto" attribute
mods[2] = new ModificationItem(DirContext.REMOVE_ATTRIBUTE,
new BasicAttribute("jpegphoto"));
Windows Active Directory: Active Directory 将"telephonenumber"定义为单值属性,与RFC 2256相悖。要使此示例针对 Active Directory 正常工作,您必须使用除"telephonenumber"之外的属性,或将DirContext.ADD_ATTRIBUTE
更改为DirContext.REPLACE_ATTRIBUTE
。
创建此修改列表后,您可以将其提供给modifyAttributes()
如下所示。
// Perform the requested modifications on the named object
ctx.modifyAttributes(name, mods);
使用属性
或者,您可以通过指定修改类型和要应用修改的属性来执行修改。
例如,以下行将用orig
中的属性(在name
中标识)替换为orig
中的属性:
ctx.modifyAttributes(name, DirContext.REPLACE_ATTRIBUTE, orig);
name
的任何其他属性保持不变。
modifyAttributes()
的这两种用法在示例程序
中演示。该程序通过使用修改列表修改属性,然后使用modifyAttributes()
的第二种形式来恢复原始属性。
添加、替换具有属性的绑定
原文:
docs.oracle.com/javase/tutorial/jndi/ops/bindattr.html
讨论了命名示例如何使用bind()
,rebind()
。DirContext
接口包含这些方法的重载版本,接受属性。您可以使用这些DirContext
方法在将绑定或子上下文添加到命名空间时将属性与对象关联起来。例如,您可以创建一个Person
对象并将其绑定到命名空间,并同时关联有关该Person
对象的属性。
添加具有属性的绑定
DirContext.bind()
用于向上下文添加具有属性的绑定。它接受对象的名称、要绑定的对象和一组属性作为参数。
// Create the object to be bound
Fruit fruit = new Fruit("orange");
// Create attributes to be associated with the object
Attributes attrs = new BasicAttributes(true); // case-ignore
Attribute objclass = new BasicAttribute("objectclass");
objclass.add("top");
objclass.add("organizationalUnit");
attrs.put(objclass);
// Perform bind
ctx.bind("ou=favorite, ou=Fruits", fruit, attrs);
此示例
创建一个Fruit
类的对象,并将其绑定到名称"ou=favorite"
中,相对于ctx
命名为"ou=Fruits"
的上下文。此绑定具有"objectclass"
属性。如果随后在ctx
中查找名称"ou=favorite, ou=Fruits"
,则会获取fruit
对象。然后获取"ou=favorite, ou=Fruits"
的属性,您将获得创建对象时使用的属性。以下是此示例的输出。
# java Bind
orange
attribute: objectclass
value: top
value: organizationalUnit
value: javaObject
value: javaNamingReference
attribute: javaclassname
value: Fruit
attribute: javafactory
value: FruitFactory
attribute: javareferenceaddress
value: #0#fruit#orange
attribute: ou
value: favorite
显示的额外属性和属性值用于存储有关对象(fruit
)的信息。这些额外属性在本教程中将更详细地讨论。
如果您运行此示例两次,则第二次尝试将失败,并显示NameAlreadyBoundException
。这是因为名称"ou=favorite"
已经绑定在"ou=Fruits"
上下文中。为了使第二次尝试成功,您需要使用rebind()
。
替换具有属性的绑定
DirContext.rebind()
用于添加或替换绑定及其属性。它接受与bind()
相同的参数。然而,rebind()
的语义要求,如果名称已经绑定,则将解除绑定,并绑定新给定的对象和属性。
// Create the object to be bound
Fruit fruit = new Fruit("lemon");
// Create attributes to be associated with the object
Attributes attrs = new BasicAttributes(true); // case-ignore
Attribute objclass = new BasicAttribute("objectclass");
objclass.add("top");
objclass.add("organizationalUnit");
attrs.put(objclass);
// Perform bind
ctx.rebind("ou=favorite, ou=Fruits", fruit, attrs);
当您运行此示例
时,它将替换bind()
示例创建的绑定。
# java Rebind
lemon
attribute: objectclass
value: top
value: organizationalUnit
value: javaObject
value: javaNamingReference
attribute: javaclassname
value: Fruit
attribute: javafactory
value: FruitFactory
attribute: javareferenceaddress
value: #0#fruit#lemon
attribute: ou
value: favorite
搜索
原文:
docs.oracle.com/javase/tutorial/jndi/ops/search.html
目录提供的最有用的功能之一是其黄页或搜索服务。您可以组成一个由您正在寻找的条目的属性组成的查询,并将该查询提交给目录。然后目录返回满足查询条件的条目列表。例如,您可以要求目录返回所有击球平均分大于 200 的条目,或者所有代表姓氏以"Sch"开头的人的条目。
DirContext
接口提供了几种搜索目录的方法,具有逐渐增加的复杂性和功能。搜索目录的各个方面在以下部分中介绍:
-
基本搜索
-
搜索过滤器
-
搜索控件
基本搜索
原文:
docs.oracle.com/javase/tutorial/jndi/ops/basicsearch.html
最简单的搜索形式要求您指定条目必须具有的属性集,以及执行搜索的目标上下文的名称。
以下代码创建了一个属性集matchAttrs
,其中包含两个属性"sn"
和"mail"
。 它指定符合条件的条目必须具有一个姓氏("sn"
)属性,其值为"Geisel"
,以及一个任何值的"mail"
属性。 然后调用DirContext.search()
在上下文"ou=People"
中搜索具有matchAttrs
指定属性的条目。
// Specify the attributes to match
// Ask for objects that has a surname ("sn") attribute with
// the value "Geisel" and the "mail" attribute
// ignore attribute name case
Attributes matchAttrs = new BasicAttributes(true);
matchAttrs.put(new BasicAttribute("sn", "Geisel"));
matchAttrs.put(new BasicAttribute("mail"));
// Search for objects that have those matching attributes
NamingEnumeration answer = ctx.search("ou=People", matchAttrs);
你可以按照以下方式打印结果。
while (answer.hasMore()) {
SearchResult sr = (SearchResult)answer.next();
System.out.println(">>>" + sr.getName());
printAttrs(sr.getAttributes());
}
printAttrs()
类似于getAttributes()
示例中打印属性集的代码。
运行这个例子
会产生以下结果。
# java SearchRetAll
>>>cn=Ted Geisel
attribute: sn
value: Geisel
attribute: objectclass
value: top
value: person
value: organizationalPerson
value: inetOrgPerson
attribute: jpegphoto
value: [B@1dacd78b
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: facsimiletelephonenumber
value: +1 408 555 2329
attribute: cn
value: Ted Geisel
attribute: telephonenumber
value: +1 408 555 5252
返回选定的属性
前面的例子返回满足指定查询条件的条目关联的所有属性。 您可以通过向search()
传递要包含在结果中的属性标识符数组来选择要返回的属性。 在之前显示的创建matchAttrs
之后,您还需要创建属性标识符数组,如下所示。
// Specify the ids of the attributes to return
String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"};
// Search for objects that have those matching attributes
NamingEnumeration answer = ctx.search("ou=People", matchAttrs, attrIDs);
这个例子
返回具有属性"sn"
、"telephonenumber"
、"golfhandicap"
和"mail"
的条目,这些条目具有一个属性"mail"
,并且具有一个值为"Geisel"
的"sn"
属性。 这个例子产生以下结果。(该条目没有"golfhandicap"
属性,因此不返回。)
# java Search
>>>cn=Ted Geisel
attribute: sn
value: Geisel
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: telephonenumber
value: +1 408 555 5252
过滤器
原文:
docs.oracle.com/javase/tutorial/jndi/ops/filter.html
除了使用一组属性指定搜索外,还可以以搜索过滤器的形式指定搜索。搜索过滤器是以逻辑表达式形式表达的搜索查询。DirContext.search()
接受的搜索过滤器语法在RFC 2254中有描述。
以下搜索过滤器指定符合条件的条目必须具有值为"Geisel"
的"sn"
属性和任何值的"mail"
属性:
(&(sn=Geisel)(mail=*))
以下代码创建一个过滤器和默认的SearchControls
,并使用它们执行搜索。该搜索等同于基本搜索示例中呈现的搜索。
// Create the default search controls
SearchControls ctls = new SearchControls();
// Specify the search filter to match
// Ask for objects that have the attribute "sn" == "Geisel"
// and the "mail" attribute
String filter = "(&(sn=Geisel)(mail=*))";
// Search for objects using the filter
NamingEnumeration answer = ctx.search("ou=People", filter, ctls);
运行此示例
会产生以下结果。
# java SearchWithFilterRetAll
>>>cn=Ted Geisel
attribute: sn
value: Geisel
attribute: objectclass
value: top
value: person
value: organizationalPerson
value: inetOrgPerson
attribute: jpegphoto
value: [B@1dacd75e
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: facsimiletelephonenumber
value: +1 408 555 2329
attribute: cn
value: Ted Geisel
attribute: telephonenumber
value: +1 408 555 5252
搜索过滤器语法快速概述
搜索过滤器语法基本上是前缀表示法中的逻辑表达式(即,逻辑运算符出现在其参数之前)。以下表列出了用于创建过滤器的符号。
符号 | 描述 |
---|---|
& | 合取(即,与 — 列表中的所有项目必须为真) |
| | 析取(即,或 — 一个或多个备选项必须为真) |
! | 否定(即,非 — 被否定的项目必须为假) |
= | 等于(根据属性匹配规则) |
~= | 大致相等(根据属性匹配规则) |
>= | 大于(根据属性匹配规则) |
<= | 小于(根据属性匹配规则) |
=* | 存在(即,条目必须具有属性,但其值无关紧要) |
* | 通配符(表示该位置可以出现零个或多个字符);用于指定要匹配的属性值时使用 |
\ | 转义(用于在属性值中出现'*'、'('或')'时进行转义) |
每个过滤器中的项目都是使用属性标识符和属性值或表示属性值的符号组成的。例如,项目"sn=Geisel"
表示"sn"
属性必须具有属性值"Geisel"
,而项目"mail=*"
表示"mail"
属性必须存在。
每个项目必须用一组括号括起来,如"(sn=Geisel)"
。这些项目使用逻辑运算符(如"&"(合取))组合,以创建逻辑表达式,如"(& (sn=Geisel) (mail=*))"
。
每个逻辑表达式都可以进一步由其他本身是逻辑表达式的项目组成,就像"(| (& (sn=Geisel) (mail=*)) (sn=L*))"
中那样。最后一个示例请求具有"sn"
属性为"Geisel"
和"mail"
属性的条目,或者其"sn"
属性以字母"L"开头的条目。
有关语法的完整描述,请参阅RFC 2254。
返回选定属性
前面的示例返回满足指定过滤器的条目关联的所有属性。您可以通过设置搜索控件参数来选择要返回的属性。您可以创建一个要包含在结果中的属性标识符数组,并将其传递给SearchControls.setReturningAttributes()
。以下是一个示例。
// Specify the ids of the attributes to return
String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"};
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(attrIDs);
这个示例等同于基本搜索部分中的返回选定属性示例。运行这个示例
会产生以下结果。(该条目没有"golfhandicap"
属性,因此不会返回。)
# java SearchWithFilter
>>>cn=Ted Geisel
attribute: sn
value: Geisel
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: telephonenumber
value: +1 408 555 5252
范围
原文:
docs.oracle.com/javase/tutorial/jndi/ops/scope.html
默认的SearchControls
指定搜索在命名上下文中执行(SearchControls.ONELEVEL_SCOPE
)。这个默认设置在搜索过滤器部分的示例中使用。
除了这个默认设置外,您还可以指定搜索在整个子树或仅在命名对象中执行。
搜索子树
对整个子树进行搜索将搜索命名对象及其所有后代。要使搜索以这种方式执行,请将SearchControls.SUBTREE_SCOPE
传递给SearchControls.setSearchScope()
如下所示。
// Specify the ids of the attributes to return
String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"};
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(attrIDs);
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
// Specify the search filter to match
// Ask for objects that have the attribute "sn" == "Geisel"
// and the "mail" attribute
String filter = "(&(sn=Geisel)(mail=*))";
// Search the subtree for objects by using the filter
NamingEnumeration answer = ctx.search("", filter, ctls);
这个示例
搜索上下文ctx
的子树,查找满足指定过滤器的条目。它在这个子树中找到了满足过滤器的条目"cn= Ted Geisel, ou=People"
。
# java SearchSubtree
>>>cn=Ted Geisel, ou=People
attribute: sn
value: Geisel
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: telephonenumber
value: +1 408 555 5252
搜索命名对象
你也可以搜索命名对象。例如,测试命名对象是否符合搜索过滤器非常有用。要搜索命名对象,请将SearchControls.OBJECT_SCOPE
传递给setSearchScope()
。
// Specify the ids of the attributes to return
String[] attrIDs = {"sn", "telephonenumber", "golfhandicap", "mail"};
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(attrIDs);
ctls.setSearchScope(SearchControls.OBJECT_SCOPE);
// Specify the search filter to match
// Ask for objects that have the attribute "sn" == "Geisel"
// and the "mail" attribute
String filter = "(&(sn=Geisel)(mail=*))";
// Search the subtree for objects by using the filter
NamingEnumeration answer =
ctx.search("cn=Ted Geisel, ou=People", filter, ctls);
这个示例
测试对象"cn=Ted Geisel, ou=People"
是否满足给定的过滤器。
# java SearchObject
>>>
attribute: sn
value: Geisel
attribute: mail
value: Ted.Geisel@JNDITutorial.example.com
attribute: telephonenumber
value: +1 408 555 5252
该示例找到一个答案并将其打印出来。请注意,结果的名称是空字符串。这是因为对象的名称始终相对于搜索上下文命名(在本例中为"cn=Ted Geisel, ou=People"
)。
结果计数
原文:
docs.oracle.com/javase/tutorial/jndi/ops/countlimit.html
有时,查询可能产生太多答案,你希望限制返回的答案数量。你可以通过使用计数限制搜索控件来实现这一点。默认情况下,搜索没有计数限制 - 它将返回它找到的所有答案。要设置搜索的计数限制,请将数字传递给SearchControls.setCountLimit()
。
以下示例
将计数限制设置为 1。
// Set the search controls to limit the count to 1
SearchControls ctls = new SearchControls();
ctls.setCountLimit(1);
如果程序尝试获取超过计数限制数量的结果,那么将抛出SizeLimitExceededException
。因此,如果程序设置了计数限制,则应该区分此异常和其他NamingException
,或者跟踪计数限制并不请求超过该数量的结果。
指定搜索的计数限制是控制应用程序消耗资源(如内存和网络带宽)的一种方式。控制消耗资源的其他方法包括缩小你的搜索过滤器(更具体地说明你要寻找什么)、在适当的上下文中开始搜索,并使用适当的范围。
时间限制
原文:
docs.oracle.com/javase/tutorial/jndi/ops/timelimit.html
对搜索设置时间限制会对搜索操作等待答复的时间上限。当您不希望等待太长时间以获取答复时,这很有用。如果在搜索操作完成之前超过了指定的时间限制,则会抛出TimeLimitExceededException
。
要设置搜索的时间限制,请将毫秒数传递给SearchControls.setTimeLimit()
。以下示例
将时间限制设置为 1 秒。
// Set the search controls to limit the time to 1 second (1000 ms)
SearchControls ctls = new SearchControls();
ctls.setTimeLimit(1000);
要使此特定示例超过其时间限制,您需要重新配置它以使用要么是慢的服务器,要么是具有大量条目的服务器。或者,您可以使用其他策略使搜索时间超过 1 秒。
时间限制为零意味着没有设置时间限制,对目录的调用将无限期等待答复。
故障排除提示
原文:
docs.oracle.com/javase/tutorial/jndi/ops/faq.html
运行成功编译的使用 JNDI 类的程序时可能遇到的最常见问题如下。
-
没有初始上下文
-
连接被拒绝
-
连接失败
-
程序挂起
-
找不到名称
-
无法连接到任意主机
-
无法访问系统属性进行配置
-
无法使用 CRAM-MD5 进行身份验证
1. 您收到了NoInitialContextException
。
原因:您没有指定用于初始上下文的实现。具体来说,Context.INITIAL_CONTEXT_FACTORY
环境属性未设置为将创建初始上下文的工厂类名。或者,您没有使程序可用于服务提供者类(由Context.INITIAL_CONTEXT_FACTORY
命名)。
解决方案:将Context.INITIAL_CONTEXT_FACTORY
环境属性设置为您正在使用的初始上下文实现的类名。有关详细信息,请参阅配置部分。
如果已设置属性,请确保类名未拼写错误,并且所命名的类对您的程序可用(在其类路径中或安装在 JRE 的 jre/lib/ext
目录中)。Java 平台包括用于 LDAP、COS 命名、DNS 和 RMI 注册表的服务提供者。所有其他服务提供者必须安装并添加到执行环境中。
2. 您收到了CommunicationException
,指示“连接被拒绝”。
原因:由Context.PROVIDER_URL
环境属性标识的服务器和端口未被服务器提供。也许有人已禁用或关闭了运行服务器的机器。或者,您可能拼写错误了服务器的名称或端口号。
解决方案:检查该端口上确实有服务器在运行,并在必要时重新启动服务器。您执行此检查的方式取决于您使用的 LDAP 服务器。通常,可用管理控制台或工具来管理服务器。您可以使用该工具验证服务器的状态。
3. LDAP 服务器对其他实用程序(如其管理控制台)做出响应,但似乎没有对您程序的请求做出响应。
原因:服务器未对 LDAP v3 连接请求做出响应。一些服务器(特别是公共服务器)未正确响应 LDAP v3,而是忽略请求而不是拒绝它们。此外,一些 LDAP v3 服务器在处理 Oracle 的 LDAP 服务提供者自动发送的控件时存在问题,并经常返回服务器特定的失败代码。
解决方案。尝试将环境属性"java.naming.ldap.version"
设置为"2"
。LDAP 服务提供程序默认尝试使用 LDAP v3 连接到 LDAP 服务器;如果失败,则使用 LDAP v2。如果服务器悄悄忽略了 v3 请求,则提供程序将假定请求成功。为了解决这样的服务器问题,您必须显式设置协议版本以确保服务器的正确行为。
如果服务器是一个 v3 服务器,那么在创建初始上下文之前尝试设置以下环境属性:
env.put(Context.REFERRAL, "throw");
这将关闭 LDAP 提供程序自动发送的控制。(查看JNDI 教程获取详细信息。)
4. 程序挂起。
原因:一些服务器(特别是公共服务器)在尝试执行会生成太多结果或需要服务器检查太多条目才能生成答案的搜索时,不会响应(甚至不会给出负面答复)。这些服务器试图限制它们在每个请求基础上消耗的资源量。
或者,您尝试使用安全套接字层(SSL)与不支持它的服务器/端口进行通信,反之亦然(也就是说,您尝试使用普通套接字与 SSL 端口进行通信)。
最后,服务器要么由于负载过重而响应非常缓慢,要么由于某种原因根本不响应。
解决方案:如果您的程序因服务器试图限制其资源的使用而挂起,则重试您的请求,使用将返回单个结果或仅返回少量结果的查询。这将帮助您确定服务器是否存活。如果是,则可以扩大您的初始查询并重新提交。
如果您的程序因 SSL 问题而挂起,则需要找出端口是否为 SSL 端口,然后适当设置Context.SECURITY_PROTOCOL
环境属性。如果端口是 SSL 端口,则应将此属性设置为"ssl"
。如果不是 SSL 端口,则不应设置此属性。
如果您的程序因为以上原因之一而挂起,属性com.sun.jndi.ldap.read.timeout
就派上用场了,用于指定读取超时。该属性的值是表示 LDAP 操作的毫秒级读取超时的整数的字符串表示。如果 LDAP 提供程序在该时间段内无法获得 LDAP 响应,则会中止读取尝试。整数应大于零。小于或等于零的整数表示未指定读取超时,相当于一直等待响应直到收到。
如果未指定此属性,则默认是等待响应直到收到为止。
例如,
env.put("com.sun.jndi.ldap.read.timeout", "5000");
会导致 LDAP 服务提供程序在服务器在 5 秒内没有回复时中止读取尝试。
5. 您收到NameNotFoundException
。
原因:当您为 LDAP 初始化初始上下文时,您提供了根专有名称。例如,如果您为初始上下文设置Context.PROVIDER_URL
环境属性为"ldap://ldapserver:389/o=JNDITutorial"
,然后提供了一个名称,如"cn=Joe,c=us"
,那么您传递给 LDAP 服务的完整名称将是"cn=Joe,c=us,o=JNDITutorial"
。如果这确实是您打算的名称,请检查您的服务器,确保它包含这样的条目。
此外,如果您为认证目的提供了不正确的专有名称,Oracle 目录服务器也会返回此错误。例如,如果您将Context.SECURITY_PRINCIPAL
环境属性设置为"cn=Admin, o=Tutorial"
,而"cn=Admin, o=Tutorial"
不是 LDAP 服务器上的条目,那么 LDAP 提供程序将抛出NameNotFoundException
。实际上,Oracle 目录服务器应返回与认证相关的错误,而不是“未找到名称”。
解决方案:验证您提供的名称是否是服务器上已存在的条目。您可以通过列出条目的父上下文或使用其他工具(如目录服务器的管理控制台)来实现这一点。
在尝试部署使用 JNDI 类的小程序时,您可能会遇到一些问题。
6. 当您的小程序尝试与在与加载小程序的机器不同的机器上运行的目录服务器通信时,您会收到AppletSecurityException
。
原因:您的小程序未经签名,因此只能连接到加载它的机器。或者,如果小程序已经签名,浏览器尚未授予小程序连接到目录服务器机器的权限。
解决方案:如果您希望允许小程序连接到运行在任意机器上的目录服务器,则需要对您的小程序和小程序将使用的所有 JNDI JAR 文件进行签名。有关签署 jar 文件的信息,请参见签署和验证 JAR 文件。
7. 当您的小程序尝试使用系统属性设置环境属性时,您会收到AppletSecurityException
。
原因:Web 浏览器限制对系统属性的访问,并且如果您尝试读取它们,则会抛出SecurityException
。
解决方案:如果您需要为您的小程序获取输入,则尝试使用小程序参数。
8. 当在 Firefox 中运行的小程序尝试使用 CRAM-MD5 对 LDAP 服务器进行身份验证时,您会收到AppletSecurityException
。
原因: Firefox 禁用对 java.security
包的访问。LDAP 提供程序使用了 java.security.MessageDigest
提供的消息摘要功能来实现 CRAM-MD5。
解决方案: 使用 Java 插件。
课程:LDAP 用户的高级主题
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/index.html
LDAP教程中的课程提供了 LDAP 和 JNDI 之间的映射细节。它们还提供了通过 JNDI 访问 LDAP 服务的提示和技巧。
LDAP
X.500 是 CCITT 关于目录服务的标准,是 OSI 服务套件的一部分。X.500 标准定义了一个协议(其中之一)用于客户端应用程序访问 X.500 目录,称为目录访问协议(DAP)。它建立在开放系统互连(OSI)协议栈之上。
因特网社区意识到需要类似 X.500 的服务,但面临不同的底层网络基础设施(TCP/IP 而不是 OSI),设计了一种基于 X.500 DAP 协议的新协议,称为轻量级 DAP,或 LDAP。RFC 2251定义了现在称为版本 3的 LDAP(或 LDAP v3),这是其前身 LDAP v2 的改进版本,其规范在RFC 1777中指定。
LDAP 的目标是设计一种易于实现的协议,特别关注能够构建小型和简单的客户端。它试图通过大量使用字符串和尽可能减少结构的使用来实现简化。例如,DN 在协议中表示为字符串,属性类型名称和许多属性值也是如此。
该协议由客户端向服务器发送请求,服务器做出响应,尽管不一定按照请求发送的顺序。每个请求都带有一个 ID 标记,以便匹配请求和响应。该协议可以在 TCP 或 UDP 上运行,尽管 TCP 版本最常用。
由于对客户端的关注,LDAP 社区还定义了有关 DN 的字符串表示(RFC 2553)、搜索过滤器(RFC 1960)和属性语法(RFC 1778)的标准,以及基于 C 语言的 API(RFC 1823),以及用于访问 LDAP 服务的 URL 格式(RFC 1959)。
LDAP v3 支持国际化、各种认证机制、引荐和通用部署机制。它允许通过使用扩展和控制向协议添加新功能,而无需对协议进行更改。
LDAP v3
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/ldap.html
国际化
国际化是通过国际字符集(ISO 10646)来处理的,用于表示协议元素是字符串(如 DN)。版本 3 还不同于版本 2,它使用 UTF-8 来编码其字符串。
认证
除了匿名、简单(明文密码)认证外,LDAP v3 还使用简单认证和安全层(SASL)认证框架(RFC 2222)允许使用不同的认证机制与 LDAP 一起使用。SASL 指定了一种挑战-响应协议,其中数据在客户端和服务器之间交换,用于认证目的。
目前定义了几种 SASL 机制:DIGEST-MD5, CRAM-MD5, Anonymous, External, S/Key, GSSAPI, 和 Kerberos v4。LDAP v3 客户端可以使用任何这些 SASL 机制,前提是 LDAP v3 服务器支持它们。此外,可以在不必更改 LDAP 的情况下使用新的(尚未定义的)SASL 机制。
转发
转发是服务器发送回客户端的信息,指示请求的信息可以在另一个位置(可能在另一个服务器上)找到。在 LDAP v2 中,服务器应处理转发而不将其返回给客户端。这是因为处理转发可能非常复杂,因此会导致更复杂的客户端。随着服务器的构建和部署,发现转发很有用,但并不是所有服务器都支持服务器端转发处理。因此,找到了一种方法来改进协议以允许返回转发。这是通过将转发放置在“部分结果”错误响应的错误消息中来完成的。
LDAP v3 明确支持转发,并允许服务器直接将转发返回给客户端。本课程不涵盖转发,但您可以随时参考JNDI 教程来管理应用程序中的转发。
部署
诸如 LDAP 之类的常见协议对确保所有目录客户端和服务器“说同一种语言”非常有用。当在网络中部署许多不同的目录客户端应用程序和目录服务器时,所有这些实体讨论相同的对象也非常有用。
目录模式指定了目录可能具有的对象类型以及每种对象类型可能具有的强制和可选属性,等等。LDAP v3 基于 X.500 标准为网络中常见的对象(如国家、地点、组织、用户/人员、组和设备)定义了一个模式(RFC 2252和RFC 2256)。它还定义了客户端应用程序访问服务器模式的方法,以便了解特定服务器支持的对象和属性类型。
LDAP v3 进一步定义了一组用于表示属性值的语法(RFC 2252)。编写需要访问模式的 Java 应用程序,请参考JNDI 教程。
扩展
除了预定义的操作集合,如“搜索”和“修改”,LDAP v3 还定义了一个“扩展”操作。 “扩展”操作以请求作为参数并返回响应。请求包含标识请求的标识符和请求的参数。响应包含执行请求的结果。 “扩展”操作请求/响应对称为扩展。例如,可以有一个用于启动 TLS 的扩展,这是客户端向服务器发出的激活启动 TLS 协议的请求。
这些扩展可以是标准的(由 LDAP 社区定义)或专有的(由特定目录供应商定义)。编写需要使用扩展的应用程序,请参考JNDI 教程。
控制
另一种添加新功能的方法是使用控制。LDAP v3 允许通过使用控制来修改任何操作的行为。可以在操作中发送任意数量的控制,并且可以在其结果中返回任意数量的控制。例如,您可以在“搜索”操作中发送一个排序控制,告诉服务器根据"name"
属性对搜索结果进行排序。
像扩展一样,这些控制可以是标准的或专有的。标准控制在平台中提供。编写需要使用控制的应用程序,请参考JNDI 教程。
JNDI 作为 LDAP API
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/jndi.html
JNDI 和 LDAP 模型都定义了一个层次化的命名空间,您可以在其中命名对象。命名空间中的每个对象都可以具有用于搜索该对象的属性。在这个高层次上,这两个模型是相似的,因此不足为奇 JNDI 很好地映射到 LDAP。
模型
您可以将 LDAP 条目视为 JNDI DirContext
。每个 LDAP 条目包含一个名称和一组属性,以及一个可选的子条目集。例如,LDAP 条目"o=JNDITutorial"
可能具有其属性"objectclass"
和"o"
,并且可能具有其子条目"ou=Groups"
和"ou=People"
。
在 JNDI 中,LDAP 条目"o=JNDITutorial"
被表示为一个具有名称"o=JNDITutorial"
的上下文,其具有两个子上下文,分别命名为"ou=Groups"
和"ou=People"
。LDAP 条目的属性由Attributes
接口表示,而单个属性由Attribute
接口表示。
请查看本课程的下一部分以了解如何通过 JNDI 访问 LDAP 操作的详细信息。
名称
由于联邦制度的结果,您提供给 JNDI 上下文方法的名称可以跨越多个命名空间。这些被称为复合名称。当使用 JNDI 访问 LDAP 服务时,您应该意识到字符串名称中的斜杠字符("/")对 JNDI 具有特殊含义。如果 LDAP 条目的名称包含此字符,则需要对其进行转义(使用反斜杠字符"")。例如,具有名称"cn=O/R"
的 LDAP 条目必须呈现为字符串"cn=O\\/R"
以供 JNDI 上下文方法使用。有关名称的更多信息,请查看JNDI 教程。LdapName和Rdn类简化了 LDAP 名称的创建和操作。
在协议中使用的 LDAP 名称始终是完全限定的名称,用于标识从 LDAP 命名空间的根(由服务器定义)开始的条目。以下是一些完全限定的 LDAP 名称的示例。
cn=Ted Geisel, ou=Marketing, o=Some Corporation, c=gb
cn=Vinnie Ryan, ou=People, o=JNDITutorial
在 JNDI 中,然而,名称始终是相对的;也就是说,你总是相对于上下文命名一个对象。例如,你可以将条目"cn=Vinnie Ryan"
命名为相对于名为"ou=People, o=JNDITutorial"
的上下文。或者你可以将条目"cn=Vinnie Ryan, ou=People"
命名为相对于名为"o=JNDITutorial"
的上下文。或者,你可以创建一个指向 LDAP 服务器命名空间根的初始上下文,并将条目命名为"cn=Vinnie Ryan, ou=People, o=JNDITutorial"
。
在 JNDI 中,你还可以使用 LDAP URL 来命名 LDAP 条目。请参阅JNDI 教程中关于 LDAP URL 的讨论。
LDAP 操作如何映射到 JNDI API
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/operations.html
LDAP 定义了一组操作或请求(参见 RFC 2251)。在 JNDI 中,这些映射到 DirContext
和 LdapContext
接口上的操作(它们是 Context
的子接口)。例如,当调用者调用 DirContext
方法时,LDAP 服务提供程序通过向 LDAP 服务器发送 LDAP 请求来实现该方法。
下表显示了 LDAP 中的操作如何对应到 JNDI 方法。
LDAP 操作 | 对应的 JNDI 方法 |
---|---|
绑定 | JNDI 中创建与 LDAP 服务器的初始连接的对应方式是创建一个 InitialDirContext 。当应用程序创建初始上下文时,通过环境属性提供客户端身份验证信息。要更改现有上下文的身份验证信息,请使用 Context.addToEnvironment() 和 Context.removeFromEnvironment() 。 |
解绑 | Context.close() 用于释放上下文使用的资源。它与 LDAP 的 "unbind" 操作不同之处在于,在给定的服务提供程序实现中,资源可以在上下文之间共享,因此关闭一个上下文不会释放所有资源,如果这些资源正在与另一个上下文共享。如果您的意图是释放所有资源,请确保关闭所有上下文。 |
搜索 | JNDI 中对应的方法是重载 DirContext.search() ,接受一个搜索过滤器(RFC 2254)。查看 过滤器 示例。 |
修改 | JNDI 中对应的方法是重载 DirContext.modifyAttributes() ,接受一个 DirContext.ModificationItem 数组。查看 修改属性 部分的示例。 |
add | JNDI 中对应的方法是DirContext.bind() 和DirContext.createSubcontext() 。您可以使用其中任一方法来添加新的 LDAP 条目。使用bind() ,您不仅可以为新条目指定一组属性,还可以指定要与属性一起添加的 Java 对象。请参阅使用 Attributes 添加、替换绑定部分以获取示例。 |
delete | JNDI 中对应的方法是Context.unbind() 和Context.destroySubcontext() 。您可以使用其中任一方法来删除 LDAP 条目。 |
modify DN/RDN | JNDI 中对应的方法是Context.rename() 。请参阅重命名对象部分以获取更多详细信息。 |
compare | JNDI 中对应的操作是一个适当受限的DirContext.search() 。请参阅 LDAP 比较部分以获取示例。 |
abandon | 当您关闭一个上下文时,所有未完成的请求都会被放弃。同样,当您关闭一个NamingEnumeration 时,相应的 LDAP“搜索”请求也会被放弃。 |
extended operation | JNDI 中对应的方法是LdapContext.extendedOperation() 。请参阅JNDI 教程以获取更多详细信息。 |
LDAP 错误代码与 JNDI 异常的映射方式
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/exceptions.html
LDAP 定义了一组状态代码,这些代码是 LDAP 服务器发送的 LDAP 响应中返回的(参见RFC 2251)。在 JNDI 中,错误条件被指示为NamingException
的子类的已检查异常。请参阅 Naming Exceptions 部分以获取 JNDI 异常类的概述。
LDAP 服务提供程序将从 LDAP 服务器接收的 LDAP 状态代码转换为适当的NamingException
子类。以下表格显示了 LDAP 状态代码与 JNDI 异常之间的映射。
LDAP 状态代码 | 含义 | 异常或操作 |
---|---|---|
0 | 成功 | 报告成功。 |
1 | 操作错误 | NamingException |
2 | 协议错误 | CommunicationException |
3 | 超出时间限制。 | TimeLimitExceededException |
4 | 大小限制超出。 | SizeLimitExceededException |
5 | 比较为假。 | 由DirContext.search() 使用。不会生成异常。 |
6 | 比较为真。 | 由DirContext.search() 使用。不会生成异常。 |
7 | 不支持的身份验证方法。 | AuthenticationNotSupportedException |
8 | 需要强身份验证。 | AuthenticationNotSupportedException |
9 | 正在返回部分结果。 | 如果环境属性"java.naming.referral" 设置为"ignore" 或错误内容不包含引荐,则抛出PartialResultException 。否则,使用内容构建引荐。 |
10 | 遇到引荐。 | 如果环境属性"java.naming.referral" 设置为"ignore" ,则忽略。如果属性设置为"throw" ,则抛出ReferralException 。如果属性设置为"follow" ,则 LDAP 提供程序处理引荐。如果已超过"java.naming.ldap.referral.limit" 属性,则抛出LimitExceededException 。 |
11 | 超出管理限制。 | LimitExceededException |
12 | 请求的不可用关键扩展。 | OperationNotSupportedException |
13 | 需要机密性。 | AuthenticationNotSupportedException |
14 | SASL 绑定正在进行中。 | 用于 LDAP 提供程序在认证过程中的内部使用。 |
16 | 不存在此属性。 | NoSuchAttributeException |
17 | 未定义的属性类型。 | InvalidAttributeIdentifierException |
18 | 不匹配。 | InvalidSearchFilterException |
19 | 约束违规。 | InvalidAttributeValueException |
20 | 属性或值已在使用中。 | AttributeInUseException |
21 | 无效的属性语法。 | InvalidAttributeValueException |
32 | 不存在此对象。 | NameNotFoundException |
33 | 别名问题。 | NamingException |
34 | 无效的 DN 语法。 | InvalidNameException |
35 | 是一个叶子。 | 由 LDAP 提供程序使用;通常不会生成异常。 |
36 | 别名解引用问题。 | NamingException |
48 | 不适当的身份验证。 | AuthenticationNotSupportedException |
49 | 无效凭证 | AuthenticationException |
50 | 访问权限不足 | NoPermissionException |
51 | 忙碌 | ServiceUnavailableException |
52 | 不可用 | ServiceUnavailableException |
53 | 不愿执行 | OperationNotSupportedException |
54 | 检测到循环。 | NamingException |
64 | 命名违规 | InvalidNameException |
65 | 对象类违规 | SchemaViolationException |
66 | 非叶子节点不允许。 | ContextNotEmptyException |
67 | RDN 上不允许。 | SchemaViolationException |
68 | 条目已存在。 | NameAlreadyBoundException |
69 | 禁止对象类修改。 | SchemaViolationException |
71 | 影响多个 DSA。 | NamingException |
80 | 其他 | NamingException |
安全性
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/security.html
LDAP 服务提供了一个通用的目录服务。它可以用来存储各种信息。所有 LDAP 服务器都有一套系统来控制谁可以读取和更新目录中的信息。
要访问 LDAP 服务,LDAP 客户端首先必须向服务进行身份验证。也就是说,它必须告诉 LDAP 服务器谁将访问数据,以便服务器可以决定客户端被允许看到和执行什么操作。如果客户端成功向 LDAP 服务器进行身份验证,那么当服务器随后收到来自客户端的请求时,它将检查客户端是否被允许执行该请求。这个过程称为访问控制。
LDAP 标准提出了 LDAP 客户端可以向 LDAP 服务器进行身份验证的方式(RFC 2251和RFC 2829)。这些内容在 LDAP 身份验证部分和身份验证机制部分中进行了概述。本课程还包含了如何使用匿名、简单和 SASL 身份验证机制的描述。
不同的 LDAP 服务器实现以不同的方式支持访问控制。本课程不讨论这个问题。
LDAP 服务的另一个安全方面是支持使用安全通道与客户端通信,例如发送和接收包含密码和密钥等秘密信息的属性。LDAP 服务器为此目的使用 SSL。本课程还展示了如何与 LDAP 服务提供者一起使用 SSL。
LDAP 的认证方式
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/authentication.html
在 LDAP 中,认证信息是在"bind"操作中提供的。在 LDAP v2 中,客户端通过发送包含认证信息的"bind"操作与 LDAP 服务器建立连接。
在 LDAP v3 中,此操作具有相同的目的,但是是可选的。发送 LDAP 请求而不执行"bind"操作的客户端被视为匿名客户端(有关详细信息,请参见匿名部分)。在 LDAP v3 中,"bind"操作可以在连接期间的任何时候发送,可能会多次发送。客户端可以在连接的中间发送"bind"请求以更改其身份。如果请求成功,则所有使用旧身份的连接上的未完成请求都将被丢弃,并且连接将与新身份关联。
在"bind"操作中提供的认证信息取决于客户端选择的认证机制。有关认证机制的讨论,请参见认证机制。
使用 JNDI 进行 LDAP 认证
在 JNDI 中,认证信息是在环境属性中指定的。当您使用InitialDirContext
类(或其超类或子类)创建初始上下文时,您提供一组环境属性,其中一些可能包含认证信息。您可以使用以下环境属性来指定认证信息。
-
Context.SECURITY_AUTHENTICATION
("java.naming.security.authentication"
).指定要使用的认证机制。对于 JDK 中的 LDAP 服务提供程序,这可以是以下字符串之一:"none","simple",sasl_mech,其中sasl_mech是一组以空格分隔的 SASL 机制名称。有关这些字符串的描述,请参见认证机制。
-
Context.SECURITY_PRINCIPAL
("java.naming.security.principal"
).指定进行认证的用户/程序的名称,取决于
Context.SECURITY_AUTHENTICATION
属性的值。有关详细信息和示例,请参见本课程的接下来几节。 -
Context.SECURITY_CREDENTIALS
("java.naming.security.credentials"
).指定进行认证的用户/程序的凭据,取决于
Context.SECURITY_AUTHENTICATION
属性的值。有关详细信息和示例,请参见本课程的接下来几节。
创建初始上下文时,底层 LDAP 服务提供程序从这些环境属性中提取认证信息,并使用 LDAP 的“绑定”操作将其传递给服务器。
以下示例
展示了如何通过使用简单的明文密码,客户端向 LDAP 服务器进行身份验证。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Authenticate as S. User and password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
为上下文使用不同的认证信息
如果要为现有上下文使用不同的认证信息,则可以使用Context.addToEnvironment()
和Context.removeFromEnvironment()
来更新包含认证信息的环境属性。随后对上下文的方法调用将使用新的认证信息与服务器通信。
以下示例
展示了如何在创建上下文后将上下文的认证信息更改为"none"
。
// Authenticate as S. User and the password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
// Change to using no authentication
ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "none");
// ... do something useful with ctx
认证失败
认证可能因多种原因而失败。例如,如果提供了不正确的认证信息,比如不正确的密码或主体名称,那么会抛出AuthenticationException
。
这里是一个变体的示例
。这次,不正确的密码导致认证失败。
// Authenticate as S. User and give an incorrect password
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "notmysecret");
这将产生以下输出。
javax.naming.AuthenticationException: [LDAP: error code 49 - Invalid Credentials]
...
因为不同的服务器支持不同的认证机制,您可能请求服务器不支持的认证机制。在这种情况下,将抛出AuthenticationNotSupportedException
。
这里是一个变体的示例
。这次,不支持的认证机制("custom"
)导致认证失败。
// Authenticate as S. User and the password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "custom");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
这将产生以下输出。
javax.naming.AuthenticationNotSupportedException: custom
...
身份验证机制
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/auth_mechs.html
不同版本的 LDAP 支持不同类型的身份验证。LDAP v2 定义了三种身份验证类型:匿名、简单(明文密码)和 Kerberos v4。
LDAP v3 支持匿名、简单和 SASL 身份验证。SASL 是简单身份验证和安全层(RFC 2222)的缩写。它指定了一种挑战-响应协议,客户端和服务器之间交换数据以进行身份验证,并建立安全层以进行后续通信。通过使用 SASL,LDAP 可以支持 LDAP 客户端和服务器协商的任何类型的身份验证。
本课程包含了如何使用匿名、简单和 SASL 身份验证进行身份验证的描述。
指定身份验证机制
身份验证机制是通过使用Context.SECURITY_AUTHENTICATION
环境属性来指定的。该属性可以具有以下值之一。
属性名称 | 属性值 |
---|---|
sasl_mech | 一个以空格分隔的 SASL 机制名称列表。使用列出的 SASL 机制之一(例如,"CRAM-MD5" 表示使用RFC 2195中描述的 CRAM-MD5 SASL 机制)。 |
none |
不使用身份验证(匿名) |
simple |
使用弱身份验证(明文密码) |
默认机制
如果客户端没有指定任何身份验证环境属性,则默认身份验证机制为"none"
。然后客户端将被视为匿名客户端。
如果客户端在不显式指定Context.SECURITY_AUTHENTICATION
属性的情况下指定身份验证信息,则默认身份验证机制为"simple"
。
匿名
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/anonymous.html
正如刚才所述,如果没有设置任何身份验证环境属性,那么默认的身份验证机制是"none"
。如果客户端将Context.SECURITY_AUTHENTICATION
环境属性设置为"none"
,那么身份验证机制就是"none"
,所有其他身份验证环境属性都将被忽略。您只有在明确希望忽略可能已设置的任何其他身份验证属性时才需要这样做。无论哪种情况,客户端都将被视为匿名客户端。这意味着服务器不知道也不关心客户端是谁,并且将允许客户端访问(读取和更新)任何已配置为可被任何未经身份验证的客户端访问的数据。
因为 Naming and Directory Operations 课程中的所有目录示例都没有设置任何身份验证环境属性,所以它们都使用匿名身份验证。
这里是一个示例
,明确将Context.SECURITY_AUTHENTICATION
属性设置为"none"
(尽管这样做并不是严格必要的,因为这是默认值)。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Use anonymous authentication
env.put(Context.SECURITY_AUTHENTICATION, "none");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
简单
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/simple.html
简单身份验证包括向 LDAP 服务器发送客户端(用户)的完全限定 DN 和客户端的明文密码(参见RFC 2251和RFC 2829)。这种机制存在安全问题,因为密码可以从网络中读取。为了避免以这种方式暴露密码,您可以在加密通道(如 SSL)中使用简单身份验证机制,前提是 LDAP 服务器支持。
LDAP v2 和 v3 都支持简单身份验证。
要使用简单身份验证机制,必须设置三个身份验证环境属性如下。
Context.SECURITY_AUTHENTICATION
。
设置为"simple"
。
Context.SECURITY_PRINCIPAL
。
设置为正在进行身份验证的实体的完全限定 DN(例如,"cn=S. User, ou=NewHires, o=JNDITutorial"
)。它的类型为java.lang.String
。
Context.SECURITY_CREDENTIALS
。
设置为主体的密码(例如,"mysecret"
)。它的类型为java.lang.String
,char
数组(char[]
)或byte
数组(byte[]
)。如果密码是java.lang.String
或char
数组,则在传输到服务器时使用 UTF-8 进行编码以供 LDAP v3 使用,使用 ISO-Latin-1 供 LDAP v2 使用。如果密码是byte[]
,则按原样传输到服务器。
查看本节中早期的示例,演示如何使用简单身份验证。
注意: 如果您向Context.SECURITY_CREDENTIALS
环境属性提供空字符串、空的byte
/char
数组或null
,则身份验证机制将是"none"
。这是因为 LDAP 要求简单身份验证的密码不能为空。如果未提供密码,则协议会自动将身份验证转换为"none"
。
SASL
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/sasl.html
LDAP v3 协议使用SASL来支持可插拔身份验证。这意味着 LDAP 客户端和服务器可以根据客户端和服务器所需的保护级别协商和使用可能是非标准和/或定制的身份验证机制。LDAP v2 协议不支持 SASL。
目前定义了几种 SASL 机制:
-
匿名(RFC 2245)
-
CRAM-MD5(RFC 2195)
-
Digest-MD5(RFC 2831)
-
外部(RFC 2222)
-
Kerberos V4(RFC 2222)
-
Kerberos V5(RFC 2222)
-
SecurID(RFC 2808)
-
S/Key(RFC 2222)
LDAP 服务器支持的 SASL 机制
在上述列表中,流行的 LDAP 服务器(如 Oracle、OpenLDAP 和 Microsoft)支持外部、摘要-MD5 和 Kerberos V5。RFC 2829提议将摘要-MD5 用作 LDAP v3 服务器的强制默认机制。
这是一个简单的程序
,用于查找 LDAP 服务器支持的 SASL 机制列表。
// Create initial context
DirContext ctx = new InitialDirContext();
// Read supportedSASLMechanisms from root DSE
Attributes attrs = ctx.getAttributes(
"ldap://localhost:389", new String[]{"supportedSASLMechanisms"});
运行此程序针对支持外部 SASL 机制的服务器产生的输出如下。
{supportedsaslmechanisms=supportedSASLMechanisms:
EXTERNAL, GSSAPI, DIGEST-MD5}
指定身份验证机制
要使用特定的 SASL 机制,您需要在Context.SECURITY_AUTHENTICATION
环境属性中指定其 IANA 注册的机制名称。您还可以指定 LDAP 提供程序尝试的机制列表。通过指定一个有序的以空格分隔的机制名称列表来实现。LDAP 提供程序将使用它找到实现的第一个机制。
这是一个示例,要求 LDAP 提供程序尝试获取 DIGEST-MD5 机制的实现,如果不可用,则使用 GSSAPI 的实现。
env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5 GSSAPI");
您可以从应用程序的用户获取此身份验证机制列表。或者您可以通过类似于之前显示的调用询问 LDAP 服务器获取它。LDAP 提供程序本身不会向服务器查询此信息。它只是尝试定位和使用指定机制的实现。
平台中的 LDAP 提供程序内置支持外部、摘要-MD5 和 GSSAPI(Kerberos v5)SASL 机制。您可以添加对其他机制的支持。
指定身份验证机制的输入
一些机制,如 External,不需要额外的输入,仅凭借机制名称就足以进行认证。External 示例展示了如何使用 External SASL 机制。
大多数其他机制需要一些额外的输入。根据机制的不同,输入的类型可能会有所不同。以下是一些机制常见的输入要求。
-
认证 ID。执行认证的实体的身份。
-
授权 ID。如果认证成功,应该进行访问控制检查的实体的身份。
-
认证凭据。例如,密码或密钥。
认证和授权 ID 可能会有所不同,如果程序(如代理服务器)代表另一个实体进行认证。认证 ID 是通过使用Context.SECURITY_PRINCIPAL
环境属性指定的。它的类型是java.lang.String
。
认证 ID 的密码/密钥是通过使用Context.SECURITY_CREDENTIALS
环境属性指定的。它的类型是java.lang.String
、char
数组(char[]
)或byte
数组(byte[]
)。如果密码是byte
数组,则会使用 UTF-8 编码将其转换为char
数组。
如果已设置"java.naming.security.sasl.authorizationId"
属性,则其值将用作授权 ID。其值必须是java.lang.String
类型。默认情况下,空字符串将用作授权 ID,这将指示服务器从客户端的认证凭据中派生授权 ID。
Digest-MD5 示例展示了如何使用Context.SECURITY_PRINCIPAL
和Context.SECURITY_CREDENTIALS
属性进行 Digest-MD5 认证。
如果某个机制需要除了已经描述的之外的输入,那么你需要为该机制定义一个回调对象供其使用,你可以在JNDI 教程中查看回调示例。本课程的下一部分将讨论如何使用 SASL Digest-MD5 认证机制。SASL 策略、GSS API(Kerberos v5) 和 CRAM-MD5 机制在 JNDI 教程中有介绍。
Digest-MD5
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/digest.html
Digest-MD5 认证是 LDAP v3 服务器所需的认证机制(RFC 2829)。因为 SASL 的使用是 LDAP v3 的一部分(RFC 2251),仅支持 LDAP v2 的服务器不支持 Digest-MD5。
Digest-MD5 机制在 RFC 2831 中有描述。它基于 HTTP 摘要认证(RFC 2251)。在 Digest-MD5 中,LDAP 服务器发送包含各种认证选项以及一个特殊令牌给 LDAP 客户端的数据。客户端通过发送加密响应来回应,该响应指示其选择的认证选项。响应被加密以证明客户端知道其密码。LDAP 服务器然后解密并验证客户端的响应。
要使用 Digest-MD5 认证机制,必须设置认证环境属性如下。
Context.SECURITY_AUTHENTICATION
。
设置为字符串"DIGEST-MD5"
。
Context.SECURITY_PRINCIPAL
。
设置为主体名称。这是一个特定于服务器的格式。一些服务器支持登录用户 id 格式,例如 UNIX 或 Windows 登录屏幕中定义的格式。其他服务器接受专有名称。还有一些使用 RFC 2829 中定义的授权 id 格式。在该 RFC 中,名称应为字符串"dn:"
,后跟被认证实体的完全限定 DN,或者字符串"u:"
,后跟用户 id。一些服务器接受多种格式。一些格式的示例是"cuser"
,"dn: cn=C. User, ou=NewHires, o=JNDITutorial"
,和"u: cuser"
。此属性的数据类型必须是java.lang.String
。
Context.SECURITY_CREDENTIALS
。
设置为主体的密码(例如,"mysecret"
)。它的类型可以是java.lang.String
,char
数组(char[]
),或byte
数组(byte[]
)。如果密码是java.lang.String
或char[]
,则使用 UTF-8 进行编码以传输到服务器。如果密码是byte[]
,则原样传输到服务器。
以下示例
展示了客户端如何使用 Digest-MD5 对 LDAP 服务器进行认证。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Authenticate as C. User and password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5");
env.put(Context.SECURITY_PRINCIPAL,
"dn:cn=C. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
注意: Oracle Directory Server, v5.2 支持具有明文密码的用户的 Digest-MD5 认证机制。您必须在创建用户之前设置密码加密模式。如果您已经创建了用户,请删除并重新创建。要使用管理控制台设置密码加密模式,请选择“配置”选项卡和“数据”节点。在“密码”窗格中,选择“无加密(CLEAR)”选项以进行“密码加密”。服务器接受简单的用户名(即具有一个条目的"uid"
属性的值)和用户名称的“dn:”格式。有关详细信息,请参阅服务器的文档。
指定领域
领域 定义了选择身份验证实体(Context.SECURITY_PRINCIPAL
属性的值)的命名空间。服务器可能有多个领域。例如,大学的服务器可能配置为具有两个领域,一个用于学生用户,另一个用于教师用户。领域配置由目录管理员完成。一些目录具有默认的单个领域。例如,Oracle Directory Server, v5.2,使用机器的完全限定主机名作为默认领域。
在 Digest-MD5 认证中,您必须对特定领域进行身份验证。您可以使用以下身份验证环境属性来指定领域。如果您不指定领域,则服务器提供的任何一个领域都将被使用。
java.naming.security.sasl.realm
设置为主体的领域。这是一个部署特定和/或服务器特定的区分大小写的字符串。它标识应选择主体名称(Context.SECURITY_PRINCIPAL
)的领域或域。如果此领域与服务器提供的领域之一不匹配,则身份验证失败。
以下示例
显示了如何设置环境属性以使用 Digest-MD5 进行身份验证并指定领域。要使此示例在您的环境中起作用,您必须更改源代码,以便领域值反映在您的目录服务器上的配置。
// Authenticate as C. User and password "mysecret" in realm "JNDITutorial"
env.put(Context.SECURITY_AUTHENTICATION, "DIGEST-MD5");
env.put(Context.SECURITY_PRINCIPAL,
"dn:cn=C. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
env.put("java.naming.security.sasl.realm", "JNDITutorial");
如果您需要使用隐私保护和其他 SASL 属性,请参阅 JNDI 教程。
SSL 和自定义套接字
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/ssl.html
除了 SASL 身份验证外,大多数 LDAP 服务器允许通过 SSL 访问其服务。SSL 对于 LDAP v2 服务器特别有用,因为 v2 协议不支持 SASL 身份验证。
启用 SSL 的服务器通常以两种方式支持 SSL。在最基本的方式中,服务器支持 SSL 端口以及普通(未受保护)端口。服务器支持 SSL 的另一种方式是通过使用“启动 TLS 扩展”(RFC 2830)。此选项仅适用于 LDAP v3 服务器,并在该部分中有详细描述。
使用 SSL 套接字属性
默认情况下,JDK 中的 LDAP 服务提供程序在与 LDAP 服务器通信时使用普通套接字。要求使用 SSL 套接字,请将Context.SECURITY_PROTOCOL
属性设置为"ssl"
。
在以下示例
中,LDAP 服务器在端口 636 上提供 SSL。要运行此程序,您必须在 LDAP 服务器上的端口 636 上启用 SSL。此过程通常由目录管理员执行。
服务器要求: LDAP 服务器必须设置为具有 X.509 SSL 服务器证书并启用 SSL。通常,您必须首先从证书颁发机构(CA)为服务器获取签名证书。然后,按照目录供应商的说明启用 SSL。不同的供应商有不同的工具来执行此操作。
对于Oracle Directory Server, v5.2,请在管理控制台中使用“管理证书”工具生成证书签名请求(CSR)。将 CSR 提交给 CA 以获取 X.509 SSL 服务器证书。使用管理控制台,将证书添加到服务器的证书列表中。如果 CA 的证书尚未在服务器的受信任 CA 列表中,则也需安装 CA 的证书。通过在管理控制台中使用“配置”选项卡启用 SSL。在左窗格中选择服务器。在右窗格中选择“加密”选项卡。选中“为此服务器启用 SSL”和“使用此密码族:RSA”的复选框,确保您添加的服务器证书在证书列表中。
客户端要求: 您需要确保客户端信任您将要使用的 LDAP 服务器。您必须在 JRE 的受信任证书数据库中安装服务器的证书(或其 CA 的证书)。这里是一个例子。
# cd *JAVA_HOME*/lib/security
# keytool -import -file server_cert.cer -keystore jssecacerts
有关如何使用安全工具的信息,请参阅安全指南。有关 JSSE 的信息,请参阅Java 安全套接字扩展(JSSE)参考指南。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:636/o=JNDITutorial");
// Specify SSL
env.put(Context.SECURITY_PROTOCOL, "ssl");
// Authenticate as S. User and password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires,o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
注意: 如果您使用 SSL 连接到未使用 SSL 的端口的服务器,则您的程序将挂起。同样,如果您使用普通套接字连接到服务器的 SSL 套接字,则您的应用程序将挂起。这是 SSL 协议的特性。
使用 LDAPS URL
通过使用Context.SECURITY_PROTOCOL
属性请求使用 SSL,您还可以通过使用 LDAPS URL 请求使用 SSL。LDAPS URL 类似于 LDAP URL,只是 URL 方案为 "ldaps" 而不是 "ldap"。它指定与 LDAP 服务器通信时使用 SSL。
在以下示例
中,LDAP 服务器在端口 636 上提供 SSL。要运行此程序,您必须在 LDAP 服务器上的端口 636 上启用 SSL。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
// Specify LDAPS URL
env.put(Context.PROVIDER_URL, "ldaps://localhost:636/o=JNDITutorial");
// Authenticate as S. User and password "mysecret"
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL,
"cn=S. User, ou=NewHires, o=JNDITutorial");
env.put(Context.SECURITY_CREDENTIALS, "mysecret");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
LDAPS URL 可以在任何接受 LDAP URL 的地方使用。查看JNDI 教程了解有关 LDAP 和 LDAPS URL 的详细信息。
客户端身份验证:使用外部 SASL 机制的 SSL
SSL 在比 LDAP 更低的层次提供身份验证和其他安全服务。如果在 SSL 上已经完成了身份验证,则 LDAP 层可以通过使用External SASL 机制从 SSL 中使用该身份验证信息。
以下示例
类似于先前的 SSL 示例
,只是它不使用简单身份验证,而是使用外部 SASL 身份验证。通过使用 External,您无需提供任何主体或密码信息,因为它们会从 SSL 中获取。
服务器要求: 此示例要求 LDAP 服务器允许基于证书的客户端身份验证。此外,LDAP 服务器必须信任(CA 的)接收到的客户端证书,并且必须能够将客户端证书中的所有者可分辨名称映射到其了解的主体。请按照您的目录供应商的说明执行这些任务。
客户端要求: 此示例要求客户端具有 X.509 SSL 客户端证书。此外,证书必须存储为密钥库文件中的第一个密钥条目。如果此条目受密码保护,则必须与密钥库具有相同的密码。有关 JSSE 密钥库的更多信息,请参阅Java 安全套接字扩展(JSSE)参考指南。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>(11);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:636/o=JNDITutorial");
// Principal and credentials will be obtained from the connection
env.put(Context.SECURITY_AUTHENTICATION, "EXTERNAL");
// Specify SSL
env.put(Context.SECURITY_PROTOCOL, "ssl");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
...
要运行此程序以便使用客户端证书进行身份验证,您必须提供(作为系统属性)包含客户端证书的密钥库的位置和密码。以下是运行程序的示例。
java -Djavax.net.ssl.keyStore=MyKeystoreFile \
-Djavax.net.ssl.keyStorePassword=mysecret \
External
如果您没有提供密钥库,程序将使用匿名身份验证运行,因为在 SSL 上不存在客户端凭据。
此示例展示了实现基于证书的客户端身份验证的最基本方法。 通过编写和使用访问客户端证书的自定义套接字工厂,可以以更灵活的方式实现更高级的方法,也许通过使用 LDAP 目录。 下一节将展示如何在 JNDI 应用程序中使用自定义套接字工厂。
使用自定义套接字
当使用 SSL 时,默认情况下,LDAP 提供程序将使用套接字工厂,javax.net.ssl.SSLSocketFactory
,用于创建与服务器通信的 SSL 套接字,使用默认的 JSSE 配置。 JSSE 可以以多种方式进行自定义,详细信息请参阅Java 安全套接字扩展(JSSE)参考指南。 但是,有时这些自定义不足以满足需求,您需要对 LDAP 服务提供程序使用的 SSL 套接字或一般套接字进行更多控制。 例如,您可能需要能够绕过防火墙的套接字,或者 JSSE 套接字使用非默认的缓存/检索策略来管理其信任和密钥存储。 要设置 LDAP 服务提供程序使用的套接字工厂实现,请将"java.naming.ldap.factory.socket"
属性设置为套接字工厂的完全限定类名。 此类必须实现javax.net.SocketFactory
抽象类,并提供实现getDefault()
方法的实例,该方法返回套接字工厂的实例。 请参阅Java 安全套接字扩展(JSSE)参考指南。
这里是一个产生普通套接字的自定义套接字工厂
的示例。
public class CustomSocketFactory extends SocketFactory {
public static SocketFactory getDefault() {
System.out.println("[acquiring the default socket factory]");
return new CustomSocketFactory();
}
...
}
请注意,此示例每次创建新的 LDAP 连接时都会创建CustomSocketFactory
的新实例。 这对某些应用程序和套接字工厂可能是合适的。 如果要重用相同的套接字工厂,getDefault()
应返回一个单例。
要在 JNDI 程序中使用此自定义套接字工厂,请设置"java.naming.ldap.factory.socket"
属性,如以下示例
所示。
// Set up the environment for creating the initial context
Hashtable<String, Object> env = new Hashtable<String, Object>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Specify the socket factory
env.put("java.naming.ldap.factory.socket", "CustomSocketFactory");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// ... do something useful with ctx
"java.naming.ldap.factory.socket"
属性可用于在每个上下文基础上设置套接字工厂。另一种控制 LDAP 服务提供程序使用的套接字的方法是通过使用java.net.Socket.setSocketImplFactory()
为整个程序中使用的所有套接字设置套接字工厂。使用这种方法不够灵活,因为它影响所有套接字连接,而不仅仅是 LDAP 连接,因此应谨慎使用。
更多 LDAP 操作
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/rename.html
LDAP 课程的其余部分介绍了 JNDI 提供的执行某些有趣的 LDAP 操作的能力。
重命名对象
您可以使用Context.rename()
来重命名目录中的对象。在LDAP v2中,这对应于“修改 RDN”操作,该操作将在同一上下文中重命名条目(即重命名同级)。在LDAP v3中,这对应于“修改 DN”操作,类似于“修改 RDN”,只是旧条目和新条目不需要在同一上下文中。您可以使用Context.rename()
来重命名叶条目或内部节点。在 Naming and Directory Operations 课程中展示的示例重命名了一个叶条目。以下代码
将内部节点从"ou=NewHires"
重命名为"ou=OldHires"
:
ctx.rename("ou=NewHires", "ou=OldHires");
注意: Oracle Directory Server v5.2 不支持重命名内部节点。如果您运行此示例,则会收到ContextNotEmptyException
。
将条目重命名为 DIT 的不同部分
使用 LDAP v3,您可以将条目重命名为 DIT 的不同部分。要通过Context.rename()
实现此目的,必须使用一个既是新条目又是旧条目的共同祖先的上下文。例如,要将"cn=C. User, ou=NewHires, o=JNDITutorial"
重命名为"cn=C. User, ou=People, o=JNDITutorial"
,您必须使用由"o=JNDITutorial"
命名的上下文。以下是演示此操作的示例
。如果您尝试在 LDAP v2 服务器上运行此示例,则会收到InvalidNameException
,因为版本 2 不支持此功能。
ctx.rename("cn=C. User, ou=NewHires", "cn=C. User, ou=People");
注意: Oracle Directory Server v5.2 不支持使用不同父节点进行重命名。如果您使用该服务器运行此示例,则会收到OperationNotSupportedException
(表示“协议错误”)。
保留旧名称属性
在 LDAP 中,当您重命名条目时,您可以选择将条目的旧 RDN 保留为更新后条目的属性。例如,如果您将条目"cn=C. User"
重命名为"cn=Claude User"
,您可以指定是否要保留旧 RDN"cn=C. User"
作为属性。
要指定在使用Context.rename()
时是否要保留旧名称属性,请使用"java.naming.ldap.deleteRDN"
环境属性。如果此属性的值为"true"
(默认值),则旧的 RDN 将被移除。如果其值为"false"
,则旧的 RDN 将保留为更新条目的属性。完整示例在这里
。
// Set the property to keep RDN
env.put("java.naming.ldap.deleteRDN", "false");
// Create the initial context
DirContext ctx = new InitialDirContext(env);
// Perform the rename
ctx.rename("cn=C. User, ou=NewHires", "cn=Claude User,ou=NewHires");
LDAP 比较
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/compare.html
LDAP 的“比较”操作允许客户端询问服务器是否具有指定条目的属性/值对。这使得服务器可以保留某些属性/值对的机密性(即,不对一般“搜索”访问公开),同时仍允许客户端有限使用它们。例如,一些服务器可能会将此功能用于密码,尽管客户端在“比较”操作本身中传递明文密码是不安全的。
要在 JNDI 中实现这一点,请对以下方法使用适当限制的参数:
-
search(Name name, String filter, SearchControls ctls)
-
search(Name name, String filterExpr, Object[]filterArgs, SearchControls ctls)
-
过滤器必须是“(name=value)”的形式。不能使用通配符。
-
搜索范围必须是
SearchControls.OBJECT_SCOPE
。 -
你必须要求不返回任何属性。如果不符合这些条件,那么这些方法将使用 LDAP 的“搜索”操作而不是 LDAP 的“比较”操作。
这里有一个示例
,会导致使用 LDAP 的“比较”操作。
// Value of the attribute
byte[] key = {(byte)0x61, (byte)0x62, (byte)0x63, (byte)0x64,
(byte)0x65, (byte)0x66, (byte)0x67};
// Set up the search controls
SearchControls ctls = new SearchControls();
ctls.setReturningAttributes(new String[0]); // Return no attrs
ctls.setSearchScope(SearchControls.OBJECT_SCOPE); // Search object only
// Invoke search method that will use the LDAP "compare" operation
NamingEnumeration answer = ctx.search("cn=S. User, ou=NewHires",
"(mySpecialKey={0})",
new Object[]{key}, ctls);
如果比较成功,结果枚举将包含一个名称为空且不包含任何属性的单个项目。
搜索结果
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/result.html
当您在DirContext
接口中使用搜索方法时,您将获得一个NamingEnumeration
。NamingEnumeration
中的每个项目都是一个SearchResult
,其中包含以下信息:
-
名称
-
对象
-
类名
-
属性
名称
每个SearchResult
包含满足搜索过滤器的 LDAP 条目的名称。您可以通过使用getName()
来获取条目的名称。该方法返回 LDAP 条目相对于目标上下文的复合名称
。目标上下文是name
参数解析到的上下文。在 LDAP 术语中,目标上下文是搜索的基本对象。以下是一个示例。
NamingEnumeration answer = ctx.search("ou=NewHires",
"(&(mySpecialKey={0}) (cn=*{1}))", // Filter expression
new Object[]{key, name}, // Filter arguments
null); // Default search controls
在本示例中,目标上下文由"ou=NewHires"
命名。answer
中的SearchResult
中的名称相对于"ou=NewHires"
。例如,如果getName()
返回"cn=J. Duke"
,那么相对于ctx
的名称将是"cn=J. Duke, ou=NewHires"
。
如果您使用SearchControls.SUBTREE_SCOPE
或SearchControls.OBJECT_SCOPE
执行搜索,并且目标上下文本身满足搜索过滤器,则返回的名称将是""(空名称),因为这是相对于目标上下文的名称。
这并不是全部。如果搜索涉及引荐(请参阅JNDI 教程)或解引用别名(请参阅JNDI 教程),那么相应的SearchResult
将具有不相对于目标上下文的名称。相反,它们将是直接引用条目的 URL。要确定getName()
返回的名称是相对还是绝对,请使用isRelative()
。如果此方法返回true
,则名称相对于目标上下文;如果返回false
,则名称是一个 URL。
如果名称是一个 URL,您需要使用该 URL,则可以将其传递给了解 URL 的初始上下文(请参阅JNDI 教程)。
如果您需要获取条目的完整 DN,可以使用NameClassPair.getNameInNamespace()
。
对象
如果搜索是请求返回条目对象的(使用SearchControls.setReturningObjFlag()
调用为true
),那么SearchResult
将包含表示条目的对象。要检索此对象,您需要调用getObject()
。如果之前将java.io.Serializable
、Referenceable
或Reference
对象绑定到 LDAP 名称,则使用来自条目的属性来重建该对象(请参阅JNDI 教程中的示例)。否则,使用来自条目的属性创建代表 LDAP 条目的DirContext
实例。在任一情况下,LDAP 提供程序会在对象上调用DirectoryManager.getObjectInstance()
并返回结果。
类名
如果搜索是请求返回条目对象,则类名是从返回的对象派生的。如果搜索请求包括检索 LDAP 条目的"javaClassName"
属性,则类名是该属性的值。否则,类名为"javax.naming.directory.DirContext"
。类名是从getClassName()
获取的。
属性
当执行搜索时,您可以通过向其中一个search()
方法提供参数或使用SearchControls.setReturningAttributes()
设置搜索控件来选择返回属性。如果没有明确指定属性,则将返回所有 LDAP 条目的属性。要指定不返回任何属性,必须传递一个空数组(new String[0]
)。
要检索 LDAP 条目的属性,您需要在SearchResult
上调用getAttributes()
。
响应控件
查看JNDI 教程中的“控件和扩展”课程,了解如何检索搜索结果的响应控件的详细信息。
LDAP 非请求通知
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/unsol.html
LDAP v3(RFC 2251)定义了非请求通知,即 LDAP 服务器向客户端发送的无需客户端引发的消息。在 JNDI 中,非请求通知由UnsolicitedNotification
接口表示。
由于服务器异步发送非请求通知,您可以使用与接收有关命名空间更改和对象内容更改通知相同的事件模型。通过在EventContext
或EventDirContext
上注册UnsolicitedNotificationListener
来注册接收非请求通知的兴趣。
这里是一个示例
展示了UnsolicitedNotificationListener
的实现。
public class UnsolListener implements UnsolicitedNotificationListener {
public void notificationReceived(UnsolicitedNotificationEvent evt) {
System.out.println("received: " + evt);
}
public void namingExceptionThrown(NamingExceptionEvent evt) {
System.out.println(">>> UnsolListener got an exception");
evt.getException().printStackTrace();
}
}
以下是一个示例
,展示了如何向事件源注册UnsolicitedNotificationListener
的实现。请注意,只有EventContext.addNamingListener()
中的监听器参数与非请求通知相关。名称和范围参数与非请求通知无关。
// Get the event context for registering the listener
EventContext ctx = (EventContext)
(new InitialContext(env).lookup("ou=People"));
// Create the listener
NamingListener listener = new UnsolListener();
// Register the listener with the context (all targets equivalent)
ctx.addNamingListener("", EventContext.ONELEVEL_SCOPE, listener);
运行此程序时,您需要将其指向一个可以生成非请求通知并促使服务器发出通知的 LDAP 服务器。否则,程序将在一分钟后悄无声息地退出。
实现UnsolicitedNotificationListener
的监听器也可以实现其他NamingListener
接口,比如NamespaceChangeListener
和ObjectChangeListener
。
连接管理
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/connect.html
JNDI 为访问命名和目录服务提供了高级接口。JNDI Context
实例与底层网络连接之间的映射可能不是一对一的。只要保留接口语义,服务提供者就可以自由共享和重用连接。应用程序开发人员通常不需要了解Context
实例如何创建和使用连接的细节。当开发人员需要调整程序时,这些细节是有用的。
本课程描述了 LDAP 服务提供者如何使用连接。它描述了何时创建连接以及如何指定特殊的连接参数,例如多个服务器和连接超时。本课程还展示了如何在支持的网络环境中动态发现和使用 LDAP 服务器。
创建的连接最终必须关闭。本课程包含一个描述客户端和服务器如何进行连接关闭的部分。
最后,本课程向您展示如何使用连接池使使用许多短暂连接的应用程序更有效率。
注意: 本课程中提供的信息仅适用于 JDK 中的 LDAP 服务提供者。其他供应商的 LDAP 服务提供者可能不使用相同的连接管理策略。
创建
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/create.html
创建连接的几种方式。最常见的方式是从创建初始上下文开始。当您使用 LDAP 服务提供程序创建InitialContext
、InitialDirContext
或InitialLdapContext
时,会立即与Context.PROVIDER_URL
属性中命名的目标 LDAP 服务器建立连接。每次创建初始上下文时,都会创建一个新的 LDAP 连接。有关如何更改此行为的信息,请参见 Pooling 部分。
如果属性值包含多个 URL,则依次尝试每个 URL,直到成功创建连接为止。然后将属性值更新为成功的 URL。有关如何使用 URL 列表创建初始上下文的示例,请参见JNDI 教程。
还有三种直接创建连接的方式。
-
通过将 URL 作为初始上下文的名称参数传递。当将 LDAP 或 LDAPS URL 作为名称参数传递给初始上下文时,URL 中的信息将用于创建到 LDAP 服务器的新连接,而不管初始上下文实例本身是否连接到 LDAP 服务器。实际上,初始上下文可能未连接到任何服务器。有关如何将 URL 用作名称的更多信息,请参见JNDI 教程。
-
另一种创建连接的方式是使用
Reference
。当传递包含 LDAP 或 LDAPS URL 的Reference
给NamingManager.getObjectInstance()
或DirectoryManager.getObjectInstance()
时,将使用 URL 中指定的信息创建一个新连接。 -
最后,当手动或自动跟随引荐时,引荐中的信息将用于创建新连接。有关引荐的信息,请参见JNDI 教程。
共享连接
从一个Context
实例派生的Context
实例和NamingEnumeration
s将共享相同的连接,直到对其中一个Context
实例进行更改使共享不再可能。 例如,如果您从初始上下文调用Context.lookup()
,Context.listBindings()
或DirContext.search()
并获得其他Context
实例,则所有这些Context
实例将共享相同的连接。
这里是一个示例
。
// Create initial context
DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context
Context ctx2 = (Context)ctx.lookup("");
// Get a child context
Context ctx3 = (Context) ctx.lookup("ou=NewHires");
在这个示例中,ctx
,ctx2
和ctx3
将共享相同的连接。
共享是无论Context
实例如何产生都会进行的。 例如,通过遵循引荐获得的Context
实例将与引荐共享相同的连接。
当您更改与连接相关的Context
实例的环境属性,例如用户的主体名称或凭据时,您进行这些更改的Context
实例将获得自己的连接(如果连接是共享的)。 未来从此Context
实例派生的Context
实例将共享这个新连接。 先前共享旧连接的Context
实例不受影响(即它们继续使用旧连接)。
这里是一个使用两个连接的示例
。
// Create initial context (first connection)
DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context
DirContext ctx2 = (DirContext)ctx.lookup("");
// Change authentication properties in ctx2
ctx2.addToEnvironment(Context.SECURITY_PRINCIPAL,
"cn=C. User, ou=NewHires, o=JNDITutorial");
ctx2.addToEnvironment(Context.SECURITY_CREDENTIALS, "mysecret");
// Method on ctx2 will use new connection
System.out.println(ctx2.getAttributes("ou=NewHires"));
ctx2
最初与ctx
共享相同的连接。 但是当其主体和密码属性更改时,它将无法再使用ctx
的连接。 LDAP 提供程序将自动为ctx2
创建一个新连接。
同样,如果您使用LdapContext.reconnect()
来更改Context
实例的连接控制,如果连接正在共享,则Context
实例将获得自己的连接。
如果Context
实例的连接未被共享(即没有Context
派生自它),则对其环境或连接控制的更改不会导致创建新连接。 相反,与连接相关的任何更改将应用于现有连接。
创建超时
并非所有连接创建都成功。如果 LDAP 提供程序在一定的超时期限内无法建立连接,则会中止连接尝试。默认情况下,此超时期限是网络(TCP)超时值,大约几分钟。要更改超时期限,您可以使用"com.sun.jndi.ldap.connect.timeout"
环境属性。此属性的值是表示连接超时的整数的字符串表示。
这里是一个示例
。
// Set up environment for creating initial context
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Specify timeout to be 5 seconds
env.put("com.sun.jndi.ldap.connect.timeout", "5000");
// Create initial context
DirContext ctx = new InitialDirContext(env);
// do something useful with ctx
在这个示例中,如果在 5 秒内无法创建连接,将抛出异常。
如果Context.PROVIDER_URL
属性包含多个 URL,则提供程序将对每个 URL 使用超时。例如,如果有 3 个 URL 并且超时已指定为 5 秒,则提供程序将总共等待最多 15 秒。
请查看连接池部分,了解此属性如何影响连接池。
关闭
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/close.html
正常的垃圾回收会在不再使用时删除Context
实例。被垃圾回收的Context
实例使用的连接将自动关闭。因此,您无需显式关闭连接。然而,网络连接是有限资源,对于某些程序,您可能希望控制它们的增殖和使用。本节包含如何关闭连接以及如何在服务器关闭连接时收到通知的信息。
显式关闭
您可以调用Context.close()
来指示您不再需要使用Context
实例。如果要关闭的Context
实例正在使用专用连接,则该连接也将关闭。如果Context
实例正在与其他Context
和未终止的NamingEnumeration
实例共享连接,则在所有这些Context
和NamingEnumeration
实例上调用close()
之前,连接将不会关闭。
在连接创建示例部分的example
中,必须在关闭底层连接之前关闭所有三个Context
实例。
// Create initial context
DirContext ctx = new InitialDirContext(env);
// Get a copy of the same context
Context ctx2 = (Context)ctx.lookup("");
// Get a child context
Context ctx3 = (Context) ctx.lookup("ou=NewHires");
// do something useful with ctx, ctx2, ctx3
// Close the contexts when we're done
ctx.close();
ctx2.close();
ctx3.close();
强制隐式闭包
如前所述,对于不再在作用域内的Context
和NamingEnumeration
实例,Java 运行时系统最终会对它们进行垃圾回收,从而清理close()
将要执行的状态。要强制进行垃圾回收,您可以使用以下代码。
Runtime.getRuntime().gc();
Runtime.getRuntime().runFinalization();
根据程序的状态,执行此过程可能会导致严重(临时)性能下降。如果需要确保连接已关闭,请跟踪Context
实例并显式关闭它们。
检测连接关闭
LDAP 服务器通常在空闲超时后关闭不再使用的连接。当您随后在使用此连接的Context
实例上调用方法时,该方法将抛出CommunicationException
。要检测服务器关闭Context
实例正在使用的连接时,您需要在Context
实例上注册一个UnsolicitedNotificationListener
。在 LDAP 未经请求通知部分显示了一个示例
。尽管该示例设计用于从服务器接收未经请求的通知,但也可用于检测服务器关闭连接。启动程序后,停止 LDAP 服务器并观察监听器的namingExceptionThrown()
方法被调用。
连接池
译文:
docs.oracle.com/javase/tutorial/jndi/ldap/pool.html
连接创建部分描述了何时创建连接。它描述了多个Context
实例如何共享相同的连接。
LDAP 服务提供程序支持的另一种连接共享类型称为连接池。在这种共享类型中,LDAP 服务提供程序维护一组(可能是)先前使用过的连接,并根据需要将它们分配给Context
实例。当Context
实例完成连接(关闭或垃圾回收)时,连接将被返回到池中以供将来使用。请注意,这种共享形式是顺序的:从池中检索连接,使用连接,将连接返回到池中,然后再次从池中检索连接以供另一个Context
实例使用。
连接池是针对每个 Java 运行时系统进行维护的。在某些情况下,使用连接池可以显著提高性能。例如,如果使用连接池,则处理包含对同一 LDAP 服务器的四个引用引用的搜索响应仅需要一个连接。如果没有使用连接池,这种情况将需要四个单独的连接。
本课程的其余部分将更详细地描述如何使用连接池。
如何使用连接池
通过向传递给初始上下文构造函数的环境属性添加属性"com.sun.jndi.ldap.connect.pool"
来请求连接池。这里是一个示例
。
// Set up environment for creating initial context
Hashtable env = new Hashtable(11);
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/o=JNDITutorial");
// Enable connection pooling
env.put("com.sun.jndi.ldap.connect.pool", "true");
// Create one initial context (Get connection from pool)
DirContext ctx = new InitialDirContext(env);
// do something useful with ctx
// Close the context when we're done
ctx.close(); // Return connection to pool
// Create another initial context (Get connection from pool)
DirContext ctx2 = new InitialDirContext(env);
// do something useful with ctx2
// Close the context when we're done
ctx2.close(); // Return connection to pool
此示例连续创建两个初始上下文。第二个初始上下文将重用第一个使用的连接。要运行此程序并观察如何检索连接并将其返回到池中,请使用以下命令行。
#java -Dcom.sun.jndi.ldap.connect.pool.debug=fine UsePool
这应该产生以下输出。
Create com.sun.jndi.ldap.LdapClient@5d173[localhost:389]
Use com.sun.jndi.ldap.LdapClient@5d173
{ou=ou: NewHires, objectclass=objectClass: top, organizationalUnit}
Release com.sun.jndi.ldap.LdapClient@5d173
Use com.sun.jndi.ldap.LdapClient@5d173
{ou=ou: People, objectclass=objectClass: top, organizationalunit}
Release com.sun.jndi.ldap.LdapClient@5d173
通过包含或省略"com.sun.jndi.ldap.connect.pool"
属性,您可以决定何时何地使用连接池,从而在每个上下文的基础上控制连接池。在前面的示例中,如果在创建第二个初始上下文之前从环境属性中删除此属性,则第二个初始上下文将不使用池化连接。
LDAP 提供程序通过应用程序的指示来跟踪连接是否正在使用。它假定维护打开上下文句柄的应用程序正在使用连接。因此,为了使 LDAP 提供程序正确管理池化连接,您必须勤于在不再需要的上下文上调用Context.close()
。
坏连接会被 LDAP 提供程序自动检测并从池中移除。无论是否使用连接池,上下文最终使用坏连接的概率是相同的。
创建超时
LDAP 服务提供程序维护的连接池可能具有限制的大小;这在连接池配置部分有详细描述。当启用连接池并且没有可用的池化连接时,客户端应用程序将被阻塞,等待可用连接。您可以使用"com.sun.jndi.ldap.connect.timeout"
环境属性来指定等待池化连接的时间。如果省略此属性,应用程序将无限期等待。
此属性还用于指定建立 LDAP 连接的超时期限,如连接创建部分所述。
何时不使用池化!!
池化连接旨在被重复使用。因此,如果您计划对可能改变底层连接状态的Context
实例执行操作,则不应该对该Context
实例使用连接池。例如,如果您计划在Context
实例上调用 Start TLS 扩展操作,或者计划在创建初始上下文后更改安全相关属性(如"java.naming.security.principal"
或"java.naming.security.protocol"
),则不应该对该Context
实例使用连接池,因为 LDAP 提供程序不会跟踪任何此类状态更改。在这种情况下使用连接池,可能会 compromise 您的应用程序的安全性。
配置
原文:
docs.oracle.com/javase/tutorial/jndi/ldap/config.html
连接池是针对每个 Java 运行时配置和维护的。连接不会跨不同的运行时共享。要使用连接池,不需要任何配置。只有在想要自定义池化方式时才需要配置,比如控制池的大小和哪些类型的连接被池化。
你可以在程序启动时通过一些系统属性来配置连接池。请注意这些是系统属性,不是环境属性,并且它们影响所有连接池请求。
这是一个设置最大池大小为 20,首选池大小为 10,并且空闲超时为一分钟的池化连接的命令行示例。
# java -Dcom.sun.jndi.ldap.connect.pool.maxsize=20 \
-Dcom.sun.jndi.ldap.connect.pool.prefsize=10 \
-Dcom.sun.jndi.ldap.connect.pool.timeout=60000 \
UsePool
下表列出了用于配置连接池的系统属性。它们在本节的其余部分中有更详细的描述。
系统属性名称 | 描述 | 默认值 |
---|---|---|
com.sun.jndi.ldap.connect.pool.authentication |
一个以空格分隔的连接认证类型列表,可以被池化。有效类型为"none"、"simple"和"DIGEST-MD5"。 | "none simple" |
com.sun.jndi.ldap.connect.pool.debug |
一个指示要生成的调试输出级别的字符串。有效值为"fine" (跟踪连接的创建和移除)和"all"(所有调试信息)。 |
|
com.sun.jndi.ldap.connect.pool.initsize |
一个整数的字符串表示,表示在为标识创建连接时初始创建的每个连接标识的连接数。 | 1 |
com.sun.jndi.ldap.connect.pool.maxsize |
一个整数的字符串表示,表示每个连接标识可以同时维护的最大连接数。 | 无最大大小 |
com.sun.jndi.ldap.connect.pool.prefsize |
一个整数的字符串表示,表示应同时维护的每个连接标识的首选连接数。 | 无首选大小 |
com.sun.jndi.ldap.connect.pool.protocol |
一个以空格分隔的连接协议类型列表,可以被池化。有效类型为"plain"和"ssl"。 | "plain" |
com.sun.jndi.ldap.connect.pool.timeout |
一个整数的字符串表示,表示空闲连接在池中可以保持的毫秒数,超过这个时间将被关闭并从池中移除。 | 无超时 |
被池化的内容
当您请求Context
实例使用连接池时,可以通过使用"com.sun.jndi.ldap.connect.pool"
环境属性,所使用的连接可能会被池化,也可能不会。默认规则是允许使用简单或无身份验证的普通(非 SSL)连接进行池化。您可以通过使用系统属性来更改此默认设置,以包括 SSL 连接和 DIGEST-MD5 身份验证类型。要允许普通和 SSL 连接都被池化,请将"com.sun.jndi.ldap.connect.pool.protocol"
系统属性设置为字符串"plain ssl"
。要允许匿名(无)、简单和 DIGEST-MD5 身份验证类型的连接被池化,请将com.sun.jndi.ldap.connect.pool.authentication
系统属性设置为字符串"none simple DIGEST-MD5"
。
有几个环境属性会自动使Context
实例无法使用池化连接。如果Context
实例将其"java.naming.ldap.factory.socket"
属性设置为自定义套接字工厂类,或将其"java.naming.security.sasl.callback"
属性设置为自定义回调处理程序类,或将其"com.sun.jndi.ldap.trace.ber"
属性设置为启用协议跟踪,则Context
实例无法使用池化连接。
连接是如何被池化的
当Context
实例请求使用池化连接时,LDAP 提供程序需要确定是否可以通过现有的池化连接满足请求。它通过为每个池化连接分配连接标识并检查传入请求是否具有与其池化连接之一的连接标识相同来实现这一点。
连接标识是创建可能经过身份验证的 LDAP 连接所需的参数集。其组成取决于请求的身份验证类型,如下表所示。
身份验证类型 | 连接标识内容 |
---|---|
none |
-
连接控制
-
主机名、端口号如在
"java.naming.provider.url"
属性中指定的,引荐或提供给初始上下文的 URL。 -
以下属性的内容:
java.naming.security.protocol java.naming.ldap.version
|
simple |
---|
-
所有列出的关于无身份验证的信息
-
以下属性的内容:
java.naming.security.principal java.naming.security.credentials
|
DIGEST-MD5 |
---|
-
所有列出的关于简单身份验证的信息
-
以下属性的内容:
java.naming.security.sasl.authorizationId java.naming.security.sasl.realm javax.security.sasl.qop javax.security.sasl.strength javax.security.sasl.server.authentication javax.security.sasl.maxbuffer javax.security.sasl.policy.noplaintext javax.security.sasl.policy.noactive javax.security.sasl.policy.nodictionary javax.security.sasl.policy.noanonymous javax.security.sasl.policy.forward javax.security.sasl.policy.credentials
|
池大小
LDAP 提供程序维护连接池;每个池保存具有相同连接标识的连接(正在使用或空闲)。有三个大小影响每个池的管理。这些大小是全局的,影响所有池。
初始池大小是 LDAP 服务提供程序在首次创建池时(即应用程序首次为该连接标识请求池化连接时)创建的每个连接标识的连接数。池中每个连接的身份验证是按需执行的,随着连接的使用而进行。默认情况下,初始池大小为 1,并且可以通过使用系统属性"com.sun.jndi.ldap.connect.pool.initsize"
进行更改。通常在应用程序启动时使用,以向服务器预先提供一定数量的连接。
最大池大小是 LDAP 服务提供程序可以同时维护的每个连接标识的最大连接数。使用中和空闲连接都计入此数字。当池大小达到此数字时,对应连接标识的新连接将无法创建,直到池中的连接被移除(即物理连接被关闭)。当池大小达到最大值且池中的所有连接都在使用中时,应用程序从该池请求连接时将被阻塞,直到池中的连接变为空闲或被移除。最大池大小为 0 意味着没有最大大小:对池化连接的请求将使用现有的空闲池化连接或新创建的池化连接。
首选池大小是 LDAP 服务提供程序应该维护的每个连接标识的首选连接数。使用中和空闲连接都计入此数字。当应用程序请求使用池化连接且池大小小于首选大小时,LDAP 提供程序将创建并使用新的池化连接,而不管是否有空闲连接可用。当应用程序完成对池化连接的使用(通过在共享连接上调用Context.close()
)且池大小大于首选大小时,LDAP 提供程序将关闭并从池中移除池化连接。首选池大小为 0 意味着没有首选大小:对池化连接的请求只会在没有空闲连接可用时才会创建新连接。
请注意,最大池大小会覆盖初始和首选池大小。例如,将首选池大小设置为大于最大池大小实际上是将其设置为最大池大小。
空闲连接
当应用程序完成对池化连接的使用(通过在共享连接的所有上下文上调用Context.close()
),底层的池化连接被标记为空闲,等待重新使用。默认情况下,空闲连接会一直保留在池中,直到被垃圾回收。如果设置了"com.sun.jndi.ldap.connect.pool.timeout"
系统属性,LDAP 提供程序将自动关闭并移除空闲时间超过指定时期的池化连接。
常见问题
译文:
docs.oracle.com/javase/tutorial/jndi/ldap/faq.html
这节课回答了用户在使用 JNDI 访问 LDAP 服务时经常遇到的常见问题。一些常见问题在命名和目录操作课程的 Trouble Shooting Tips 中得到解答。
-
上下文:
-
上下文是否安全用于多线程访问?
-
为什么 LDAP 提供程序忽略我的安全环境属性?
-
为什么我一直收到 CommunicationException?
-
如何获取 LDAP 消息的跟踪?
-
如何使用不同的身份验证机制,如 Kerberos?
-
更改密码时是否应启用 SSL?
属性:
-
当我请求一个属性时,为什么返回另一个属性?
-
如何知道属性值的类型?
-
如何以除了字符串或字节数组之外的形式获取属性的值?
-
为什么在我的搜索中将"*"作为属性值不按预期工作?
搜索:
-
为什么搜索过滤器中的通配符不总是起作用?
-
当我知道目录中还有更多条目时,为什么我只返回n个条目?
-
如何在我的搜索中传递控件?
-
我如何查看返回的搜索结果数量?
名称:
-
为什么我的搜索结果中的名称是空字符串?
-
为什么我的搜索结果中的名称是一个 URL 字符串?
-
传递给上下文方法的 Name 参数是什么类型?
-
我可以将从 NameParser 获取的名称传递给 Context 方法吗?
-
我用于
Context.SECURITY_PRINCIPAL
属性的名称与目录之间有什么关系? -
为什么我从目录中读取的名称中有奇怪的引号和转义字符?
-
如何获取 LDAP 条目的完整 DN?
1. 上下文是否安全用于多线程访问,还是需要对上下文进行锁定/同步访问?
答案取决于实现。这是因为Context
和DirContext
接口没有指定同步要求。JDK 中的 LDAP 实现针对单线程访问进行了优化。如果有多个线程访问相同的Context
实例,则每个线程在使用时需要锁定Context
实例。这也适用于从相同Context
实例派生的任何NamingEnumeration
。然而,多个线程可以同时访问不同的Context
实例(甚至是从同一个初始上下文派生的实例)而无需锁定。
2. 如果我不设置Context.SECURITY_CREDENTIALS
("java.naming.security.credentials"
)属性或将其设置为空字符串,为什么 LDAP 提供程序会忽略我的安全环境属性?
如果您向Context.SECURITY_CREDENTIALS
环境属性提供空字符串、空的byte
/char
数组或null
,即使Context.SECURITY_AUTHENTICATION
属性设置为"simple"
,也会发生匿名绑定。这是因为对于简单认证,LDAP 要求密码不能为空。如果未提供密码,则协议会自动将认证转换为"none"
。
3. 当我尝试创建初始上下文时为什么会一直收到CommunicationException
?
您可能正在与仅支持 LDAP v2 的服务器通信。请参考JNDI 教程中的杂项课程,了解如何设置版本号的示例。
4. 我如何追踪 LDAP 消息?
尝试使用"com.sun.jndi.ldap.trace.ber"
环境属性。如果此属性的值是java.io.OutputStream
的实例,则 LDAP 提供程序发送和接收的 BER 缓冲区的跟踪信息将写入该流。如果属性的值为null
,则不会写入跟踪输出。
例如,以下代码将把跟踪输出发送到System.err
。
env.put("com.sun.jndi.ldap.trace.ber", System.err);
5. 我如何使用不同的认证机制,比如 Kerberos?
参考JNDI 教程中的 GSS-API/Kerberos v5 认证部分,了解如何使用 Kerberos 认证。要使用其他认证机制,请参阅JNDI 教程中的使用任意 SASL 机制部分。
6. 在更改密码时应该启用 SSL 吗?
这实际取决于您正在使用的目录服务器。一些目录服务器如果未启用 SSL,则不允许您更改密码,但有些允许。启用 SSL 可以确保密码在通信通道中得到保护。
7. 当我请求一个属性时,为什么会返回另一个属性?
您正在使用的属性名称可能是另一个属性的同义词。在这种情况下,LDAP 服务器可能返回规范属性名称而不是您提供的属性名称。当您查看服务器返回的Attributes
时,需要使用规范名称而不是同义词。
例如,"fax"
可能是规范属性名称"facsimiletelephonenumber"
的同义词。如果您请求"fax"
,服务器将返回名为"facsimiletelephonenumber"
的属性。有关同义词和属性名称的其他问题,请参阅 Naming and Directory Operations 课程。
8. 我如何知道属性值的类型?
属性值可以是java.lang.String
或byte[]
。请参阅JNDI 教程的杂项部分,了解哪些属性值以byte[]
形式返回。要以编程方式执行此操作,您可以使用instanceof
运算符检查从 LDAP 提供程序返回的属性值。
9. 如何以字符串或字节数组以外的形式获取属性值?
目前你不能。LDAP 提供程序仅返回java.lang.String
或byte[]
类型的属性值。请参阅JNDI 教程的杂项部分。
10. 为什么在我的搜索中将"*"作为属性值不起作用?
当您使用以下形式的search()
时,属性值被视为文字;也就是说,目录条目中的属性应该正好包含该值:search(Name name, Attributes matchingAttrs)
要使用通配符,您应该使用search()
的字符串过滤形式,如下所示。search(Name name, String filter, SearchControls ctls)
search(Name name, String filterExpr, Object[]filterArgs, SearchControls ctls)
对于最后一种形式,通配符字符必须出现在filterExpr
参数中,而不是在filterArgs
中。filterArgs
中的值也被视为文字。
11. 为什么搜索过滤器中的通配符不总是起作用?
通配符出现在属性值之前或之后(例如在"attr=*I*"
中)表示服务器将使用属性的子字符串匹配规则搜索匹配的属性值。如果属性的定义没有子字符串匹配规则,则服务器无法找到属性。您必须使用相等或“存在”过滤器进行搜索。
12. 当我知道目录中还有更多条目时,为什么我只返回了n个条目?一些服务器配置为限制可以返回的条目数。其他服务器在搜索期间也限制可以检查的条目数。请检查您的服务器配置。
13. 我如何在我的搜索中传递控件?
本教程中未解释控件。查看JNDI 教程。
14. 我如何知道我得到了多少搜索结果?
在枚举结果时必须进行计数。LDAP 不提供这些信息。
15. 为什么我在我的SearchResult
中得到一个空字符串作为名称?
getName()
总是返回一个相对于搜索的目标上下文的名称。因此,如果目标上下文满足搜索过滤器,则返回的名称将是""(空名称),因为这是相对于目标上下文的名称。有关详细信息,请参阅搜索结果部分。
16. 为什么我在我的SearchResult
中得到一个 URL 字符串作为名称?
LDAP 条目是通过跟随别名或引荐而检索的,因此它的名称是一个 URL。有关详细信息,请参阅搜索结果课程。
17. 传递给Context
和DirContext
方法的Name
参数是什么类型?- 一个CompoundName
还是一个CompositeName
?
字符串形式接受复合名称的字符串表示。也就是说,使用字符串名称等同于调用new CompositeName(stringName)
并将结果传递给Context
/DirContext
方法。Name
参数可以是实现Name
接口的任何对象。如果它是CompositeName
的一个实例,则该名称被视为复合名称;否则,它被视为复合名称。
18. 我可以将从NameParser
得到的名称传递给Context
方法吗?
这与前一个问题有关。是的,你可以。NameParser.parse()
返回一个实现了Name
接口的复合名称。这个名称可以传递给Context
方法,这些方法将把它解释为一个复合名称。
19. 我在Context.SECURITY_PRINCIPAL
属性中使用的名称与目录之间有什么关系?
你可以将主要名称视为来自不同命名空间的名称。参见RFC 2829和安全部分,了解 LDAP 认证机制的详细信息。JDK 中的 LDAP 服务提供程序接受一个字符串主要名称,直接传递给 LDAP 服务器。一些 LDAP 服务器接受 DN,而其他一些支持RFC 2829提出的方案。
20. 为什么我从目录中读取的名称中会有奇怪的引号和转义字符?
JDK 中的 LDAP 名称解析器在引号规则方面比较保守,但仍然生成“正确”的名称。另外,请记住,NamingEnumeration
返回的条目名称是可以传递回Context
和DirContext
方法的复合名称。因此,如果名称中包含与复合名称语法冲突的字符(例如斜杠字符“/”),那么 LDAP 提供程序将提供一种编码,以确保斜杠字符将被视为 LDAP 名称的一部分,而不是复合名称分隔符。
开始使用LdapName
和Rdn
类,实现简单的名称操作。
21. 如何获取 LDAP 条目的完整 DN?
您可以使用NameClassPair.getNameInNamespace()
。
课程:目录中的 Java 对象
原文:
docs.oracle.com/javase/tutorial/jndi/objects/index.html
传统上,目录被用来存储数据。用户和程序员将目录视为包含一组属性的目录条目的层次结构。您可以从目录中查找条目并提取感兴趣的属性。
对于用 Java 编程语言编写的应用程序,Java 对象有时可能会在应用程序之间共享。对于这样的应用程序,能够将目录用作 Java 对象的存储库是有意义的。目录为分布在网络上的 Java 应用程序提供了一个集中管理的、可能是复制的服务。例如,应用程序服务器可能使用目录来注册代表其管理的服务的对象,以便客户端稍后可以搜索目录以定位所需的服务。JNDI 用作服务目录的示例是 Apache DS。有关更多信息,请访问Apache Directory。
JNDI 提供了目录的面向对象视图,从而允许将 Java 对象添加到目录中并从中检索,而无需客户端管理数据表示问题。本课程讨论了在基本水平上使用目录存储和检索 Java 对象。JNDI 提供了用于创建和存储从目录访问的对象的对象和状态工厂。
对象工厂
对象工厂是对象的生产者。它接受有关如何创建对象的一些信息,例如引用,然后返回该对象的实例。有关对象工厂和对象在目录中存储的格式的详细信息,请参阅JNDI 教程。
状态工厂
状态工厂将一个对象转换为另一个对象。输入是对象和可选属性,提供给 Context.bind(),输出是另一个对象和可选属性,将存储在底层命名服务或目录中。有关状态工厂以及如何编写自己的状态工厂的详细信息,请参阅JNDI 教程。
课程的下一部分讨论了如何访问目录中的对象。它描述了可序列化对象如何存储和读取在目录中。有关其他类型的对象,请查看JNDI 教程。
存储和读取对象
原文:
docs.oracle.com/javase/tutorial/jndi/objects/store.html
应用程序和服务可以以不同的方式使用目录来存储和定位对象:
-
存储(对象的副本)对象本身。
-
存储对象的引用。
-
存储描述对象的属性。
一般来说,Java 对象的序列化形式包含对象的状态和对象的引用,以紧凑的寻址信息表示,可用于联系对象。一些示例在查找对象课程中给出。对象的属性用于描述对象的属性;属性可能包括寻址和/或状态信息。
使用这三种方式中的哪一种取决于正在构建的应用程序/系统以及它需要与其他应用程序和系统进行交互的方式,这些应用程序和系统将共享目录中存储的对象。另一个因素是服务提供商和底层目录服务提供的支持。
在将对象存储在目录中时,所有应用程序都使用以下方法之一:
-
Context.bind()
-
DirContext.bind()
-
Context.rebind()
-
DirContext.rebind()
应用程序将要存储的对象传递给这些方法之一。然后,根据服务提供商支持的对象类型,对象将被转换为底层目录服务可接受的表示形式。
本课程展示了如何在目录中存储可序列化对象,一旦对象被存储,你可以简单地使用lookup()
从目录中获取对象的副本,而不管实际存储了什么类型的信息。
你不仅可以通过使用lookup()
来获取对象,还可以在列出上下文时以及搜索上下文或其子树时获取对象。在所有这些情况下,对象工厂可能会参与其中。对象工厂在JNDI 教程中有详细讨论。
对于存储以下对象类型,请参考 JNDI 教程:
-
Referenceable 对象和 JNDI
Reference
s添加、替换或移除绑定课程中的 bind()示例使用 Referenceable 对象。
-
带有属性的对象(
DirContext
) -
RMI(Java 远程方法调用)对象(包括使用 IIOP 的对象)
-
CORBA 对象
在继续之前: 要成功运行这些示例,您必须在服务器中关闭模式检查,或者将附带本教程的Java 模式
添加到服务器中。这项任务通常由目录服务器的管理员执行。有关更多信息,请参阅软件设置课程。
Windows Active Directory: Context.rebind()
和 DirContext.rebind()
在 Active Directory 上不起作用,因为这些方法通过读取要更新的条目的属性,删除该条目,然后添加一个包含修改后属性的新条目来工作。Active Directory 返回一些用户无法设置的属性,导致最终添加步骤失败。解决此问题的方法是使用DirContext.getAttributes()
获取并保存要保留的属性。然后,使用DirContext.bind()
将条目删除并添加回来,其中包括保存的属性(以及其他要添加的属性)。
可序列化对象
原文:
docs.oracle.com/javase/tutorial/jndi/objects/serial.html
序列化 一个对象意味着将其状态转换为字节流,以便可以将字节流还原为对象的副本。如果其类或其任何超类实现了 java.io.Serializable
接口或其子接口 java.io.Externalizable
,则 Java 对象是 可序列化 的。反序列化 是将对象的序列化形式转换回对象的过程。
例如,java.awt.Button
类实现了 Serializable
接口,因此您可以序列化一个 java.awt.Button
对象并将该序列化状态存储在文件中。稍后,您可以读取序列化状态并将其反序列化为一个 java.awt.Button
对象。
Java 平台规定了可序列化对象序列化的默认方式。一个(Java)类可以覆盖这种默认序列化,并定义其自己的序列化对象的方式。对象序列化规范详细描述了对象序列化。
当对象被序列化时,用于标识其类的信息被记录在序列化流中。然而,类的定义("类文件")本身并未记录。反序列化对象的系统有责任确定如何定位和加载必要的类文件。例如,一个 Java 应用程序可能在其类路径中包含一个包含序列化对象的类文件的 JAR 文件,或者通过使用存储在目录中的信息加载类定义,如本课程后面解释的那样。
绑定可序列化对象
如果底层服务提供程序支持该操作,您可以将可序列化对象存储在目录中,就像 Oracle 的 LDAP 服务提供程序一样。
以下示例调用 Context.bind
将 AWT 按钮绑定到名称 "cn=Button"
。要将属性与新绑定关联起来,您可以使用 DirContext.bind
。要覆盖现有绑定,使用 Context.rebind
和 DirContext.rebind
。
// Create the object to be bound
Button b = new Button("Push me");
// Perform the bind
ctx.bind("cn=Button", b);
您可以使用 Context.lookup
读取对象,如下所示。
// Check that it is bound
Button b2 = (Button)ctx.lookup("cn=Button");
System.out.println(b2);
运行 此示例
会产生以下输出。
# java SerObj
java.awt.Button[button0,0,0,0x0,invalid,label=Push me]
指定代码库
注意: 这里描述的程序适用于在遵循RFC 2713中定义的模式的目录服务中绑定可序列化对象。这些程序可能不适用于支持将可序列化对象与指定代码库绑定的其他命名和目录服务。
当一个序列化对象像前面的例子中所示绑定在目录中时,从目录中读取序列化对象的应用程序必须能够访问必要的类定义以对对象进行反序列化。
或者,你可以在目录中记录一个代码库,无论是在绑定对象时还是随后通过使用DirContext.modifyAttributes
添加属性。你可以使用任何属性来记录这个代码库,并让你的应用程序从目录中读取该属性并适当使用它。或者你可以使用在中指定的"javaCodebase"
属性。在后一种情况下,Oracle 的 LDAP 服务提供者将自动使用该属性根据需要加载类定义。"javaCodebase"
应包含代码库目录或 JAR 文件的 URL。如果代码库包含多个 URL,则每个 URL 必须用空格字符分隔。
以下示例类似于绑定java.awt.Button
的示例。不同之处在于它使用了一个用户定义的Serializable
类,Flower
,并提供了一个包含Flower
类定义位置的"javaCodebase"
属性。以下是执行绑定的代码。
String codebase = ...;
// Create the object to be bound
Flower f = new Flower("rose", "pink");
// Perform the bind and specify the codebase
ctx.bind("cn=Flower", f, new BasicAttributes("javaCodebase", codebase));
当你运行这个例子
时,你必须提供Flower.class
类文件安装位置的 URL。例如,如果Flower.class
安装在 Web 服务器web1
上,目录为example/classes
,那么你可以按照以下方式运行这个例子。
# java SerObjWithCodebase http://web1/example/classes/
pink rose
之后,你可以从类路径中删除Flower.class
并运行任何查找或列出此对象的程序,而无需直接引用Flower
类。如果你的程序直接引用Flower
,那么你必须使其类文件可供编译和执行。
课程:JDK 5.0 和 JDK 6 中的新功能
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/index.html
在这节课中,我们将讨论 JDK 5.0 和 JDK 6 版本中 JNDI 和 LDAP 服务提供程序支持的以下新功能。
-
从搜索结果中检索出区分名称(DN)。
-
使用标准 LDAP 控件。
-
操作 LDAP 名称。
-
设置 LDAP 操作的读取超时。
检索 Distinguished Name
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/dn.html
在 JDK 5.0 之前的版本中,没有直接的方法从搜索结果中获取 Distinguished Name (DN)。SearchResults.getName()
方法始终返回相对于执行搜索的上下文的名称。为了获取搜索条目的绝对或完整名称,需要一定的记录来跟踪祖先上下文。在 JDK 5.0 中添加了以下两个新的 API,用于在对上下文执行搜索、列出或列出绑定操作时从 NameClassPair 中检索绝对名称:
-
NameClassPair.getNameInNameSpace(Name name)
-
NameClassPair.getNameInNameSpace(String name)
这是一个从 LDAP 搜索中检索 DN 的示例:
public static void printSearchEnumeration(NamingEnumeration retEnum) {
try {
while (retEnum.hasMore()) {
SearchResult sr = (SearchResult) retEnum.next();
System.out.println(">>" + sr.getNameInNamespace());
}
} catch (NamingException e) {
e.printStackTrace();
}
}
完整的示例可以从这里
获取。该程序生成以下输出:
>>cn=Jon Ruiz, ou=People, o=JNDITutorial
>>cn=Scott Seligman, ou=People, o=JNDITutorial
>>cn=Samuel Clemens, ou=People, o=JNDITutorial
>>cn=Rosanna Lee, ou=People, o=JNDITutorial
>>cn=Maxine Erlund, ou=People, o=JNDITutorial
>>cn=Niels Bohr, ou=People, o=JNDITutorial
>>cn=Uri Geller, ou=People, o=JNDITutorial
>>cn=Colleen Sullivan, ou=People, o=JNDITutorial
>>cn=Vinnie Ryan, ou=People, o=JNDITutorial
>>cn=Rod Serling, ou=People, o=JNDITutorial
>>cn=Jonathan Wood, ou=People, o=JNDITutorial
>>cn=Aravindan Ranganathan, ou=People, o=JNDITutorial
>>cn=Ian Anderson, ou=People, o=JNDITutorial
>>cn=Lao Tzu, ou=People, o=JNDITutorial
>>cn=Don Knuth, ou=People, o=JNDITutorial
>>cn=Roger Waters, ou=People, o=JNDITutorial
>>cn=Ben Dubin, ou=People, o=JNDITutorial
>>cn=Spuds Mackenzie, ou=People, o=JNDITutorial
>>cn=John Fowler, ou=People, o=JNDITutorial
>>cn=Londo Mollari, ou=People, o=JNDITutorial
>>cn=Ted Geisel, ou=People,o=JNDITutorial
标准 LDAP 控件
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/controls-std.html
在 LDAP v3 中,控件是通过将其与对服务器或客户端有用的更多信息相关联来增强现有 LDAP 操作的消息。控件可以是请求控件或响应控件。请求控件与 LDAP 请求一起从客户端发送到服务器。响应控件与 LDAP 响应一起从服务器发送到客户端。任何一种都由接口javax.naming.ldap.Control
表示。
如果您以前没有使用控件进行编程,请查看JNDI 教程中的控件课程。
在本课程中,我们将讨论添加到 JDK 5.0 的标准 LDAP 控件。必要的 LDAP 控件已经在 JNDI/LDAP 服务提供程序的com.sun.jndi.ldap.ctl
包下支持的 LDAP Booster Pack 扩展包中支持。由 IETF 标准化的 LDAP 控件现在通过以下类在 JDK 的javax.naming.ldap
包中提供。
-
ManageReferralControl
( RFC 3296 ) -
PagedResultsControl
( RFC 2696 ) -
PagedResultsResponseControl
-
SortControl
( RFC 2891 ) -
SortKey
-
SortResponseControl
-
BasicControl
分页结果控件
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/paged-results.html
BasicControl
javax.naming.ldap.BasicControl
实现了 javax.naming.ldap.Control
,作为扩展其他控件的基本实现。
分页结果控件
分页结果控件对于希望以受控方式接收搜索结果的 LDAP 客户端非常有用,受页面大小限制。页面大小可以由客户端根据其资源的可用性(如带宽和处理能力)进行配置。
服务器使用 cookie(类似于 HTTP 会话 cookie 机制)来维护搜索请求的状态,以跟踪发送给客户端的结果。分页结果控件在 RFC 2696 中指定。下面的类提供了支持分页结果控件所需的功能。
-
javax.naming.ldap.PagedResultsControl
-
javax.naming.ldap.PagedResultsResponseControl
如何使用分页结果控件?
下面的示例说明了客户端执行搜索并请求页面大小限制为 5 的客户端-服务器交互。服务器返回的整个结果集包含 21 个条目。
-
客户端发送搜索请求,请求页面大小为 5 的分页结果。
// Activate paged results int pageSize = 5; // 5 entries per page byte[] cookie = null; int total; ctx.setRequestControls(new Control[]{ new PagedResultsControl(pageSize, Control.CRITICAL) }); // Perform the search NamingEnumeration results = ctx.search("", "(objectclass=*)", new SearchControls());
-
服务器响应包含条目以及搜索结果中总共 21 个条目的指示,还有一个不透明的 cookie,客户端在检索后续页面时要使用该 cookie。
// Iterate over a batch of search results sent by the server while (results != null && results.hasMore()) { // Display an entry SearchResult entry = (SearchResult)results.next(); System.out.println(entry.getName()); // Handle the entry's response controls (if any) if (entry instanceof HasControls) { // ((HasControls)entry).getControls(); } } // Examine the paged results control response Control[] controls = ctx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { if (controls[i] instanceof PagedResultsResponseControl) { PagedResultsResponseControl prrc = (PagedResultsResponseControl)controls[i]; total = prrc.getResultSize(); cookie = prrc.getCookie(); } else { // Handle other response controls (if any) } } }
-
客户端发送相同的搜索请求,返回不透明的 cookie,并请求下一页。
// Re-activate paged results ctx.setRequestControls(new Control[]{ new PagedResultsControl(pageSize, cookie, Control.CRITICAL) });
-
服务器响应包含五个条目,并指示还有更多条目。客户端重复执行第 4 步中执行的搜索,直到服务器返回空 cookie,表示服务器不再发送更多条目。
完整的 JNDI 示例可以在 这里
找到。
注意: 分页搜索控件受 Windows Active Directory 服务器支持。Oracle Directory Server 版本 5.2 不支持。
排序控制
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/sort.html
当客户端希望服务器发送排序后的搜索结果时,使用排序控制。服务器端排序在客户端需要根据某些标准对结果进行排序但无法自行执行排序过程的情况下非常有用。排序控制在RFC 2891中指定。下面的类提供了支持排序控制所需的功能。
-
javax.naming.ldap.SortControl
-
javax.naming.ldap.SortKey
-
javax.naming.ldap.SortResponseControl
SortKey 是一个基于其排序结果的键的有序列表。
如何使用排序控制?
下面的示例说明了客户端执行搜索请求服务器端基于属性“cn”进行排序的客户端-服务器交互。
-
客户端发送搜索请求请求
// Activate sorting String sortKey = "cn"; ctx.setRequestControls(new Control[] { new SortControl(sortKey, Control.CRITICAL) }); // Perform a search NamingEnumeration results = ctx.search("", "(objectclass=*)", new SearchControls());
-
服务器响应的条目是根据“cn”属性及其对应的匹配规则排序的。
// Iterate over sorted search results while (results != null && results.hasMore()) { // Display an entry SearchResult entry = (SearchResult)results.next(); System.out.println(entry.getName()); // Handle the entry's response controls (if any) if (entry instanceof HasControls) { // ((HasControls)entry).getControls(); } } // Examine the sort control response Control[] controls = ctx.getResponseControls(); if (controls != null) { for (int i = 0; i < controls.length; i++) { if (controls[i] instanceof SortResponseControl) { SortResponseControl src = (SortResponseControl)controls[i]; if (! src.isSorted()) { throw src.getException(); } } else { // Handle other response controls (if any) } } }
完整的 JNDI 示例可以在这里
找到。
注意: 排序控制由 Oracle Directory Server 和 Windows Active Directory 服务器都支持。
管理引荐控件
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/mdsaIT.html
管理引荐控件(RFC 3296)允许在执行 LDAP 操作时将引荐和其他特殊对象操作为普通对象。换句话说,管理引荐控件告诉 LDAP 服务器将引荐条目返回为普通条目,而不是返回"引荐"错误响应或继续引用。 JDK 5.0 中的新类使您可以在 LDAP 请求中发送管理引荐控件:
javax.naming.ldap.ManageReferralControl
JDK 中的 LDAP 服务提供程序将自动发送此控件以及任何请求。您还可以通过将 Context.REFERRAL 环境属性设置为"ignore"来显式启用它。有关引荐处理的更多信息,请查看 JNDI 教程的引荐部分。
这是一个示例,它在 LDAP 请求中发送了管理引荐控件。
// Create initial context
LdapContext ctx = (LdapContext) new InitialDirContext(env);
ctx.setRequestControl(new Control[] new ManageReferralControl());
// Set controls for performing subtree search
SearchControls ctls = new SearchControls();
ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
// Perform search
NamingEnumeration answer = ctx.search("", "(objectclass=*)", ctls);
// Print the answer
while (answer.hasMore()) {
System.out.println(">>>" +
((SearchResult)answer.next()).getName());
}
// Close the context when we're done
ctx.close();
完整示例可以在这里
找到。
注意 1: 以上示例将要求您使用配置文件refserver.ldif
设置第二个服务器。服务器必须支持 LDAP v3 和 RFC 3296。如果服务器不支持这种方式的引荐,则示例将无法正常工作。配置文件包含引荐,指向您设置的原始服务器。它假定原始服务器位于本地机器的端口 389 上。如果您在另一台机器或端口上设置了服务器,则需要编辑 refserver.ldif 文件中的"ref"条目,并将"localhost:389"替换为适当的设置。第二个服务器应在本地机器的端口 489 上设置。如果您在另一台机器或端口上设置了第二个服务器,则需要相应地调整初始上下文的 Context.PROVIDER_URL 环境属性的设置。
设置目录服务器通常由目录或系统管理员执行。有关更多信息,请参阅软件设置课程。
注意 2: Windows Active Directory:由于 Active Directory 不支持管理引荐控件,本课程中的所有示例都无法针对 Active Directory 工作。
操作 LdapName(专有名称)
原文:
docs.oracle.com/javase/tutorial/jndi/newstuff/ldapname.html
Distinguished Name(DN)在 LDAP 中以字符串表示形式使用。用于表示 DN 的字符串格式在RFC 2253中指定。DN 由称为相对专有名称(RDN)的组件组成。以下是 DN 的示例:
"CN=John Smith, O=Isode Limited, C=GB"
它由以下 RDN 组成:
-
CN=John Smith
-
O=Isode Limited
-
C=GB
下面的类分别表示 DN 和 RDN。
-
javax.naming.ldap.LdapName
-
javax.naming.ldap.Rdn
LdapName 类实现了javax.naming.Name
接口,类似于javax.naming.CompoundName
和javax.naming.CompositeName
类。
LdapName 和 Rdn 类允许轻松操作 DN 和 RDN。使用这些 API,通过将名称和值配对,轻松构造 RDN。可以使用 RDN 列表构造 DN。同样,可以从它们的字符串表示中检索 DN 和 RDN 的各个组件。
LdapName
可以使用其在RFC 2253中定义的字符串表示形式或使用 Rdns 列表创建 LdapName。当使用前一种方式时,指定的字符串将根据 RFC2253 中定义的规则进行解析。如果字符串不是有效的 DN,则会抛出InvalidNameException
。以下是使用构造函数解析 LDAP 名称并打印其组件的示例。
String name = "cn=Mango,ou=Fruits,o=Food";
try {
LdapName dn = new LdapName(name);
System.out.println(dn + " has " + dn.size() + " RDNs: ");
for (int i = 0; i < dn.size(); i++) {
System.out.println(dn.get(i));
}
} catch (InvalidNameException e) {
System.out.println("Cannot parse name: " + name);
}
使用输入"cn=Mango,ou=Fruits,o=Food"
运行此示例会产生以下结果:
cn=Mango,ou=Fruits,o=Food has 3 RDNs:
o=Food
ou=Fruits
cn=Mango
LdapName 类包含方法,用于将其组件作为 RDN 和字符串访问,修改 LdapName,比较两个 LdapName 是否相等,并获取名称的字符串表示形式。
访问 LDAP 名称的名称组件:
以下是您可以使用的方法来访问名称组件作为 RDN 和字符串:
getRdn(int posn)
get(int posn)
getRdns()
getAll()
getPrefix(int posn)
getSuffix(int posn)
clone()
要检索 LdapName 中特定位置的组件,您可以使用 getRdn()或 get()。前面的构造函数示例展示了其用法的示例。getRdns()
返回所有 RDN 的列表,getAll()
将 LdapName 的所有组件作为枚举返回。
最右边的 RDN 位于索引 0,最左边的 RDN 位于索引 n-1。例如,专有名称:"cn=Mango, ou=Fruits, o=Food"按以下顺序编号,范围从 0 到 2:{o=Food, ou=Fruits, cn=Mango}
您还可以将 LdapName 的后缀或前缀作为 LdapName 实例获取。这里有一个示例
,展示了如何获取 LDAP 名称的后缀和前缀。
LdapName dn = new LdapName("cn=Mango, ou=Fruits, o=Food");
Name suffix = dn.getSuffix(1); // 1 <= index < cn.size()
Name prefix = dn.getPrefix(1); // 0 <= index < 1
运行此程序时,它会生成以下输出:
cn=Mango ou=Fruits
o=Food
要复制 LdapName,您可以使用 clone()。
修改 LDAP 名称
以下是您可以使用的方法来修改 LDAP 名称:
add(Rdn rdn)
add(String comp)
add(int posn, String comp)
addAll(List suffixRdns)
addAll(Name suffix)
addAll(int posn, List suffixRdns)
addAll(int posn, Name suffix)
remove(int posn)
创建 LdapName 实例后,您可以向其添加和删除组件。这里有一个示例
,演示了如何将一个 LdapName 附加到现有 LdapName 中,在开头和结尾添加组件,以及删除第二个组件。
LdapName dn = new LdapName("ou=Fruits,o=Food");
LdapName dn2 = new LdapName("ou=Summer");
System.out.println(dn.addAll(dn2)); // ou=Summer,ou=Fruits,o=Food
System.out.println(dn.add(0, "o=Resources"));
// ou=Summer,ou=Fruits,o=Food,o=Resources
System.out.println(dn.add("cn=WaterMelon"));
// cn=WaterMelon,ou=Summer,ou=Fruits,o=Food,o=Resources
System.out.println(dn.remove(1)); // o=Food
System.out.println(dn);
// cn=WaterMelon,ou=Summer,ou=Fruits,o=Resources
比较 LDAP 名称
以下是您可以使用的方法来比较两个 LDAP 名称:
compareTo(Object name)
equals(Object name)
endsWith(List)
endWith(Name name)
startsWith(List rdns)
startsWith(Name name)
isEmpty()
您可以使用compareTo()
对 LdapName 实例列表进行排序。equals()
方法可让您确定两个 LdapNames 是否在语法上相等。如果两个 LdapNames 在相同顺序中具有相同(大小写匹配)的组件,则它们是相等的。
使用startsWith()
和endsWith()
,您可以了解一个 LdapName 是否以另一个 LdapName 开头或结尾;也就是说,一个 LdapName 是否是另一个 LdapName 的后缀或前缀。
便利方法isEmpty()
使您能够确定 LdapName 是否具有零个组件。您还可以使用表达式size() == 0
执行相同的检查。
这里有一个示例,CompareLdapNames
,使用了其中一些比较方法。
LdapName one = new LdapName("cn=Vincent Ryan, ou=People, o=JNDITutorial");
LdapName two = new LdapName("cn=Vincent Ryan");
LdapName three = new LdapName("o=JNDITutorial");
LdapName four = new LdapName("");
System.out.println(one.equals(two)); // false
System.out.println(one.startsWith(three)); // true
System.out.println(one.endsWith(two)); // true
System.out.println(one.startsWith(four)); // true
System.out.println(one.endsWith(four)); // true
System.out.println(one.endsWith(three)); // false
System.out.println(one.isEmpty()); // false
System.out.println(four.isEmpty()); // true
System.out.println(four.size() == 0); // true
获取字符串表示
下面的方法可以让您获取根据 RFC 2253 中指定的语法格式化的 LDAP 名称的字符串表示:
toString()
当您使用 LdapName 构造函数时,您提供 LDAP 名称的字符串表示,并获得一个 LdapName 实例。要执行相反操作,即获取 LdapName 实例的字符串表示,您可以使用toString()
。toString()
的结果可以再次传递给构造函数,以生成与原始 LdapName 实例相等的 LdapName 实例。这里有一个示例,LdapNametoString
:
LdapName dn = new LdapName(name);
String str = dn.toString();
System.out.println(str);
LdapName dn2 = new LdapName(str);
System.out.println(dn.equals(dn2)); // true
作为上下文方法的参数的 LdapName
Context 方法要求将复合名称或复合名称作为参数传递给其方法。因此,可以直接将 LdapName 传递给上下文方法,如示例中所示,LookupLdapName
:
// Create the initial context
Context ctx = new InitialContext(env);
// An LDAP name
LdapName dn = new LdapName("ou=People,o=JNDITutorial");
// Perform the lookup using the dn
Object obj = ctx.lookup(dn);
类似地,当上下文方法从 list()、listBindings() 或 search() 操作返回结果时,可以通过调用getNameInNamespace()
检索 DN。可以直接从 DN 构造 LdapName,如示例中所示,RetrievingLdapName
:
while (answer.hasMore()) {
SearchResult sr = (SearchResult) answer.next();
String name = sr.getNameInNamespace();
System.out.println(name);
LdapName dn = new LdapName(name);
// do something with the dn