Java9-秘籍-九-

86 阅读1小时+

Java9 秘籍(九)

原文:Java 9 Recipes

协议:CC BY-NC-SA 4.0

二十、JSON 和 XML 处理

JSON 是最新的,也是最广泛使用的媒体形式之一,用于在两台或多台机器之间发送通信。在扩展形式中,它代表 JavaScript 对象符号。在 Java 9 的规划阶段,计划在发行版中包含一个标准的 JSON 处理(JSON-P) API,但是,增强建议并没有包含在发行版中。相反,通过简单地包含 JSON-P 库(目前包含在 Java EE 中),使用 JSON 数据仍然非常容易。即将发布的 JSON-P 的部分计划是为 Java SE 提供直接支持。

XML APIs 对 Java 开发人员总是可用的,通常作为第三方库提供,可以添加到运行时类路径中。从 Java 7 开始,Java API for XML Processing (JAXP)、Java API for XML Binding (JAXB)和 Java API for XML Web Services(JAX-WS)都包含在核心运行时库中。您将遇到的最基本的 XML 处理任务只涉及几个用例:编写和读取 XML 文档,验证这些文档,以及使用 JAXB 来帮助编组/解组 Java 对象。

本章提供了执行 XML 和 JSON-P 任务的方法。JSON-P 方法需要包含 JSON-P API,这可以通过向 maven 应用添加依赖项来实现。在本章中,您将学习如何创建 JSON,以及将它写入磁盘并执行解析。

注意

本章示例的源代码可以在 org.java9recipes.chapter20 包中找到。

20-1.编写 XML 文件

问题

您希望创建一个 XML 文档来存储应用数据。

解决办法

若要编写 XML 文档,请使用 javax . XML . stream . XML streamwriter 类。下面的代码循环访问 Patient 对象的数组,并将数据写入。xml 文件。这个示例代码来自 org . Java 9 recipes . chapter 20 . recipe 20 _ 1。DocWriter 示例:

import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamWriter;
...
public void run(String outputFile) throws FileNotFoundException, XMLStreamException,
        IOException {
    List<Patient> patients = new ArrayList<>();
    Patient p1 = new Patient();
    Patient p2 = new Patient();
    Patient p3 = new Patient();
    p1.setId(BigInteger.valueOf(1));
    p1.setName("John Smith");
    p1.setDiagnosis("Common Cold");
    p2.setId(BigInteger.valueOf(2));
    p2.setName("Jane Doe");
    p2.setDiagnosis("Broken Ankle");
    p3.setId(BigInteger.valueOf(3));
    p3.setName("Jack Brown");
    p3.setDiagnosis("Food Allergy");
    patients.add(p1);
    patients.add(p2);
    patients.add(p3);
    XMLOutputFactory factory = XMLOutputFactory.newFactory();
    try (FileOutputStream fos = new FileOutputStream(outputFile)) {
        XMLStreamWriter writer = factory.createXMLStreamWriter(fos, "UTF-8");
        writer.writeStartDocument();
        writer.writeCharacters("\n");
        writer.writeStartElement("patients");
        writer.writeCharacters("\n");
        for (Patient p : patients) {
            writer.writeCharacters("\t");
            writer.writeStartElement("patient");
            writer.writeAttribute("id", String.valueOf(p.getId()));
            writer.writeCharacters("\n\t\t");
            writer.writeStartElement("name");
            writer.writeCharacters(p.getName());
            writer.writeEndElement();
            writer.writeCharacters("\n\t\t");
            writer.writeStartElement("diagnosis");
            writer.writeCharacters(p.getDiagnosis());
            writer.writeEndElement();
            writer.writeCharacters("\n\t");
            writer.writeEndElement();
            writer.writeCharacters("\n");
        }
        writer.writeEndElement();
        writer.writeEndDocument();
        writer.close();
    }

}

前面的代码写入了以下文件内容:

<?xml version="1.0" ?>
<patients>
    <patient id="1">
        <name>John Smith</name>
        <diagnosis>Common Cold</diagnosis>
    </patient>
    <patient id="2">
        <name>Jane Doe</name>
        <diagnosis>Broken ankle</diagnosis>
    </patient>
    <patient id="3">
        <name>Jack Brown</name>
<diagnosis>Food allergy</diagnosis>
</patient>
</patients>

它是如何工作的

Java 标准库提供了几种编写 XML 文档的方法。一个模型是 XML 的简单 API(SAX)。更新、更简单、更高效的模型是 XML 流 API(StAX)。这个菜谱使用 javax.xml.stream 包中定义的 StAX。编写 XML 文档需要五个步骤:

  1. 创建文件输出流。

  2. 创建 XML 输出工厂和 XML 输出流编写器。

  3. 在 XML 流编写器中包装文件流。

  4. 使用 XML 流编写器的写入方法创建文档并写入 XML 元素。

  5. 关闭输出流。

使用 java.io.FileOutputStream 类创建文件输出流。您可以使用 try-block 来打开和关闭该流。在第九章中了解更多关于新 try-block 语法的信息。

javax . XML . stream . xmloutputfactory 提供了一个创建输出工厂的静态方法。使用工厂创建 javax . XML . stream . XML streamwriter。

一旦有了编写器,就将文件流对象包装在 XML 编写器实例中。您将使用各种写方法来创建 XML 文档元素和属性。最后,当您完成写入文件时,只需关闭 writer。XMLStreamWriter 实例的一些更有用的方法如下:

  • writeStartDocument()

  • writestartelemont_)

  • writeendelemont_)

  • writeEndDocument()

  • writesttribute _)

创建文件和 XMLStreamWriter 后,应该总是通过调用 writeStartDocumentMethod()方法来开始文档。接下来,通过组合使用 writeStartElement()和 writeEndElement()方法来编写单个元素。当然,元素可以有嵌套元素。您有责任按正确的顺序调用这些方法来创建格式良好的文档。使用 writeAttribute()方法将属性名称和值放入当前元素中。您应该在调用 writeStartElement()方法后立即调用 writeAttribute()。最后,用 writeEndDocument()方法通知文档结束,并关闭 Writer 实例。

使用 XMLStreamWriter 的一个有趣之处是它不格式化文档输出。除非您专门使用 writeCharacters()方法来输出空格和换行符,否则输出将流至单个无格式行。当然,这不会使生成的 XML 文件无效,但是它确实给人们阅读带来了不便和困难。因此,您应该考虑使用 writeCharacters()方法根据需要输出空格和换行符,以创建人类可读的文档。如果不需要文档具有可读性,可以安全地忽略这种编写额外空白和换行符的方法。不管格式如何,XML 文档都是格式良好的,因为它符合正确的 XML 语法。

该示例代码的命令行使用模式如下:

java org.java9recipes.chapter20.recipe20_1.DocWriter <outputXmlFile>

调用此应用以如下方式创建名为 patients.xml 的文件:

java org.java9recipes.chapter20.recipe20_1.DocWriter patients.xml

20-2.读取 XML 文件

问题

您需要解析 XML 文档,检索已知的元素和属性。

解决方案 1

使用 javax . XML . stream . XML streamreader 接口读取文档。使用这个 API,您的代码将使用类似于 SQL 中的类似光标的接口提取 XML 元素,依次处理每个元素。org.java9recipes.DocReader 中的以下代码片段演示了如何读取在前面的配方中生成的 patients.xml 文件:

public void cursorReader(String xmlFile)
throws FileNotFoundException, IOException, XMLStreamException {
    XMLInputFactory factory = XMLInputFactory.newFactory();
    try (FileInputStream fis = new FileInputStream(xmlFile)) {
        XMLStreamReader reader = factory.createXMLStreamReader(fis);
        boolean inName = false;
        boolean inDiagnosis = false;
        String id = null;
        String name = null;
        String diagnosis = null;

        while (reader.hasNext()) {
            int event = reader.next();
            switch (event) {
                case XMLStreamConstants.START_ELEMENT:
                    String elementName = reader.getLocalName();
                    switch (elementName) {
                        case "patient":
                            id = reader.getAttributeValue(0);
                            break;
                        case "name":
                            inName = true;
                            break;
                        case "diagnosis":
                            inDiagnosis = true;
                            break;
                        default:
                            break;
                    }
                    break;
                case XMLStreamConstants.END_ELEMENT:
                    String elementname = reader.getLocalName();
                    if (elementname.equals("patient")) {
                        System.out.printf("Patient: %s\nName: %s\nDiagnosis: %s\n\n",id, name,
                        diagnosis);
                        id = name = diagnosis = null;
                        inName = inDiagnosis = false;
                    }
                    break;
                case XMLStreamConstants.CHARACTERS:
                    if (inName) {
                        name = reader.getText();
                        inName = false;
                    } else if (inDiagnosis) {
                        diagnosis = reader.getText();
                        inDiagnosis = false;
                    }
                    break;
                default:
                    break;
            }
        }
        reader.close();
    }
}

解决方案 2

使用 XMLEventReader 通过面向事件的接口读取和处理事件。这个 API 也被称为面向迭代器的 API。以下代码与解决方案 1 中的代码非常相似,只是它使用了面向事件的 API,而不是面向光标的 API。这个代码片段可以从同一个 org . Java 9 recipes . chapter 20 . recipe 20 _ 1 获得。解决方案 1 中使用的 DocReader 类:

public void eventReader(String xmlFile)
        throws FileNotFoundException, IOException, XMLStreamException {
    XMLInputFactory factory = XMLInputFactory.newFactory();
    XMLEventReader reader = null;
    try(FileInputStream fis = new FileInputStream(xmlFile)) {
        reader = factory.createXMLEventReader(fis);
        boolean inName = false;
        boolean inDiagnosis = false;
        String id = null;
        String name = null;
        String diagnosis = null;

        while(reader.hasNext()) {
            XMLEvent event = reader.nextEvent();
            String elementName = null;
            switch(event.getEventType()) {
                case XMLEvent.START_ELEMENT:
                    StartElement startElement = event.asStartElement();
                    elementName = startElement.getName().getLocalPart();
                    switch(elementName) {
                        case "patient":
                            id = startElement.getAttributeByName(QName.valueOf("id")).getValue();
                            break;
                        case "name":
                            inName = true;
                            break;
                        case "diagnosis":
                            inDiagnosis = true;
                            break;
                        default:
                            break;
                    }
                    break;
                case XMLEvent.END_ELEMENT:
                    EndElement endElement = event.asEndElement();
                    elementName = endElement.getName().getLocalPart();
                    if (elementName.equals("patient")) {
                        System.out.printf("Patient: %s\nName: %s\nDiagnosis: %s\n\n",id, name, diagnosis);
                        id = name = diagnosis = null;
                        inName = inDiagnosis = false;
                    }
                    break;
                case XMLEvent.CHARACTERS:
                    String value = event.asCharacters().getData();
                    if (inName) {
                        name = value;
                        inName = false;
                    } else if (inDiagnosis) {
                        diagnosis = value;
                        inDiagnosis = false;
                    }
                    break;
            }
        }
    }
    if(reader != null) {
        reader.close();
    }
}

它是如何工作的

Java 提供了几种读取 XML 文档的方法。一种方法是使用 StAX,一种流模型。它比旧的 SAX API 更好,因为它允许您读写 XML 文档。尽管 StAX 不如 DOM API 强大,但它是一个优秀而高效的 API,对内存资源的消耗较少。

StAX 提供了两种读取 XML 文档的方法:游标 API 和迭代器 API。面向游标的 API 利用游标从头到尾遍历 XML 文档,一次指向一个元素,并且总是向前移动。迭代器 API 将 XML 文档流表示为一组离散的事件对象,按照它们在源 XML 中的读取顺序提供。此时,面向事件的迭代器 API 优于游标 API,因为它为 XMLEvent 对象提供了以下好处:

  • XMLEvent 对象是不可变的,即使 StAX 解析器已经转移到后续事件,这些对象也可以保持不变。您可以将这些 XMLEvent 对象传递给其他进程,或者将它们存储在列表、数组和映射中。

  • 您可以子类化 XMLEvent,根据需要创建您自己的专用事件。

  • 您可以通过添加或移除事件来修改传入的事件流,这比游标 API 更灵活。

