Java9-秘籍-八-

104 阅读33分钟

Java9 秘籍(八)

原文:Java 9 Recipes

协议:CC BY-NC-SA 4.0

十七、使用 JavaServer Faces 的 Java Web 应用

Java 开发不仅仅是在桌面上。数以千计的企业应用是使用 Java 企业版(Java EE)编写的,这使得开发复杂、健壮和安全的应用成为可能。开发 Java EE 应用的最主流和最成熟的框架是 JavaServer Faces (JSF)。JDK 9 可以与一些 Java EE 应用服务器(如 GlassFish)一起使用,以支持 Java 9 特性的使用。虽然 Java EE 和 JSF 太大了,无法在一章中涵盖,但这将让您对使用 Java 9 和 Java EE 进行 web 开发的世界有所了解。

在这一章中,我将介绍 JSF 框架的基础知识,从开发一个基本的应用到创建一个复杂的前端。在整个过程中,我将介绍一些重要的信息,比如如何正确地确定控制器类的范围,以及如何生成 web 应用模板。最终,您将能够开始开发 Java web 应用,或者维护现有的 JSF 项目。

由于 web 应用开发包含许多相互关联的过程,因此建议利用 NetBeans 之类的集成开发环境来更轻松地组织 web 项目。在本章中,我将使用 NetBeans IDE 8.2 演示配方的解决方案。但是,您可以使用任意数量的 Java IDEs 将这些基本概念应用到项目中。

注意

这本书是使用 GlassFish 5 应用服务器和 JDK 9 的早期访问版本编写的。要配置服务器使用 JDK 9,修改 GlassFish<>/config/asenv . conf 文件并添加 AS_JAVA 属性,指向 JDK 9 的安装。接下来,修改< >/bin/asadmin 文件,使最后一行如下所示:

exec " JAVA"addmodulesJAVA.annotations.commonjar"JAVA "-add-modules JAVA . annotations . common-jar " AS _ INSTALL _ LIB/client/appserver-CLI . jar " " $ @ "

17-1.创建和配置 Web 项目

问题

您希望创建并配置一个简单的 Java web 应用项目,该项目将利用 JSF web 框架。

解决办法

有许多不同的项目格式可用于创建 web 应用。其中最灵活的是 Maven web 应用格式。随着时间的推移,Apache Maven 构建系统使组织构建和扩展应用的功能变得容易,因为它包含一个健壮的依赖管理系统。在该解决方案中,利用 NetBeans IDE 生成一个 Maven Web 应用项目,然后配置该项目以开发 JSF 应用。

首先,打开 NetBeans IDE 并选择“文件”、“新建项目”,然后在“新建项目”窗口中,选择“Maven”类别和“Web 应用”项目(图 17-1 ),然后单击“下一步”

A323910_3_En_17_Fig1_HTML.jpg

图 17-1。NetBeans Maven Web 应用项目

将应用命名为“HelloJsf”,并将其放入硬盘上的一个目录中。将“包名”更改为 org.java9recipes,并保留所有其他默认值(图 17-2 )。

A323910_3_En_17_Fig2_HTML.jpg

图 17-2。新的 Java Web 应用配置

接下来,选择应用将部署到的服务器,以及 Java EE 版本。在这种情况下,我将利用 Payara 5 服务器(GlassFish 也足够了)和 Java EE 7(图 17-3 )。

A323910_3_En_17_Fig3_HTML.jpg

图 17-3。选择服务器和 Java EE 版本

创建项目后,右键单击项目并选择“Properties ”,为 JSF 配置它并分配一个 Java 平台。在属性菜单中,选择“框架”类别,然后选择“添加”并选择 JSF。接下来,在同一窗口中点击“组件”选项卡,并选择“PrimeFaces”(图 17-4 )。

A323910_3_En_17_Fig4_HTML.jpg

图 17-4。配置项目属性

单击“OK”保存项目属性,项目现在可以使用 JSF 作为框架,连同 PrimeFaces UI 库一起构建了。

它是如何工作的

web 应用的开发需要编排许多不同的文件。虽然可以在不使用 IDE 的情况下开发 Java EE web 应用,但是使用开发环境几乎是一件小事。在这个方法中,NetBeans IDE 用于配置基于 Maven 的 web 应用。Maven 是一个类似于 Apache Ant 的构建系统,它对于应用项目的组织非常有用。Maven 不一定比 Ant 好,但是更容易上手使用。Ant 和 Maven 都是构建系统;然而,Maven 使用约定胜于配置,因此它假设了许多默认配置,以便用户可以非常容易地使用。另一方面,Ant 需要在使用之前配置并编写一个构建脚本。Maven 的一个关键组件是它使依赖关系管理变得非常容易。它已经成为最流行的项目格式之一,在 NetBeans 中开发 Maven 项目可以创建可移植的项目。

在项目创建向导中,必须填写许多字段,尽管许多缺省值可以保留。最重要的是,为应用设置适当的包命名约定,并选择服务器和 Java EE 版本。

注意

使用向导时完成的设置可以在项目创建后通过进入项目属性进行更改。

一旦初始向导完成,就会生成一个基本的 Maven web 项目。此时,通过更改项目属性,可以将项目配置为利用 web 框架、不同版本的 JDK 等。右键单击 NetBeans 项目以进入项目属性屏幕,并利用类别选择来查看或更改与选定类别相关的属性。在这种情况下,选择“框架”类别将允许您添加一个 web 框架,如 JSF。当框架添加到项目中时,框架的所有管道和配置都已完成。另外,在选择 JSF 时,选择框架属性上的“Components”选项卡,并添加将要使用的任何其他 JSF 库。在这种情况下,添加“PrimeFaces ”,因为本章开发的应用将利用 PrimeFaces 组件库。

一旦配置好框架,请确保在属性对话框中选择“源代码”类别,并选择将用于应用编码的 JDK 版本的“源代码/二进制格式”。在这种情况下,选择 1.8,因为在撰写本文时,Java 9 还没有被认证为可以在应用服务器上运行。接下来,在属性对话框中选择“Build”->“Compile”类别,并确保“Java Platform”选项与“Source/Binary Format”类别中选择的选项一致。

完成这些选择后,配置就完成了。在项目属性中选择“确定”。该项目将被修改以包括新的视图(index.xhtml 和 welcomePrimefaces.xhtml)(图 17-5 )。对于 JSF 配置,web.xml 部署描述符也将改变。欢迎文件现在将指向 index.xhtml,JSF 框架的关键组件 FacesServlet 将被配置。

A323910_3_En_17_Fig5_HTML.jpg

图 17-5。完全为 JSF 配置的 Maven Web 项目

JSF 应用的 web.xml 配置通常如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.1"  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd">
    <context-param>
        <param-name>javax.faces.PROJECT_STAGE</param-name>
        <param-value>Development</param-value>
    </context-param>
    <servlet>
        <servlet-name>Faces Servlet</servlet-name>
        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>Faces Servlet</servlet-name>
        <url-pattern>/faces/*</url-pattern>
    </servlet-mapping>
    <session-config>
        <session-timeout>
            30
        </session-timeout>
    </session-config>
    <welcome-file-list>
        <welcome-file>faces/index.xhtml</welcome-file>
    </welcome-file-list>
</web-app>

此时,右键单击 NetBeans 项目并选择“运行”这将导致应用被编译并部署到在项目属性中或在项目创建时选择的应用服务器上(图 17-6 和 17-7 )。

A323910_3_En_17_Fig6_HTML.jpg

图 17-6。部署的 HelloJsf 应用

A323910_3_En_17_Fig7_HTML.jpg

图 17-7。选择“欢迎使用 PrimeFaces”链接

这就是在 NetBeans 中创建和配置 JSF 项目的过程。在下一个菜谱中,我将深入研究 JSF 的世界,因为 HelloJsf 应用被修改以添加一些功能。

17-2.开发 JSF 应用

问题

您已经创建了一个配置了 JSF 的 Maven web 项目,并且希望向应用添加功能。

解决办法

构建应用,使其包含一个 HTML 表单,其中包含许多要填充的字段。提交表单时,表单将调用一个控制器方法。

首先,创建一个 Java 类,它将被用作保存表单中提交的数据的容器。在名为 org . Java 9 recipes . hello JSF . model 的包中创建一个新的 Java 类,并将其命名为 User。在该类中,暂时创建三个 String 类型的私有字段:firstName、lastName 和 email。接下来,右键单击文件并从上下文菜单中选择“Refactor->Encapsulate Fields ”,为这些字段生成访问器方法(getters 和 setters)。这将打开“封装字段”对话框,在该对话框中,您应该选择所有用于创建访问器方法的字段,然后单击“重构”(图 17-8 )。

A323910_3_En_17_Fig8_HTML.jpg

图 17-8。封装字段

接下来,创建上下文和依赖注入(CDI)托管 bean。右键单击项目的“Source Packages”节点,创建一个名为 org.java9recipes.hellojsf.jsf 的新包,它将用于打包应用的所有托管 bean 控制器类。接下来,在名为 HelloJsfController 的新包中创建一个新的 Java 类,并使该类实现 java.io.Serializable,以便它能够钝化。用@ViewScoped 对该类进行注释,以表明该 bean 将在视图作用域中进行管理(关于作用域的更多信息,请参见配方 17-6)。此外,用@Named 对该类进行注释,这使得控制器类可注入,并且还允许在 JSF 视图中从表达式语言引用该类。接下来,创建一个 user 类型的私有字段,将字段命名为 User,并封装字段以生成访问器方法。在生成的 getUser()方法中,执行检查以查看用户字段是否为空,如果是,则实例化一个新用户。此时,该类应该如下所示:

package org.java9recipes.hellojsf.jsf;

import javax.faces.view.ViewScoped;
import javax.inject.Named;
import org.java9recipes.hellojsf.model.User;

@Named
@ViewScoped
public class HelloJsfController implements java.io.Serializable {

    private User user;

    public User getUser() {
          if(user == null){
            user = new User();
        }
        return user;
    }

    public void setUser(User user) {
        this.user = user;
    }

}

最后,创建一个具有 void 返回类型的公共方法,并将其命名为 createUser()。当有人单击表单上的提交按钮时,将调用该方法。在该方法中,只需在屏幕上打印一条消息,表明用户已经成功创建。为此,获取当前 FacesContext 实例的句柄,该实例属于当前会话。一旦获得,通过传递一个 null 作为第一个参数向其添加一个新的 FacesMessage,因为该消息不会被分配给任何单个组件,并传递该消息作为第二个参数。最后,将用户对象设置为 null,以便创建新的用户对象。

注意

FacesContext 包含关于 JSF 请求的状态信息。FacesContext 在 JSF 请求处理生命周期的不同阶段都会更新。

一旦完成,该方法应该如下所示。

public void createUser(){
    FacesContext context = FacesContext.getCurrentInstance();
    context.addMessage(null, new FacesMessage("Successfully Added User: " +
                        user.getFirstName() + " " + user.getLastName()));
    user = null;
}

接下来,是时候创建视图了。在这种情况下,请在 NetBeans IDE 中打开 index.xhtml 视图文件,并添加构成表单的 html 标记和 JSF 组件。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <p:messages id="messages"/>
            <br/>
            <p:outputLabel for="firstName" value="First: "/>
            <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>
            <br/>
            <p:outputLabel for="lastName" value="Last: " />
            <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>
            <br/>
            <p:outputLabel for="email" value="Email: " />
            <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            <br/><br/>
            <p:commandButton id="submitUser" value="Submit" ajax="false"
                             action="#{helloJsfController.createUser()}"/>

        </h:form>
    </h:body>
</html>

一旦生成了视图并创建了 CDI 控制器,就可以通过右键单击项目并选择“Run”来构建和运行应用该屏幕将类似于图 17-9 中的屏幕。

A323910_3_En_17_Fig9_HTML.jpg

图 17-9。JSF 形式

它是如何工作的

JSF 是 Sun Microsystems 在 2004 年开发的,旨在帮助简化 web 应用开发,并使 web 应用更易于管理/支持。它是 JavaServer Pages (JSP)框架的发展,增加了更有组织的开发生命周期和更容易利用现代 web 技术的能力。JSF 使用 XHTML 格式的 XML 文件构建视图,使用 Java 类构建应用逻辑,这使得它能够遵循 MVC 架构。JSF 应用中的每个请求都由 FacesServlet 处理。FacesServlet 负责构建组件树、处理事件、确定导航和呈现响应。JSF 现在已经成为一个成熟的网络框架,与之前的版本相比有很多优势。还有大量的组件和函数库可用于扩展 JSF 应用。

该框架非常强大,包括与 Ajax 和 HTML5 等技术的轻松集成,使得开发动态内容毫不费力。JSF 可以很好地处理数据库,使用 JDBC、企业 Java Bean (EJB)或 RESTful 技术来处理后端。JavaBeans 被称为 JSF 托管 Beans,用于应用逻辑并支持每个视图中的动态内容。根据所使用的范围(配方 17-6),它们可以坚持不同的寿命。视图可以调用 beans 中的方法来执行数据操作和表单处理等操作。属性也可以在 beans 中声明,在视图中公开,并使用标准的表达式语言进行计算,这提供了一种与服务器之间传递值的便捷方式。JSF 允许开发人员使用预先存在的验证和转换标签定制他们的应用,这些标签可以用在带有视图的组件上。构建定制验证器和定制组件也很容易,它们可以应用于视图中的组件。简而言之,JSFs 的成熟使得使用该框架开发任何 web 应用都变得很容易。

在这个解决方案中,创建了一个名为 HelloJsf 的小应用。application 视图包含一个用于提交几个数据字段的简单 HTML 表单、一个用于向后端提交表单的按钮和一个用于显示响应的消息组件。名为 UserController 的控制器类是 ViewScoped 的,这意味着该类中的对象范围将在视图的生命周期内保留。一旦用户导航到另一个视图或关闭窗口,对象就会被销毁。一个名为 User 的对象用作在应用中传递用户数据的容器,用户在控制器类中声明,并通过访问器方法供视图使用。

private User user;

/**
 * @return the user
 */
