65.9K
CodeProject 正在变化。 阅读更多。
Home

Java 后端 Web 应用教程第一部分:用七个步骤构建一个最小化应用程序

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2015年11月19日

CPOL

36分钟阅读

viewsIcon

176557

downloadIcon

1721

了解如何用最小的努力构建一个 Java 后端 Web 应用,使用 Java Server Faces (JSF) 作为用户界面技术,Java Persistence API (JPA) 进行对象到存储映射,以及 MySQL 数据库。

引言

本文摘录自书籍《使用 Java、JPA 和 JSF 构建后端 Web 应用》,该书提供在线开放访问。它展示了如何用最小的努力构建一个 Java 后端应用,使用Java Server Faces (JSF) 作为用户界面技术,Java Persistence API (JPA) 进行对象到存储映射,以及MySQL 数据库。

如果您想先看看它是如何工作的以及它看起来是什么样的,您可以从我们的服务器上运行本文讨论的最小化应用

一个分布式 Web 应用至少包含两个部分:前端部分,至少负责渲染用户界面 (UI) 页面;后端部分,至少负责持久化数据存储。一个后端 Web 应用是一个分布式 Web 应用,其中几乎所有工作都由后端组件完成,包括数据验证和 UI 页面创建,而前端仅由 Web 浏览器渲染基于 HTML 表单的 UI 页面。通常,分布式 Web 应用可以通过 HTTP 连接被多个用户同时访问。

在 Java/JPA/JSF 后端应用的情况下,应用的后端部分可以在运行支持 Java EE 规范Java ServletsJava Expression Language (EL)JPAJSF 的 Web 服务器的服务器上执行,例如开源服务器Tomcat/TomEE

本教程中讨论的 Java 后端数据管理应用的最小化版本仅包含一个完整应用所需的最少整体功能。它只处理一种对象类型 (Book) 并支持四个标准数据管理操作 (Create/Read/Update/Delete),但需要通过 CSS 规则对用户界面进行样式化,并添加应用整体功能的其他重要部分来增强。

背景

与 JavaScript 相比,Java 有什么不同?

  1. 没有程序不是类:任何 Java 程序都必须至少包含一个类。

  2. 没有对象不是类:要创建对象,必须使用(或定义)一个类来

    • 定义对象的属性槽的属性

    • 定义可以应用于该对象(以及所有实例化该类的其他对象)的方法和函数

  3. 没有全局变量,没有全局方法:在 Java 中,所有变量和方法都必须在类的上下文中定义,类提供了它们的命名空间。

  4. 类、属性和方法使用可见性级别定义:publicprotectedprivate

  5. Java 是强类型的:属性、参数和变量必须声明为某种类型。

  6. 类型参数:类和复杂数据结构(如列表)可以使用类型参数定义。例如,请参阅此教程

  7. 数组是静态的:数组的大小是固定的,运行时无法更改。另请参阅下面的数组部分。

  8. Java 程序在执行前必须编译

  9. 速度:Java 的速度大约是优化 JavaScript 的两倍。

理解 Java 中的可见性级别用于定义可能的访问级别非常重要

  子类 世界
public
受保护的 n
无修饰符 n n
私有的 n n n

通常,属性被定义为私有,具有公共的 getter 和 setter,因此它们只能在定义它们的类的级别上直接访问。

JavaBean 类和实体类

一个JavaBean 类(或简单地称为bean 类)是一个具有默认构造函数的 Java 类,其中所有属性都是可序列化的,并且具有 getset 方法(也称为“getter”和“setter”)。Java bean 是使用 bean 类创建的对象。

一个JPA 实体类(或简单地称为实体类)是一个带有 @Entity 注释的 JavaBean 类,这意味着 JavaEE 运行时环境(如TomEE PLUS Web 服务器提供的)将负责其实例的持久化存储。

在七步中构建一个最小化的 Java Web 应用

在本章中,我们将使用Java Persistence API (JPA) 进行对象到存储映射,并使用Java Server Faces (JSF) 作为用户界面技术来构建一个简单的 Java Web 应用程序。此类应用程序需要一个 Web 服务器(作为后端)环境来执行 Java 代码,但还包括 HTML、CSS 以及可能在用户计算机上执行的一些辅助 JavaScript 前端代码。由于几乎所有数据处理(包括约束验证)都在后端进行,而前端仅渲染用户界面,因此我们可以将 Java/JPA/JSF Web 应用程序归类为后端 Web 应用

JPA 是一个 Java API,用于管理 Java 应用程序中的持久化数据。它使用Java Persistence Query Language (JPQL),这是一种平台独立的面向对象的查询语言,深受 SQL 的启发,其查询表达式与 SQL 查询表达式非常相似,但它们是在 JPA 实体对象的上下文中执行的。

JSF 是一个 Java 规范,用于为 Web 应用程序构建基于组件的用户界面。其当前版本 JSF 2 默认使用Facelets 作为其模板技术。相比之下,JSF 1 使用JavaServer Pages (JSP) 作为其默认模板技术。

在本教程中,我们将展示如何使用TomEE Web 服务器开发、部署和运行一个简单的示例应用,该服务器为 Java/JPA/JSF Web 应用提供执行环境。我们假设您已经在计算机上安装了TomEE PLUS Web 服务器、MySQL DBMS 和ANT 构建工具。

我们示例应用程序的目的是管理有关书籍的信息。为了简单起见,在本章中,我们将处理一个对象类型 Book,如图 图 1.1 所示。

图 1.1. 对象类型 Book

以下是模型类 Book 的示例数据填充

表 1.1. Book 的示例数据

ISBN 标题 年份
006251587X 编织网络 2000
0465026567 哥德尔、埃舍尔、巴赫 1999
0465030793 我是一个奇特的循环 2008

我们需要数据管理应用程序做什么?应用程序必须支持四种标准的用例

  1. 创建一条新的书籍记录,允许用户输入要添加到已存储书籍记录集合中的书籍数据。

  2. 从数据存储检索(或读取)所有书籍,并以列表形式显示它们。

  3. 更新一本书籍记录的数据。

  4. 删除一本书籍记录。

这四个标准用例以及相应的数据管理操作通常用首字母缩写CRUD 来总结。