要使用 StAX 读取文档,请在文件输入流上创建一个 XML 事件读取器。使用 hasNext()方法检查事件是否仍然可用,并使用 nextEvent()方法读取每个事件。nextEvent()方法将返回特定类型的 XMLEvent,它对应于 XML 文件中的开始和停止元素、属性和值数据。当您使用完这些对象时,记得关闭您的阅读器和文件流。

您可以像这样调用示例应用,使用 patients.xml 文件作为您的参数:

java org.java9recipes.chapter20.recipe20_2.DocReader <xmlFile>

20-3.转换 XML

问题

您希望将 XML 文档转换为另一种格式,例如 HTML。

解决办法

使用 javax.xml.transform 包将 xml 文档转换为另一种文档格式。

下面的代码演示如何读取源文档,应用可扩展样式表语言(XSL)转换文件,并生成转换后的新文档。使用 org . Java 9 recipes . chapter 20 . recipe 20 _ 3 中的示例代码。TransformXml 类读取 patients.xml 文件并创建 patients.xml 文件。下面的代码片段展示了这个类的重要部分:

import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.Transformer;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
...
public void run(String xmlFile, String xslFile, String outputFile)
        throws FileNotFoundException, TransformerConfigurationException, TransformerException {
    InputStream xslInputStream = new FileInputStream(xslFile);
    Source xslSource = new StreamSource(xslInputStream);
    TransformerFactory factory = TransformerFactory.newInstance();
    Transformer transformer = factory.newTransformer(xslSource);
    InputStream xmlInputStream = new FileInputStream(xmlFile);
    StreamSource in = new StreamSource(xmlInputStream);
    StreamResult out = new StreamResult(outputFile);
    transformer.transform(in, out);    
    ...
}

它是如何工作的

javax.xml.transform 包包含将 xml 文档转换成任何其他文档类型所需的所有类。最常见的用例是将面向数据的 XML 文档转换成用户可读的 HTML 文档。

从一种文档类型转换到另一种文档类型需要三个文件:

  • XML 源文档

  • 将 XML 元素映射到新文档元素的 XSL 转换文档

  • 目标输出文件

XML 源文档当然是您的源数据文件。它通常包含易于编程解析的面向数据的内容。然而,人们不容易阅读 XML 文件,尤其是复杂的、数据丰富的文件。相反,人们更愿意阅读正确呈现的 HTML 文档。

XSL 转换文档指定如何将 XML 文档转换成不同的格式。XSL 文件通常包含一个 HTML 模板,该模板指定了动态字段,这些字段将保存源 XML 文件的提取内容。

在这个例子的源代码中,您会发现两个源文档:

  • 第二十章/recipe20_3/patients.xml

  • 第二十章/收件人 20_3/patients.xsl

patients.xml 文件很短,包含以下数据:

<?xml version="1.0" encoding="UTF-8"?>
<patients>
    <patient id="1">
        <name>John Smith</name>
        <diagnosis>Common Cold</diagnosis>
    </patient>
    <patient id="2">
        <name>Jane Doe</name>
        <diagnosis>Broken ankle</diagnosis>
    </patient>
    <patient id="3">
        <name>Jack Brown</name>
        <diagnosis>Food allergy</diagnosis>
    </patient>
</patients>

patients.xml 文件定义了名为 patients 的根元素。它有三个嵌套的病人元素。患者元素包含三段数据:

  • 患者标识符,作为患者元素的 id 属性提供

  • 患者姓名,作为姓名子元素提供

  • 患者诊断,作为诊断子元素提供

转换 xsl 文档(patients.xsl)也很小,它只是使用 XSL 将患者数据映射为更易于用户阅读的 HTML 格式:

<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0">
<xsl:output method="html"/>
<xsl:template match="/">
<html>
<head>
    <title>Patients</title>
</head>
<body>
    <table border="1">
        <tr>
            <th>Id</th>
            <th>Name</th>
            <th>Diagnosis</th>
        </tr>
        <xsl:for-each select="patients/patient">
        <tr>
            <td>
        <xsl:value-of select="@id"/>
            </td>
            <td>
        <xsl:value-of select="name"/>
            </td>
            <td>
        <xsl:value-of select="diagnosis"/>
            </td>
            </tr>
        </xsl:for-each>
    </table>
</body>
</html>
        </xsl:template>
        </xsl:stylesheet>

使用这个样式表,示例代码将 XML 转换成包含所有患者及其数据的 HTML 表。在浏览器中呈现时,HTML 表格应该如图 20-1 所示。

A323910_3_En_20_Fig1_HTML.jpg

图 20-1。HTML 表格的常见呈现

使用这个 XSL 文件将 XML 转换成 HTML 文件的过程很简单,但是每一步都可以通过额外的错误检查和处理来增强。对于此示例,请参考解决方案一节中前面的代码。

最基本的转换步骤如下:

  1. 将 XSL 文档作为源对象读入 Java 应用。

  2. 创建一个 Transformer 实例,并提供您的 XSL 源实例供它在操作过程中使用。

  3. 创建表示源 XML 内容的 SourceStream。

  4. 为输出文档创建一个 StreamResult 实例,在本例中是一个 HTML 文件。

  5. 使用 Transformer 对象的 transform()方法来执行转换。

  6. 根据需要关闭所有相关的流和文件实例。

如果您选择执行示例代码,您应该以下列方式调用它,使用 patients.xml、patients.xsl 和 patients.xml 作为参数:

java org.java9recipes.chapter20.recipe20_3.TransformXml <xmlFile><xslFile><outputFile>

20-4.验证 XML

问题

您希望确认您的 XML 是有效的——它符合已知的文档定义或模式。

解决办法

使用 javax.xml.validation 包验证您的 XML 是否符合特定的模式。以下代码片段来自 org . Java 9 recipes . chapter 20 . recipe 20 _ 4。ValidateXml 演示了如何根据 Xml 模式文件进行验证:

import java.io.File;
import java.io.IOException;
import javax.xml.XMLConstants;
import javax.xml.transform.Source;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import org.xml.sax.SAXException;
...
public void run(String xmlFile, String validationFile) {
    boolean valid = true;
    SchemaFactory sFactory =
            SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
    try {
        Schema schema = sFactory.newSchema(new File(validationFile));
        Validator validator = schema.newValidator();
        Source source = new StreamSource(new File(xmlFile));
        validator.validate(source);
    } catch (SAXException | IOException | IllegalArgumentException ex) {
        valid = false;
    }
    System.out.printf("XML file is %s.\n", valid ? "valid" : "invalid");
}
...

它是如何工作的

使用 XML 时,验证它以确保语法正确,并确保 XML 文档是指定 XML 模式的实例是很重要的。验证过程包括比较模式和 XML 文档,找出任何差异。javax.xml.validation 包提供了根据各种模式可靠地验证 xml 文件所需的所有类。将用于 XML 验证的最常见模式被定义为 XMLConstants 类中的常量 URIs:

  • XMLConstants。W3C _ XML _ 架构 _NS_URI

  • XML 常量。松弛 _NS_URI

首先为特定类型的模式定义创建一个 SchemaFactory。SchemaFactory 知道如何解析特定的模式类型,并为验证做准备。使用 SchemaFactory 实例创建架构对象。模式对象是模式定义语法的内存表示。您可以使用 Schema 实例来检索理解这种语法的验证器实例。最后,使用 validate()方法检查 XML。如果在验证过程中出现任何问题,方法调用将生成几个异常。否则,validate()方法会安静地返回,您可以继续使用 XML 文件。

注意

XML 模式在 2001 年第一次获得万维网联盟(W3C)的“推荐”地位。竞争模式从此变得可用。一个竞争模式是 XML 下一代正则语言(RELAX NG)模式。RELAX NG 可能是一种更简单的模式,它的规范也定义了一种非 XML 的紧凑语法。这个食谱的例子使用了 XML 模式。

使用以下命令行语法运行示例代码,最好使用示例。xml 文件和验证文件分别作为 resources/patients.xml 和 patients.xsl 提供:

java org.java9recipes.chapter20.recipe20_4.ValidateXml <xmlFile><validationFile>

20-5.为 XML 模式创建 Java 绑定

问题

您希望生成一组 Java 类(Java 绑定),它们代表 XML 模式中的对象。

解决办法

JDK 提供了一个工具,可以将模式文档转换成有代表性的 Java 类文件。使用 <jdk_home>/bin/xjc 命令行工具为 XML 模式生成 Java 绑定。要从配方 20-3 中为 patients.xsd 文件创建 Java 类,您可以在控制台中发出以下命令:</jdk_home>

xjc –p org.java9recipes.chapter20.recipe20_5 patients.xsd

该命令将处理 patients.xsd 文件,并创建处理用该模式验证的 XML 文件所需的所有类。对于此示例,patients.xsd 文件如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="patients">
<xs:complexType>
<xs:sequence>
<xs:element maxOccurs="unbounded" name="patient" type="Patient"/>
</xs:sequence>
</xs:complexType>
</xs:element>
<xs:complexType name="Patient">
<xs:sequence>
<xs:element name="name" type="xs:string"/>
<xs:element name="diagnosis" type="xs:string"/>
</xs:sequence>
<xs:attribute name="id" type="xs:integer" use="required"/>
</xs:complexType>
</xs:schema>

在前面的 xsd 文件上执行的 xjc 命令在 org . Java 9 recipes . chapter 20 . recipe 20 _ 5 包中创建以下文件:

  • ObjectFactory.java

  • Patients.java

  • Patient.java

它是如何工作的

JDK 包括了 <jdk_home>/bin/xjc 实用程序。xjc 实用程序是一个命令行应用,它从模式文件创建 Java 绑定。源模式文件可以有多种类型,包括 XML 模式、RELAX NG 等。</jdk_home>

xjc 命令有几个选项来执行它的工作。一些最常见的选项指定了源模式文件、生成的 Java 绑定文件的包以及将接收 Java 绑定文件的输出目录。

您可以通过使用工具的–help 选项获得所有命令行选项的详细描述:

xjc –help

Java 绑定包含带注释的字段,这些字段对应于 XML 模式文件中定义的字段。这些注释标记了模式文件的根元素和所有其他子元素。这在 XML 处理的下一步中非常有用,包括解组或编组这些绑定。

20-6.将 XML 解组到 Java 对象

问题

您希望解组一个 XML 文件,并创建其对应的 Java 对象树。

解决办法

解组是将数据格式(在本例中为 XML)转换成对象的内存表示形式以便用于执行任务的过程。JAXB 提供了一个解组服务,它解析一个 XML 文件,并根据您在 Recipe 20-4 中创建的绑定生成 Java 对象。以下代码可以从 org . Java 9 recipes . chapter 20 . recipe 20-6 包中读取 patients.xml 文件,以创建 patients 根对象及其 Patient 对象列表:

public void run(String xmlFile, String context)
        throws JAXBException, FileNotFoundException {
    JAXBContext jc = JAXBContext.newInstance(context);
    Unmarshaller u = jc.createUnmarshaller();
    FileInputStream fis = new FileInputStream(xmlFile);
    Patients patients = (Patients)u.unmarshal(fis);
    for (Patient p: patients.getPatient()) {
        System.out.printf("ID: %s\n", p.getId());
        System.out.printf("NAME: %s\n", p.getName());
        System.out.printf("DIAGNOSIS: %s\n\n", p.getDiagnosis());
    }
}

如果您在 chapter 20/recipe 20 _ 6/patients . XML 文件上运行示例代码并使用 org.java9recipes.chapter20 上下文,应用将在遍历患者对象列表时向控制台打印以下内容:

ID: 1
NAME: John Smith
DIAGNOSIS: Common Cold

ID: 2
NAME: Jane Doe
DIAGNOSIS: Broken ankle

ID: 3
NAME: Jack Brown
DIAGNOSIS: Food allergy
注意

前面的输出直接来自 Java Patient 类的实例,该类是由 XML 表示创建的。代码不直接打印 XML 文件的内容。相反,在 XML 被整理成适当的 Java 绑定实例之后,它打印 Java 绑定的内容。