public User getUser() {
    if(user == null){
        user = new User();
    }
    return user;
}

/**
 * @param user the user to set
 */
public void setUser(User user) {
    this.user = user;
}

CDI 控制器类包含 JSF 应用视图的业务逻辑。在该解决方案中,名为 HelloJsfController 的类管理 HelloJsf 应用的处理和数据。控制器的代码可以在配方 17-4 中看到。控制器负责向 JSF 视图显示字段和操作方法,以便可以将数据直接提交到字段中并进行相应的处理。控制器还有助于与终端用户的通信,因为可以创建消息来清楚地指示处理是否成功或者是否出现了问题,并且可以使消息对视图可用。

应用的视图是一个 XHTML 文件 index.xhtml,它包含一个通过 JSF

标记的 HTML 表单。在视图的顶部,导入了所需的名称空间,以便可以利用 PrimeFaces 和 JSF HTML 组件。该表单由许多 HTML 元素和 JSF 组件组成。PrimeFaces 组件必须以“p”为前缀,因为 PrimeFaces 名称空间被分配给该字母。每个 JSF 组件都包含许多属性,可用于设置值和配置组件的行为和功能。消息组件用于显示通过 FacesContext 提供的消息。p:outputLabel 组件呈现为 HTML 标签,p:inputText 组件呈现为 Text 类型的 HTML 输入元素。p:inputText 组件的 value 属性包含 JSF 表达式语言,引用 HelloJsfController 用户对象字段。最后,p:commandButton 组件呈现一个 HTML 按钮(类型为“submit”的输入元素)来提交表单。commandButton 的 action 属性也利用 JSF 表达式语言来调用名为 createUser()的控制器操作方法。ajax="false "属性表明 ajax 不应该用来异步处理表单值,而是应该提交和刷新表单。

这个菜谱包含了很多信息,但是它展示了用托管控制器类开发一个简单的 JSF 视图是多么容易。在实际应用中,数据可能存储在 RDBMS 中,如 Oracle 等。下一个诀窍是如何添加一个数据库并将其绑定到应用来存储和检索用户对象。

17-3.开发数据模型

问题

您希望将来自 Java EE 应用的数据存储在关系数据库中。

解决办法

将应用中的数据绑定到 Java 对象,以便可以使用这些对象来存储和检索数据库中的数据。在大多数情况下,Java 持久性 API (JPA)是处理 Java 对象形式的数据的合适选择。在前面的配方中,开发了一个 JSF 应用,用于将用户对象提交给 CDI 控制器。在这个配方中,数据将被绑定到一个实体类,然后使用 JPA 从关系数据存储中存储/检索。

出于这个菜谱的目的,将使用 Apache derby 数据库。首先,创建一个数据库表来存储用户对象。以下 SQL 可用于生成表,该表包括标识为 ID 的主键字段。

CREATE TABLE HELLO_USER (
ID                          NUMERIC PRIMARY KEY,
FIRST_NAME                  VARCHAR(100),
LAST_NAME                   VARCHAR(50),
EMAIL                       VARCHAR(150));

一旦创建了数据库表,就生成一个相应的实体类。对于此解决方案,将使用 NetBeans IDE 来自动创建该类。为此,右键单击 HelloJsf 项目的“Source Packages”节点,并创建一个名为 org . Java 9 recipes . hello JSF . Entity 的包。出现“从数据库新建实体类”对话框后,为 Apache Derby 数据库选择或创建一个 JDBC 数据源。选中后,从可用表列表中选择用户表,并将其添加到“选定的表”列表中,然后选择“下一步”在随后的对话框屏幕上,接受所有默认设置,点击“完成”并创建实体类(图 17-10 )。

A323910_3_En_17_Fig10_HTML.jpg

图 17-10。在 NetBeans IDE 中从数据库创建实体类

一旦创建了实体类,开发一个 EJB 或 JAX-RS RESTful web 服务类来处理相应的实体。在此解决方案中,将使用 NetBeans IDE 开发一个 EJB,首先在名为 org . Java 9 recipes . hello JSF . session 的项目中创建另一个新包。此包将用于保存会话 beans 或 EJB。接下来,右键单击新创建的包,并从上下文菜单中选择“实体类的会话 Beans”。这将打开允许选择实体类的对话框,以便 NetBeans IDE 可以自动创建相应的会话 beans(图 17-11 )。

A323910_3_En_17_Fig11_HTML.jpg

图 17-11。选择实体类以生成会话 beans

选择后,选择“下一步”,最后选择“完成”以创建 EJB。这样做之后,NetBeans IDE 将生成一个名为 AbstractFacade 的抽象类,它将由生成的任何实体类进行扩展。NetBeans IDE 还将生成会话 bean hellojsfacade。一旦生成了这些类,应用的模型就完成了,控制器将能够成功地处理数据。

它是如何工作的

企业应用的模型是最重要的组件之一,因为数据是企业的核心。要为 Java EE 应用生成模型,必须有一个数据存储,通常是一个 RDBMS,并且必须编码一个对象关系映射策略,以代码格式表示数据库。在这个解决方案中,模型由三个类组成:实体类、包含标准对象关系映射方法的抽象类和扩展抽象类的 EJB。

实体类本质上是一个普通的旧 Java 对象(POJO ),它将数据库表表示为一个 Java 对象。实体类为数据库表的每一列都声明了一个字段,并为每个字段定义了访问器方法。注释使实体类像魔术一样工作,通过几个简单的注释执行绑定类的任务,随后将字段绑定到数据库表及其列。@Entity 注释告诉编译器这是一个实体类。表 17-1 列出了一些常见的实体类注释。

表 17-1。公共实体类注释
|

注释

|

描述

| | --- | --- | | @实体 | 将类标记为实体类。 | | @表格 | 将实体类映射到数据库表。 | | @Id | 降级实体类的主键字段。 | | @XmlRootElement | 将类映射到 XML 元素。 | | @ NamedQueries | 将名称映射到预定义查询的@NamedQuery 元素列表。 | | @可嵌入 | 降级嵌入的类。 |

通过用@Table 对实体类进行注释,并将数据库表的名称指定为属性,将实体类映射到命名的数据库表。为了方便起见,NetBeans IDE 还向实体类添加了几个注释,分别是@XmlRootElement 和@NamedQueries。@XmlRootElement 注释将 XML 根元素与类相关联,从而使实体类可用于基于 XML 的 API,如 JAX-RS 和 JAXB。@NamedQueries 注释为实体提供了许多命名查询(每个字段一个),使得按名称查询实体类变得容易,而不是每次需要查询时都编写 JPQL。实体类也总是包含一个主键,它通过@Id 注释来表示,数据库表的每一列都用@Column 映射到类字段。Bean 验证也可以添加到实体类的字段中,为添加到相关实体类字段中的任何输入或内容提供验证。最后,一个实体类包含一个 equals()方法来帮助比较对象和实体,以及一个 toString()方法。HelloUser 的最终实体类应该如下所示:

@Entity
@Table(name = "HELLO_USER")
@XmlRootElement
@NamedQueries({
    @NamedQuery(name = "HelloUser.findAll", query = "SELECT h FROM HelloUser h"),
    @NamedQuery(name = "HelloUser.findById", query = "SELECT h FROM HelloUser h WHERE h.id = :id"),
    @NamedQuery(name = "HelloUser.findByFirstName", query = "SELECT h FROM HelloUser h WHERE h.firstName = :firstName"),
    @NamedQuery(name = "HelloUser.findByLastName", query = "SELECT h FROM HelloUser h WHERE h.lastName = :lastName"),
    @NamedQuery(name = "HelloUser.findByEmail", query = "SELECT h FROM HelloUser h WHERE h.email = :email")})
public class HelloUser implements Serializable {

    private static final long serialVersionUID = 1L;
    @Id
    @Basic(optional = false)
    @NotNull
    @Column(name = "ID")
    private Integer id;
    @Size(max = 100)
    @Column(name = "FIRST_NAME")
    private String firstName;
    @Size(max = 50)
    @Column(name = "LAST_NAME")
    private String lastName;
    // @Pattern(regexp="[a-z0-9!#$%&'*+/=?^_`{|}∼-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}∼-]+)*@(?:a-z0-9?\\.)+a-z0-9?", message="Invalid email")//if the field contains e-mail address consider using this annotation to enforce field validation
    @Size(max = 150)
    @Column(name = "EMAIL")
    private String email;

    public HelloUser() {
    }

    public HelloUser(Integer id) {
        this.id = id;
    }

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    @Override
    public int hashCode() {
        int hash = 0;
        hash += (id != null ? id.hashCode() : 0);
        return hash;
    }

    @Override
    public boolean equals(Object object) {
        // TODO: Warning - this method won't work in the case the id fields are not set
        if (!(object instanceof HelloUser)) {
            return false;
        }
        HelloUser other = (HelloUser) object;
        if ((this.id == null && other.id != null) || (this.id != null && !this.id.equals(other.id))) {
            return false;
        }
        return true;
    }

    @Override
    public String toString() {
        return "org.java9recipes.hellojsf.entity.HelloUser[ id=" + id + " ]";
    }

}

一旦生成了一个实体类,就可以生成一个会话 bean 来促进实体类的工作。会话 bean(又名 EJB)声明一个 PersistenceContext,它提供与底层数据存储的通信。然后它调用 PersistenceContext 来执行任意数量的 JPA 任务,比如通过实体类数据创建、更新或删除数据库中的记录。NetBeans IDE 生成 AbstractFacade 抽象类,该类由项目的所有实体类扩展。这个类本质上包含了允许对实体进行基本操作的方法:create()、findAll()、edit()和 remove(),使开发人员能够自动访问这些方法,而无需为每个实体类重新编码。这给开发人员留下了一个没有任何编码的全功能会话 bean。如果需要创建针对实体的附加查询或工作,开发人员可以相应地修改会话 bean 的内容,在本例中是 HelloJsfFacade。

EJB 必须用@Stateful 或@Stateless 注释,以指定该类是有状态会话 bean 还是无状态会话 bean。有状态会话 bean 可以绑定到单个用户会话,允许在整个用户会话中管理状态。无状态更常用于在应用的所有用户会话中共享会话 bean。名为 HelloJsfFacade 的简单无状态会话 bean 如下所示:

@Stateless
public class HelloUserFacade extends AbstractFacade<HelloUser> {

    @PersistenceContext(unitName = "org.java9recipes_HelloJsf_war_1.0-SNAPSHOTPU")
    private EntityManager em;

    @Override
    protected EntityManager getEntityManager() {
        return em;
    }

    public HelloUserFacade() {
        super(HelloUser.class);
    }

}

本菜谱中讨论的类和代码构成了应用的模型。总之,该模型将应用绑定到底层数据存储,从而使得使用 Java 对象创建、移除、更新和删除数据成为可能,而不是通过 SQL 直接使用数据库。关于开发实体类的更多信息,请参见在线 Java EE 教程:docs.oracle.com/javaee/7/tutorial/

17-4.编写视图控制器

问题

您已经开发了一个 JSF 视图,其中包含绑定字段和一个表单,您需要创建业务逻辑来处理表单并简化会话 bean 的工作。

解决办法

创建一个托管 bean 控制器类(CDI bean ),该类可用于将操作和字段绑定到 JSF 视图,并简化需要在 EJB 会话 bean 中执行的工作。在这个解决方案中,HelloJsfController 类(如下所示)被用作 CDI 控制器。

@Named
@ViewScoped
public class HelloJsfController implements java.io.Serializable {

    private User user;

    /**
     * @return the user
     */
    public User getUser() {
        if(user == null){
            user = new User();
        }
        return user;
    }

    /**
     * @param user the user to set
     */
    public void setUser(User user) {
        this.user = user;
    }

    public void createUser(){
        FacesContext context = FacesContext.getCurrentInstance();
        context.addMessage(null, new FacesMessage("Successfully Added User: " +
                            user.getFirstName() + " " + user.getLastName()));
        user = null;
    }

}