为了通过键盘和计算机屏幕输入数据,我们使用HTML 表单,它们为 Web 应用程序提供了用户界面技术。

对于任何数据管理应用程序,我们需要一种技术来将数据存储在持久化记录中,例如硬盘或固态硬盘。JPA 允许使用多种不同的数据存储技术,包括许多 SQL 数据库管理系统 (DBMS),如 Oracle、MySQL 和 PostgreSQL。我们不必对应用程序代码进行太多更改即可从一种存储技术切换到另一种。将正确的驱动程序实现添加到我们的 Java 运行时环境,正确设置 DBMS 并更改数据库访问配置就足够了。下面,在第 3 步中,我们将解释如何为 MySQL 设置 JPA 配置。

1. 第 1 步 - 设置文件夹结构

在第一步中,我们为应用程序代码设置文件夹结构。本章中的应用程序名称将是“Public Library”,我们将使用相应的应用程序文件夹名称“publicLibrary”。然后我们创建应用程序结构。有许多方法可以做到这一点,例如使用Eclipse 开发环境(并创建和配置一个动态 Web 项目)。在本教程中,我们将展示如何手动完成,因此除了ANT 之外,无需使用特殊工具即可更轻松地编译和部署应用程序。为了方便起见,我们提供了一个 ANT 脚本(可在本教程末尾下载),该脚本能够创建 Web 应用程序的文件夹结构,将其编译为Web 应用程序Archive (WAR) 文件,然后将其部署到 TomEE Web 服务器执行。应用程序结构(与 Eclipse 的动态 Web 项目结构兼容,因此可以导入到 Eclipse 中)如下所示:

publicLibrary
  src
    pl
      model
      ctrl
    META-INF
      persistence.xml
  WebContent
    views
      books
    WEB-INF
      templates
      faces-config.xml
      web.xml

此文件夹结构包含以下部分

  1. src 文件夹包含应用代码文件夹 pl,定义应用的 Java 包名(作为“public library”的简写),以及包含配置文件的 META-INF 文件夹。

    1. 应用代码文件夹 pl 在其子文件夹 modelctrl 中包含模型和控制器代码,而视图/UI 代码包含在 WebContent 文件夹中;

    2. 最重要的配置文件(也是我们为此应用唯一需要的)是 persistence.xml 文件。它包含数据库连接的配置。此文件的内容将在第 3.2 节中讨论。

  2. WebContent 文件夹包含各种 Web 资源,包括模板文件和自定义视图文件。

    1. views 存储我们应用程序的自定义视图文件,因此它代表 MVC 模式的视图部分。请注意,不严格要求命名为 views,但这样做很有意义,因为它代表了这里的内容。我们将在本教程稍后详细讨论其内容。

    2. WEB-INF 文件夹包含项目使用的库(jar 文件,作为 lib 子文件夹的一部分),用于您页面的 JSF 模板文件(作为 templates 子文件夹的一部分),faces-config.xml 文件,其中存储了 facelets 配置数据,以及 web.xml 配置文件,该文件特定于用于运行我们应用程序的 Tomcat (TomEE) 环境服务器。

2. 第 2 步 - 编写模型代码

在第二步中,我们为我们的应用创建模型类,每种模型类使用一个单独的 Java 源文件(扩展名为 .java)。在上面图 1.1 中显示的信息设计模型中,只有一个类代表 Book 对象类型。因此,我们在 src/pl/model 文件夹中创建一个文件 Book.java,其代码如下:

@Entity @Table( name="books")
public class Book {
  @Id private String isbn;
  private String title;
  private int year;
  // default constructor, required for entity classes
  public Book() {}
  // constructor
  public Book( String isbn, String title, int year) {
    this.setIsbn( isbn);
    this.setTitle( title);
    this.setYear( year);
  }
  // getter and setter methods
  public String getIsbn() {return isbn;}
  public void setIsbn( String isbn) {this.isbn = isbn;}
  public String getTitle() {return title;}
  public void setTitle( String title) {this.title = title;}
  public int getYear() {return year;}
  public void setYear( int year) {this.year = year;}
  // CRUD data management methods
  public static void add(...) {...}
  public static List<Book> retrieveAll(...) {...}
  public static Book retrieve(...) {...}
  public static void update(...) {...}
  public static void destroy(...) {...}
  public static void clearData(...) {...}
  public static void createTestData(...) {...}
}

请注意,模型类 Book 被编码为JPA 实体类,这是一个 JavaBean 类,并添加了以下 JPA 注释:

  1. @Entity 注释将类指定为实体类,这意味着该类的实例将被持久化存储。

  2. @Table( name="books") 注释指定用于存储 Book 实体的数据库表名称。此注释是可选的,默认表名与类名相同但小写(在本例中为 book)。

  3. @Id 注释标记标准标识符属性,这意味着底层 SQL 数据库表中的相应列被指定为主键。在我们的示例中,isbn 用作标准标识符属性,books 表的相应 isbn 列存储主键值。

在实体类 Book 中,我们还定义了以下静态(类级别)方法:

  1. Book.add 用于创建新的 Book 实例。

  2. Book.retrieveAll 用于从持久化数据存储中检索所有 Book 实例。

  3. Book.retrieve 用于通过标准标识符检索特定的 Book 实例。

  4. Book.update 用于更新现有的 Book 实例。

  5. Book.destroy 用于删除 Book 实例。

  6. Book.createTestData 用于创建一些示例书籍记录作为测试数据。

  7. Book.clearData 用于清空书籍数据库表。

这些方法将在以下各节中讨论。

JPA 用于数据管理和对象到存储映射的体系结构基于实体管理器的概念,该实体管理器提供数据管理方法 persist 用于保存新创建的实体,find 用于检索实体,remove 用于删除实体。

由于实体管理器对数据库的访问操作是在事务的上下文中执行的,因此我们的数据管理方法具有 UserTransaction 类型的参数 ut。在实体管理器能够调用数据库写方法 persist 之前,需要使用 ut.begin() 启动事务。在执行完所有写(和状态更改)操作后,使用 ut.commit() 完成事务(并提交所有更改)。

2.1. 将 Book 对象存储在数据库表 books