它是如何工作的

将 XML 文件解组为 Java 对象表示至少有两个标准:

  • 一个格式良好且有效的 XML 文件

  • 一组相应的 Java 绑定

Java 绑定不必通过 xjc 命令自动生成。一旦您获得了一些 Java 绑定和注释特性的经验,您可能更喜欢通过手工制作 Java 绑定来创建和控制 Java 绑定的所有方面。无论您的偏好是什么,Java 的解组服务都利用绑定及其注释将 XML 对象映射到目标 Java 对象,并将 XML 元素映射到目标对象字段。

使用以下语法执行该配方的示例应用,用 patients.xml 和 org . Java 9 recipes . chapter 20 . recipe 20 _ 6 替换相应的参数:

java org.java9recipes.chapter20.recipe20_6.UnmarshalPatients <xmlfile><context>

20-7.用 JAXB 构建 XML 文档

问题

您需要将对象的数据写入 XML 表示。

解决办法

假设您已经按照 Recipe 20-4 中的描述为 XML 模式创建了 Java 绑定文件,那么您可以使用 JAXBContext 实例来创建一个编组器对象。然后使用 Marshaller 对象将 Java 对象树序列化为 XML 文档。下面的代码演示了这一点:

public void run(String xmlFile, String context)
        throws JAXBException, FileNotFoundException {
    Patients patients = new Patients();
    List<Patient> patientList = patients.getPatient();
    Patient p = new Patient();
    p.setId(BigInteger.valueOf(1));
    p.setName("John Doe");
    p.setDiagnosis("Schizophrenia");
    patientList.add(p);

    JAXBContext jc = JAXBContext.newInstance(context);
    Marshaller m = jc.createMarshaller();
    m.marshal(patients, new FileOutputStream(xmlFile));
}

前面的代码生成了一个无格式但格式良好的有效 XML 文档。为了提高可读性,XML 文档的格式如下:

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
    <patients>
    <patient id="1">
        <name>John Doe</name>
        <diagnosis>Schizophrenia</diagnosis>
    </patient>
    </patients>
注意

前面代码中的 getPatient()方法返回患者对象的列表,而不是单个患者。在这个例子中,这是从 XSD 模式生成的 JAXB 代码的一个奇怪的命名。

它是如何工作的

编组器对象理解 JAXB 注释。在处理类时,它使用 JAXB 注释来提供用 XML 创建对象树所需的上下文。

您可以从 org . Java 9 recipes . chapter 20 . recipe 20 _ 7 运行前面的代码。使用以下命令行封送患者应用:

java org.java9recipes.chapter20.recipe20_7.MarshalPatients <xmlfile><context>

上下文参数指的是您将封送的 Java 类的包。在前面的示例中,因为代码封送了一个 Patients 对象树,所以正确的上下文是 Patients 类的包名。在本例中,上下文是 org.java9recipes.chapter20。

20-8.解析 XML 目录

问题

出于安全目的或其他需要,您需要解析 XML 目录,以便将远程外部引用指向本地目录。

解决办法

利用 Java 9 中的标准 XML 目录 API。在本例中,使用 API 读取并解析本地目录。

public static void main(String[] args) {
        // Create a CatalogFeatures object
        CatalogFeatures defaults = CatalogFeatures.defaults();

        // Resolve using properties
        // System.setProperty("javax.xml.catalog.files", "catalog.xml");

        // Resolve by passing
        Catalog catalog = CatalogManager.catalog(defaults, "catalog.xml", "catalog-alt.xml");

        // Use CatalogFeatures to specify catalog files and/or additional features
        // CatalogFeatures catalogFeatures = CatalogFeatures.builder()
        //         .with(Feature.FILES, "catalog.xml")
        //         .with(Feature.RESOLVE, "ignore")
        //         .build();

        // Stream and filter to find the catalog matching your specification
        Optional<Catalog> cat = catalog.catalogs()
                .filter((c)->c.matchURI("calstblx.dtd") != null)
                .findFirst();

        // Do something with catalog
    }

它是如何工作的

JDK 历来将 XML 解析器作为其核心的一部分。然而,这个解析器是私有的,仅由 JDK 使用。随着时间的推移,实现公共 XML 解析器的需求变得越来越明显,因此私有解析器被改进为一个新的公共 API。API 允许管理 XML 目录和解析器的创建,它实现了 OASIS XML 目录 1.1 规范,并且实现了现有的 JAXP 接口。

有许多关键的接口和类组成了 Catalog API。目录接口可用于表示实体目录。CatalogManager 用于通过传递 CatalogFeatures 配置对象以及包含 XML 目录文件路径的变量参数来解析目录。它还可以用于生成 CatalogResolvers。还可以通过指定“javax.xml.catalog.files”属性来传递一个或多个目录文件的路径,如示例所示。

System.setProperty("javax.xml.catalog.files", "catalog.xml");

CatalogFeatures 对象包含许多属性和功能,调用 CatalogFeatures.defaults()方法可以获得默认实现。要为 CatalogFeatures 对象指定不同的值,可以利用构建器模式来指示每个不同特性的值。这些特征可以在表 20-1 中看到。

表 20-1。目录功能
|

特征

|

财产

|

描述

| | --- | --- | --- | | 文件 | javax.xml.catalog.files | 分号分隔的目录文件列表。 | | 更喜欢 | javax.xml.catalog.prefer | 指示公共标识符和系统标识符之间的首选项。 | | 推迟 | javax.xml.catalog.defer | 指示只有在需要时才会读取委托目录。 | | 分解 | javax.xml.catalog.resolve | 确定未找到匹配目录时要采取的操作。 |

有关 CatalogFeatures 的更多信息,请参考 JavaDoc(download . Java . net/Java/JDK 9/docs/API/javax/XML/catalog/catalog features . html)。

可以调用 Catalog.catalogs()方法,使用当前目录中的 nextCatalog 条目生成一个备选目录流。这种解析可以用来匹配 XML 目录中的条目。

XML Catalog API 是对 JDK 的一个很好的补充,使得在需要时利用本地目录而不是远程目录变得容易。Java 很早就有了目录解析器,但是它不能在 JDK 内部之外使用。新的 API 是旧的私有 API 的更新形式,它完全符合 OASIS XML Catalogs 1.1 规范。

20-9.使用 JSON

问题

您对在 Java SE 9 应用中使用 JSON 感兴趣。

解决办法

将 JSON-P API 作为依赖项添加到 Java SE 9 应用中。有几个选项可以添加依赖项。可以下载 JAR 并将其放入类路径,或者如果使用 Maven 之类的构建工具,只需添加项目存储库的坐标。下面几行摘自 POM 文件(Maven 的项目对象模块),说明如何添加依赖项。

<dependencies>
        <dependency>
            <groupId>javax.json</groupId>
            <artifactId>javax.json-api</artifactId>
            <version>1.0</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.json</artifactId>
            <version>1.0.4</version>
        </dependency>
...
</dependencies>

它是如何工作的

随着 Java EE 7 的发布,JavaScript Object Notation(JSON-P)API 被添加到 Java 企业平台中。JSON-P,也称为“JSON 处理”,已经成为使用 Java 构建 JSON 对象的标准方式。因为 Java 9 没有捆绑 JSON 构建和解析 API,所以必须引入必要的依赖项来利用标准化的 JSON-P API。JSON-P 是 Java EE 的一部分,但是在这一点上 Java SE 没有提供支持。因此,通过将下载的 JAR 文件添加到类路径中,或者将 Maven 坐标添加到项目 POM 文件中,很容易包含 API。在解决方案中,我介绍了如何利用 Maven 坐标。但是,请确保相应地更新版本。

20-10.构建 JSON 对象

问题

您希望在 Java 应用中构建一个 JSON 对象。

解决办法

利用 JSON-P API 构建一个 JSON 对象。在下面的代码中,构建了一个属于一本书的 JSON 对象。

public JsonObject buildBookObject() {
    JsonBuilderFactory factory = Json.createBuilderFactory(null);
    JsonObject obj = factory.createObjectBuilder()
            .add("title", "Java 9 Recipes")
            .add("author", "Josh Juneau")
            .add("projectCoordinator", "Jill Balzano")
            .add("editor", "Jonathan Gennick")
            .build();

    return obj;
}

它是如何工作的

JSON-P API 包括一个 helper 类,可以使用 builder 模式创建 JSON 对象。使用 JsonObjectBuilder,可以通过一系列方法调用构建 JSON 对象,每个方法调用都建立在另一个方法调用的基础上——因此,就有了构建器模式。一旦构建了 JSON 对象,就可以调用 JsonObjectBuilder.build()方法来返回 JSON object。

在这个菜谱的例子中,您构建了一个 JSON 对象,它提供了关于一本书的详细信息。JsonObjectBuilder.beginObject()方法用于表示正在创建一个新对象。add 方法用于添加更多的名称/值属性,非常类似于映射。因此,下面一行添加了一个名为 title 的属性,其值为“Java 9 Recipes”:

.add("title", "Java 9 Recipes")

对象可以相互嵌入,在一个 JsonObject 中创建子部分的层次结构。例如,在第一次调用 add()之后,通过调用 jsonbuilderfactory . createobjectbuilder()作为 add()操作的值,并传递嵌入对象的名称,可以将另一个对象嵌入到初始 JsonObject 中。嵌入对象也可以包含属性;因此,要向嵌入对象添加属性,请在嵌入对象中调用 add()方法。JsonObjects 可以根据需要包含任意多的嵌入对象。如果我们要修改示例中的源代码,按名字和姓氏分解作者,下面几行代码演示了嵌入对象定义的开始和结束:

.add("author", factory.createObjectBuilder()
    .add("first", "Josh")
    .add("last", "Juneau"))
.add("projectCoordinator", "Jill Balzano")

JsonObject 也可能有一个相关子对象的数组。若要添加子对象的数组,请调用 jsonbuilderfactory . createarraybuilder()方法,并将数组的名称作为参数传递。数组可以由对象组成,甚至可以由对象、数组等的层次结构组成。

一旦创建了 JsonObject,就可以将其传递给客户机。WebSockets 可以很好地将 JsonObjects 传递回客户机,但是有许多不同的技术可以用来与 JSON 通信。

20-11.将 JSON 对象写入文件

问题

您已经生成或解析了一个 JSON 对象,并且希望将它以文件格式存储在磁盘上。

解决办法

利用 JSON-P API 构建一个 JSON 对象,然后将它存储到文件系统中。JsonWriter 类可以在磁盘上创建一个文件,然后将 JSON 写入该文件。在下面的例子中,在配方 20-10 中生成的 JsonObject 使用这种技术写入磁盘。

public static void writeJson() {
    JsonObject jsonObject = buildBookObject();
    try (javax.json.JsonWriter jsonWriter = Json.createWriter(new FileWriter("Book.json"))) {
        jsonWriter.writeObject(jsonObject);
    } catch (IOException ex) {
        System.out.println(ex);
    }
}

它是如何工作的

JsonWriter 类可用于将 JsonObject 写入 Java writer 对象。通过将 Writer 对象作为参数传递给 Json.createWriter()方法来实例化 JsonWriter。创建 JsonWriter 后,可以调用 JsonWriter.writeObject()方法,传递要编写的 JsonObject。一旦编写了 JsonObject,就可以通过调用它的 close()方法来关闭 JsonWriter。这些是将 JSON 对象写入 Java Writer 类类型所必需的唯一步骤。

20-12.解析 JSON 对象

问题

您创建的应用需要能够读取 JSON 对象并相应地解析它。

解决办法

利用 JsonReader 对象读取 JSON 对象,然后利用 JsonParser 对象对 JSON 数据执行操作。下面的例子演示了如何从磁盘读取一个文件,然后解析它以显示一些内容。

public void parseObject() {
    Reader fileReader = new InputStreamReader(getClass().getResourceAsStream("Book.json"));
    JsonParser parser = Json.createParser(fileReader);
    while (parser.hasNext()) {
        Event ev = parser.next();
        System.out.println(ev);
        if (ev.equals(Event.VALUE_STRING)) {
            System.out.println(parser.getString());
        }
    }
}