在前面的菜谱中,数据模型被添加到 HelloJsf 应用中。接下来,需要修改控制器类和视图以利用数据模型。要修改控制器,只需添加一个新的 HelloUser 类型的私有字段,并为其生成访问器方法。在 getHelloUser 方法中,首先检查字段是否为 null,如果是,实例化一个新的实例。

...
private HelloUser helloUser;
...
public HelloUser getHelloUser() {
        if(helloUser == null){
            helloUser = new HelloUser();
        }
        return helloUser;
}

public void setHelloUser(HelloUser helloUser) {
        this.helloUser = helloUser;
}
...

接下来,将 EJB 注入控制器类,这样就可以持久保存一个新的 HelloUser。为此,注入一个新的 HelloUserFacade 类型的私有字段,如下所示:

@EJB
private HelloUserFacade helloUserFacade;

最后,创建一个名为 createAndPersistUser()的新操作方法,它通常与 createUser()方法做相同的事情。然而,这个新方法将通过调用 EJB 把一个更高级的对象持久化到数据库中。

public void createAndPersistUser(){
    FacesContext context = FacesContext.getCurrentInstance();
    helloUserFacade.create(helloUser);
    context.addMessage(null, new FacesMessage("Successfully Persisted User: " +
                        user.getFirstName() + " " + user.getLastName()));
    helloUser = null;
}

数据模型现在已经集成到控制器逻辑中。当用户单击视图中的按钮时,它应该调用控制器中的操作方法 createAndPersistUser()。表单中包含的字段也通过控制器处理,因为用户对象被注入并暴露给用户界面。

它是如何工作的

JSF 管理的 bean 控制器用于促进 Java EE 应用的视图和会话 bean 之间的工作。过去,受管 bean 控制器遵循一套不同的规则,因为 JSF 包含自己的一套用于开发受管 bean 的注释。在 Java EE 的最近版本中,特定于 JSF 的托管 bean 已经被淘汰,CDI beans 已经取而代之,从而允许更具内聚性和通用性的控制器类。

在该解决方案中,该类实现 java.io.Serializable,因为在会话突然结束的情况下,可能需要将它保存到磁盘上。这个类用@Named 进行了注释,使它可以通过 JSF 表达式语言进行注入和访问。该类还用指定的 CDI 作用域进行了注释,在本例中为@ViewScoped,以指示控制器的 CDI 作用域。有许多不同的范围,这些包括在配方 17-6 中。ViewScoped 意味着控制器状态将在视图的生存期内保存。用户对象在控制器中被声明为私有字段,并且可以通过访问器方法作为属性进行访问。最后,该类包含一个名为 createUser()的方法,该方法是公共的,它创建一个 FacesMessage 对象并将其放入当前的 FacesContext 中以在屏幕上显示。然后,用户对象被设置为 null。

控制器类的修改版本(包括数据模型)声明了 HelloUser 类型的实例字段。创建 HelloUser 字段的访问器方法,如果该字段为空,则在 getter 方法中创建一个新实例。HelloUserFacade 被注入到控制器中,以便它可以用来执行数据模型事务(也称为:数据库事务)。createAndPersistUser()方法调用 HelloUserFacade create()方法,传递一个 HelloUser 实例将对象持久化到数据库中。类似地,如果希望编辑 HelloUser 对象,可以调用 HelloUserFacade edit()方法。最后,如果希望删除用户,可以调用 remove()方法。

控制器类可以包含任意数量的动作方法和字段声明,但是,管理控制器的大小以使控制器不负责执行过多的工作是很重要的。如果一个控制器类包含太多的功能,例如,如果它用于支持多个视图,那么它会变得很麻烦,很难维护。要了解更多关于控制器类的 CDI 作用域,请参阅配方 17-6。

17-5.开发异步视图

问题

与旧式的提交和响应 web 应用不同,您希望生成一个现代化的 ui,它可以异步提交数据和发布响应,而无需刷新浏览器页面或重新呈现视图,从而提供更好的用户体验。

解决办法

将异步 JavaScript 和 XML 合并到您的应用中,以异步方式向服务器发送数据,并在不刷新的情况下呈现响应。为 JSF 应用创建基于 AJAX 的视图有很多种方法,这个方法将演示如何利用 PrimeFaces AJAX API。在这个解决方案中,将创建一个名为 helloAjax.xhtml 的新视图(图 17-12 ),它通常是利用 Ajax 提交表单的原始 index.xhtml 视图的副本。该视图还将异步更新 messages 组件,显示控制器类生成的消息。helloAjax.xhtml 中还添加了一个 dataTable 组件,它被异步更新以显示已经创建并保存到数据库中的用户列表。增强视图如下所示:

A323910_3_En_17_Fig12_HTML.jpg

图 17-12。异步形式
<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:p="http://primefaces.org/ui">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
    <h:body>
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <h:inputText id="firstNameType" value="#{helloJsfController.freeText}">
                <f:ajax execute="@this" event="keyup" listener="#{helloJsfController.displayText}"
                        render="messages"/>
            </h:inputText>
            <p:messages id="messages"/>
            <br/>
            <p:panelGrid columns="2" style="width: 100%">

                <p:outputLabel for="firstName" value="First: "/>
                <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>

                <p:outputLabel for="lastName" value="Last: " />
                <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>

                <p:outputLabel for="email" value="Email: " />
                <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            </p:panelGrid>
            <br/>
            <p:commandButton id="submitUser" value="Submit"
                             action="#{helloJsfController.createUser()}"
                             update="messages, helloUsers"/>

            <br/>
            <p:dataTable id="helloUsers" var="user" value="#{helloJsfController.helloUserList}">
                <p:column headerText="First Name">
                    <h:outputText value="#{user.firstName}"/>
                </p:column>
                <p:column headerText="Last Name">
                    <h:outputText value="#{user.lastName}"/>
                </p:column>
                <p:column headerText="Email">
                    <h:outputText value="#{user.email}"/>
                </p:column>
            </p:dataTable>

        </h:form>
    </h:body>
</html>

它是如何工作的

将 AJAX 的原理应用于 JSF 视图是非常容易的。有几种不同的方法来应用 AJAX 功能,但是最简单的方法是利用一个复杂的用户界面框架,比如 PrimeFaces,它包含了内置的 AJAX 功能。事实上,许多 PrimeFaces 组件默认执行 ajax 提交,因此它们包含一个 AJAX 属性,可以设置为 false 以便以同步方式操作。

在这个配方的解决方案中,PrimeFaces commandButton 用于将表单内容异步发送到控制器类。一旦调用了 action 方法,数据就被持久化,并生成 FacesMessage,然后响应被发送回视图。当视图收到响应时,它异步更新 commandButton update 属性中列出的组件,即 messages 组件和 helloUsers dataTable 组件。

通过在组件的开始和结束标签之间嵌入标签,可以异步提交没有 PrimeFaces 的 JSF 组件的内容。f:ajax 标签使用 execute 属性来指示视图的哪个部分将被异步执行或提交,使用 onevent 属性来指示哪个 JavaScript 事件应该调用异步操作,使用 listener 属性来绑定操作方法,等等。例如,通过使用 f:ajax 标记,下面的 inputText 组件变成了异步的:

<h:inputText id="firstNameType" value="#{helloJsfController.freeText}">
   <f:ajax execute="@this" event="keyup" listener="#{helloJsfController.displayText}"
                        render="messages"/>
</h:inputText>

在该示例中,当 keyup 事件发生时,在 inputText 字段中键入的值被提交给 helloJsfController.freeText 属性。displayText()操作方法也被调用,该方法将 freeText 属性的内容放入 FacesMessage 中,如下所示。一旦动作被调用并且请求被发回,消息组件就会被更新,因为 f:ajax 的 render 属性指定了它的 id。

public void displayText(AjaxBehaviorEvent evt){
    FacesContext context = FacesContext.getCurrentInstance();
    System.out.println("test: " + freeText);
    context.addMessage(null, new FacesMessage(freeText));
}

有许多不同的技术可以用来异步更新 JSF 视图。在众多可用的 UI 库中,还有更多可用的异步组件。虽然这个解决方案演示了 PrimeFaces 以及 f:ajax 标签的使用,但是可以就这个主题写一些小书。JSF 是一个成熟的网络框架,提供了大量的工具来完成这项工作。选择最适合这种情况的方法,享受使用 AJAX 和限制暴露于底层 JavaScript 的便利。

17-6.应用正确的范围

问题

您正在开发一个 JSF 应用,并且您希望确保控制器被配置为根据功能和需求在正确的时间内保持在范围内。

解决办法

利用 CDI 作用域将所需的作用域应用于控制器类。例如,如果控制器类包含与整个会话相关的逻辑和数据,则使用 javax . enterprise . context . session scoped 对该类进行注释。但是,如果控制器类仅与请求级别相关,则使用 javax . enterprise . context . request scoped 对该类进行注释。根据此逻辑将每个不同的范围应用于控制器类。

它是如何工作的

控制器类范围可以完全改变应用运行的方式。控制器在范围内的时间长短会对应用的各个视图产生很大的影响。幸运的是,对不同的控制器应用不同的作用域是一件容易的事情。然而,编程方法会根据控制器类所处的范围而发生巨大的变化。CDI 提供了许多可以利用的示波器,如表 17-2 所示。

表 17-2。CDI 示波器
|

范围

|

持续时间

| | --- | --- | | @应用范围 | 状态在应用中的所有用户会话之间共享。 | | @依赖 | 对象接收与客户端 bean 相同的生命周期。(默认范围) | | @会话范围 | 开发人员控制对话的开始和结束,并且在整个对话过程中维护状态。 | | @ RequestScoped | 状态持续单个 HTTP 请求的持续时间。 | | @会话范围 | 状态在用户会话期间持续存在。 | | javax.faces.view.ViewScoped | 只要 NavigationHandler 没有导致导航到不同的 viewId,状态就会持续。 |

如前所述,在应用作用域时要记住的一件事是,控制器选择的作用域会影响应用的其余部分。如果控制器将包含在整个用户会话中有用的数据,那么@SessionScoped 可能是最佳选择。请记住,@SessionScoped bean 中的所有数据都将在整个会话期间保留。因此,如果在 bean 中声明并填充了列表,则必须以编程方式刷新或更改 bean 的内容。如果使用某个范围导致 bean 在整个用户会话过程中被刷新,情况就不一样了。例如,如果同一个 bean 是@RequestScoped,那么列表中的数据将在每次发出请求时被重新查询和填充。

注意

范围还会对与其他受管 beans 的交互产生很大影响。注入相同范围的 beans 是很重要的

17-7.生成和应用模板

问题

您希望在应用的所有视图中应用相同的可视化模板。

解决办法

利用 Facelets 模板并应用于每个视图。要创建模板,必须首先开发一个新的 XHTML 视图文件,然后向其中添加适当的 HTML/JSF/ XML 标记。来自其他视图的内容将取代 ui:一旦模板被应用到一个或多个 JSF 视图,就在模板中插入元素。下面的源代码来自一个名为 template.xhtml 的模板,该模板将应用于 HelloJsf 应用中的所有视图:

<html 
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:p="http://primefaces.org/ui">

    <h:head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <h:outputStylesheet library="css" name="default.css"/>
        <h:outputStylesheet library="css" name="cssLayout.css"/>

        <title>Hello JSF</title>

    </h:head>

    <h:body>

        <p:growl id="growl" life="3000" />

        <p:layout fullPage="true">
            <p:layoutUnit position="north" size="65" header="#{bundle.AppName}">
                <h:form id="menuForm">
                    <p:menubar>
                        <p:menuitem value="Home" outcome="/index.xhtml" icon="ui-icon-home"/>

                           <p:menuitem value="Hello Main" outcome="/helloUser.xhtml" />
                              <p:menuitem value="PrimeFaces" outcome="/welcomePrimefaces.xhtml" />
                           <p:menuitem value="Hello Ajax" outcome="/helloAjax.xhtml" />

                    </p:menubar>
                </h:form>
            </p:layoutUnit>

            <p:layoutUnit position="south" size="60">
                <ui:insert name="footer"/>
            </p:layoutUnit>

            <p:layoutUnit position="center">
                <ui:insert name="content"/>
            </p:layoutUnit>

        </p:layout>

    </h:body>

</html>