我们实体类 Book 的实例是特殊的 Java 对象,代表可以序列化的“实体”(或业务对象),换句话说,可以将它们转换为数据库记录或数据库表。因此,可以将它们显示为表,如 表 1.2 所示。

表 1.2. Book 对象表示为表

ISBN 标题 年份
006251587X 编织网络 2000
0465026567 哥德尔、埃舍尔、巴赫 1999
0465030793 我是一个奇特的循环 2008
 

我们在示例应用中使用的数据存储技术是MySQL,用于创建 books 数据库表的 (My)SQL 代码如下:

CREATE TABLE IF NOT EXISTS books (
  isbn VARCHAR(10) NOT NULL PRIMARY KEY,
  title VARCHAR(128),
  year SMALLINT
);

虽然也可以手动创建数据库模式(借助 CREATE TABLE 语句,如上面所示),但我们将在本教程稍后展示 JPA 如何自动生成数据库模式。在任何一种情况下,数据库设置(包括用户账户和相关权限(创建、更新等))都必须在 JPA 应用程序能够连接到它之前手动完成。

2.2. 创建一个新的 Book 实例并存储它

Book.add 方法负责使用“实体管理器”创建新的 Book 实例并将其保存到数据库。

public static void add( EntityManager em, UserTransaction ut, 
    String isbn, String title, int year) throws Exception {
  ut.begin();
  Book book = new Book( isbn, title, year);
  em.persist( book);
  ut.commit();
}

为了存储新对象,会调用给定“实体管理器”的 persist 方法。它负责创建相应的 SQL INSERT 语句并执行它。

2.3. 检索所有 Book 实例

Book 这样的实体类的实例是从数据库中检索的,借助一个用Java Persistence Query LanguageJPQL)表示的相应查询。这些查询类似于 SQL 查询。它们使用类名而不是表名,属性名而不是列名,对象变量而不是行变量。

Book.retrieveAll 方法中,首先创建一个查询,要求所有 Book 实例,然后使用 query.getResultList() 执行此查询,将其结果集分配给列表变量 books

public static List<Book> retrieveAll( EntityManager em) {
  Query query = em.createQuery( "SELECT b FROM Book b", Book.class);
  List<Book> books = query.getResultList();
  return books;
}

2.4. 更新 Book 实例

要更新现有的 Book 实例,我们首先使用 em.find 从数据库中检索它,然后设置已更改值的那些属性。

public static void update( EntityManager em, 
    UserTransaction ut, String isbn, String title, 
    int year) throws Exception {
  ut.begin();
  Book book = em.find( Book.class, isbn);
  if (!title.equals( book.getTitle())) book.setTitle( title);
  if (year != book.getYear()) book.setYear( year);
  ut.commit();
}

请注意,在调用 find 方法检索实体时,第一个参数必须是对相关实体类的引用(此处:Book.class),以便 JPA 运行时环境可以识别从中检索实体数据的数据库表。第二个参数必须是实体主键的值。

请注意,在更新的情况下,我们不必使用 persist 来保存更改。当使用 ut.commit() 完成事务时,JPA 运行时环境会自动管理保存。

2.5. 删除 Book 实例

可以从数据库中删除一个书籍实体,如下面的示例代码所示:

public static void destroy( EntityManager em, 
    UserTransaction ut, String isbn) throws Exception {
  ut.begin();
  Book book = em.find( Book.class, isbn);
  em.remove( book);
  ut.commit();
}

要从数据库中删除实体,我们首先需要使用 find 方法检索它,如更新情况一样。然后,必须由“实体管理器”调用 remove 方法,最后使用 ut.commit() 完成事务。

2.6. 创建测试数据

为了能够测试我们的代码,我们可以创建一些测试数据并将其保存在数据库中。我们可以使用以下过程来完成此操作:

public static void createTestData( EntityManager em, 
    UserTransaction ut) throws Exception {
  Book book = null;
  Book.clearData( em, ut);  // first clear the books table
  ut.begin();
  book = new Book("006251587X","Weaving the Web", 2000);
  em.persist( book);
  book = new Book("0465026567","Gödel, Escher, Bach", 1999);
  em.persist( book);
  book = new Book("0465030793","I Am A Strange Loop", 2008);
  em.persist( book);
  ut.commit();
}

清空数据库后,我们依次创建 3 个 Book 实体类的实例,并使用 persist 保存它们。

2.7. 清空所有数据

以下过程通过删除所有行来清空我们的数据库:

public static void clearData( EntityManager em, 
    UserTransaction ut) throws Exception {
  ut.begin();
  Query deleteStatement = em.createQuery( "DELETE FROM Book");
  deleteStatement.executeUpdate();
  ut.commit();
}

JPA 不提供直接方法来从特定类的整个数据集中删除。但是,这可以通过使用 JPQL 语句轻松实现,如上面的代码所示。JPQL 代码可以理解为:从与实体类 Book 关联的数据库表中删除所有行

3. 第 3 步 - 配置应用

在本节中,我们将展示如何:

  1. 在控制器类中配置一个应用以连接数据库,

  2. 获取执行数据库操作所需的 EntityManagerUserTransaction 实例,

  3. 将应用打包成 WAR 文件并部署到 Web 服务器执行。

3.1. 创建 EntityManager 和 UserTransaction 对象

控制器类包含将视图连接到模型以及所有不属于模型也不属于视图的方法的代码,例如与数据库服务器建立连接。在我们的示例应用程序中,这个类是 src/pl/ctrl 文件夹中的 pl.ctrl.BookController

JPA 需要一个 EntityManager 对象来执行 JPQL 查询(使用 SELECT)和数据操作语句(使用 INSERTUPDATEDELETE)。此外,为了执行数据库写操作,还需要一个 UserTransaction 对象来启动和完成事务。在独立应用程序中,程序员必须手动创建“实体管理器”和事务,使用工厂模式,如下面的代码片段所示:

EntityManagerFactory emf = 
    Persistence.createEntityManagerFactory("MinimalApp");
EntityManager em = emf.createEntityManager();
EntityTransaction et = em.getTransaction();

一个支持 JPA 的 Java Web 应用程序通常运行在称为“容器”的环境中(在本例中为 TomEE),该容器负责创建 EntityManagerUserTransaction 对象(如果使用了正确的注释)。负责此功能的代码是控制器类(例如 pl.ctrl.BookController)的一部分,因为控制器负责管理数据库连接。