在这个例子中,名为 Book.json 的 Json 文件被读取和解析。当解析过程中遇到 VALUE_STRING 事件时,将打印该字符串。还会打印每个遇到的事件。以下输出是结果:

START_OBJECT
KEY_NAME
VALUE_STRING
Java 9 Recipes
KEY_NAME
VALUE_STRING
Josh Juneau
KEY_NAME
VALUE_STRING
Jill Balzano
KEY_NAME
VALUE_STRING
Jonathan Gennick
END_OBJECT

它是如何工作的

一旦 JSON 对象被持久化到磁盘上,以后就需要将它读回以供使用。JsonReader 对象负责这项任务。要创建 JsonReader 对象,请调用 Json.createReader()方法,传递 InputStream 或 Reader 对象。一旦创建了 JsonReader 对象,就可以通过调用它的 readObject 方法来生成 JsonObject。

为了执行某些任务,必须对 JSON 对象进行解析,以便只找到对当前任务有用的内容。利用 JSON 解析器可以使这样的工作变得更容易,因为解析器能够将对象分解成多个部分,以便可以根据需要检查每个不同的部分,从而产生想要的结果。

javax.json.Json 类包含一个静态工厂方法 createParser(),该方法接受一组输入并返回一个可迭代的 JsonParser。表 20-2 列出了通过 createParser()方法接受的不同可能的输入类型。

表 20-2。createParser 方法输入类型
|

输入类型

|

方法调用

| | --- | --- | | 输入流 | createpresser(input stream in) | | JsonArray | createpresser(jsonaarray arr) | | JsonObject | createParser(JsonObject obj) | | 读者 | createParser(阅读器阅读器) |

一旦创建了 JsonParser,就可以将它变成事件对象的迭代器。每个事件都与 JSON 对象中的不同结构相关联。例如,当创建 JSON 对象时,会发生 START_OBJECT 事件,添加名称/值对会触发 KEY_NAME 和 VALUE_STRING 事件。可以利用这些事件从 JSON 对象中获取所需的信息。在本例中,事件名称只是打印到服务器日志中。然而,在现实生活的应用中,条件最有可能测试每个迭代,以找到一个特定的事件,然后执行一些处理。表 20-3 列出了不同的 JSON 事件,以及每个事件发生的时间描述。

表 20-3。JSON 对象事件
|

事件

|

出现

| | --- | --- | | 开始 _ 对象 | 对象的开始。 | | 结束对象 | 对象的结尾。 | | 开始 _ 数组 | 数组的开始。 | | END _ 数组 | 数组结尾。 | | KEY_NAME | 密钥的名称。 | | 值 _ 字符串 | 字符串格式的名称/值对的值。 | | 值 _ 数字 | 数值格式的名称/值对的值。 | | 值 _ 真 | 布尔格式的名称/值对的值。 | | VALUE_FALSE | 布尔格式的名称/值对的值。 | | 值为空 | 名称/值对的值为 NULL。 |

摘要

XML 通常用于在不同的应用之间传输数据,或者将某种类型的数据存储到文件中。因此,理解在应用开发平台中使用 XML 的基础非常重要。本章概述了如何使用 Java 执行一些处理 XML 的关键任务。本章从编写和阅读 XML 的基础开始。然后演示了如何将 XML 转换成不同的格式,以及如何根据 XML 模式进行验证。

这一章还提到了使用 JSON。尽管 Java SE 9 没有附带 JSON API,但是可以很容易地利用 JSON-P API 来生成、编写和解析 JSON 数据。本章演示了如何执行这些任务。

二十一、网络

今天,编写一个不以某种方式在互联网上通信的应用已经很少见了。从向另一台机器发送数据,到从远程网页上抓取信息,网络在当今的计算世界中扮演着不可或缺的角色。Java 使用新的 I/O (NIO)和 Java 平台(NIO . 2)API 的更多新的 I/O 特性,使得通过网络进行通信变得容易。Java SE 7 包含了一些新特性,使得多播变得更加容易。随着这些新特性的加入,Java 平台包含了大量的编程接口来帮助完成网络任务。Java 9 引入了新的 HTTP/2 客户机,它提供了一个简单明了的 API,并对旧的 HTTP/1.1 客户机进行了性能改进。

本章并不试图涵盖 Java 语言中的每一个网络特性,因为这个主题相当大。然而,它确实提供了一些对广大开发人员最有用的方法。您了解了一些标准的网络概念,比如套接字,以及 Java 语言最新版本中引入的一些新概念。如果你觉得这一章很有趣,并且想学习更多关于 Java 网络的知识,你可以在网上找到很多资源。要了解更多信息,最好的地方可能是位于download . Oracle . com/javase/tutorial/networking/index . html的 Oracle 文档。

21-1.监听服务器上的连接

问题

您希望创建一个服务器应用来侦听来自远程客户端的连接。

解决办法

设置一个服务器端应用,该应用利用 java.net.ServerSocket 来监听指定端口上的请求。下面的 Java 类代表了一个将被部署到服务器上的类,它监听端口 1234 上的传入请求。当收到请求时,传入的消息被打印到命令行,响应被发送回客户端。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;

public class SocketServer {

public static void main(String a[]) {
        final int httpd = 1234;
        ServerSocket ssock = null;
        try {
            ssock = new ServerSocket(httpd);
            System.out.println("have opened port 1234 locally");

            Socket sock = ssock.accept();
            System.out.println("client has made socket connection");

    communicateWithClient(sock);

System.out.println("closing socket");
} catch (Exception e) {
System.out.println(e);
} finally {
try{
ssock.close();
} catch (IOException ex) {
System.out.println(ex);
}
}
}
    public static void communicateWithClient(Socket socket) {
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(
                      socket.getOutputStream(), true);

            String s = null;
            out.println("Server received communication!");
            while ((s = in.readLine()) != null) {
                System.out.println("received from client: " + s);
                out.flush();
                break;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                in.close();
                out.close();
            } catch (IOException ex) {
                ex.printStackTrace();
            }
        }
    }
}

该配方与配方 21-2 协同工作,由此该示例启动服务器,并且该程序的执行将简单地打印“已在本地打开端口 1234”,但是与配方 21-2 中构建的客户端一起执行该程序将导致 SocketServer 的以下输出:

have opened port 1234 locally
client has made socket connection
received from client: Here is a test.
closing socket
注意

要运行这两个配方,使它们相互配合,首先启动 SocketServer 程序,这样客户机就可以使用服务器程序中打开的端口创建一个套接字。SocketServer 启动后,启动 SocketClient 程序来查看两者的协同工作。

警告

这个 SocketServer 程序在您的机器上打开一个端口(1234)。请确保您的计算机上运行了防火墙设置;否则,您将向所有人开放 1234 端口。这可能会导致您的机器受到攻击。开放的端口为攻击者创造了入侵机器的漏洞,就像让你家的门开着一样。请注意,该方法中的示例具有最小的攻击配置文件,因为服务器仅运行一次,并且在会话关闭前仅打印来自客户端的一条消息。

它是如何工作的

服务器应用可用于通过来自一个或多个客户端应用的直接通信在服务器上执行工作。客户端应用通常与服务器应用通信,向服务器发送消息或数据进行处理,然后断开连接。服务器应用通常会侦听客户端应用,然后在连接被接收和接受后,针对客户端请求执行一些处理。为了让客户端应用连接到服务器应用,服务器应用必须侦听连接,然后以某种方式处理连接数据。您不能简单地针对任何给定的主机和端口号组合运行客户端,因为这样做可能会导致拒绝连接错误。服务器端应用必须做三件事:打开一个端口,接受并建立客户端连接,然后以某种方式与客户端连接进行通信。在这个菜谱的解决方案中,SocketServer 类完成了这三项工作。

从 main()方法开始,该类首先在端口 1234 上打开一个新的套接字。这是通过创建一个新的 ServerSocket 实例并向其传递一个端口号来实现的。端口号不得与服务器上当前使用的任何其他端口冲突。值得注意的是,低于 1024 的端口通常保留给操作系统使用,因此选择一个高于该范围的端口号。如果试图打开一个正在使用的端口,将无法成功创建 ServerSocket,程序将会失败。接下来,调用 ServerSocket 对象的 accept()方法,返回一个新的 Socket 对象。调用 accept()方法将不会做任何事情,直到客户端尝试在已经设置的端口上连接到服务器程序。accept()方法将一直等待,直到请求连接,然后它将返回绑定到在 ServerSocket 上设置的端口的新 Socket 对象。这个套接字还包含尝试连接的客户端的远程端口和主机名,因此它包含两个端点的信息,并唯一地标识传输控制协议(TCP)连接。

此时,服务器程序可以与客户端程序进行通信,它使用 PrintWriter 和 BufferedReader 对象进行通信。在这个方法的解决方案中,communicateWithClient()方法包含从客户端程序接受消息、将消息发送回客户端,然后将控制权返回给关闭服务器套接字的 main()方法所需的所有代码。通过使用套接字的输入流生成新的 InputStreamReader 实例,可以创建新的 BufferedReader 对象。类似地,可以使用套接字的输出流创建新的 PrintWriter 对象。请注意,这段代码必须包装在一个 try-catch 块中,以防这些对象没有成功创建。

in = new BufferedReader(
                    new InputStreamReader(socket.getInputStream()));
out = new PrintWriter(     
socket.getOutputStream(), true); 

一旦成功创建了这些对象,服务器就可以与客户机通信了。它使用一个循环来完成这项工作,从 BufferedReader 对象(客户端输入流)中读取数据,并使用 PrintWriter 对象将消息发送回客户端。在这个方法的解决方案中,服务器通过发出 break 关闭连接,这导致循环结束。控制然后返回到 main()方法。

out.println("Server received communication!");
while ((s = in.readLine()) != null) {
    System.out.println("received from client: " + s);
    out.flush();
    break;
}

在真实的服务器程序中,服务器很可能会无休止地监听,而不使用中断来结束通信。为了处理多个并发的客户端,每个客户端连接将产生一个单独的线程来处理通信。服务器也会对客户端通信做一些有用的事情。在 HTML 服务器的情况下,它会向客户机发回一条 HTML 消息。在 SMTP 服务器上,客户端会向服务器发送一封电子邮件,然后服务器会处理并发送该邮件。套接字通信几乎用于任何 TCP 传输,客户端和服务器都创建新的套接字来执行成功的通信。

21-2.定义到服务器的网络连接

问题

您需要建立到远程服务器的连接。

解决办法

使用远程服务器的名称和端口号创建一个到远程服务器的套接字连接,服务器在该端口监听传入的客户端请求。下面的示例类创建一个到远程服务器的套接字连接。然后,代码向服务器发送文本消息并接收响应。在本例中,客户端尝试联系的服务器名为 server-name,端口号为 1234。

小费

要创建与客户机上运行的本地程序的连接,请将服务器名设置为 127.0.0.1。这是在这个食谱的源列表中完成的。通常像这样的本地连接仅用于测试目的。

public class SocketClient {

    public static Socket socket = null;
    public static PrintWriter out;
    public static BufferedReader in;

    public static void main(String[] args) {
        createConnection("127.0.0.1", 1234);
    }

    public static void createConnection(String host, int port) {

        try {
            //Create socket connection
            socket = new Socket(host, port);
            // Obtain a handle on the socket output
            out = new PrintWriter(socket.getOutputStream(),
                    true);
            // Obtain a handle on the socket input
            in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));
            testConnection();
            System.out.println("Closing the connection...");
            out.flush();
            out.close();
            in.close();
            socket.close();
            System.exit(0);
            } catch (UnknownHostException e) {
            System.out.println(e);
            System.exit(1);
            } catch (IOException e) {
            System.out.println(e);
            System.exit(1);
        }
    }

    public static void testConnection() {
        String serverResponse = null;
        if (socket != null && in != null && out != null) {
            System.out.println("Successfully connected, now testing...");

            try {
                // Send data to server
                out.println("Here is a test.");
                // Receive data from server
                while((serverResponse = in.readLine()) != null)
                System.out.println(serverResponse);
                } catch (IOException e) {
                System.out.println(e);
                System.exit(1);
            }
        }
    }
}