模板定义了应用视图的整体结构。但是,它可以使用 CSS 样式表来声明模板中每个元素的格式。样式表应该包含在应用的资源目录中,以便视图可以访问它。也可以在模板中使用 JSF EL。如果使用 EL,通常由会话或应用范围的受管 bean 来驱动内容。模板的 JSF 客户端视图将包含视图内容周围的标签,以及属于模板内相应标签的标记命名段周围的标签。下面的视图是前面显示的模板的客户端视图的示例。

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html 
      xmlns:h="http://xmlns.jcp.org/jsf/html"
      xmlns:f="http://xmlns.jcp.org/jsf/core"
      xmlns:p="http://primefaces.org/ui"
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets">

      <ui:composition template="layout/template.xhtml">

        <ui:define name="content">
        Hello from Facelets
        <br />
        <h:link outcome="welcomePrimefaces" value="Primefaces welcome page" />
        <br/>
        <h:form>
            <p:messages id="messages"/>
            <br/>

            <p:outputLabel for="firstName" value="First: "/>
            <p:inputText id="firstName" value="#{helloJsfController.user.firstName}"/>
            <br/>
            <p:outputLabel for="lastName" value="Last: " />
            <p:inputText id="lastName" value="#{helloJsfController.user.lastName}"/>
            <br/>
            <p:outputLabel for="email" value="Email: " />
            <p:inputText id="email" value="#{helloJsfController.user.email}"/>
            <br/>
            <p:commandButton id="submitUser" value="Submit"
                             action="#{helloJsfController.createUser()}"
                             update="messages, helloUsers"/>

            <br/>
            <p:dataTable id="helloUsers" var="user" value="#{helloJsfController.helloUserList}">
                <p:column headerText="First Name">
                    <h:outputText value="#{user.firstName}"/>
                </p:column>
                <p:column headerText="Last Name">
                    <h:outputText value="#{user.lastName}"/>
                </p:column>
                <p:column headerText="Email">
                    <h:outputText value="#{user.email}"/>
                </p:column>
            </p:dataTable>

        </h:form>
        </ui:define>

    </ui:composition>

</html>

它是如何工作的

为了创建统一的应用体验,视图应该是一致的,因为它们看起来相似,并且以统一的方式运行。开发 web 页面模板的想法已经存在很多年了,但不幸的是,许多模板实现在每个应用页面上都包含重复的标记。虽然为每个单独的 web 页面复制相同的布局是可行的,但这造成了维护上的一场噩梦。当需要更新页眉中的单个链接时会发生什么?如果模板在每个页面上都被复制,这样的难题将导致开发者访问并手动更新应用的每个网页。Facelets 视图定义语言为视图模板的开发提供了一个健壮的解决方案,这是使用 JSF 技术的主要好处之一。

Facelets 提供了将单个模板应用于应用中一个或多个视图的能力。这意味着开发人员可以创建一个视图来构造页眉、页脚和模板的其他部分,然后这个视图可以应用于任何数量的负责包含主视图内容的其他视图。这种技术减轻了诸如更改页面标题中的单个链接之类的问题,因为现在模板可以用新的链接进行更新,应用中的每个其他视图都将自动反映这种更改。

要使用 Facelets 创建模板,请创建一个 XHTML 视图,声明所需的名称空间,然后相应地添加 HTML、JSF 和 Facelets 标记,以设计所需的布局。模板可以被认为是 web 视图的“外壳”,因为它可以包含任意数量的其他视图。同样,任意数量的 JSF 视图可以应用相同的模板,因此应用的整体外观将保持不变。

负责控制视图布局的 Facelets 标记。为了利用这些 Facelets 标记,您需要在模板的元素中声明 Facelets 标记库的 XML 名称空间。注意,这里还指定了标准 JSF 标记库的 XML 名称空间。

<html 
      xmlns:ui="http://xmlns.jcp.org/jsf/facelets"
      xmlns:h="http://xmlns.jcp.org/jsf/html">
...

Facelets 包含许多特殊的标记,可以用来帮助控制页面流和布局。中的表 17-3 列出了对控制页面流和布局有用的 Facelets 标签。本例模板中使用的惟一 Facelets 标记是 ui:insert。ui:insert 标记包含一个 name 属性,该属性被设置为将包含在视图中的相应 ui:define 元素的名称。看看这个例子的源代码,您可以看到下面的 ui:insert tag:

表 17-3。Facelets 页面控件和模板标签
|

标签

|

描述

| | --- | --- | | 用户界面:组件 | 定义模板组件并指定组件的文件名 | | ui:合成 | 定义页面组合并封装所有其他 JSF 标记 | | 用户界面:调试 | 创建调试组件,该组件在呈现组件时捕获调试信息,即组件树的状态和应用中的作用域变量 | | 不明确的 | 定义由模板插入页面的内容 | | ui:装饰 | 页面的装饰部分 | | ui:片段 | 定义一个模板片段,很像 ui:component,除了没有忽略标记之外的所有内容 | | 用户界面:包括 | 允许在视图中封装和重用另一个 XHTML 页面 | | 用户界面:插入 | 将内容插入模板 | | ui:停止 | 将参数传递给包含的文件或模板 | | ui:重复 | 迭代一组数据 | | 用户界面:删除 | 从页面中删除内容 |

<ui:insert name="content">Content</ui:insert>

如果一个视图使用模板,也就是模板客户端,它必须在视图标签中列出模板。在中,视图必须指定一个与同名的标签,然后放置在开始和结束标签之间的任何内容都将被插入到视图的那个位置。但是,如果模板客户端不包含与标签同名的标签,那么将显示模板内开始和结束标签之间的内容。

摘要

Java EE web 应用的开发可能是一个非常大的主题,这一章只是触及了可以利用的许多技术中的一些。JSF web 框架是成熟和健壮的,它为开发复杂和易于使用的应用提供了许多选择。结合底层的 Java EE 技术,包括 EJB、JAX-RS、JPA 和其他技术,Java web 开发功能强大且易于上手。

十八、Nashorn 和脚本

在 Java 6 中,javax.script 包用于将脚本语言与 Java 结合在一起。它使开发人员能够将脚本语言编写的代码直接嵌入到 Java 应用中。这开创了新一代多语言应用,因为开发人员能够构建包含用 JavaScript 和 Python 等语言编写的脚本的 Java 解决方案。Java 6 中使用的 JavaScript 引擎叫做 Rhino。它是 JavaScript 引擎的一个实现,完全用 Java 开发。虽然它包含完整的 JavaScript 实现,但它是一个较旧的引擎,不再符合当前的 JavaScript 标准。

Java 8 引入了一个新的 JavaScript 引擎,叫做 Nashorn。它基于 ECMAScript-262 Edition 5.1 语言规范,支持 Java 6 中引入的 javax.script API。除了为 Java 平台带来一个现代的 JavaScript 引擎之外,Nashorn 还包含一些新特性,使得开发 JavaScript 和 Java 解决方案变得更加容易和健壮。名为 jjs 的新命令行工具提供了超越 jrunscript 的脚本功能。Nashorn 还可以完全访问 JavaFX 8 API,允许开发人员完全用 JavaScript 构建 JavaFX 应用。

JDK 9 通过在发布时包含一组来自 EMCAScript 6 规范的精选功能,进一步提高了 Nashorn 的可用性。随着时间的推移,EMCAScript 6 的更多功能可能会被纳入 JDK 9 的更新和 JDK 的后续版本中。

本章涉及使用 Nashorn 引擎来构建集成 Java 和 JavaScript 世界的解决方案。它没有涵盖 Nashorn 提供的所有功能,但是已经足够让您开始使用了。

18-1.从 Java 加载和执行 JavaScript

问题

您希望从 Java 应用中加载并执行 JavaScript 代码。

解决办法

使用 Nashorn 引擎执行 JavaScript,Nashorn 引擎是 Java 8 的下一代 JavaScript 引擎,用于执行 JavaScript 代码。可以调用 Nashorn 引擎来处理内嵌 JavaScript,或者直接在 Java 代码中处理外部 JavaScript 文件。使用 Java ScriptEngineManager 执行外部 JavaScript 文件或内嵌 JavaScript 代码。一旦获得了 ScriptEngineManager(),就获得了用于 JavaScript 代码执行的 Nashorn 引擎的实例。

在下面的示例中,Nashorn ScriptEngine 用于调用驻留在本地文件系统上的 JavaScript 文件。

public static void loadExternalJs(){
    ScriptEngineManager sem = new ScriptEngineManager();
    ScriptEngine nashorn = sem.getEngineByName("nashorn");
    try {
        nashorn.eval("load('src/org/java9recipes/chapter18/js/helloNashorn.js')");
    } catch (ScriptException ex) {
        Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
    }
}

helloNashorn.js 文件中的代码如下:

print("Hello Nashorn!");