public class BookController {
  @PersistenceContext( unitName="MinimalApp")
  private EntityManager em;
  @Resource() UserTransaction ut;

  public List<Book> getBooks() {...}
  public void refreshObject( Book book) {...}
  public String add( String isbn, String title, 
      int year) {...}
  public String update( String isbn, 
      String title, int year) {...}
  public String destroy( String isbn) {...}
}

仔细查看此代码可以看到,只需使用 @PersistenceContext 注释并提供 unitName(见下一节)即可在运行时获取 EntityManager 实例。同样,通过为用户事务引用属性 ut 使用 @Resource 注释,可以在运行时获取 UserTransaction 实例。不仅所需代码简洁明了,而且如果数据库类型更改(例如,当从 MySQL 切换到 Oracle 数据库时),此代码保持不变。

3.2. 配置 JPA 数据库连接

在上节讨论 BookController 类时,我们展示了如何获取执行数据库操作所需的 EntityManagerUserTransaction 对象。EntityManager 引用属性的 @PersistenceContext 注释需要一个 unitName,这只是一个用于标识 src/META-INF/persistence.xml 文件中定义的存储管理配置的名称。在我们示例应用程序中,此文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<persistence version="2.1" xmlns="http://java.sun.com/xml/ns/persistence" 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <persistence-unit name="MinimalApp">
    <class>pl.model.Book</class>
    <properties>	
      <!-- Request auto-generation of the database schema -->
      <property name="javax.persistence.schema-generation.database.action" 
          value="create"/>
      <!-- Use the JPA annotations for creating the database schema -->
      <property name="javax.persistence.schema-generation.create-source" 
          value="metadata"/>
    </properties>
    <jta-data-source>jdbc/MinimalApp</jta-data-source>
  </persistence-unit>
</persistence>

配置名称(“MinimalApp”)由 persistence-unit 元素的 name 属性定义。这就是我们必须为 @PersistenceContext 注释的 unitName 属性使用的值。

persistence-unit 元素有三个内容部分:

  1. 一个或多个 class 元素,每个元素都包含应用实体类的完全限定名称(例如,我们示例应用程序中的 pl.model.Book)。

  2. 一组配置 property 元素,用于提供进一步的配置设置。

  3. 一个 jta-data-source 元素,用于在 Web 服务器安装文件夹中的 config/TomEE.xml 配置文件中指定配置块。

在我们的 persistence.xml 文件中,设置了两个配置属性:

  • javax.persistence.schema-generation.database.action,可能的值有:none(默认)、createdrop-and-createdrop。它指定是否应自动创建数据库模式,并允许在创建新表之前删除现有表(使用 dropdrop-and-create)。

  • javax.persistence.schema-generation.create-source,可能的值有 metadata(默认)、scriptmetadata-then-scriptscript-then-metadata。它指定用于创建数据库模式的信息源。metadata 值强制使用 JPA 注释,而 script 值允许使用外部 DDL 脚本来定义模式。

persistence.xml 文件中的 jta-data-source 元素引用 Web 服务器安装文件夹中的 config/TomEE.xml 文件中 id 值为“MinimalApp”的 Resource 元素,该文件内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<TomEE>
  <Resource id="MinimalApp" type="DataSource"> 
    JdbcDriver com.mysql.jdbc.Driver 
    JdbcUrl jdbc:mysql://:3306/minimalapp
    UserName minimalapp 
    Password minimalapp 
    JtaManaged true 
  </Resource> 
</TomEE>

Resource 元素包含连接数据库所需的信息(即用户名、密码、访问 URL 和连接驱动程序的 Java 类名)。

3.3. 创建主模板

主模板 page.xhtml 如下所示。它有两个子模板:

  1. header.xhtml 定义通用的标题信息项(例如应用程序名称)。

  2. footer.xhtml 定义通用的页脚信息项(例如版权声明)。

这两个子模板都通过 ui:include 元素包含在主模板中。我们将所有三个模板文件添加到 WebContent/WEB-INF/templates 文件夹。

我们的 HTML5 兼容主模板 page.xhtml 的内容如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">
  <h:head>
    <title><ui:insert name="title">Public Library</ui:insert></title>
    <link href="#{facesContext.externalContext.requestContextPath}/resources/css/style.css" 
          rel="stylesheet" type="text/css" />
  </h:head>
  <body>
    <div id="header">
      <ui:insert name="header">
        <ui:include src="/WEB-INF/templates/header.xhtml"/>
      </ui:insert>
    </div>
    <div id="main">
      <ui:insert name="main"/>
    </div>
    <div id="footer">
      <ui:insert name="footer">
        <ui:include src="/WEB-INF/templates/footer.xhtml"/>
      </ui:insert>
    </div>
  </body>
</html>

在代码中,可以看到使用了一些 HTML 元素(例如 titlelinkdiv),而其他元素如 h:headui:insert 则不是 HTML 元素,而是由 JSF 在不同的命名空间中定义的。JSF 使用自己的 head 元素 h:head,因为它允许注入特殊的 HTML 代码,例如 XHR(或“AJAX”)消息所需的 script 元素。

请注意,在主模板中,我们看到了 JSF 表达式语言 (EL) 表达式的第一个示例,其中表达式以 # 开头并用花括号括起来,例如 #{expression}。这样的表达式允许读取 Java bean 或上下文对象的属性值或调用其方法。无论如何,表达式的值将被插入到从模板生成的 HTML 中。我们主模板中的示例是表达式 #{facesContext.externalContext.requestContextPath},它检索上下文对象 facesContext.externalContextrequestContextPath 属性的值。

我们的主模板定义了三个内容区域:header、main 和 footer。header 和 footer 区域由通过 ui:include 元素包含的子模板定义。

header.xhtml 子模板包含以下内容:

<div><h2>Public Library</h2></div>

footer.xhtml 子模板包含以下内容:

<div>Copyright 2014-2015, Gerd Wagner and Mircea Diaconescu</div>

main 区域是动态的,将由 facelet 生成的内容替换,如下所示。请注意,模板文件和 facelet 文件的文件扩展名均为 xhtml