如果您针对成功接受请求的服务器测试该客户机,您将看到以下结果:

Successfully connected, now testing...
注意

这个程序本身不会做任何事情。要创建一个服务器端的套接字应用来接受这个完整测试的连接,请参见配方 21-1。如果您尝试运行此类而不指定正在侦听所提供端口的服务器主机,将会收到此异常:java.net.ConnectException:连接被拒绝。

它是如何工作的

每个客户机/服务器连接都是通过套接字进行的,套接字是两个不同程序之间通信链路的端点。套接字有分配给它们的端口号,这些端口号充当 TCP/IP 层在尝试连接时使用的标识符。接受客户机请求的服务器程序通常在指定的端口号上侦听新的连接。当客户端想要向服务器发出请求时,它会利用服务器的主机名和服务器侦听的端口创建一个新的套接字,并尝试与该套接字建立连接。如果服务器接受套接字,则连接成功。

这个菜谱讨论的是套接字连接的客户端,所以我们现在不会详细讨论服务器端发生了什么。然而,关于连接的服务器端的更多信息包含在配方 21-1 中。这个配方的解决方案中的示例类代表了客户端程序如何尝试和建立与服务器端程序的连接。在这个配方中,名为 createConnection()的方法执行实际的连接。它接受将用于创建套接字的服务器主机名和端口号。在 createConnection()方法中,服务器主机名和端口号被传递给 Socket 类构造函数,从而创建一个新的 Socket 对象。接下来,使用 Socket 对象的输出流创建 PrintWriter 对象,使用 Socket 对象的输入流创建 BufferedReader 对象。

//Create socket connection
socket = new Socket(host, port);
// Obtain a handle on the socket output
out = new PrintWriter(socket.getOutputStream(),
                                  true);
// Obtain a handle on the socket input
in = new BufferedReader(new InputStreamReader(
                    socket.getInputStream()));

在创建套接字并获得套接字的输出流和输入流之后,客户端可以写入 PrintWriter,以便向服务器发送数据。类似地,为了从服务器接收响应,客户端从 BufferedReader 对象中读取。testConnection()方法用于使用新创建的套接字模拟客户端和服务器程序之间的对话。为此,需要检查 socket、in 和 out 变量,以确保它们不等于 null。如果它们不等于 null,则客户端尝试通过使用 out.println("这里是一个测试,"将消息发送到输出流来将消息发送到服务器。).然后创建一个循环,通过调用 in.readLine()方法监听来自服务器的响应,直到没有收到任何其他内容。然后它打印接收到的消息。

if (socket != null && in != null && out != null) {
    System.out.println("Successfully connected, now testing...");

    try {
        // Send data to server
        out.println("Here is a test.");
        // Receive data from server
        while((serverResponse = in.readLine()) != null)
            System.out.println(serverResponse);
    } catch (IOException e) {
        System.out.println(e);
        System.exit(1);
    }
}

java.net.Socket 类符合 java 编程语言的本质。它使开发人员能够针对独立于平台的 API 进行编码,以便与特定于不同平台的网络协议进行通信。它从开发人员那里抽象出每个平台的细节,并为实现客户机/服务器通信提供了一个简单而一致的实现。

21-3.为 InfiniBand 绕过 TCP 以获得性能提升

问题

您的应用部署在 Linux 或 Solaris 上,需要快速高效地移动数据,并且您需要消除任何可能减慢速度的瓶颈。

解决办法

使用 Sockets Direct Protocol (SDP)绕过 TCP,这可能是流程中的瓶颈。为此,请创建一个 SDP 配置文件,并设置系统属性来指定配置文件的位置。

注意

SDP 被添加到 Java SE 7 发行版中,仅用于部署在 Solaris 或 Linux 操作系统中的应用。开发 SDP 是为了支持 InfiniBand 结构上的流连接,这是 Solaris 和 Linux 都支持的。Java SE 7 版本支持 1.4.2 和 1.5 版本的 open fabrics Enterprise Distribution(OFED)。

此配置文件是一个可用于启用 SDP 的示例:

# Use SDP when binding to 192.0.2.1
bind 192.0.2.1 *

# Use SDP when connecting to all application services on 192.0.2.*
connect 192.0.2.0/24     1024-*

# Use SDP when connecting to the HTTP server or a database on myserver.org
connect myserver.org   8080
connect myserver.org   1521

以下摘录摘自终端。它是名为 SDPExample 的 Java 应用的执行,指定了 SDP 系统属性:

% java -Dcom.sun.sdp.conf=sdp.conf -Djava.net.preferIPv4Stack=true  SDPExample

它是如何工作的

有时,在执行网络通信时,应用必须尽可能快。通过 TCP 进行传输有时会降低性能,因此绕过 TCP 可能是有益的。自从 Java SE 7 发布以来,某些平台已经包含了对 SDP 的支持。SDP 支持 InfiniBand 结构上的流连接。Solaris 和 Linux 都支持 InfiniBand,因此 SDP 在这些平台上非常有用。

为了支持 SDP,您不需要对您的应用进行任何编程更改。使用 SDP 的唯一区别是,您必须创建一个 SDP 配置文件,并且在运行应用时,必须通过传递一个标志来告知 JVM 使用该协议。因为实现是透明的,所以可以为任何平台编写应用,并且那些支持 SDP 的应用可以只包含配置文件而绕过 TCP。

SDP 配置文件是一个文本文件,由绑定和连接规则组成。绑定规则指示当 TCP 套接字绑定到与给定规则匹配的地址和端口时,应该使用 SDP 协议传输。连接规则指示当未绑定的 TCP 套接字尝试连接到与给定规则匹配的地址和端口时,应该使用 SDP 协议传输。规则以指示规则类型的 bind 或 connect 关键字开头,后跟主机名或 IP 地址,以及一个端口号或端口号范围。根据在线文档,规则具有以下形式:

("bind"|"connect")1*LWSP-char(hostname|ipaddress)["/"prefix])1*LWSP-char("*"|port)É
["-"("*"|port)]

在这里显示的规则格式中,1*LWSP 字符意味着任意数量的制表符或空格可以分隔标记。方括号内的内容表示可选文本,引号表示文字文本。在该配方的解决方案中,第一条规则表明 SDP 可用于本地地址 192.0.2.1 的 IP 地址上的任何端口(表示通配符)。分配给 InfiniBand 适配器的每个本地地址都应在配置文件中用绑定规则指定。配置文件中的第一个连接规则指定,只要连接到 IP 地址 192.0.2,就应该使用 SDP。,使用 1024 或更大的端口。

connect 192.0.2.0/24     1024-*

这条规则使用了一些应该注意的特殊语法。具体来说,IP 地址的/24 后缀表示 32 位 IP 地址的前 24 位应该与指定的地址匹配。因为 IP 地址的每个部分都是 8 位,这意味着 192.0.2 应该完全匹配,最后一个字节可以是任何值。端口标识符中的破折号-*指定了 1024 或更大的范围,因为使用了通配符。配置文件中的第三和第四个连接规则指定 SDP 应该与主机名 myserver.org 和端口 8080 或 1521 一起使用。

接下来,为了启用 sdp,应该在启动应用时指定–DCOM . sun . SDP . conf 属性以及 SDP 配置文件的位置。另外,请注意,在解决方案中,属性-Djava.net.preferIPv4Stack 被设置为 true。这表示将使用 IPv4 地址格式。这是必要的,因为映射到 IPv6 的 IPv4 地址目前在 Solaris OS 或 Linux 下不可用。

尽管 SDP 只适用于 Solaris 或 Linux,但对于这些平台的用户来说,它是 JDK 的一个很好的补充。任何性能提升总是被视为一种额外的奖励,这种方法的解决方案当然属于这一类。

21-4.向一组接收者广播

问题

您希望将数据报广播到零个或多个由单个地址标识的主机。

解决办法

使用 DatagramChannel 类利用数据报多播。DatagramChannel 类使多个客户端能够连接到一个组并侦听从服务器广播的数据报。下面几组代码使用客户机/服务器方法演示了这种技术。这个类演示了一个多播客户端。

package org.java9recipes.chapter21.recipe21_4;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.NetworkInterface;
import java.net.StandardProtocolFamily;
import java.net.StandardSocketOptions;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.MembershipKey;

public class MulticastClient {

    public MulticastClient() {
    }