接下来,我们来看看一些内联 JavaScript。在下面的示例中,获取了一个 Nashorn ScriptEngine,然后创建了一个 JavaScript 函数来获取地下水池的加仑数。然后执行该函数以返回结果。

    public static void loadInlineJs(){
        ScriptEngineManager sem = new ScriptEngineManager();
        ScriptEngine nashorn = sem.getEngineByName("nashorn");
        try {
            nashorn.eval("function gallons(width, length, avgDepth){var volume =
                          avgDepth * width * length;" +
                          "return volume * 7.48; }");
            nashorn.eval("print('Gallons of water in pool: '+ gallons(16,32,5))");
        } catch (ScriptException ex) {
            Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

}

结果:

run:
Hello Nashorn!
Gallons of water in pool: 19148.800000000003

它是如何工作的

使用 Nashorn 引擎在 Java 应用中执行 JavaScript 有几种不同的方法。例如,可以从名为 jjs 的命令行界面(CLI)调用 Nashorn,或者可以使用 ScriptEngineManager。在这个菜谱中,示例涵盖了两种使用 Nashorn 执行 JavaScript 的技术,每种技术都需要使用 ScriptEngineManager,自 Java 6 以来,script engine manager 一直是 JDK 的一部分。要从 ScriptEngineManager 获取 Nashorn 引擎,首先创建 ScriptEngineManager 的新实例。一旦获得引擎,就可以通过将表示所需引擎的字符串值传递给 getEngineByName()方法来获得特定的引擎。在这种情况下,您传递名称 nashorn 来获得用于处理 JavaScript 的 Nashorn 引擎。获得 Nashorn 引擎后,您就可以通过调用引擎的 eval()方法来调用 JavaScript 文件或评估内联 JavaScript 代码了。

这个菜谱中的第一个代码示例演示了如何将一个 JavaScript 文件传递给引擎进行调用。本例中的 helloNashorn.js 包含一行 JavaScript,它打印一条消息,但不返回任何结果。执行一个. js 文件最困难的部分可能是必须确保该文件包含在类路径中,或者将文件的完整路径传递给 eval()方法。

第二个代码示例演示了如何编写和评估内联 JavaScript。首先,定义一个标识为加仑的函数,它接受三个参数,并根据池的宽度、长度和平均深度返回加仑数。在随后的 eval()调用中,调用该函数,传递参数并返回结果。本例中需要注意的重要一点是,尽管 JavaScript 跨越了多个 eval()调用,但范围是保持不变的,因此引擎中的每个 eval()调用都可以看到在之前的调用中创建的对象。

从 Java 6 开始,在 Java 代码中使用脚本语言已经成为可能。Nashorn 引擎的获取方式与其他引擎相同,通过传递一个字符串来按名称表示引擎。这个 JavaScript 引擎与以前的 rendition Rhino 的区别在于,新的 JavaScript 引擎速度更快,并且更好地符合 EMCA 标准化的 JavaScript 规范。自 JDK 8 更新 40 以来,更新的 EMCAScript 6 规范中的一些功能已经移植到 Nashorn 中。由于更新后的规范中有大量新功能,它们将随着时间的推移通过 JDK 的各个版本添加进来。JDK 9 引入了对 EMCAScript 6 特性的重要子集的支持。

18-2.通过命令行执行 JavaScript

问题

出于原型或执行目的,您希望通过命令行执行 JavaScript

解决方案 1

调用 jjs 工具,它是 Java 的一部分。要执行 JavaScript 文件,从命令行调用 jjs 工具,然后传递要执行的 JavaScript 文件的完全限定名(如果不在类路径中,则包括路径)。例如,要执行 helloNashorn.js,请使用以下命令:

jjs /src/org/java9recipes/chapter18/js/helloNashorn.js
Hello Nashorn!

要将参数传递给 JavaScript 文件进行处理,请以相同的方式调用脚本,但要包括尾随的破折号-,后跟要传递的参数。例如,以下代码位于名为 helloParameter.js 的文件中:

#! /usr/bin/env
var parameter = $ARG[0];
print(parameter ? "Hello ${parameter}!": "Hello Nashorn!");

使用以下命令调用这个 JavaScript 文件,传递参数 Oracle:

jjs /src/org/java9recipes/chapter18/js/helloParameter.js – Oracle

结果如下:

Hello Oracle!

jjs 工具也可以用作交互式解释器,只需不带任何选项地执行 jjs 即可。命令解释器允许您在完全交互式的 JavaScript 环境中工作。在下面的代码行中,jjs 工具被调用来打开一个命令 shell,并声明和执行一个函数。最后,退出命令 shell。

jjs
jjs> function gallon(width, length, avgDepth){return (avgDepth * width * length) * 7.48;}
function gallon(width, length, avgDepth){return (avgDepth * width * length) * 7.48;}
jjs> gallon(16,32,5)
19148.800000000003
jjs> exit()

解决方案 2

利用 JSR 223 jrunscript 工具来执行 JavaScript。要执行 JavaScript 文件,请从命令行调用 jrunscript 工具,并传递要执行的 JavaScript 文件的完全限定名(如果不在类路径中,则包括路径)。例如,要执行 helloNashorn.js,请使用以下命令:

jrunscript /src/org/java9recipes/chapter18/js/helloNashorn.js
Hello Nashorn!

也许您想以内联方式传递 JavaScript 代码,而不是执行 JavaScript 文件。在这种情况下,您可以使用–e 标志调用 jrunscript,并以内联方式传递脚本。

jrunscript -e "print('Hello Nashorn')"
Hello Nashorn
注意

如果使用 jrunscript 实用程序,则字符串插值不可用。因此,您必须使用串联来实现类似的效果。要了解更多关于字符串插值的信息,请参考配方 18-3。

与 jjs 类似,jrunscript 工具也接受传递给 JavaScript 文件进行处理的参数。要使用 jrunscript 工具传递参数,只需在调用脚本时将它们附加到命令中,每个参数用空格分隔。例如,要调用文件 helloParameter.js 并传递参数,请执行以下命令:

jrunscript src/org/java9recipes/chapter18/js/helloParameter.js Oracle

与 jjs 类似,jrunscript 工具可以执行交互式解释器,允许您动态开发和构建原型,如下图所示。

A323910_3_En_18_Figa_HTML.jpg

它是如何工作的

自从 Java SE 6 发布以来,使用来自 Java 的脚本语言已经成为可能。在这个菜谱中,演示了通过命令行或终端执行 JavaScript 的两种解决方案。在解决方案 1 中,您看到了 jjs 命令行工具,这是 Java 8 中的新功能。这个工具可以用来调用一个或多个 JavaScript 文件,或者启动一个交互式 Nashorn 解释器。在这个例子中,您了解了如何在传递参数和不传递参数的情况下调用 JavaScript 文件。您还了解了如何调用 jjs 作为交互式解释器。该工具包含几个有用的选项。要查看完整列表,请参考位于docs . Oracle . com/javase/9/docs/technotes/tools/windows/jjs . html的在线文档。jjs 工具是与 Nashorn 一起使用的理想工具,因为它包含比 jrunscript 工具更多的选项,jrunscript 工具在解决方案 2 中进行了演示。

jrunscript 工具是在 Java 6 中引入的,它允许您从命令行执行脚本或调用交互式解释器,类似于 jjs。不同之处在于,jrunscript 还允许您通过传递–l 标志以及脚本引擎名称来使用其他脚本语言。

jrunscript –l js myTest.js

jrunscript 工具也包含选项,但是与 jjs 提供的选项相比,它是有限的。要查看 jrunscript 的所有可用选项,请参考位于docs . Oracle . com/javase/9/docs/technotes/tools/windows/jrunscript . html的在线文档。

18-3.在字符串中嵌入表达式

问题

当通过 jjs 实用程序调用 JavaScript 时,您希望引用字符串中的表达式或值。

解决办法

当通过 jjs 工具将 Nashorn 用作 shell 脚本语言时,可以通过将表达式或值包含在双引号文本字符串中的美元符号$和花括号{}中,将它们嵌入到字符串中。以下 JavaScript 驻留在名为 recipe18_3.js 的文件中,它可以作为 shell 脚本由 jjs 工具执行。在这个例子中,字符串插值是有效的,因为通过将 shebang 作为第一行添加,脚本已经变得可执行。有关 shebang 的更多信息,请参考配方 18-10。

#! /usr/bin/env
function gallons(width, length, avgDepth){var volume = avgDepth * width * length;
                                         return volume * 7.48; }                                 
print("Gallons of water in pool: ${gallons(16,32,5)}");       

通过 jjs 执行 JavaScript 文件,如下所示:

jjs src/org/java9recipes/chapter18/js/recipe18_3.js
Gallons of water in pool: 19148.800000000003
注意

这个示例 JavaScript 文件不能从 ScriptEngineManager 运行,因为它包含一个 shebang(它是一个可执行脚本)。

它是如何工作的

当您使用 Nashorn 的 shell 脚本特性时,您可以通过用美元符号和花括号${...}.这个概念在 Unix 世界中被称为字符串插值,Nashorn 借用这个概念来简化开发用于评估和显示信息的 shell 脚本。字符串插值可以改变字符串的内容,用值替换变量和表达式。使用这个特性,很容易将变量的内容嵌入到字符串中,而不需要执行手动连接。

在这个食谱的例子中,一个存储在. js 文件中的脚本包含一个嵌入式表达式,它调用一个 JavaScript 函数来返回计算出的液体加仑数。这可能是现实世界场景中最有用的技术,但是在使用 jjs 工具作为交互式解释器时,也可以使用嵌入式表达式。

jjs -scripting
jjs> "The current date is ${Date()}"
The current date is Wed Apr 30 2014 23:44:41 GMT-0500 (CDT)
注意

如果您没有使用 jjs 的脚本特性,字符串插值将不可用。此外,文本字符串必须用双引号括起来,因为单引号中的字符串不会被插入。在示例中,shebang(!usr/bin/env)用于使脚本可执行,从而调用 jjs 的脚本特性。

18-4.传递 Java 参数

问题

您希望将 Java 参数传递给 JavaScript 来使用。

解决办法

利用 javax.script.SimpleBindings 实例为任何 Java 字段提供基于字符串的名称,然后将 SimpleBindings 实例传递给 JavaScript 引擎调用。在下面的例子中,一个 Java 字符串参数被传递给 Nashorn 引擎,然后通过 JavaScript 打印出来。

String myJavaString = "This is a Java parameter!";
SimpleBindings simpleBindings = new SimpleBindings();
simpleBindings.put("myString", myJavaString);
ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine nashorn = sem.getEngineByName("nashorn");
nashorn.eval("print (myString)", simpleBindings);

结果如下:

This is a Java parameter!

在 SimpleBindings 实例中可以传递多个 Java 类型值。在下面的示例中,在单个 SimpleBindings 实例中传递三个浮点值,然后将它们传递给一个 JavaScript 函数。

float width = 16;
float length = 32;
float depth = 5;
SimpleBindings simpleBindings2 = new SimpleBindings();
simpleBindings2.put("globalWidth", width);
simpleBindings2.put("globalLength", length);
simpleBindings2.put("globalDepth", depth);
nashorn.eval("function gallons(width, length, avgDepth){var volume = avgDepth * width * length; "+
        "        return volume * 7.48; }   " +
        "print(gallons(globalWidth, globalLength, globalDepth));", simpleBindings2);

结果:

19148.800000000003

它是如何工作的

要将 Java 字段值传递给 JavaScript,请使用 javax.script.SimpleBindings 构造,它基本上是一个 HashMap,可用于将值绑定和传递给 ScriptEngineManager。当值以这种方式传递给 Nashorn 引擎时,它们可以作为 JavaScript 引擎中的全局变量来访问。

18-5.将返回值从 JavaScript 传递到 Java

问题

您希望调用一个 JavaScript 函数,并将结果返回给调用它的 Java 类。

解决办法

创建一个与 Nashorn 一起使用的 ScriptEngine,然后将 JavaScript 函数传递给它进行评估。接下来,从引擎创建一个 Invocable,然后调用它的 invokeFunction()方法,传递 JavaScript 函数的基于字符串的名称,以及要使用的参数数组。在下面的示例中,一个名为 gals 的 JavaScript 函数被传递给 ScriptEngine 进行评估,稍后将使用这种技术调用它。然后,它返回一个双精度值。

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("nashorn");

// JavaScript code in a String
String gallonsFunction = "function gallons(width, length, avgDepth){var volume = avgDepth * width * length; "
        + " return volume * 7.48; } ";
try {
    // evaluate script
    engine.eval(gallonsFunction);
    double width = 16.0;
    double length = 32.0;
    double depth = 5.0;
    Invocable inv = (Invocable) engine;
    double returnValue = (double) inv.invokeFunction("gallons",
                                  new Double[]{width,length,depth});
    System.out.println("The returned value:" + returnValue);

} catch (ScriptException | NoSuchMethodException ex) {
    Logger.getLogger(Recipe18_5.class.getName()).log(Level.SEVERE, null, ex);
}

结果如下:

run:
The returned value:19148.800000000003

在下面的示例中,调用了一个 JavaScript 文件并返回一个字符串值。JavaScript 文件的名称是 recipe18_5.js,其内容如下:

function returnName( name){
    return "Hello " + name;
}

接下来,使用 ScriptEngine 创建一个 Invocable 并调用外部 JavaScript 文件中的 JavaScript 函数。

engine.eval("load('/path-to/src/org/java9recipes/chapter18/recipe18_05/js/recipe18_5.js')");
Invocable inv2 = (Invocable) engine;
String returnValue2 = (String) inv2.invokeFunction("returnName", new String[]{"Nashorn"});
System.out.println("The returned value:" + returnValue2);

它是如何工作的

嵌入式脚本最有用的特性之一是能够将通过脚本引擎调用的代码与 Java 应用集成在一起。为了有效地集成脚本引擎代码和 Java 代码,这两者必须能够相互传递值。这个菜谱涵盖了将值从 JavaScript 返回到 Java 的概念。为此,设置一个 ScriptEngine,然后将其强制转换为 javax . script . invokable 对象。然后,可以使用 Invocable 对象来执行脚本函数和方法,从这些调用中返回值。

Invocable 对象使您能够执行指定的 JavaScript 函数或方法,并将值返回给调用者。Invocable 还可以返回一个接口,该接口将提供调用脚本对象的成员函数的方法。为了提供这个功能,Invocable 对象包含了几个方法(见表 18-1 )。

表 18-1。可调用的方法
|

方法

|

描述

| | --- | --- | | getInterface(类) | 使用解释器编译的函数返回接口的实现。 | | getInterface(对象,类) | 使用已在解释器中编译的脚本对象的成员函数返回接口的实现。 | | invokeFunction(字符串,对象) | 对顶级过程和函数的调用。返回一个对象。 | | invokeFunction(对象,字符串,对象) | 调用上一次执行期间编译的脚本对象的方法。 |

在生成 Invocable 之前,JavaScript 文件或函数必须由 ScriptEngine 进行评估。该示例演示了如何调用 eval()方法来计算内联 JavaScript 函数(名为 gallonsFunction 的字符串),以及如何计算外部 JavaScript 文件。调用 eval()方法后,可以将 ScriptEngine 强制转换为 Invocable 对象,如下所示:

Invocable inv = (Invocable) engine;

然后可以调用 Invocable 来执行被评估的脚本代码中的函数或方法。表 18-1 列出了可以使用的调用方法。

在这个菜谱的例子中,invokeFunction 方法用于调用脚本中包含的函数。invokeFunction 的第一个参数是被调用函数的基于字符串的名称,第二个参数是作为参数传递的对象列表。Invocable 从 JavaScript 函数调用中返回一个对象,该对象可以被强制转换为适当的 Java 类型。

在 Java 和 ScriptEngine 实例之间共享值非常有用。在现实生活中,调用外部 JavaScript 文件并能够在 Java 代码和脚本之间来回传递值可能非常有用。如果需要,可以修改底层 JavaScript 文件,而无需重新编译应用。当您的应用包含一些需要不时更改的业务逻辑时,这种情况会非常有用。假设您有一个规则处理器,可以用来计算字符串,并且规则在不断发展。在这种情况下,可以将规则引擎编写为外部 JavaScript 文件,从而实现对该文件的动态更改。

18-6.使用 Java 类和库

问题

您希望在 Nashorn 解决方案中调用 Java 类和库。

解决办法

使用 Java.type()函数创建基于 Java 类或库的 JavaScript 对象。将您想要使用的 Java 类的基于字符串的全限定名称传递给该函数,并将其赋给一个变量。下面的代码表示一个名为 Employee 的 Java 对象,它将在这个应用中通过一个 JavaScript 文件使用。

package org.java9recipes.chapter18.recipe18_06;

import java.util.Date;
public class Employee {
    private int age;
    private String first;
    private String last;
    private String position;
    private Date hireDate;

    public Employee(){

    }

    public Employee(String first,
                    String last,
                    Date hireDate){
        this.first = first;
        this.last = last;
        this.hireDate = hireDate;
    }

    /**
     * @return the first
     */
    public String getFirst() {
        return first;
    }

    /**
     * @param first the first to set
     */
    public void setFirst(String first) {
        this.first = first;
    }

    /**
     * @return the last
     */
    public String getLast() {
        return last;
    }

    /**
     * @param last the last to set
     */
    public void setLast(String last) {
        this.last = last;
    }

...
}

接下来,让我们看看使用 Employee 类的 JavaScript 文件。这段 JavaScript 代码创建了几个雇员实例,然后将它们打印出来。它还使用 java.util.Date 类来演示标准 java 类的使用。

var oldDate = Java.type("java.util.Date");
var array = Java.type("java.util.ArrayList");
var emp = Java.type("org.java9recipes.chapter18.recipe18_06.Employee");

var empArray = new array();
var emp1 = new emp("Josh", "Juneau", new oldDate());
var emp2 = new emp("Joe", "Blow", new oldDate());
empArray.add(emp1);
empArray.add(emp2);
empArray.forEach(function(value, index, ar){
    print("Employee: " + value);
    print("Hire Date: " + value.hireDate);
});

最后,使用 ScriptEngineManager 执行 JavaScript 文件:

ScriptEngineManager sem = new ScriptEngineManager();
ScriptEngine nashorn = sem.getEngineByName("nashorn");
try {
    nashorn.eval("load('/path-to/employeeFactory.js');");
} catch (ScriptException ex) {
    Logger.getLogger(NashornInvoker.class.getName()).log(Level.SEVERE, null, ex);
}

结果如下:

Employee: Josh Juneau
Hire Date: Thu April 24 23:03:53 CDT 2016
Employee: Joe Blow
Hire Date: Fri April 25 12:00:00 CDT 2016

它是如何工作的

在 Nashorn 解决方案中使用 Java 类和库是非常自然的。该配方中的示例演示了如何使用专门为定制应用生成的 Java 类,以及如何使用 Java SE 中的 Java 类和库。为了使这些类对 JavaScript 可用,您必须从 JavaScript 内部调用 Java.type 函数,并传递要使用的基于字符串的 Java 类的完全限定名。Java.type 函数返回 Java 类型的 JavaScript 引用。在下面的示例摘录中,java.util.Date、java.util.ArrayList 和 Employee 类可用于使用这种技术的 JavaScript。

var oldDate = Java.type("java.util.Date");
var array = Java.type("java.util.ArrayList");
var emp = Java.type("org.java9recipes.chapter18.recipe18_06.Employee");

一旦 JavaScript 可以使用这些类型,就可以以类似于 Java 的方式调用它们。例如,在示例中,new oldDate()用于实例化 java.util.Date 的新实例。一个重要的区别是,您不使用 getters 和 setters 来调用 Java 属性。相反,您省略了方法的“get”或“set”部分,以小写字母开始字段名,从而直接调用字段。这使得从 JavaScript 中访问属性变得非常容易,并且更有效率和可读性。从脚本中的 forEach 循环可以看到这种访问的示例。要访问 employee hireDate 属性,只需调用 employee.hireDate 而不是 employee.getHireDate()。

从 JavaScript 中无缝访问 Java 的能力使得创建无缝的 Java 和 JavaScript 集成成为可能。

18-7.在 Nashorn 中访问 Java 数组和集合

问题

您需要从 Nashorn 解决方案中访问 Java 数组或集合。

解决办法

使用 Java.type 函数将 Java 数组强制转换为 JavaScript。一旦被强制,您可以通过调用 new 来实例化数组,然后通过按数字指定成员的索引来访问成员。在下面的例子中,Java int 数组类型是在 JavaScript 中创建的,然后它被实例化并用于存储。

jjs> var intArray = Java.type("int[]");
jjs> var intArr = new intArray(5);
jjs> intArr[0] = 0;
0
jjs> intArr[1] = 1;
1
jjs> intArr[0]
0
jjs> intArr.length
5

使用集合非常相似。要访问 Java 集合类型,需要调用 Java.type 函数,传递要创建的类型的基于字符串的名称。一旦获得了类型引用,就可以从 JavaScript 实例化和访问它。

jjs> var ArrayList = Java.type("java.util.ArrayList")
jjs> var array = new ArrayList();
jjs> array.add('hi');
true
jjs> array.add('bye');
true
jjs> array
[hi, bye]
jjs> var map = Java.type("java.util.HashMap")
jjs> var jsMap = new map();
jjs> jsMap.put(0, "first");
null
jjs> jsMap.put(1, "second");
null
jjs> jsMap.get(1);
second

它是如何工作的

要在 JavaScript 中使用 Java 数组和集合,需要调用 Java.type()函数并传递想要访问的 Java 类型的名称,将其赋给一个 JavaScript 变量。然后,JavaScript 变量可以被实例化,并以与在 Java 代码中使用 Java 类型相同的方式被利用。这个菜谱中的例子演示了如何从 JavaScript 中访问 Java 数组、数组列表和 HashMaps。

当使用 JavaScript 中的 Java 数组类型时,数组的类型必须传递给 Java.type()函数,包括一组空括号。一旦获得了类型并将其分配给 JavaScript 变量,就可以通过在括号中包含数组的静态大小来实例化它,就像在 Java 语言中实例化数组一样。类似地,可以通过指定索引来分配和检索数组中的值,从而访问数组。要返回并将一个 JavaScript 数组传递给 Java,可以使用 Java.to()函数,将 JavaScript 数组传递给它的 Java 类型的对应物。在下面的代码中,JavaScript 字符串数组被强制转换为 Java 类型。

jjs> var strArr = ["one","two","three"]
jjs> var javaStrArr = Java.type("java.lang.String[]");
jjs> var javaArray = Java.to(strArr, javaStrArr);
jjs> javaArray[1];
two
jjs> javaArray.class
class Ljava.lang.String;

集合与数组非常相似,因为必须使用 Java.type()函数来获取 Java 类型并将其赋给 JavaScript 变量。然后实例化该变量,并以与 Java 语言中相同的方式访问集合类型。

18-8.实现 Java 接口

问题

您希望利用 Nashorn 解决方案中的 Java 接口。

解决办法

创建接口的新实例,传递由属性组成的 JavaScript 对象。JavaScript 对象属性将实现接口中定义的方法。在下面的例子中,一个用于声明雇员职位类型的接口在一个 JavaScript 文件中实现。该示例演示自定义方法的实现,以及默认方法的使用。下面的代码是将在 JavaScript 中实现的接口 PositionType。

import java.math.BigDecimal;

public interface PositionType {

    public double hourlyWage(BigDecimal hours, BigDecimal wage);

    /**
     * Hourly salary calculation
     * @param wage
     * @return
     */
    public default BigDecimal yearlySalary(BigDecimal wage){
        return (wage.multiply(new BigDecimal(40))).multiply(new BigDecimal(52));
    }
}

接下来,让我们看看 JavaScript 文件中实现 PositionType 接口的代码。

var somePosition = new org.java9recipes.chapter18.recipe18_08.PositionType({
  hourlyWage: function(hours, wage){
      return hours * wage;
  }
});

print(somePosition instanceof Java.type("org.java9recipes.chapter18.recipe18_08.PositionType"));
var bigDecimal = Java.type("java.math.BigDecimal");

print(somePosition.hourlyWage(new bigDecimal(40), new bigDecimal(12.75)));

它是如何工作的

在 JavaScript 中使用 Java 接口有利于创建符合实现标准的对象。然而,在 JavaScript 中使用接口与在 Java 解决方案中使用接口有点不同。例如,接口不能在 Java 中实例化。在 JavaScript 中使用它们时,情况并非如此;您必须实例化接口类型的对象才能使用它。

该示例演示了 PositionType 接口的实现,该接口用于在雇员职位中定义许多方法。这些方法用于计算雇员的时薪和年薪。为了利用 JavaScript 中的 PositionType 接口,new 关键字用于实例化该接口的一个实例,并将其分配给一个 JavaScript 变量。当实例化接口时,一个 JavaScript 对象被传递给构造函数。对象包含接口中每个非默认方法的实现,方法是标识方法的名称,后跟实现。在该示例中,在实例化上只实现了一个方法,它被标识为 hourlyWage()。如果实现了一个以上的方法,那么这些实现将用逗号分隔。

尽管在 JavaScript 中使用 Java 接口有点不同,但它们确实有好处。实际上,他们在 JavaScript 中执行的任务和在 Java 中一样。在 Java 中,为了实现一个接口,你必须创建一个实现它的对象。您在 JavaScript 中做同样的事情,除了为了创建实现对象,您必须实例化接口的一个实例。

18-9.扩展 Java 类

问题

您希望在 Nashorn JavaScript 解决方案中扩展一个具体的 Java 类。

解决办法

首先,通过调用 JavaScript 文件中的 Java.type()函数,获得要扩展的 Java 类的引用。然后通过调用 Java.extend()函数创建子类,并将引用传递给将要扩展的类,以及包含将要更改的实现的 JavaScript 对象。

下面的代码是 Employee 类的代码,稍后将从一个 JavaScript 文件中扩展。

package org.java9recipes.chapter18.recipe18_09;

import java.math.BigDecimal;
import java.util.Date;

public class Employee {
    private int age;
    private String first;
    private String last;
    private String position;
    private Date hireDate;

    ...

    public BigDecimal grossPay(BigDecimal hours, BigDecimal rate){
        return hours.multiply(rate);
    }
}

下面是用于扩展和使用该类的 JavaScript 代码:

var Employee = Java.type("org.java9recipes.chapter18.recipe18_09.Employee");
var bigDecimal = Java.type("java.math.BigDecimal");
var Developer = Java.extend(Employee, {
    grossPay: function(hours, rate){
        var bonus = 500;
        return hours.multiply(rate).add(new bigDecimal(bonus));
    }
});

var javaDev = new Developer();
javaDev.first = "Joe";
javaDev.last = "Dynamic";
print(javaDev + "'s gross pay for the week is: " + javaDev.grossPay(new bigDecimal(60),
                                                                    new bigDecimal(80)));

结果如下:

Joe Dynamic's gross pay for the week is: 5300

它是如何工作的

要从 JavaScript 中扩展一个标准的 Java 类,您需要调用 Java.extend()函数,传递您想要扩展的 Java 类,以及一个 JavaScript 对象,该对象包含将在子类中更改的任何字段或函数。对于这个菜谱中的例子,扩展了一个名为 Employee 的 Java 类。然而,同样的技术可以用来扩展任何其他 Java 接口,比如 Runnable、Iterator 等等。

在本例中,为了获得 JavaScript 中的 Employee 类,调用了 Java.type()函数,并传递了完全限定的类名。从调用中收到的对象存储在名为 Employee 的 JavaScript 变量中。接下来,通过调用 Java.extend()函数并传递 Employee 类以及一个 JavaScript 对象来扩展该类。在该示例中,发送给 Java.extend()函数的 JavaScript 对象包括 Employee 类 grossPay()方法的不同实现。从 Java.extend()函数返回的对象然后被实例化,并通过 JavaScript 访问。

当您使用 Nashorn 解决方案时,在 JavaScript 中扩展 Java 类可能是一个非常有用的特性。共享 Java 对象的能力使得访问现有的 Java 解决方案并在其上构建成为可能。

18-10.在 Unix 中创建可执行脚本

问题

您希望让您的 JavaScript 文件成为可执行文件。

解决办法

通过添加一个 shebang(!)作为脚本的第一行,后面是 jjs 可执行文件的位置路径。在下面的例子中,一个非常简单的 JavaScript 文件通过包含一个 shebang 变得可执行,它指向 jjs 工具的符号链接。

#! /usr/bin/env jjs
print('I am an executable');

要执行脚本,必须给它适当的权限。应用 chmod a+x 权限(在 Unix 中)使脚本可执行。

chmod a+x src/org/java9recipes/chapter18/recipe18_10/jsExecutable.js

该脚本现在可以作为可执行文件调用,如以下命令所示:

Juneau$ ./src/org/java9recipes/chapter18/recipe18_10/jsExecutable.js
I am an executable

它是如何工作的

要使脚本可执行,只需在第一行添加一个 shebang。shebang 在基于 Unix 的操作系统中用来告诉程序加载器,脚本的第一行应该被视为解释器指令,脚本应该被传递给解释器执行。在这个方法的解决方案中,脚本的第一行告诉程序加载器应该使用 jjs 工具执行脚本的内容:

#! /usr/bin/env jjs

通过以这种方式调用 jjs 工具,脚本选项被自动启用,允许您在脚本中利用脚本特性。以下列表包括在启用脚本选项的情况下通过 jjs 执行时可以使用的额外脚本功能:

  • 字符串插值:(见配方 18-3)

    var threeyr = 365 * 3;
    print("The number of days in three years is ${threeyr}");
    
  • Shell 调用:调用外部程序的能力

  • 可以使用特殊的环境变量(ARGARG 和ENV)

用 JavaScript 开发可执行脚本的能力非常强大。不仅 JavaScript 世界唾手可得,而且整个 Java 世界都是可用的,因为您可以将 Java 类和库导入到脚本中。

18-11.用 Nashorn 实现 JavaFX

问题

您希望使用 JavaScript 实现一个 Java GUI。

解决方案 1

使用 JavaScript 开发一个 JavaFX 应用,并将其存储在 JavaScript 文件中。使用 jjs 工具和–FX 选项调用该文件。以下代码是用 JavaScript 编写的 JavaFX 应用。JavaFX 应用可用于收集汽车数据。

var ArrayList = Java.type("java.util.ArrayList");
var Scene = javafx.scene.Scene;
var Button = javafx.scene.control.Button;
var TextField = javafx.scene.control.TextField;
var GridPane = javafx.scene.layout.GridPane;
var Label = javafx.scene.control.Label;
var TextArea = javafx.scene.control.TextArea;

var carList = new ArrayList();
var carCount = "There are currently no cars";
var car = {
    make:"",
    model:"",
    year:"",
    description:""
};
print(carCount);
function start(primaryStage) {

    primaryStage.title="Car Form JS Demo";

    var grid = new GridPane();
    grid.hgap = 10;
    grid.vgap = 10;

    var makeLabel = new Label("Make:");
    grid.add(makeLabel, 0, 1);

    var makeText = new TextField();
    grid.add(makeText, 1, 1);

    var modelLabel = new Label("Model:");
    grid.add(modelLabel, 0, 2);

    var modelText = new TextField();
    grid.add(modelText, 1, 2);

    var yearLabel = new Label("Year:");
    grid.add(yearLabel, 0, 3);

    var yearText = new TextField();
    grid.add(yearText, 1, 3);

    var descriptionLabel = new Label("Description:");
    grid.add(descriptionLabel, 0, 4);

    var descriptionText = new TextArea();
    grid.add(descriptionText, 1, 4);

    var button = new Button("Add Car");
    button.onAction = function(){
        print("Adding Car:" + makeText.text);
        car.make=makeText.text;
        car.model=modelText.text;
        car.year=yearText.text;
        car.description=descriptionText.text;
        carList.add(car);   
        carCount = "The number of cars is: "+ carList.size();
        print(carCount);
    };
    grid.add(button, 0,5);

    primaryStage.scene = new Scene(grid, 800, 500);
    primaryStage.show();
}

最终的应用如图 [18-1 所示。

A323910_3_En_18_Fig1_HTML.jpg

图 18-1。用 JavaScript 编写的 JavaFX 应用

解决方案 2

使用 Java 编写 JavaFX 应用,并使用 ScriptEngine 嵌入 JavaScript 应用实现。下面的 Java 类称为 CarCollector.java,它实现 Java FX . application . application。Java 类实现 start()方法,该方法包含一个 ScriptEngine 来嵌入实现应用的 JavaScript 代码。

package org.java9recipes.chapter18.recipe18_11;

import java.io.FileReader;
import javafx.application.Application;
import javafx.stage.Stage;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;

public class CarCollector extends Application {

    private final String SCRIPT = getClass().getResource("carCollector.js").getPath();

    public static void main(String args[]) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        try {
            ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
            engine.put("primaryStage", stage);
            engine.eval(new FileReader(SCRIPT));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

接下来,让我们看一下名为 carCollector.js 的 JavaScript 文件,它实现了该应用。注意,代码不包含 start()函数,因为应用 start()方法已经在 Java 代码中实现了。JavaScript 文件仅仅包含实现。

var ArrayList = Java.type("java.util.ArrayList");
var Scene = javafx.scene.Scene;
var Button = javafx.scene.control.Button;
var TextField = javafx.scene.control.TextField;
var GridPane = javafx.scene.layout.GridPane;
var Label = javafx.scene.control.Label;
var TextArea = javafx.scene.control.TextArea;

var carList = new ArrayList();
var carCount = "There are currently no cars";
var car = {
    make: "",
    model: "",
    year: "",
    description: ""
};
print(carCount);

primaryStage.title = "Car Form JS Demo";

var grid = new GridPane();
grid.hgap = 10;
grid.vgap = 10;

var makeLabel = new Label("Make:");
grid.add(makeLabel, 0, 1);

var makeText = new TextField();
grid.add(makeText, 1, 1);

var modelLabel = new Label("Model:");
grid.add(modelLabel, 0, 2);

var modelText = new TextField();
grid.add(modelText, 1, 2);

var yearLabel = new Label("Year:");
grid.add(yearLabel, 0, 3);

var yearText = new TextField();
grid.add(yearText, 1, 3);

var descriptionLabel = new Label("Description:");
grid.add(descriptionLabel, 0, 4);

var descriptionText = new TextArea();
grid.add(descriptionText, 1, 4);

var button = new Button("Add Car");
button.onAction = function() {
    print("Adding Car:" + makeText.text);
    car.make = makeText.text;
    car.model = modelText.text;
    car.year = yearText.text;
    car.description = descriptionText.text;
    carList.add(car);
    carCount = "The number of cars is: " + carList.size();
    print(carCount);
};
grid.add(button, 0, 5);

primaryStage.scene = new Scene(grid, 800, 500);
primaryStage.show();

它是如何工作的

Nashorn 引擎可以完全访问 JavaFX API。这意味着可以构建完全或部分用 JavaScript 编写的 JavaFX 应用。这个配方的两个解决方案演示了每一种技术。第一个解决方案演示了如何完全用 JavaScript 开发 JavaFX 应用。当您使用解决方案 1 中演示的技术时,可以通过使用 jjs 工具并指定–FX 选项来执行 JavaScript 实现,如下所示:

jjs –fx recipe18_11.js

解决方案 2 演示了如何从 Java 代码构建一个 JavaFX 应用,嵌入用 JavaScript 编写的实现代码。要使用这种技术,可以通过扩展 javafx.application.Application 类并覆盖 start()方法来构造一个标准的 JavaFX 应用类。在 start()方法中,创建一个 Nashorn ScriptEngine 对象,并使用它嵌入一个包含应用实现的 JavaScript 文件。在调用引擎的 eval()方法并传递 JavaScript 文件之前,使用引擎的 put()方法将 JavaFX 阶段传递给引擎。

engine.put("primaryStage", stage);

深入研究一下 JavaScript 代码,任何 JavaFX API 类都可以通过使用 Java.type()函数并传递完全限定的类名来导入。将导入的类分配给 JavaScript 变量,稍后将在应用构造中使用这些变量。完全用 JavaScript 编写时,必须创建一个 start()函数来包含 JavaFX 应用阶段构造。另一方面,当您使用 Java 代码启动应用时,没有必要创建 start()函数。在这个示例中,GridPane 布局用于构建一个捕获汽车数据的表单。每个表单域都由一个标签和一个 TextField 或 TextArea 构成。单击按钮时,汽车数据存储在 JavaScript 对象中。

关于这两种实现中的 JavaScript 代码,有一些事情需要注意。语法与 Java 代码略有不同,因为没有使用 getters 和 setters。此外,按钮操作处理程序的实现是一个简单的 JavaScript 函数。

使用 JavaScript 构建 JavaFX 应用可能是使用 Java 代码的有趣替代方案。语法有使用以前的 JavaFX Script 语言的感觉,比 Java 稍微简单一点。如果使用完整的 JavaScript 实现,不用重新编译就能更改应用也是一件好事。

18-12.利用 ECMAScript6 功能

问题

您希望利用 ECMAScript6 的一些特性,比如模板字符串、更多的作用域选项和新的循环结构。

解决办法

利用 Java 9 中 ECMAScript6 新特性的子集。初始版本包括 ECMAScript6 新特性的一小部分,但是该特性集将随着后续 Java 9 版本的发布而扩展。

要利用这些新功能,请使用本章前面的方法中描述的解决方案之一,使用更新的 ECMAScript6 语法。在这个菜谱中,打开 jjs 实用程序并输入下面的例子来查看新的特性。

模板字符串功能的工作原理是允许字符串包含动态变量,这样变量就可以改变,从而改变字符串的文本。以下示例演示了如何利用模板字符串:

jjs> var customer = {name:"Josh"}
jjs> var message = `Hello ${customer.name}`

ECMAScript 中添加了 let 关键字,允许使用块范围的变量:

let name = "Josh";
console.log("first: " + name)
if (name.length > 1){
    let name = "Duke";
    console.log(name);
}
console.log(name);

输出:

first: Josh
Duke
Josh

ECMAScript6 包含新的循环结构,例如 for-in:

var names = ['Josh', 'Duke']
for (var x of names){
    console.log(x);
}

它是如何工作的

ECMAScript 6 中有许多新特性,其中一些特性是 Java 9 中 Nashorn 的一部分。事实上,一些新特性进入了 Java 8,Update 40,那些是 letconstblock scope 。新特性的列表如此之大,以至于试图在一个版本中把它们都放入 Nashorn 是一项艰巨的任务。因此,Nashorn for Java 9 的初始版本包含了新特性的另一个子集,在后续的 Java 9 版本中将会添加更多的新特性。

Java 9 的初始版本包含以下新的 Nashorn ECMAScript 6 特性:

  • 模板字符串

  • let、const 和 block 范围

  • 迭代器..循环的

  • 地图、集合、武器地图和武器集合

  • 标志

  • 二进制和八进制文字

Java 9 中 Nashorn 引擎的未来版本计划提供以下特性:

  • 箭头功能

  • 增强的对象文字

  • 解构分配

  • 默认、静止和展开参数

  • 统一码

  • 子类内置

  • 承诺

  • 委托书

  • 数学、数字、字符串和对象 API

  • 反射 API

摘要

Nashorn 使开发人员能够利用 Java 生态系统中的现代 JavaScript 功能。Nashorn 引擎可以完全访问所有 Java APIs,包括 JavaFX。新的 jjs 工具提供了脚本功能,允许开发人员创建完全用 JavaScript 编写的可执行脚本。最后,我们介绍了 ECMAScript6 的一些新特性,这些特性已经被添加到 Java 9 中的 Nashorn 引擎中。

十九、电子邮件

电子邮件通知是当今企业系统不可或缺的一部分。Java 通过提供 JavaMail API 来支持电子邮件通知。使用这个 API,您可以发送电子邮件来响应一个事件(比如一个完整的表单或一个最终的脚本)。您还可以使用 JavaMail API 来检查 IMAP 或 POP3 邮箱。

要遵循本章中的方法,请确保您已经设置了允许电子邮件通信的防火墙。大多数情况下,防火墙允许与电子邮件服务器进行出站通信而不会出现问题,但是如果您运行自己的本地 SMTP(电子邮件)服务器,您可能需要配置防火墙以允许电子邮件服务器正常运行。

注意

JavaMail API 包含在 Java EE 下载中。如果您正在使用 Java SE,您将需要下载并安装 JavaMail API。

19-1.安装 JavaMail

问题

您希望安装 JavaMail,供您的应用在发送电子邮件通知时使用。

解决办法

从 Oracle 的 JavaMail 网站下载 JavaMail。目前,您需要的下载位于

www.oracle.com/technetwork/java/javamail/

下载后,解压并添加 JavaMail。jar 文件作为项目的依赖项(mail.jar 和 lib*)。罐子)。

它是如何工作的

JavaMail API 包含在 Java EE SDK 中,但是如果您正在使用 Java SE SDK,则需要下载 JavaMail API 并将其添加到您的 Java SE 项目中。通过下载和添加依赖项,您可以访问健壮的电子邮件 API,该 API 允许您发送和接收电子邮件。

注意

如果您使用的不是 Java SE 6 或更高版本,您还需要 JavaBeans 激活框架(JAF)来使用 JavaMail。它包含在 Java SE 6 和更新的版本中。

19-2.发送电子邮件

问题

您需要您的应用来发送电子邮件。

解决办法

使用 Transport()方法,可以向特定的收件人发送电子邮件。在此解决方案中,通过 smtp.somewhere.com 服务器构建并发送电子邮件:

private void start() {
    Properties properties = new Properties();
    properties.put("mail.smtp.host", "smtp.somewhere.com");
    properties.put("mail.smtp.auth", "true");

    Session session = Session.getDefaultInstance(properties, new MessageAuthenticator("username","password"));

    Message message = new MimeMessage(session);
    try {
        message.setFrom(new InternetAddress("someone@somewhere.com"));
        message.setRecipient(Message.RecipientType.TO, new InternetAddress("someone@somewhere.com"));
        message.setSubject("Subject");
        message.setContent("This is a test message", "text/plain");
        Transport.send(message);
    } catch (MessagingException e) {
        e.printStackTrace();
    }
}

class MessageAuthenticator extends Authenticator {
    PasswordAuthentication authentication = null;

    public MessageAuthenticator(String username, String password) {
        authentication = new PasswordAuthentication(username,password);
    }

    @Override
    protected PasswordAuthentication getPasswordAuthentication() {
        return authentication;
    }
} 

它是如何工作的

要利用 JavaMail API,首先要创建一个作为标准 Map 对象的 Properties 对象(实际上,它是从它继承而来的),在这个对象中放置 JavaMail 服务可能需要的不同属性。主机名是使用 mail.smtp.host 属性设置的,如果主机需要身份验证,则必须将 mail.smtp.auth 属性设置为 true。配置完 properties 对象后,获取一个 javax.mail.Session,它将保存电子邮件消息的连接信息。

创建会话时,如果服务需要鉴定,您可以指定登录信息。当连接到局域网之外的 SMTP 服务时,这可能是必要的。要指定登录信息,您必须创建一个 Authenticator 对象,它将包含 getPasswordAuthentication()方法。在本例中,有一个标识为 MessageAuthenticator 的新类,它扩展了 Authenticator 类。通过使 getPasswordAuthentication()方法返回 PasswordAuthentication 对象,可以指定用于 SMTP 服务的用户名/密码。

Message 对象表示实际的电子邮件,并公开电子邮件属性,如发件人/收件人/主题和内容。设置这些属性后,调用 Transport.send()静态方法发送电子邮件。

小费

如果不需要身份验证信息,可以调用 session . getdefaultinstance(properties,null)方法,为 Authenticator 参数传递一个 null。

19-3.将文件附加到电子邮件中

问题

您需要在电子邮件中附加一个或多个文件。

解决办法

创建包含不同部分的消息(称为多部分消息)可以让您发送文件和图像等附件。您可以指定电子邮件的正文和附件。包含不同部分的邮件被称为多用途 Internet 邮件扩展(MIME)邮件。它们在 javax.mail API 中由 MimeMessage 类表示。以下代码创建了这样一条消息:

Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
message.setSubject("Subject");

// Create Mime "Message" part
MimeBodyPart messageBodyPart = new MimeBodyPart();
messageBodyPart.setContent("This is a test message", "text/plain");

// Create Mime "File" part
MimeBodyPart fileBodyPart = new MimeBodyPart();
fileBodyPart.attachFile("<path-to-attachment>/attach.txt");

MimeBodyPart fileBodyPart2 = new MimeBodyPart();
fileBodyPart2.attachFile("<path-to-attachment>/attach2.txt");

// Piece the body parts together
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(messageBodyPart);
multipart.addBodyPart(fileBodyPart);
//add another body part to supply another attachment
multipart.addBodyPart(fileBodyPart2);

// Set the content of the message to be the MultiPart
message.setContent(multipart);
Transport.send(message);

它是如何工作的

在 JavaMail API 中,您可以创建一个 MIME 电子邮件。这种类型的消息允许它包含不同的正文部分。在该示例中,生成了一个纯文本正文部分(包含电子邮件显示的文本),然后创建了两个附件正文部分,其中包含您要发送的附件。根据附件的类型,Java API 将自动为附件正文部分选择合适的编码。

在创建了每个正文部分之后,通过创建一个多部分对象并将每个单独的部分(纯文本和附件)添加到其中,可以将它们组合在一起。一旦多部分对象被组装成包含所有部分,它就被指定为 MimeMessage 的内容并被发送(就像配方 19-2 中一样)。

19-4.发送 HTML 电子邮件

问题

您想要发送包含 HTML 的电子邮件。

解决办法

您将电子邮件的内容类型指定为 text/html,并将 html 字符串作为邮件正文发送。在下面的示例中,使用 HTML 内容构造了一封电子邮件,然后发送出去。

MimeMessage message = new MimeMessage(session);
try {
    message.setFrom(new InternetAddress(from));
    message.setRecipient(Message.RecipientType.TO, new InternetAddress(to));
    message.setSubject("Subject Test");

    // Create Mime Content
    MimeBodyPart messageBodyPart = new MimeBodyPart();
    String html = "<H1>Important Message</H1>" +
                  "<b>This is an important message...</b>"+
                  "<br/><br/>" +
                  "<i>Be sure to code your Java today!</i>" +
                  "<H2>It is the right thing to do!</H2>";
    messageBodyPart.setContent(html, "text/html; charset=utf-8");

    MimeBodyPart fileBodyPart = new MimeBodyPart();
    fileBodyPart.attachFile("/path-to/attach.txt");

    MimeBodyPart fileBodyPart2 = new MimeBodyPart();
    fileBodyPart2.attachFile("/path-to/attach2.txt");

    Multipart multipart = new MimeMultipart();
    multipart.addBodyPart(messageBodyPart);
    multipart.addBodyPart(fileBodyPart);
    //add another body part to supply another attachment
    multipart.addBodyPart(fileBodyPart2);
    message.setContent(multipart);
    Transport.send(message);
} catch (MessagingException | IOException e) {
    e.printStackTrace();
}

它是如何工作的

发送包含 HTML 内容的电子邮件与发送包含标准文本的电子邮件基本相同,唯一的区别是内容类型。当您在电子邮件的邮件正文部分设置内容时,您可以将内容设置为 text/html,以便将内容视为 html。有各种方法来构造 HTML 内容,包括使用链接、照片或任何其他有效的 HTML 标记。在这个例子中,一些基本的 HTML 标签被嵌入到一个字符串中。

虽然示例代码在实际系统中可能不是很有用,但是生成包含在电子邮件中的动态 HTML 内容是很容易的。最基本的形式是,动态生成的 HTML 可以是连接在一起形成 HTML 的文本字符串。

19-5.向一组收件人发送电子邮件

问题

您希望向多个收件人发送同一封电子邮件。

解决办法

使用 JavaMail API 中的 setRecipients()方法向多个收件人发送电子邮件。setRecipients()方法允许您一次指定多个收件人。例如:

// Main send body
    message.setFrom(new InternetAddress("someone@somewhere.com"));
    message.setRecipients(Message.RecipientType.TO, getRecipients(emails));
    message.setSubject("Subject");
    message.setContent("This is a test message", "text/plain");
    Transport.send(message);

// ------------------

    private Address[] getRecipients(List<String> emails) throws AddressException {
        Address[] addresses = new Address[emails.size()];
        for (int i =0;i < emails.size();i++) {
            addresses[i] = new InternetAddress(emails.get(i));
        }
        return addresses;
    }

它是如何工作的

通过使用 Message 对象的 setRecipients()方法,可以在同一封邮件上指定多个收件人。setRecipients()方法接受 Address 对象的数组。在这个配方中,因为您有一个字符串集合,所以您创建一个与集合大小相同的数组,并创建 InternetAddress 对象来填充该数组。使用多个电子邮件地址发送电子邮件(相对于单个电子邮件)要高效得多,因为只有一条消息从您的客户端发送到目标邮件服务器。然后,每个目标邮件服务器将向其拥有邮箱的所有收件人发送邮件。例如,如果您要向五个不同的 yahoo.com 帐户发送邮件,yahoo.com 邮件服务器只需接收邮件的一个副本,它就会将邮件发送给邮件中指定的所有 yahoo.com 收件人。

小费

如果要发送批量邮件,您可能需要将收件人类型指定为“密件抄送”,这样收到的电子邮件就不会显示收到该电子邮件的其他所有人。为此,请指定 Message。setRecipients()方法中的 RecipientType.BCC。

19-6.检查电子邮件

问题

您需要检查是否有新邮件到达指定的电子邮件帐户。

解决办法

您可以使用 javax.mail.Store 连接、查询和检索来自 Internet 邮件访问协议(IMAP)电子邮件帐户的邮件。例如,下面的代码连接到一个 IMAP 帐户,从该 IMAP 帐户中检索最后五封邮件,并将这些邮件标记为已读。

Session session = Session.getDefaultInstance(properties, null);
Store store = session.getStore("imaps");
    store.connect(host,username,password);
    System.out.println(store);
    Folder inbox = store.getFolder(folder);
    inbox.open(Folder.READ_WRITE);
    int messageCount = inbox.getMessageCount();
    int startMessage = messageCount - 5;
    int endMessage = messageCount;
    if (messageCount < 5) startMessage =0;
    Message messages[]  = inbox.getMessages(startMessage,endMessage);
for (Message message : messages) {
    boolean hasBeenRead = false;
    for (Flags.Flag flag :message.getFlags().getSystemFlags()) {
        if (flag == Flags.Flag.SEEN) {
            hasBeenRead = true;
            break;
        }
    }
    message.setFlag(Flags.Flag.SEEN, false);
    System.out.println(message.getSubject() + " "+ (hasBeenRead? "(read)" : "") + message.getContent());

}
inbox.close(true);

它是如何工作的

Store 对象允许您访问电子邮件邮箱信息。通过创建存储,然后请求收件箱文件夹,您可以访问 IMAP 帐户主邮箱中的邮件。使用文件夹对象,您可以请求从收件箱下载邮件。为此,可以使用 getMessages (start,end)方法。收件箱还提供了一个 getMessageCount()方法,该方法允许您知道收件箱中有多少电子邮件。请记住,消息从索引 1 开始。

每条消息都有一组标志,可以判断该消息是否已被阅读(标志。Flag.SEEN)或消息是否已被回复(Flags。Flag .已回答)。通过解析 SEEN 标志,您可以处理以前没有阅读过的消息。

若要将邮件设置为已读(或已回复),请调用 message.setFlag()方法。此方法允许您设置(或重置)电子邮件标志。如果要设置邮件标志,您需要以 READ_WRITE 方式打开文件夹,这样您就可以更改电子邮件标志。您还需要在代码末尾调用 inbox.close(true ),这将告诉 JavaMail API 将更改刷新到 IMAP 存储中。

小费

对于 SSL 上的 IMAP,应该使用 session.getStore("imaps ")。这就创建了一个安全的 IMAP 存储。

19-7.监控电子邮件帐户

问题

您希望监控电子邮件到达某个帐户的时间,并且希望根据邮件的内容对其进行处理。

解决办法

从配方 19-6 的实施开始。然后添加 IMAP 标志操作,为您的应用创建一个健壮的电子邮件监视器。在下面的示例中,checkForMail()方法用于处理发送到邮件列表的邮件。在这种情况下,用户可以通过在主题行中放置其中一个单词来订阅或取消订阅列表。以下示例检查新邮件的主题,并适当地处理它们。该示例还使用消息标志来删除已处理的消息,这样就不需要读取两次。无法处理的消息会被标记为已读,但会留在服务器中由人工进行故障排除。

private void checkForMail() {
        System.out.println("Checking for mail");
        Properties properties = new Properties();
        String username = "username";
        String password = "password";
        String folder = "Inbox";
        String host = "imap.server.com";

        try {
            Session session = Session.getDefaultInstance(properties, null);
            Store store = session.getStore("imaps");
            store.connect(host,username,password);
            Folder inbox = store.getFolder(folder);
            inbox.open(Folder.READ_WRITE);
            int messageCount = inbox.getMessageCount();
            Message messages[]  = inbox.getMessages(1,messageCount);
            for (Message message : messages) {
                boolean hasBeenRead = false;
                if (Arrays.asList(message.getFlags().getSystemFlags()).contains(Flags.Flag.SEEN)) {
                    continue;                     // not interested in "seen" messages
                }
                if (processMessage(message)) {
                    System.out.println("Processed :"+message.getSubject());
                    message.setFlag(Flags.Flag.DELETED, true);
                } else {
                    System.out.println("Couldn't Understand :"+message.getSubject());
                    // set it as seen, but keep it around
                    message.setFlag(Flags.Flag.SEEN, true);
                }
            }
            inbox.close(true);
        } catch (MessagingException e) {
            e.printStackTrace();
        }
    }

    private boolean processMessage(Message message) throws MessagingException {
        boolean result = false;

        String subject = message.getSubject().toLowerCase();
        if (subject.startsWith("subscribe")) {
            String emailAddress = extractAddress (message.getFrom());
            if (emailAddress != null) {
                subscribeToList(emailAddress);
                result = true;
} 

        } else if (subject.startsWith("unsubscribe")) {
            String emailAddress = extractAddress (message.getFrom());
            if (emailAddress != null) {
                unSubscribeToList(emailAddress);
                result = true;
            }
        }

        return result;
    }

    private String extractAddress(Address[] addressArray) {
        if ((addressArray == null) || (addressArray.length < 1)) return null;
        if (!(addressArray[0] instanceof InternetAddress)) return null;
        InternetAddress internetAddress = (InternetAddress) addressArray[0];
        return internetAddress.getAddress();
    }

它是如何工作的

连接到 IMAP 服务器后,该示例请求收到的所有邮件。代码会跳过标记为可见的部分。为此,菜谱使用数组。将系统消息标志的数组转换成 ArrayList。一旦创建了列表,就需要查询列表以查看它是否包含该标志。看到枚举值。如果该值存在,则该示例跳到下一项。

当发现未读消息时,该消息由 processMessage()方法处理。该方法根据主题行的开头订阅或取消订阅消息的发送方。(这类似于邮件列表,发送主题为“subscribe”的消息会将发件人添加到邮件列表中。)

在确定执行哪个命令后,代码继续从消息中提取发件人的电子邮件。为此,processMessage()调用 extractEmail()方法。每封邮件都包含一组可能的“发件人”地址。这些地址对象是通用的,因为地址对象可以代表 Internet 或新闻组地址。在检查 Address 对象确实是 InternetAddress 后,代码将 Address 对象转换为 InternetAddress,并调用 getAddress()方法,该方法包含实际的电子邮件地址。

一旦提取出电子邮件地址,菜谱根据主题行调用 subscribe 或 unsubscribe。如果消息可以被理解(意味着消息已被处理),processMessage()方法返回 true(如果它不能理解消息,则返回 false)。在 checkForMail()方法中,当 processMessage()方法返回 true 时,邮件被标记为删除(通过调用 message.setFlag(Flags。Flag.DELETED,true);否则,该邮件将被标记为已查看。这使得消息在不被理解的情况下仍然存在,或者在被处理后被删除。最后,要提交邮件的新标志(并删除已删除的邮件),需要调用 inbox.close(true)方法。

19-8.摘要

电子邮件在我们今天使用的许多系统中扮演着重要的角色。Java 语言包括 JavaMail API,它使开发人员能够在其 Java 应用中包含健壮的电子邮件功能。本章中的方法涵盖了从安装到高级使用的 JavaMail API。要了解更多关于 JavaMail 以及与部署到企业应用服务器的 Java 应用的邮件集成的信息,请参考在线文档:www . Oracle . com/tech network/Java/JavaMail/index-141777 . html