JSF 使用以下命名空间:

  • xmlns:ui="http://java.sun.com/jsf/facelets" 用于JSF Facelets 标签库,提供模板元素(如 ui:define 用于指定模板中注入 facelet 内容的区域)。

  • xmlns:h="http://java.sun.com/jsf/html" 用于JSF HTML 标签库,提供 HTML 元素的 JSF 版本,然后映射到 HTML 元素。例如 h:inputText,它映射到 HTML input 元素。

  • xmlns:f="http://java.sun.com/jsf/core" 用于JSF Core 标签库,提供独立于任何特定渲染套件的自定义操作或元素。例如,f:actionListener 可用于定义在用户单击按钮时执行的 Java 方法。

  • xmlns:p="http://xmlns.jcp.org/jsf/passthrough" 用于在 JSF HTML 元素中使用 HTML 属性,并将它们传递给生成的 HTML。例如,使用 p:type 在,<h:inputText p:type="number"> 中,可以在生成的 HTML 中创建 HTML5 input 类型属性:<input type="number">

  • xmlns:c="http://java.sun.com/jsp/jstl/core" 用于JSTL Core 标签库,提供各种功能,如处理循环和定义变量。例如,我们可以使用 <c:set var="isbn" value="#{book.isbn}"/> 创建一个名为 isbn 的变量,该变量可以在视图代码中用于条件表达式。

  • xmlns:fn="http://java.sun.com/jsp/jstl/functions" 用于JSTL Functions 标签库,提供各种实用函数,如字符串转换器。例如,我们可以使用 fn:toUpperCase 将字符串转换为大写。

3.4. 定义 facelets 中所需的托管 bean

JavaBean 类(包括实体类)可以通过 @ManagedBean 注释创建“托管 bean”,该注释允许定义一个变量名,以便在视图代码中(通常在 EL 表达式中)访问创建的 bean。在我们示例应用程序中,我们希望访问 Book bean 以及 BookController bean,因此这两个类都必须进行如下注释:

@Entity @Table( name="books")
@RequestScoped @ManagedBean( name="book")
public class Book { ... }

@SessionScoped @ManagedBean( name="bookCtrl")
public class BookController { ... }

注意如何使用作用域注释为托管 bean 指定生命周期范围。在我们的示例中,book bean 是 @RequestScoped,这意味着实例存在于 HTTP 请求和相关响应被处理期间。 bookCtrl bean 是 @SessionScoped,这意味着它在会话开始时创建,并在会话关闭时销毁。还有其他作用域可用,但本教程中我们只需要这两个作用域。

3.5. 构建 WAR 文件并将其部署到 TomEE

在本教程中,我们将展示如何使用 ANT 脚本来生成 Java Web 应用的结构,然后编译代码、构建 WAR 文件并将其部署到 TomEE Web 服务器。也可以使用 Eclipse(或 NetBeans 或其他 IDE)来完成此操作,但为了简单起见,我们使用 ANT。我们的 ANT 脚本生成的文件夹结构与 Eclipse 兼容,因此如果您想使用 Eclipse,可以简单地从现有应用程序代码中创建 Eclipse 项目。

本节的目的只是向您展示如何使用我们的 ANT 脚本来使您的生活更轻松。它无意成为 ANT 教程,因此我们不深入探讨 ANT 的具体细节。以下 ANT 任务在脚本中定义:

create app -Dappname=yourAppName -Dpkgname=yourAppPackageName

允许创建文件夹结构。您需要将 yourAppNameyourAppPackageName 替换为您应用的名称和包名。在我们示例应用中,我们使用 ant create app -Dappname=publicLibrary -Dpkgname=pl 调用任务。

脚本会创建文件夹结构以及必需的文件 src/META-INF/persistence.xmlWEB-INF/faces-config.xmlWEB-INF/web.xml。参数 yourAppPackageName 用于创建 Java 顶级包。如果省略,将使用 yourAppName 作为 Java 顶级包名。对于接下来的任务/命令,您必须确保 ANT 脚本文件位于与您的 Web 应用程序文件夹相同的文件夹中(而不是在 Web 应用程序文件夹深一层)。这样,就可以使用相同的 ANT 脚本来构建多个 Web 应用程序。

使用可选参数 -Dforce=true 将通过首先删除现有应用程序文件夹来覆盖现有应用程序。

提示:JPA/JSF 应用程序需要一组库才能运行。ANT 脚本在脚本文件所在的文件夹中查找名为 lib 的文件夹中的 jar 文件。可以通过编辑 ANT 脚本并将 lib.folder 参数设置为计算机上正确的文件夹来修改 jar 文件的位置。您可以通过本教程末尾提供的链接下载依赖 JAR 文件。

build war -Dappname=yourAppName

允许通过使用 yourAppName 作为文件名来构建 WAR 文件。生成的 WAR 文件将位于 ANT 脚本文件所在的同一个文件夹中。对于我们的示例应用,我们使用以下命令:

ant war -Dappname=publicLibrary

提示:在使用此命令之前,您必须编辑 ANT 脚本并修改 server.folder 参数的值,使其指向您的 TomEE 安装文件夹。如果出现编译错误,请尝试将 mysql-connector-java-xxxx-bin.jar 文件复制到 TomEE 安装文件夹的 lib 文件夹中。此文件以及其他一些依赖文件包含在 ZIP 存档中,可通过本教程末尾提供的链接下载。

deploy -Dappname=yourAppName

允许将与 yourAppName 关联的 WAR 文件部署到 TomEE Web 服务器。它会自动执行 build war -Dappname=yourAppName 命令,这意味着在部署之前会构建 WAR 文件。部署文件夹的位置通过使用 server.folder 属性并追加 webapps 文件夹名称来检测。对于我们的示例应用,我们调用以下命令:ant deploy -Dappname=publicLibrary

提示:我们不建议在文件夹名称中使用空格,但如果出于任何原因,应用程序名称需要包含空格,则必须将其括在双引号中,例如 create app -Dappname="Hellow World"

4. 第 4 步 - 实现创建用例

创建用例涉及在主内存中创建一个新对象,然后使用 add 方法将其保存到持久化存储中。

下面显示了 src/pl/ctrl/BookController.java 中相应的 add 操作方法代码:

public class BookController {
  ...
  public String add( String isbn, String title, int year) {
    try {
      Book.add( em, ut, isbn, title, year);
      // clear the form after saving the Book record
      FacesContext fContext = FacesContext.getCurrentInstance();
      fContext.getExternalContext().getRequestMap().remove("book");
    } catch ( Exception e) {
      e.printStackTrace();
    } 
    return "create";
  }
}

BookController::add 操作方法调用 Book.add 模型类方法来创建和保存 Book 实例。它返回在触发该操作的视图所在的文件夹中找到的视图文件的名称。该文件(在本例中为 create.xhtml)将在执行操作后显示。在上面的第 5 行和第 6 行,使用 FacesContext 对象,在创建 Book 实例后清除表单。 src/pl/model/Book.javaadd 方法的代码如下:

 

public class Book {
  ...
  public static void add( EntityManager em, UserTransaction ut,
      String isbn, String title, int year) throws Exception {
    ut.begin();
    Book book = new Book( isbn, title, year);
    em.persist( book);
    ut.commit();
  }
}

 

现在我们需要为创建用例创建 facelet 视图文件 WebContent/views/books/create.xhtml。这样的 facelet 文件本质上定义了一个带有数据绑定动作绑定的 HTML 表单。

数据绑定是指将模型类属性绑定到表单(输入或输出)字段。例如,在下面的 facelet 代码片段中,实体属性 book.isbn 被绑定到表单输入字段“isbn”。

<h:outputLabel for="isbn" value="ISBN: " />
<h:inputText id="isbn" value="#{book.isbn}" />

动作绑定是指将方法调用表达式绑定到可操作的 UI 元素,其中被调用的方法通常是控制器操作方法,可操作的 UI 元素通常是表单按钮。例如,在下面的 facelet 代码片段中,方法调用表达式 bookCtrl.add(...) 被绑定到表单的提交按钮。

<h:commandButton value="Create" 
    action="#{bookCtrl.add( book.isbn, book.title, book.year)}"/>

在讨论了数据绑定和动作绑定之后,是时候查看 facelet 的完整代码了。

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="..."
      xmlns:h="..." xmlns:p="...">
  <ui:composition template="/WEB-INF/templates/page.xhtml">
    <ui:define name="main">
      <h:form id="createBookForm">
        <h:panelGrid columns="2">
          <h:outputLabel for="isbn" value="ISBN: " />
          <h:inputText id="isbn" value="#{book.isbn}" />
          <h:outputLabel for="title" value="Title: " />
          <h:inputText id="title" value="#{book.title}" />
          <h:outputLabel for="year" value="Year: " />
          <h:inputText id="year" p:type="number" value="#{book.year}" />
        </h:panelGrid>
        <h:commandButton value="Create" 
            action="#{bookCtrl.add( book.isbn, book.title, book.year)}"/>
      </h:form>
      <h:button value="Main menu" outcome="index" />
    </ui:define>
  </ui:composition>
</html>

此 facelet 替换 page.xhtml 中定义的模板的 main 区域,因为 ui:define 元素的 name 属性已设置为“main”。

h:outputLabel 元素可用于创建表单字段标签,而 h:inputText 元素用于创建 HTML 输入元素。可以通过使用特殊命名空间前缀(xmlns:p="http://xmlns.jcp.org/jsf/passthrough")来指定 type 属性的 HTML5 类型,强制将其“传递”。这样,year 输入字段可以定义为 number 类型,以便由浏览器中的相应数字控件呈现。

h:commandButton 元素允许创建作为 input 元素(type="submit")呈现的提交按钮,并将其绑定到单击按钮时要执行的操作。 action 属性的值是方法调用表达式。在我们创建用例中,我们希望在单击按钮时,创建并保存一个具有相应表单字段提供的值的 Book 实例。

5. 第 5 步 - 实现检索/列出所有用例

此用例对应于 Create-Retrieve-Update-Delete (CRUD) 四个基本数据管理用例中的“检索/读取”。

首先,对于列出对象用例,我们必须在控制器类(src/pl/ctrl/BookController.java 文件)中添加一个方法,该方法从 books 数据库表中读取所有 Book 记录,然后将此信息传递给视图。控制器操作方法代码如下:

public class BookController {
  ...
  public List<Book> getBooks() {
    return Book.retrieveAll( em);
  }
  ...
}

getBooks 方法返回一个 Book 实例列表,这些实例是通过调用 Book 模型类的静态 retrieveAll 方法获得的。 Book.retrieveAll 方法的代码如下:

public class Book {
  ...
  public static List<Book> retrieveAll( EntityManager em) {
    Query query = em.createQuery( "SELECT b FROM Book b");
    List<Book> books = query.getResultList();
    return books;
  }
  ...
}

代码很简单,并且如第 2.3 节中已讨论的,它使用 JPQL 语句从 books 表中检索 Book 记录并创建相应的 Book 实例。执行 JPQL 查询所需的 EntityManager 对象是从 BookController 对象传递给 Book.retrieveAll 的,如第 3.1 节所述。

现在,是时候定义用于显示数据库中所有记录的表的 facelet 视图了。我们应用程序对应的视图文件位于 WebContent/views/books 文件夹下。对于检索/列出所有用例,我们创建一个名为 listAll.xhtml 的文件,其内容如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="..."
      xmlns:h="..." xmlns:f="...">
  <ui:composition template="/WEB-INF/templates/page.xhtml">
    <ui:define name="main">
      <h:dataTable value="#{bookCtrl.books}" var="b">
        <h:column>
          <f:facet name="header">ISBN: </f:facet>
          #{b.isbn}
        </h:column>
        <h:column>
          <f:facet name="header">Title: </f:facet>
          #{b.title}
        </h:column>
        <h:column>
          <f:facet name="header">Year: </f:facet>
          #{b.year}
        </h:column>
      </h:dataTable>
      <h:button value="Main menu" outcome="index" />
    </ui:define>
  </ui:composition>
</html>

ui:composition 元素指定了应用哪个模板(即 template="/WEB-INF/templates/page.xhtml")以及渲染时由该 facelet 替换哪个视图块(<ui:define name="main">)。