    public static void main(String[] args) {
        try {
            // Obtain Supported network Interface
            NetworkInterface networkInterface = null;
            java.util.Enumeration<NetworkInterface> enumNI = NetworkInterface.getNetworkInterfaces();
            java.util.Enumeration<InetAddress> enumIA;
            NetworkInterface ni;
            InetAddress ia;
            ILOOP:
            while (enumNI.hasMoreElements()) {
                ni = enumNI.nextElement();
                enumIA = ni.getInetAddresses();
                while (enumIA.hasMoreElements()) {
                    ia = enumIA.nextElement();
                    if (ni.isUp() && ni.supportsMulticast()
                            && !ni.isVirtual() && !ni.isLoopback()
                            && !ia.isSiteLocalAddress()) {
                        networkInterface = ni;
                        break ILOOP;
                    }
                }
            }

            // Address within range
            int port = 5239;
            InetAddress group = InetAddress.getByName("226.18.84.25");

            final DatagramChannel client = DatagramChannel.open(StandardProtocolFamily.INET);

            client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
            client.bind(new InetSocketAddress(port));
            client.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface);

            System.out.println("Joining group: " + group + " with network interface " + networkInterface);
            // Multicasting join
            MembershipKey key = client.join(group, networkInterface);
            client.open();

            // receive message as a client
            final ByteBuffer buffer = ByteBuffer.allocateDirect(4096);
            buffer.clear();
            System.out.println("Waiting to receive message");
            // Configure client to be passive and non.blocking
            // client.configureBlocking(false);
            client.receive(buffer);
            System.out.println("Client Received Message:");
            buffer.flip();
            byte[] arr = new byte[buffer.remaining()];
            buffer.get(arr, 0, arr.length);

            System.out.println(new String(arr));
            System.out.println("Disconnecting...performing a single test pass only");
            client.disconnect();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

接下来,可以使用服务器类将数据报广播到多播客户端所连接的地址。下面的代码演示了一个多播服务器:

package org.java9recipes.chapter21.recipe21_4;

import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;

public class MulticastServer extends Thread {

    protected ByteBuffer message = null;

    public MulticastServer() {
    }

    public static void main(String[] args) {

        MulticastServer server = new MulticastServer();
        server.start();

    }

    @Override
    public void run() {

        try {

            // send the response to the client at "address" and "port"
            InetAddress address = InetAddress.getByName("226.18.84.25");
            int port = 5239;

            DatagramChannel server = DatagramChannel.open().bind(null);
            System.out.println("Sending datagram packet to group " + address + " on port " + port);
            message = ByteBuffer.wrap("Hello to all listeners".getBytes());
            server.send(message, new InetSocketAddress(address, port));

            server.disconnect();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

服务器可以向作为该组成员的每个客户端广播消息。应该首先启动客户端,然后启动服务器。一旦服务器启动,它将广播消息,客户端将接收到它。

它是如何工作的

多播是在单次传输中向一组听众广播消息的能力。多播的一个很好的类比是无线电。成千上万的人可以收听同一个广播事件并听到相同的信息。计算机在向听众发送信息时也可以做类似的事情。一组客户机可以调谐到相同的地址和端口号,以接收服务器向该地址和端口广播的消息。Java 语言通过数据报消息传递提供多播功能。数据报是独立的、无保障的消息,可以通过网络传递给客户端。(无保障意味着到达、到达时间和内容是不可预测的。)与通过 TCP 发送的消息不同,发送数据报是一个非阻塞事件,发送方不会收到收到消息的通知。数据报是使用用户数据报协议(UDP)而不是 TCP 发送的。只要消息的顺序、可靠性和数据完整性不是至关重要的,通过 UDP 发送多播消息的能力是 TCP 的一个优势。

Java 通过 MulticastChannel 接口促进了多播消息传递。实现 MulticastChannel 接口的类启用了多播,因此可以向组广播并接收组广播。DatagramChannel 就是这样一个类,它是面向数据报的套接字的可选通道。在这个配方的解决方案中,客户端和服务器程序都用于通过多播消息传递进行通信,并且 DatagramChannel 类用于通信的两端。如果 DatagramChannel 要用于接受多播消息,则必须以特定的方式进行配置。具体来说,需要在打开的 DatagramChannel 客户端上设置一些选项。我们将很快讨论这些选项。创建接收多播消息的客户端需要以下步骤。

  1. 打开 DatagramChannel。

  2. 设置多播所需的 DatagramChannel 选项。

  3. 将客户端加入多播组并返回 MembershipKey 对象。

  4. 打开客户端。

在该方案的解决方案中,客户端应用首先获取对将用于接收广播消息的网络接口的引用。多播需要设置网络接口。接下来,选择端口号以及多播 IP 地址。群组或注册的收听者将使用 IP 地址来收听广播。端口号不得使用,否则将引发异常。对于 IPv4 多播,IP 地址的范围必须从 224.0.0.0 到 239.255.255.255,包括 224.0.0 和 239.255.255。这个端口和 IP 地址与服务器用来广播消息的端口和 IP 地址相同。接下来,使用 StandardProtocolFamily.INET 打开一个新的 DatagramChannel。INET 或 StandardProtocolFamily。INET6,分别对应 IPv4 和 IPv6。DatagramChannel 上设置的第一个选项是 StandardSocketOptions。SO_REUSEADDR,并将其设置为 true。这表明多个客户端将能够“重用”该地址或同时使用它。这需要为多播的发生进行设置。然后,使用新的 InetSocketAddress 实例将客户端绑定到该端口。最后,标准的 SocketOptions。IP_MULTICAST_IF 选项设置为使用的网络接口。此选项代表由面向数据报的套接字发送的多播数据报的传出接口。

client.setOption(StandardSocketOptions.SO_REUSEADDR, true);
client.bind(new InetSocketAddress(port));
client.setOption(StandardSocketOptions.IP_MULTICAST_IF, networkInterface); 

一旦设置了这些选项并且端口已经绑定到 DatagramChannel,它就准备好加入侦听器组了。这可以通过调用 DatagramChanneljoin(inet address,NetworkInterface)方法,传递客户端将使用的组地址和网络接口来完成。因此,会产生一个 Java . nio . channels . membership key 对象,这是一个表示 ip 多播组成员资格的令牌。最后,调用 DatagramChannelopen()方法,打开通道监听广播。此时,客户端准备好接收多播消息,并等待接收消息。

MembershipKey key = client.join(group, networkInterface);
client.open();

客户端接下来的几行代码负责从服务器接收消息。为了接收广播的消息,创建一个 ByteBuffer,然后最终传递给 DatagramChannel 的 receive()方法。调用 receive()方法后,客户端将暂停,直到收到消息。您可以通过调用 datagram channel configure blocking(boolean)方法并传递一个 false 值来禁用此功能。接下来,通过使用 flip()方法将缓冲区索引重新定位在 0,然后将从索引 0 开始到最后一个索引的文本拉入 byte[]中,将 ByteBuffer 转换为字符串值并打印出来。最后,完成后一定要断开客户端。它包装了客户端代码部分。

// Configure client to be passive and non.blocking
// client.configureBlocking(false);
client.receive(buffer);
// client pauses until a message is received... in this case
System.out.println("Client Received Message:");
buffer.flip();
byte[] arr = new byte[buffer.remaining()];
buffer.get(arr, 0, arr.length);

System.out.println(new String(arr));
System.out.println("Disconnecting...performing a single test pass only");
client.disconnect();
注意

在这个配方的例子中,执行了一次传递,然后客户端被断开。对于扩展监听,您需要一个带有超时的循环,并为结束状态提供测试。

服务器代码相当简单。可以看到 MulticastServer 类扩展了 Thread。这意味着这个服务器应用可以在一个独立于应用中其他代码的线程中运行。如果有另一个类启动了 MulticastServer 类的 run()方法,它将在与启动它的类不同的线程中运行。run()方法必须存在于任何扩展 Thread 的类中。有关线程和并发的更多信息,请参考第十章。

大部分服务器代码驻留在 run()方法中。为了加入多播组,使用客户机注册时使用的同一 IP 地址创建一个新的 InetAddress 对象。服务器代码中也声明了相同的端口号,这两个对象稍后将在代码块中用于发送消息。一个新的 DatagramChannel 被打开并绑定到 null。null 值很重要,因为通过将 SocketAddress 设置为 null,套接字将被绑定到一个自动分配的地址。接下来,创建一个 ByteBuffer,其中包含一条将广播给任何侦听器的消息。然后使用 DatagramChannel 的 send(ByteBuffer,InetSocketAddress)方法发送消息。解决方案中的 send()方法接受作为 ByteBuffer 对象的消息,以及使用地址和端口创建的新 InetSocketAddress,该地址和端口在块的开头声明。告诉过你我们会回来的!

server.send(message, new InetSocketAddress(address, port));

此时,客户端将收到服务器发送的消息。至于这个配方的解决方案中展示的客户端,它将会断开连接。通常,在真实的场景中,不同的类最有可能启动服务器,并且它的 run()方法将包含一个循环,该循环将继续执行,直到所有消息都已广播完毕或者循环被告知停止。在用户启动关机之前,客户端可能不会断开连接。

注意

如果您的笔记本电脑或服务器使用不同于标准 IPv4 的网络协议,则结果可能会有所不同。请确保在将您的代码发送到生产环境之前进行足够多的测试。

21-5.生成和读取 URL

问题

您希望在应用中以编程方式生成 URL。一旦创建了 URL,您就想从其中读取数据,以便在您的应用中使用。

解决办法

利用 java.net.URL 类来创建 URL。根据您尝试使用的地址,有几种不同的方法来生成 URL。这个解决方案演示了创建 URL 对象的一些选项,以及说明不同之处的注释。一旦创建了 URL 对象,就会将其中一个 URL 读入 BufferedReader 并打印到命令行。

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;

public class GenerateAndReadUrl {

    public static void main(String[] args) {
        try {
            // Generate absolute URL
            URL url1 = new URL("http://www.java.net");
            System.out.println(url1.toString());
            // Generate URL for pages with a common base
            URL url2 = new URL(url1, "search/node/jdk8");

            // Generate URL from different pieces of data
            URL url3 = new URL("http", "java.net", "search/node/jdk8");

            readFromUrl(url1);

        } catch (MalformedURLException ex) {
            ex.printStackTrace();
        }
    }

    /**
     * Open URL stream as an input stream and print contents to command line.
     *
     * @param url
     */
    public static void readFromUrl(URL url) {
        try {
            BufferedReader in = new BufferedReader(
                    new InputStreamReader(
                    url.openStream()));

            String inputLine;

            while ((inputLine = in.readLine()) != null) {
                System.out.println(inputLine);
            }

            in.close();
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}

运行该程序将导致来自 URL 资源(标识为 url1)的 HTML 被打印到命令行。

它是如何工作的

由于 java.net.URL 类完成了所有繁重的工作,所以用 Java 代码创建 URL 相当简单。URL 是指向互联网上的资源的字符串。有时用 Java 代码创建 URL 很有用,这样您就可以从 URL 指向的 Internet 资源中读取内容,或者将内容推送到该资源中。在这个配方的解决方案中,创建了一些不同的 URL 对象,展示了可供使用的不同构造函数。

创建 URL 最简单的方法是将位于 Internet 上的资源的标准可读 URL 字符串传递给 java.net.URL 类,以创建 URL 的新实例。在该解决方案中,将一个绝对 URL 传递给构造函数来创建 url1 对象。

URL url1 = new URL("http://www.java.net");

创建 URL 的另一个有用的方法是向 URL 构造函数传递两个参数,并创建一个相对 URL。将相对 URL 建立在另一个 URL 的位置上是很有用的。例如,如果一个特定的站点有许多不同的页面,您可以创建一个 URL,指向相对于主站点 URL 的一个子页面。这个配方的解决方案中的 url2 对象就是这种情况。

URL url2 = new URL(url1, "search/node/jdk8");

如您所见,路径 search/node/jdk8 是相对于 url1 的。最终,url2 对象的人类可读格式被表示为www.java.net/search/node/jdk8。还有几个构造函数可以创建接受两个以上参数的 URL 对象。这些构造函数如下:

new URL (String protocol, String host, String port, String path);
new URL (String protocol, String host, String path);

在该解决方案中,演示了此处显示的两个构造函数中的第二个。资源的协议、主机名和路径被传递给构造函数来创建 url3 对象。这最后两个构造函数通常在动态生成 URL 时最有用。

21-6.解析 URL

问题

您希望以编程方式从 URL 收集信息,以便在应用中使用。

解决办法

使用内置的 URL 类方法解析 URL。在下面名为 Parse URL 的示例类中,创建了一个 URL 对象,然后使用内置的 Url 类方法对其进行解析,以收集有关该 Url 的信息。从 URL 中检索到信息后,它被打印到命令行,然后用于创建另一个 URL。

import java.net.MalformedURLException;
import java.net.URL;

public static void main(String[] args) {
URL url1 = null;
URL url2 = null;
try {
            // Generate absolute URL
            url1 = new URL("http://www.apress.com/catalogsearch/result/?q=juneau");

            String host = url1.getHost();
            String path = url1.getPath();
            String query = url1.getQuery();
            String protocol = url1.getProtocol();
            String authority = url1.getAuthority();
            String ref = url1.getRef();

            System.out.println("The URL " + url1.toString() + " parses to the following:\n");
            System.out.println("Host: " + host + "\n");
            System.out.println("Path: " + path + "\n");
            System.out.println("Query: " + query + "\n");
            System.out.println("Protocol: " + protocol + "\n");
            System.out.println("Authority: " + authority + "\n");
            System.out.println("Reference: " + ref + "\n");

            url2 = new URL(protocol + "://" + host + path + "?q=java");

        } catch (IOException ex) {
            ex.printStackTrace();

        }
    }

执行此代码时,将显示以下几行:

The URL http://www.apress.com/catalogsearch/result/?q=juneau parses to the following:

Host: www.apress.com

Path: /catalogsearch/result/

Query: q=juneau

Protocol: http

Authority: www.apress.com

Reference: null

它是如何工作的

当在应用中构造和使用 URL 时,提取与 URL 相关的信息有时是有益的。使用 URL 内置的类方法可以很容易地做到这一点,这些方法可以调用给定的 URL 并返回信息字符串。表 21-1 解释了 URL 类中用于获取信息的访问器方法。

表 21-1。用于查询 URL 的访问器方法
|

方法

|

返回的 URL 信息

| | --- | --- | | getAuthority() | 权威成分 | | getFile() | 文件名组件 | | getHost() | 主机名组件 | | getPath() | 道路连通区 | | getProtocol() | 协议标识符组件 | | getRef() | 参考组件 | | getQuery() | 查询组件 |

这些访问器方法中的每一个都返回一个字符串值,该值可用于提供信息或动态构造其他 URL,如示例中所做的那样。如果你看一下这个配方的结果,你可以看到通过表 21-1 中列出的访问器方法获得的关于 URL 的信息。大多数访问器都是不言自明的。然而,他们中的一些人可能需要进一步的解释。getFile()方法返回 URL 的文件名。文件名与连接 getPath()返回值和 getQuery()返回值的结果相同。getRef()方法可能不太简单。通过调用 getRef()方法返回的引用组件引用可能附加到 URL 末尾的“片段”。例如,一个片段使用井号(#)来表示,后跟一个字符串,该字符串通常对应于特定网页上的一个子部分。给定如下 URL,将使用 getRef()方法返回 recipe21_6。

www.java9recipes.org/chapters/chapter21#recipe21_6

虽然并不总是需要,但解析 URL 以获取信息的能力有时会非常有用。因为 Java 语言在 java.net.URL 类中内置了助手方法,所以收集有关 URL 的信息很容易。

21-7.发出 HTTP 请求和使用 HTTP 响应

问题

您希望从应用内部发起一个 HTTP 请求,并相应地处理响应。

解决办法

利用 HTTP/2 客户端 API,以同步或异步方式发出请求。在下面的示例代码中,向 Apress 网站发出了一个请求。该示例演示了一个同步请求,因此代码将阻塞,直到收到响应。

public static void synchronousRequest() {
    try {
        HttpResponse resp = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().response();
        int statusCode = resp.statusCode();
        String body = resp.body(HttpResponse.asString());
        System.out.println("Status Code: " + statusCode);
        // Do something with body text
    } catch (URISyntaxException | IOException | InterruptedException ex) {
        Logger.getLogger(HttpClient.class.getName()).log(Level.SEVERE, null, ex);
    }
}

运行此示例的输出应该如下所示,除非站点关闭或存在网络通信问题:

Status Code: 200

要执行异步请求,只需调用 responseAsync()方法,而不是 response()。这样做将返回 CompleteableFuture,在此基础上您可以检查状态以确定响应是否已返回。

public static void asynchronousRequest() {
    try {
        CompletableFuture<HttpResponse> cf = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().responseAsync();
        System.out.println("Request made...");

        System.out.println("Check if done...");
        while (!cf.isDone()) {
            System.out.println("Perform some other tasks while waiting...");
            // Periodically check CompletableFuture.isDone()
        }
        System.out.println("Response Received:");
        HttpResponse response = cf.get();
        int statusCode = response.statusCode();
        System.out.println("Status Code: " + statusCode);
        String body = response.body(HttpResponse.asString());
        // Do something with body text

    } catch (URISyntaxException | InterruptedException | ExecutionException ex) {
        Logger.getLogger(HttpClient.class.getName()).log(Level.SEVERE, null, ex);
    }
}

异步示例的输出如下所示:

Request made...
Check if done...
Perform some other tasks while waiting...
Perform some other tasks while waiting...
Perform some other tasks while waiting...
...
Response Received:
Status Code: 200

它是如何工作的

多年来,HTTP/1.1 客户端一直是 JDK 的一部分。事实上,自从它在 JDK 1.1 中出现以来,基本上没有什么变化。HTTP/1.1 已经过时,不再是通过 HTTP 进行通信的首选方法。作为 HTTP/1.1 的一部分,较新的标准 HTTP/2 解决了许多已经存在多年的问题。在 Java 9 中,添加了一个新的 HTTP/2 客户端 API,允许开发人员轻松地利用更新的方法,同时仍然保持向后兼容。

由于诸如行首阻塞和大量的请求/响应周期等问题,HTTP/1.1 的性能经常是一个问题。HTTP/2 协议是在 2015 年推出的,它解决了许多老问题。例如,现在发送消息时使用二进制帧,降低了解析消息的复杂性。现在一切都可以通过一个 TCP 连接发送,而不是创建多个 TCP 连接来发送大量消息。这仅仅触及了表面,HTTP/2 中已经有了更多的改进……但是这提供了一个对为什么需要这些改变的合理理解。

如前所述,Java 9 中添加了一个新的 HTTP/2 客户端 API,使得同步或异步执行 HTTP 请求和接收 HTTP 响应变得容易。在第一个示例中,演示了同步 API,调用 HttpRequest.create()方法,传递一个 URI,然后以构建器样式的模式调用 GET()和 response()方法。这会返回一个 HttpResponse 对象。

 HttpResponse resp = HttpRequest.create(
                new URI("http://www.apress.com/us/")).GET().response();

当然,这是一个阻塞调用,因为需要等到收到响应后才能完成进一步的处理。一旦接收到,HttpResponse 对象可用于返回正文、HTTP 状态代码和许多其他项目。在这个例子中,HTTP 状态代码仅仅是打印出来的,在很多情况下,状态代码和一个条件一起使用来决定如何执行处理。

看看第二个异步示例,当调用 HttpRequest.create()方法时,很容易注意到代码中的差异。将 URI 传递给 create()方法后,再次调用 GET()方法,然后调用 responseAsync()方法。对 responseAsync()的调用返回 CompletableFuture,在这种情况下,泛型用于强制返回 HttpResponse。然后可以检查 CompletableFuture,以确定是否使用 isDone()方法返回了响应。如果响应还没有被返回,则可以采取适当的动作来在稍后的时间再次保持检查,或者相应地处理接收到的响应。在本例中,while 循环用于继续循环,直到最终返回响应。为了使这段代码更适合生产,可以在完成一定次数的迭代后使用条件来暂停循环。

更新的 HTTP/2 客户端带来了一个更现代的 API 来处理 HTTP 到 Java 的转换。更新后的 API 确保用户可以执行同步或异步的请求/响应生命周期。

摘要

本章讲述了 Java 语言的一些基本网络特性。在最近的版本中,增加了一些不错的新特性,比如 SDP。然而,java.net 软件包的大部分内容多年来都没有改变,它非常健壮且易于使用。本章深入研究了使用套接字连接和 URL 以及通过 DatagramChannel 广播消息。最后,介绍了更新的 HTTP/2 客户机。

二十二、Java 模块化

Java 9 最重要的新特性之一是模块化系统,它是通过 Jigsaw 项目实现的。竖锯项目也可以被称为 JSR 376:Java 平台模块系统。该项目的目的是构建一个提供可靠配置的系统,以取代类路径系统。它还关注于在不同模块之间提供强大的封装。模块系统由构成 Java 平台的所有模块组成,因为该平台是作为该项目的一部分从头开始重新构建和模块化的。应用开发人员和库创建者也可以创建模块…无论是执行特定任务的单个模块,还是共同创建应用的多个模块。

在这一章中,将涉及模块开发和管理的基本原理。尽管 Java 模块化是一个非常大的主题,但这一章很简洁,提供了足够的信息来快速开始模块开发。对于那些有兴趣了解更多关于 Java 模块化细节的人,我推荐他们阅读更深入的书籍和文档。

22-1.构建模块

问题

您希望创建一个简单的模块,将消息打印到命令行或通过记录器。

解决办法

开发一个模块,以便它可以通过 java 可执行文件来执行。首先在文件系统的某个地方创建一个新目录…在这种情况下,将其命名为“recipe22-1”创建一个名为 module-info.java 的新文件,它是模块描述符。在该文件中,列出模块名称,如下所示:

module org.firstModule {}

接下来,在之前创建的 recipe22-1 目录中创建一个名为 org 的文件夹。接下来,在 org 文件夹中创建一个名为 firstModule 的文件夹。现在,通过在 org.firstModule 文件夹中添加一个名为 Main.java 的新文件来创建模块的主体。将以下代码放在 Main.java 文件中:

package org.firstModule;
public class Main {
    public static void main(String[] args) {
        System.out.println("This is my first module");
    }
}

它是如何工作的

最简单的模块可以用两个文件构建,一个是模块描述符,另一个是包含业务逻辑的 Java 类文件。本例的解决方案遵循这种模式来创建一个非常基本的模块,该模块执行将短语打印到命令行的单一任务。该模块打包在一个与模块名同名的目录中。在本例中,这个目录被命名为 org.firstModule,因为它遵循标准的模块命名约定。实际上,一个模块可以被命名为任何名称,只要它不与其他模块名称冲突。但是,建议使用包的反向域名模式。这导致模块名以其包含的包名为前缀。

在这个解决方案中,模块描述符包含模块名,后跟左大括号和右大括号。在更复杂的模块中,其他模块依赖项的名称可以放在大括号中,以及该模块导出供其他人使用的包的名称。模块描述符应该位于模块目录的根目录下。包含这个文件向 JVM 表明这是一个模块。这个目录可以做成一个 JAR 文件,我将在本章后面讨论,这就创建了一个模块化的 JAR。

开发简单模块必须创建的另一个文件是包含业务逻辑的 Java 类文件。这个文件应该放在 org/firstModule 目录中,包应该指示 org.firstModule。请注意,模块需要的任何依赖关系都必须在模块描述符中列出。在这个简单的模块中,没有依赖关系。在设置了这个目录结构并将这两个文件放入各自的位置后,模块开发就完成了。

22-2.编译和执行模块

问题

你已经开发了一个基本模块。现在您想编译模块并执行它。

解决办法

利用 javac 实用程序来编译模块,指定 d 标志来列出编译后的代码将放入的文件夹。在 d 选项之后,必须列出每个要编译的源文件,包括 module-info.java 描述符。用空格分隔每个文件路径。以下命令编译配方 22-1 中开发的源代码,并将结果放入名为 mods/org.firstModule 的目录中。

javac d src/mods/org.firstModule src/org.firstModule/module-info.java src/org.firstModule/org/firstModule/Main.java

现在代码已经编译好了,是时候执行模块了。这可以通过标准的 java 可执行文件来完成。但是,必须使用 Java 9 中新增的- module-path 选项来指示模块源的路径。-m 选项用于指定模块的主类。

java --module-path mods -m org.firstModule/org.firstModule.Main  

执行该模块的输出应该如下所示:

This is my first module

如果要编译的模块不止一个,那么可以使用与前面描述的技术类似的技术分别编译它们,也可以一次性编译它们。编译包含依赖项的两个模块的语法如下:

javac -d mods --module-source-path src $(find src -name "*.java")

它是如何工作的

如您所知,在 Java 应用可以被执行之前,它必须被编译。模块也是如此,它们在使用之前必须被编译。标准的 javac 实用程序得到了增强,只需列出 module-info.java 文件和每个后续文件的全限定路径,就可以适应模块的编译。模块中包含的 java 文件。d 选项用于指定编译源的目的地。在这个解决方案中,javac 实用程序被调用,目标被设置为位置 src/mods/org.firstModule。组成模块的 java 文件在后面列出,用空格隔开。如果一个特定的模块包含许多。java 源文件,然后只需在每个包后的路径中指定一个星号(*)通配符,而不是单独的文件名,就足以编译每个包了。包含在指定包中的 java 文件。

javac -d mods/src/org.firstModule src/org.firstModule/module-info.java src/org.firstModule/org/firstModule/*

用于执行大多数 java 应用的相同 Java 可执行文件可用于执行模块。在一些新选项的帮助下,java 可执行文件能够执行具有所有必需依赖项的模块。- module-path 选项指定编译后的模块所在的路径。如果有许多模块组成一个应用,请指定包含应用入口点的模块的路径。-m 选项用于指定路径应用入口点类及其完全限定名。在这个解决方案中,主类位于一个名为 org.firstModule 的目录和一个名为 org.firstModule 的包中。

22-3.创建模块依赖关系

问题

您希望开发一个依赖并利用另一个模块的模块。

解决办法

开发至少两个模块,其中一个模块依赖于另一个模块。然后在模块描述符中指定依赖关系。在前面的菜谱中开发的模块也将用于本解决方案,但它将稍作修改,以利用另一个名为 org.secondModule 的模块。

首先,通过在 src 目录中创建新目录来创建模块 org.secondModule。接下来,创建一个名为 module-info.java 的. java 文件,并把它放在这个位置。模块描述符的内容应该如下所示:

module org.secondModule {
    exports org.secondModule;
}

该模块将使 org.secondModule 包中包含的源代码对其他需要它的模块可用。模块的源代码应该放在一个名为 Calculator.java 的类中,这个文件应该放在 src/org . second module/org/second module 目录中。将以下代码复制到 Calculator.java 中:

package org.secondModule;
import java.math.BigDecimal;
public class Calculator {
    public static BigDecimal calculateRate(BigDecimal days, BigDecimal rate) {
        return days.multiply(rate);
    }
}

最初用于 org.firstModule 的代码(配方 22-1 和 22.2)应修改为使用 org.secondModule,如下所示:

package org.firstModule;
import org.secondModule.Calculator;
import java.math.BigDecimal;
public class Main {
    public static void main(String[] args) {
        System.out.println("This is my first module.");
        System.out.println("The hotel stay will cost " + Calculator.calculateRate(
             BigDecimal.TEN, new BigDecimal(22.95)
        ));
    }
}

org.firstModule 的模块描述符必须修改为需要依赖关系:

module org.firstModule {
    requires org.secondModule;
}

要编译模块,请指定 javac 命令,使用通配符来编译 src 目录中的所有代码:

javac -d mods --module-source-path src $(find src -name "*.java")

最后,要执行 org.firstModule 及其依赖项,请使用之前用于执行该模块的语法。模块系统负责收集所需的依赖关系。

它是如何工作的

一个模块可以包含零个或多个依赖项。模块的可读性取决于该模块的模块描述符中导出的内容。同样,一个模块必须需要另一个模块才能读取它。模块系统实行强封装。模块本身总是可读的,但是其他模块只能使用从该模块导出的那些包。此外,只有公共方法等可供其他模块使用。

要使一个模块依赖于另一个模块,必须在模块描述符中放置一个必需的声明,指定它所依赖的模块的名称。在解决方案中,org.firstModule 依赖于 org.secondModule,因为模块描述符声明了它。这意味着 org.firstModule 能够利用 org.secondModule 模块的 org.secondModule 包中的任何公共特性。如果 org.secondModule 中包含更多的包,那么这些包对 org.firstModule 不可用,因为它们没有在 org.secondModule 的模块描述符中导出。

Java 9 模块的模块描述符的使用胜过类路径,因为它是声明依赖关系的更健壮的方法。然而,如果一个 Java 9 模块被打包成一个 JAR(见配方 22-4),那么通过将 JAR 放入类路径中,它就可以在旧版本的 Java 上使用,而模块描述符将被忽略。

模块可以使用 javac 命令单独编译,如配方 22-2 中所示,或者可以使用通配符符号编译,如配方 22-2 和本配方解决方案中所示。模块的执行是相同的,无论它依赖于零个还是多个其他模块。

22-4.封装模块

问题

您的模块已经开发完成,您希望对其进行打包以使其可移植。

解决办法

利用增强的 jar 实用程序来打包模块,并制作可执行模块。要打包配方 22-2 中开发的模块,导航至包含 mods 和 src 目录的目录。从该目录中,通过命令行执行以下命令:

mkdir lib
jar --create --file=lib/org.firstModule@1.0.jar --module-version=1.0 --main-class=org.firstModule.Main -C mods/org.firstModule .

该实用程序会将模块打包到 lib 目录下的 JAR 文件中。然后,可以使用 java 可执行文件执行 JAR 文件,如下所示:

java -p lib -m org.firstModule

它是如何工作的

Java 9 的 jar 实用程序得到了增强,增加了许多新选项,包括一些使模块打包更容易的选项。表 22-1 列出了 jar 实用程序的选项。

表 22-1。jar 实用程序选项
|

选项

|

描述

| | --- | --- | | -c,-创建 | 创建档案 | | -I,- generate-index=FILE | 为指定的 jar 文件生成索引信息 | | -t,-列表 | 列出归档的目录 | | -u,-更新 | 更新现有的 jar 文件 | | -x,-提取 | 从 jar 文件中提取一个或多个文件 | | -C 目录 | 转到指定的目录并包含文件 | | -f,- file=FILE | jar 文件的名称 | | -v,-详细 | 生成详细输出 | | -e,- main-class=NAME | 将被打包到 jar 中的模块的主类或入口点 | | -m,-清单=文件 | 在 jar 中包含指定的清单文件信息 | | -M-没有-清单 | 省略清单 | | -模块-版本=版本 | 模块版本 | | -哈希模块=模式 | 计算并记录与指定模式匹配的模块的哈希 | | -P,-模块路径 | 用于生成哈希的模块依赖项的位置 | | -0,-不压缩 | 规定不得使用 zip 压缩 |

查看该表,有几个选项对于使用模块很重要。具体来说,如示例所示,- module-version 选项允许指定版本。另一个特定于模块的选项是- module-path,它指定用于生成散列的模块依赖的位置。

除了新选项之外,使用模块创建 JAR 文件与标准的 JAR 文件生成没有太大的不同。也许最困难的部分是在启动命令时确保您在正确的目录中。正如在解决方案中看到的,只需使用- main-class 或-e 选项指定调用 JAR 时将执行的主类。之后,在模块根目录中执行-C 目录更改,然后用“.”结束命令以指示当前目录。

一旦创建了 JAR 文件,该模块将变得可移植,这意味着它可以在其他系统上使用。

22-5.列出依赖关系或确定 JDK 内部 API 的使用

问题

您想确定现有的应用是否依赖于 Java 9 中任何不可访问的内部 JDK API。

解决办法

使用 jdeps 工具从命令行列出模块依赖关系。要查看给定模块的依赖项列表,请按如下方式指定- list-deps 选项:

jdeps --list-deps <<your-jar.jar>>

调用该命令将启动输出,其中包括指定 JAR 文件所依赖的每个包。例如,从 GlassFish 应用服务器模块目录中选择一个随机的 JAR 文件将会产生类似如下的内容:

jdeps --list-deps acc-config.jar
   java.base
   java.xml.bind
   unnamed module: acc-config.jar

还有一些应用可能会使用 JDK 内部的 API,这些 API 对于从 Java 9 开始的标准应用来说是不可访问的。jdeps 工具可以列出这样的依赖关系,从而可以确定应用是否可以在 Java 9 上顺利运行。要利用此功能,请按如下方式指定-JDK internal 选项:

jdeps –jdkinternals <<your-jar.jar>>

调用 jdeps 实用程序来检查包含对 JDK 内部 API 的依赖的 JAR,将产生如下输出:

jdeps -jdkinternals security.jar
security.jar -> java.base
   com.sun.enterprise.common.iiop.security.GSSUPName  -> sun.security.util.ObjectIdentifier                    JDK internal API (java.base)
   com.sun.enterprise.common.iiop.security.GSSUtilsContract -> sun.security.util.ObjectIdentifier                    JDK internal API (java.base)
   com.sun.enterprise.security.auth.login.LoginContextDriver -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.login.LoginContextDriver$4 -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.realm.certificate.CertificateRealm -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.auth.realm.ldap.LDAPRealm -> sun.security.x509.X500Name                            JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.ContentInfo                         JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.PKCS7                               JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.pkcs.SignerInfo                          JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.x509.AlgorithmId                         JDK internal API (java.base)
   com.sun.enterprise.security.ssl.JarSigner          -> sun.security.x509.X500Name                            JDK internal API (java.base)

Warning: JDK internal APIs are unsupported and private to JDK implementation that are
subject to be removed or changed incompatibly and could break your application.
Please modify your code to eliminate dependence on any JDK internal APIs.
For the most recent update on JDK internal API replacements, please check:
https://wiki.openjdk.java.net/display/JDK8/Java+Dependency+Analysis+Tool

JDK Internal API                         Suggested Replacement
----------------                         ---------------------
sun.security.x509.X500Name               Use javax.security.auth.x500.X500Principal @since 1.4

它是如何工作的

jdeps (Java Dependency Analysis)工具是在 Java 8 中引入的,它是一个命令行工具,用于列出 JAR 文件的静态依赖关系。

Java 9 封装了许多内部 JDK API,使得标准应用无法访问它们。在 Java 9 之前,有些情况下需要应用使用这样的内部 API。这些应用将无法在 Java 9 上按预期运行,因此在尝试在 Java 9 上运行旧代码之前,必须找到并解决这种依赖性。jdeps 工具对于发现 JAR 是否依赖于这些内部 API 非常有用,如果它们存在的话,可以列出依赖关系。如果您希望在。点文件格式,请指定-dotoutput 选项和-JDK internal,如下所示:

jdeps -dotoutput /java_dev/security-dependencies.dot  -jdkinternals security.jar

一般来说,jdeps 工具对于确定 JAR 依赖关系也很有帮助。该工具包含一个- list-deps 选项来实现这一点。简单地说,- list-deps 选项列出了指定 JAR 所依赖的每个模块。

22-6.在模块之间提供松散耦合

问题

您希望在模块之间提供松散耦合,这样一个模块可以调用另一个模块作为服务。

解决办法

利用 Java 9 模块化系统中内置的服务架构。服务消费者可以通过在模块描述符中指定“uses”子句来指定松散耦合,以指示模块使用特定的服务。下面的例子可以用于一个模块,该模块可能具有提供 web 服务发现 API 的任务。在本例中,org . Java 9 recipes . service discovery 模块既需要模块,也导出模块。然后,它还指定使用 org . Java 9 recipes . SPI . service registry 服务。

module org.java9recipes.serviceDiscovery {
    requires public.java.logging;
    exports org.java9recipes.serviceDiscovery;
    uses org.java9recipes.spi.ServiceRegistry;
}

类似地,服务提供者必须指定它正在提供特定服务的实现。可以通过在模块描述符中包含“provide”子句来做到这一点。在本例中,下面的模块描述符表明服务提供者模块为 org . Java 9 recipes . SPI . service registry 提供了 org . data registry . database registry 的实现。

module org.dataregistry {
    requires org.java9recipes.serviceDiscovery;
    provides org.java9recipes.spi.ServiceRegistry
        with org.dataregistry.DatbaseRegistry;
}

现在可以编译和使用相应的模块了,它们将强制松耦合。

它是如何工作的

模块服务的概念允许两个或更多模块之间的松散耦合。利用所提供服务的模块被称为服务消费者,而提供服务的模块被称为服务提供者。服务消费者不使用服务提供者的任何实现类,而是使用接口。为了使松散耦合能够工作,模块系统必须能够容易地识别先前解析的模块的任何使用,相反,通过一组可观察的模块来搜索服务提供者。为了便于识别服务的使用,我们在模块描述符中指定了“uses”子句,以表明模块将使用所提供的服务。另一方面,当我们在服务提供者的模块描述符中指定“provides”子句时,模块系统可以很容易地找到服务提供者。

利用模块服务 API,编译器和运行时很容易看到哪些模块利用了服务,以及哪些模块提供了服务。这加强了更强的解耦,因为编译器和链接工具可以确保提供者被适当地编译并链接到这样的服务。

22-7.链接模块

问题

您希望链接一组模块来创建模块化运行时映像。

解决办法

利用 jlink 工具来链接所述模块集,以及它们的可传递依赖关系。在下面的摘录中,从配方 22-1 中创建的模块创建了一个运行时映像。

jlink --module-path $JAVA_HOME/jmods:mods --add-modules org.firstModule --output firstmoduleapp

它是如何工作的

有时,生成模块的运行时映像是很方便的,以便于移植。jlink 工具提供了这一功能。在该解决方案中,名为 firstmoduleapp 的运行时映像是从名为 org.firstModule 的模块创建的。- module-path 选项首先指示 JVM jmods 目录的路径,然后是包含要合并到运行时映像中的模块的任何目录。- add-modules 选项用于指定应该包含在映像中的每个模块的名称。

jlink 工具包含一组选项,如表 22-2 所示。

表 22-2。jlink 选项
|

选项

|

描述

| | --- | --- | | -添加模块 | 要解析的命名模块。 | | -c,- compress= <0|1|2> | 启用压缩或资源。 | | -禁用插件 | 禁用命名插件。 | | - endian | 指定生成图像的字节顺序。 | | -忽略签名信息 | 当链接的图像将包含已签名的模块化 jar 时,抑制致命错误。签名的模块化 jar 的签名相关文件将不包括在内。 | | -极限-模块 | 限制可观察模块的数量。 | | -列表-插件 | 列出可用插件。 | | -p,-模块路径 | 模块路径。 | | -无头文件 | 从路径中排除头文件。 | | -没有手册页 | 从路径中排除手册页。 | | -输出 | 输出位置。 | | -插件模块路径 | 自定义插件模块路径。 | | -保存选项 | 将 jlink 选项保存在指定文件中。 | | -G,- strip-debug | 剥离调试信息。 | | -版本 | 版本信息。 | | @ | 从指定文件中读取选项。 |

摘要

本章提供了 Java 9 模块系统的简要概述。在本章中,你学习了如何定义一个模块,编译和执行它。您还学习了如何打包模块以及如何创建模块依赖关系。您了解了一些有用的工具,这些工具用于列出依赖项、使用 JDK 内部 API 和链接模块。最后,本章演示了如何通过使用模块服务来创建松散耦合。