h:dataTable 元素为一组记录定义了一个表视图,然后该视图被渲染为 HTML 表。它的 value 属性定义了一个记录集合的数据绑定,而 var 属性定义了一个变量名,用于从该集合中迭代访问记录。 value 属性中提供的表达式通常指定一个集合值属性(此处:books),该属性通过控制器类 BookController 中定义的相应 getter(此处:getBooks)访问。在此特定情况下,定义 getBooks 方法就足够了,因为控制器类中不需要 books 属性。无论如何,value 不允许调用方法,因此我们不能直接调用 getBooks。相反,我们必须使用(可能是虚拟的)属性 books,它在内部求值为对 getBooks 的调用,而无需检查 books 属性是否存在。

h:button JSF 元素允许创建重定向按钮。 outcome 属性的值通过省略 .xhtml 扩展名来指定 JSF 视图文件的名称(即视图文件名是 index.xhtml)。

6. 第 6 步 - 实现更新用例

此用例对应于 Create-Read-Update-Delete (CRUD) 四个基本数据管理用例中的“更新”。

创建对象用例一样,在 BookController 类(src/pl/ctrl/BookController.java 文件)中定义了一个控制器操作方法,代码如下:

public String update( String isbn, String title, int year) {
  try {
    Book.update( em, ut, isbn, title, year);
  } catch ( Exception e) {
    e.printStackTrace();
  } 
  return "update";
}

Book.update 负责更新并持久化更新由提供的 isbn 值标识的书籍条目的更改,如下所示:

public static void update( EntityManager em, UserTransaction ut,
    String isbn, String title, int year) throws Exception {
  ut.begin();
  Book book = em.find( Book.class, isbn);
  book.setTitle( title);
  book.setYear( year);
  ut.commit();
}

现在,我们创建视图,用户可以在其中选择一本书籍,以便编辑 titleyear 属性,然后保存更改。此视图的代码存储在 WebContent/views/books/update.xhtml 文件中,其内容如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:ui="..."
      xmlns:h="..." xmlns:f="..." xmlns:p="...">
  <ui:composition template="/WEB-INF/templates/page.xhtml">
    <ui:define name="main">
      <h:form id="updateBookForm">
        <h:panelGrid columns="2">
          <h:outputLabel for="selectBook" value="Select book: " />
          <h:selectOneMenu id="selectBook" value="#{book.isbn}">
            <f:selectItem itemValue="" itemLabel="---" />
            <f:selectItems value="#{bookCtrl.books}" var="b" 
                itemValue="#{b.isbn}" itemLabel="#{b.title}" />
            <f:ajax listener="#{bookCtrl.refreshObject( book)}" 
             render="isbn title year"/>
          </h:selectOneMenu>
          <h:outputLabel for="isbn" value="ISBN: " />
          <h:outputText id="isbn" value="#{book.isbn}" />
          <h:outputLabel for="title" value="Title: " />
          <h:inputText id="title" value="#{book.title}" />
          <h:outputLabel for="year" value="Year: " />
          <h:inputText id="year" p:type="number" value="#{book.year}" />
        </h:panelGrid>
        <h:commandButton value="Update" 
            action="#{bookCtrl.update( book.isbn, book.title, book.year)}"/>
      </h:form>
      <h:button value="Main menu" outcome="index" />
    </ui:define>
  </ui:composition>
</html>

在此视图中,使用 h:selectOneMenu 创建了一个单选列表(select)元素。 @value 属性允许通过将其分配给对象的属性来保存选择的值。在我们的例子中,由于选定的 Book 由其 isbn 属性值标识,该值存储在 book.isbn 属性中。使用 f:selectItems 元素填充书籍记录列表,该元素需要一个记录列表(使用 @value 属性),并定义一个变量名(使用 @var 属性),该变量绑定到列表中用于在内部用于填充选择列表的循环中光标所在的元素。属性 @itemValue@itemLabel 用于在渲染视图时提供用于创建 option HTML5 元素的信息,通过指定 option 元素的内容(即 @itemLabel 属性的值)和 option/@value 属性的值(即 @itemValue 属性的值)。

在更新视图中,用户从下拉列表中选择一本书籍,而表单的其余部分将自动填充所选书籍的详细信息(isbn、title 和 year)。ISBN 我们已经知道,它是下拉列表的选择值。但是,我们还需要显示所选书籍的 title 和 year。为此,我们使用 f:ajax JSF 元素,它允许执行一个请求(AJAX 请求),该请求会导致调用指定的方法。该方法将托管的 book 实例作为参数,并通过从数据库读取来更新其 titleyear 属性。所使用的方法是 BookController 类的一部分,名为 refreshObject。此方法代码如下:

public void refreshObject( Book book) {
  Book foundBook = Book.retrieve( em, book.getIsbn());
  book.setTitle( foundBook.getTitle());
  book.setYear( foundBook.getYear());
}

它只是使用 Book.retrieve 方法,该方法返回给定 isbn 值存储在数据库中的 Book 实例。然后,更新方法的引用参数(即 book),以便 titleyear 具有正确的值。在视图代码中,我们将 book 的引用解析为 refreshObject 方法的值。 book 属性与 h:inputTexth:outputText(对于必须只读的 isbn)JSF 元素连接,这意味着表单输入元素将显示 book 属性的值。为了强制刷新表单(这是必需的,因为表单仅在提交操作时自动更新),以便显示更新的 isbntitleyear 值,f:ajax JSF 元素允许指定要更新的表单(f:ajax 是其子元素)元素,即 f:ajax/@render="isbn title year"

最后,h:commandButton 指定 action 是调用 BookControllerupdate 操作方法,并带有 isbntitleyear 参数,从而使更改持久化。

7. 第 7 步 - 实现删除用例

此用例对应于 Create-Read-Update-Delete (CRUD) 四个基本数据管理用例中的“删除”。

下面显示了 src/pl/ctrl/BookController.java 中相应的 destroy 操作方法代码:

public String destroy( String isbn) {
  try {
    Book.destroy( em, ut, isbn);
  } catch ( Exception e) {
    e.printStackTrace();
  } 
  return "delete";
}

控制器操作方法的主要功能是调用 Book.destroy 方法,并提供要删除的 Book 条目的 isbn。 Book.destroy 方法负责根据标准标识符(即 isbn)的给定值查找正确的条目,以及将其从数据库中删除。

public static void destroy( EntityManager em, UserTransaction ut, String isbn) 
       throws Exception, HeuristicRollbackException, RollbackException {
  ut.begin();
  Book book = em.find( Book.class, isbn);
  em.remove( book);
  ut.commit();
}

通过使用 EntityManager 对象的内置 find 方法(参见 第 2.4 节)可以轻松找到具有给定 isbn 值的 Book 条目。通过调用 EntityManager 对象的 remove 方法(更多详细信息:第 2.5 节)可以从数据库中删除 Book 条目。

我们最后需要做的是创建相关的删除操作视图。该视图包含一个下拉列表,允许选择要删除的 Book,然后一个“删除”按钮执行书籍的删除。视图(WebContent/views/books/delete.xhtml)的代码如下:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="..."
      xmlns:h="..."
      xmlns:f="...">

  <ui:composition template="/WEB-INF/templates/page.xhtml">
    <ui:define name="main">
      <h:form id="deleteBookForm">
        <h:panelGrid columns="2">
          <h:outputText value="Select book: " />
          <h:selectOneMenu value="#{book.isbn}">
            <f:selectItems value="#{bookCtrl.books}" var="b" 
                itemValue="#{b.isbn}" itemLabel="#{b.title}" />
          </h:selectOneMenu>
        </h:panelGrid>
        <h:commandButton value="Delete" 
            action="#{bookCtrl.destroy( book.isbn)}"/>
      </h:form>
      <h:button value="Main menu" outcome="index" />
    </ui:define>
  </ui:composition>
</html>

更新对象用例一样,使用 h:selectOneMenu JSF 元素创建并填充包含所有书籍的下拉列表,以便我们可以选择要删除的书籍。单击“删除” h:commandButton 会执行关联的操作,该操作使用所选书籍的 isbn 值调用控制器 destroy 操作方法,从而从数据库中删除 Book 条目。

8. 运行应用并获取代码

您可以在我们的服务器上运行最小化应用,或下载代码作为 ZIP 压缩文件。

要在您的计算机上运行最小化应用,请先下载代码并编辑 ANT 脚本文件,方法是按照第 3.5 节中的说明修改 server.folder 属性值。然后,您可能需要使用 bin/shutdown.bat(Windows)或 bin/shutdown.sh(Linux)停止您的 Tomcat/TomEE 服务器。现在在您的控制台或终端中执行以下命令: ant deploy -Dappname=minimalapp。最后,启动您的 Tomcat Web 服务器(使用 bin/startup.bat(Windows OS)或 bin/startup.sh(Linux))。请耐心等待,这可能需要一些时间,具体取决于您计算机的速度。当控制台显示以下信息时,它将准备就绪: INFO: Initializing Mojarra [一些库版本和路径显示在此处] for context '/minimalapp'。最后,打开您喜欢的浏览器并输入:https://:8080/minimalapp/faces/views/books/index.xhtml

对于新项目,您可能希望下载依赖库

9. 可能的变体和扩展

9.1. Web 应用的可访问性

提供 Web 应用可访问性的推荐方法由Accessible Rich Internet Applications (ARIA) 标准定义。正如 Bryan Garaventa 在他关于不同形式可访问性的文章中总结的那样,交互式 Web 技术的可访问性有 3 个主要方面:1) 键盘可访问性,2) 屏幕阅读器可访问性,以及 3) 认知可访问性。

关于 ARIA 的进一步阅读

9.2. 使用资源 URL

每当应用程序提供关于实体(例如公共图书馆中可用的书籍)的公开信息时,就希望使用自描述的资源 URL 来发布这些信息,例如 http://publiclibrary.norfolk.city/books/006251587X,这将是检索诺福克公共图书馆中书籍“Weaving the Web”信息的资源 URL。

9.3. 离线可用性

希望 Web 应用在用户离线时仍能使用。

10. 注意事项

此应用程序的代码应通过以下方式进行扩展:

  • 为用户界面页面添加一些CSS 样式,以及

  • 添加约束验证

我们将在后续教程《构建 Java Web 应用教程 第二部分:添加约束验证》中展示如何做到这一点。

我们简要讨论了三个进一步的注意事项:样板代码、代码清晰度、使用资源 URL 和架构分离关注点。

10.1. 代码清晰度

任何该死的傻瓜都可以编写计算机能理解的代码,诀窍在于编写人类能理解的代码。”(Martin Fowler 在程序运行后

代码经常“不必要地复杂、曲折、杂乱,并且随意拼凑而成”,正如 Santiago L. Valdarrama 的帖子中所观察到的,他建议在必要时使用注释,仅用于解释不够清晰的事物,而是通过使用更好的名称、正确的结构以及正确的间距和缩进来让代码揭示其意图。

10.2. 样板代码

此 Java 示例应用程序代码的另一个问题是所需的重复性样板代码

  1. 每个模型类都需要用于存储管理方法 addupdatedestroy 等;

  2. 每个模型类和属性都需要 getter、setter 和验证检查。

虽然编写几次这些代码有助于学习应用开发,但将来在处理实际项目时,您会想避免一遍又一遍地编写它们。

10.3. 架构分离关注点

软件架构最基本原则之一是关注点分离。此原则也是模型-视图-控制器 (MVC) 架构范例的基础。它要求尽可能使软件应用程序的不同功能部分相互独立。更具体地说,它意味着使应用程序的模型类独立于

  1. 用户界面 (UI) 代码,因为应该可以使用相同的模型类与不同的 UI 技术;

  2. 存储管理代码,因为应该可以使用相同的模型类与不同的存储技术。

在本教程中,我们使模型类 Book 独立于 UI 代码,因为它不包含任何对 UI 元素的引用,也不调用任何视图方法。但是,为了简化起见,我们没有使其独立于存储管理代码,因为我们使用了 JPA 注释,这些注释将该类绑定到 JPA 对象到存储映射技术,并且我们包含了 add、update、destroy 等方法的定义,这些方法调用 JPA 环境的实体管理器的存储管理方法。因此,我们最小示例应用程序中的关注点分离是不完整的。

我们将在后续教程中展示如何实现更完整的关注点分离。

历史

  • 2015年11月19日。创建第一个版本。
© . All rights reserved.