Java-MVC-1-0-入门手册-二-

43 阅读50分钟

Java MVC 1.0 入门手册(二)

原文:Beginning Java MVC 1.0

协议:CC BY-NC-SA 4.0

四、Java MVC 的 Hello World

在第一章中,我展示了一个简单的 Hello World 风格的 Java MVC web 应用。有了如何使用 Eclipse 作为 IDE 和 Gradle 作为构建框架的知识,我们现在可以研究一种更简洁的 Hello World web 应用的方法。功能将是相同的:一个页面作为登陆页面,并要求用户输入他们的名字。在他们提交之后,控制器处理该名称,并显示一个带有个性化问候的提交响应页面。

开始 Hello World 项目

打开 Eclipse 并选择任何合适的工作空间。因为在前面的章节中我们使用了JavaMVCBook作为工作空间,所以没有理由在这个 Hello World 项目中不再使用它。请记住,我们在此工作区中添加了 JDK 1.8,因此您不必再次这样做。

开始一个新项目。选择文件➤新➤其他➤格拉德➤格拉德项目。单击 Next,这将显示 Gradle 新项目向导的第一页。这是一个欢迎页面,它显示了有关向导的一些信息。见图 4-1 。

img/499016_1_En_4_Fig1_HTML.jpg

图 4-1

Gradle 项目向导欢迎页面

如果愿意,您可以取消选中复选框,表明您是否希望在下次启动向导时看到此向导欢迎页面。单击下一步按钮。

在第二页上,如图 4-2 所示,要求您输入项目名称。输入HelloWorld。在同一页面上,您可以输入项目位置。如果选择默认位置,项目文件将在工作空间文件夹中创建。否则,您可以在文件系统的任何位置输入文件夹。例如,如果您使用版本控制系统,并且更喜欢使用文件系统的特殊版本控制区域内的项目文件夹,这是有意义的。

img/499016_1_En_4_Fig2_HTML.jpg

图 4-2

Gradle 项目向导第 2 页

为了通过本书学习和工作,使用默认的项目位置并保持适当的复选框被选中可能是放置项目的最常见的方法。这个页面上的最后一个设置允许您为新项目定义和使用一个工作集。工作集主要用于过滤在 Eclipse 的项目浏览器中看到的项目。也可以在以后应用该设置,因此您可以放心地不选中“将项目添加到工作集”复选框。单击“下一步”前进到下一页。

在向导的第三页,您可以指定一些关于梯度执行的选项。可以选择一个专用的 Gradle 安装,添加一些额外的 Gradle 程序执行参数,或者指定某个 Gradle 版本。见图 4-3 。

img/499016_1_En_4_Fig3_HTML.jpg

图 4-3

Gradle 项目向导第 3 页

对于 Hello World 样式的应用,您可以使用默认值,这将使覆盖工作区设置处于未选中状态。如果您感到好奇:如果您单击配置工作区设置,您可以调查或更改这些工作区设置。默认情况下使用 Gradle 包装器,这意味着将使用在项目创建期间安装的、在向导完成后可用的 Gradle 包装器。但是如果你愿意,你可以自由地尝试这些选项。单击 Next 将开始实际的项目生成,您可以看到向导的最后一页,它总结了向导的活动。参见图 4-4 。单击“完成”完成向导。

img/499016_1_En_4_Fig4_HTML.jpg

图 4-4

Gradle 项目向导第 4 页

在项目生成向导完成它的工作之后,新项目会出现在项目浏览器中。如果出现错误标记,可能是 JRE 版本不匹配。第三章详细描述了修复该问题的步骤。(简而言之,通过右键单击项目,然后单击 Properties,转到项目设置,导航到 Java 构建路径➤库,删除错误的 JRE 赋值,最后添加 JRE 1.8 作为库。)

为了让构建过程正确地添加库并构建一个 WAR web 应用,我们更改了build.gradle文件的内容并编写了以下代码:

/*
 * GRADLE project build file
 */

plugins {
    id 'war'
}
sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
    jcenter()
}

dependencies {
   testImplementation 'junit:junit:4.12'
   implementation 'javax:javaee-api:8.0'
   implementation 'javax.mvc:javax.mvc-api:1.0.0'
   implementation 'org.eclipse.krazo:krazo-jersey:1.1.0-M1'
   implementation 'jstl:jstl:1.2'
}

task localDeploy(dependsOn: build,
             description:">>> Local deploy task") {
  doLast {
    def FS = File.separator
    def glassfish = project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']

    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd}\n"

    def sout = new StringBuilder()
    def serr = new StringBuilder()

    def libsDir = "${project.projectDir}${FS}build" +
        "${FS}libs"
    def procStr = """${glassfish}${FS}bin${FS}asadmin
        --user ${user} --passwordfile ${temp.absolutePath}
         deploy --force=true
        ${libsDir}/${project.name}.war"""
    // For Windows:
    if(FS == "\\") procStr = "cmd /c " + procStr
    def proc = procStr.execute()
    proc.waitForProcessOutput(sout, serr)
    println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)

    temp.delete()
  }
}

task localUndeploy(
             description:">>> Local undeploy task") {
  doLast {
    def FS = File.separator
    def glassfish = project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']

    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd}\n"

    def sout = new StringBuilder()
    def serr = new StringBuilder()
    def procStr = """${glassfish}${FS}bin${FS}asadmin
        --user ${user} --passwordfile ${temp.absolutePath}
         undeploy ${project.name}"""
    // For Windows:
    if(FS == "\\") procStr = "cmd /c " + procStr
    def proc = procStr.execute()

    proc.waitForProcessOutput(sout, serr) println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)

    temp.delete()
  }
}

这个配置添加了 Jakarta EE 8 API(dependencies { }部分的javax:javaee-api:8.0)、Java MVC 库(javax.mvc:javax.mvc- api:1.0.0org.eclipse.krazo:krazo-jersey:1.1.0-M1),以及作为前端视图模板引擎的 JSTL(jstl:jstl:1.2)。构建文件还包含两个定制任务——localDeploylocalUndeploy——帮助您在本地开发 GlassFish 服务器上部署项目。我们在前一章讨论了这些任务。

为了让构建正确工作,将gradle.properties文件添加到项目文件夹:

glassfish.inst.dir = /path/to/your/glassfish5.1
glassfish.user = admin
glassfish.passwd =

这些设置由自定义任务中的project.properties['..' ]表达式处理。它们告诉我们 GlassFish 在哪里,以及联系它所需的用户凭证。根据您的需要修改属性项(admin和空密码是 GlassFish 服务器的默认密码)。右键单击项目,然后选择“Gradle ➤”“刷新 Gradle 项目”以更新项目库分配。

项目现在已经设置好了,您可以开始添加 Java 类和资源文件了。

Hello World 模型

不要混淆 Java MVC 应用的模型层和数据库模型。在应用的 MVC 部分中,所有的“模型”都是一个数据容器,用于保存要在不同页面之间以及页面和控制器组件之间传输的值。对于我们的 Hello World 应用,模型非常小——它由用户在登录页面上作为用户名输入的单个字符串组成。

对于许多 MVC web 应用来说,引入保存模型值的 Java 类是有意义的。因此,对于这个 Hello World 应用,您可能希望考虑一个如下所示的模型类:

public class HelloWorldModel {
    private String userName;
    public String getUserName() {
        return userName; }
    public void setUserName(String userName) {
        this.userName = userName; }
}

然而,对于这种简单的情况,通常如果出于某种原因你不想引入模型类,Java MVC 提供了一个模型值容器机制。在控制器类中,您只需使用@Inject让 Java MVC(更准确地说,CDI 部分)注入一个javax.mvc.Models实例:

import javax.inject.Inject;
import javax.mvc.Models;
...

public class SomeController {
    @Inject
    private Models models;
    ...
}

然后,您可以在控制器中编写以下内容:

...
// somehow get String 'name' from the request
String name = ...; models.put("name", name);
...

并把它写在网页上:

...
Hello ${name}
...

对于这个简单的 Hello World 应用,我们使用Models数据容器作为用户名,所以我们不引入任何专用的模型类。

Hello World View

该视图需要两个页面:一个登录页面要求用户输入姓名,另一个问候页面显示刚刚输入的姓名。我们调用登陆页面index.jsp,它必须进入src/main/webapp/WEB-INF/views文件夹。

路径是由 Gradle 规定的惯例;下面的WEB-INF/views路径将页面标记为 Java MVC 控制的视图。index.jsp页面代码如下:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
   <meta charset="UTF-8">
   <title>Hello World</title>
</head>
<body>
   <form method="post" action="${mvc.uriBuilder(
        'HelloWorldController#greeting').build()}">
     Enter your name: <input type="text" name="name"/>
     <input type="submit" value="Submit" />
    </form>
</body>
</html>

Java MVC 允许两种模板引擎:JSP 和 Facelets。我们使用 JSP(你可以从<%@ ...>中看到,这在 Facelets 中是不存在的)。

来自form标签的action属性遵循 Java MVC 框架规定的特殊语法——${ mvc. ... }按照约定连接到一个特殊的对象,无需进一步的配置工作。例如,这个对象有一个uriBuilder()方法,允许我们从 Java MVC 控制器构造针对某个方法的表单动作。在这个例子中,它是HelloWorldController控制器(没有包的控制器的类名)和它的greeting()方法。

将视图页面放在某个地方不足以让 web 应用正常工作。作为附加步骤,我们需要宣布index.jsp是登录页面。这意味着必须重定向一个http://localhost:8080/HelloWo rld/mvc来通过控制器运行,并在被加载的index.jsp页面中结束。为此,我们使用了两个 Java 类。第一个将/mvc添加到目标 URL:

package book.javamvc.helloworld;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/mvc")
public class App extends Application {
}

可以如图所示放入book.javamvc.helloworld包中。这个类是故意空的——@ApplicationPath注释和javax.ws.rs.core.Application超类导致了期望的行为。

第二个类RootRedirector,确保"/"""路径(在mvc之后)被转发到mvc/hello,稍后控制器将把它作为GET动词(针对index.jsp)获取:

package book.javamvc.helloworld;

import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Redirecting http://localhost:8080/HelloWorld/
 * This way we don't need a <welcome-file-list> in web.xml
 */
@WebFilter(urlPatterns = "/")
public class RootRedirector extends HttpFilter {
    private static final long serialVersionUID =
          7332909156163673868L;

    @Override
    protected void doFilter(final HttpServletRequest req,
          final HttpServletResponse res,
          final FilterChain chain) throws IOException {
         res.sendRedirect("mvc/hello");
    }
}

响应页面被称为greeting.jsp,我们将它放在src/main/webapp/WEB-INF/views文件夹中的index.jsp旁边:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Hello World</title>
</head>
<body>
  Hello ${name}
</body>
</html>

你可以看到它在功能上非常有限。它只是输出用登录页面中输入的内容替换了NAME"Hello NAME"字符串。它通过${name}引用名称,它寻址模型值name(见下一节)。

Hello World 控制器

控制器类对来自浏览器页面的用户输入做出反应,并控制页面之间的导航。它被称为HelloWorldController,我们将它放在book.javamvc.helloworld包中:

package book.javamvc.helloworld;

import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/hello")
@Controller
public class HelloWorldController {
    @Inject
    private Models models;

    @GET
    public String showIndex() {
        return "index.jsp";
    }

    @POST
    @Path("/greet")
    public Response greeting(
          @MvcBinding @FormParam("name") String name) {
        models.put("name", name);
        return Response.ok("greeting.jsp").build();
    }
}

控制器类看起来非常类似于 RESTful 服务的 JAX-RS 控制器。主要区别在于,我们不让请求以 JSON 结构或其他形式返回数据值。相反,这些方法应该返回页面说明符。HelloWorld的控制器通过类的‘@Path注释监听 URL /hello的 HTTP GET动词,而对于showIndex()方法没有额外的@Path注释。因此,对于/hello,登录页面index.jsp将被加载。

greeting()方法从hello/greet URL 连接到POST s,因为来自类的@Path和来自方法的@Path被连接在一起。这里我们需要 HTTP POST动词,因为我们想要将这个方法连接到一个form提交。相应地,对于/hello/greet,将加载响应页面greeting.jsp

CDI 注入了Models实例。它是一个通用的数据容器,以防您不想引入 Java beans 来保存模型值。它由一个POST提供,只需编写${someName}就可以在响应视图中使用,其中someNamePOST参数的名称。

Caution

Models实例是请求范围的,这意味着模型值只存在于对POST动作的直接响应中。

利用 Gradle 构建 Hello World

为了构建 Hello World web 应用,您有两种选择。首先,您可以使用 Eclipse Gradle 插件构建一个可从 Eclipse 内部部署的项目。为此,进入 Gradle Tasks 视图,打开 HelloWorld 抽屉,在build部分找到 WAR 任务。见图 4-5 。要启动任务,请双击任务名称。然后视图自动切换到 Gradle Executions 窗口,如图 4-6 所示。在那里,您可以大致了解 Gradle 在执行任务时到底做了什么。

img/499016_1_En_4_Fig6_HTML.jpg

图 4-6

梯度执行视图

img/499016_1_En_4_Fig5_HTML.jpg

图 4-5

Hello World Gradle 任务

构建完成后,您可以在build/libs文件夹中找到 WAR 文件。如果在项目浏览器中看不到它,请左键单击该项目,然后按 F5 键更新视图。如果您仍然看不到它,您可能需要移除滤镜。打开项目浏览器的菜单,转到过滤器和自定义➤预设过滤器(见图 4-7 ),并确保 Gradle Build Folder 复选框未选中。

img/499016_1_En_4_Fig7_HTML.jpg

图 4-7

项目浏览器视图过滤器

第二个选项包括从控制台调用 Gradle 包装器。转到项目目录,然后输入以下内容:

./gradlew war

或者,如果您的系统默认不使用合适的 Java,请输入以下内容(输入您的 JDK 路径):

JAVA_HOME=/path/to/jdk ./gradlew war

在此之后,您应该可以在build/libs文件夹中找到 WAR 文件。

启动 Jakarta EE 服务器

第二章描述了安装和操作 GlassFish Jakarta EE 服务器。对于 Hello World 示例,请确保您遵循了这条线索,并确保 GlassFish 正在您的本地系统上运行。

部署和测试 Hello World

要构建和部署项目,您还有两个选择。在 Eclipse 中,首先必须确保两个定制的 Gradle 任务— localDeploylocalUndeploy—对于 Eclipse Gradle 插件是可见的。为此,请打开 Gradle Tasks 视图的菜单,并确保选中“显示所有任务”项。见图 4-8 。

img/499016_1_En_4_Fig8_HTML.jpg

图 4-8

显示所有任务

然后定制任务出现在视图的另一部分,如图 4-9 所示。要调用任何自定义任务,只需双击任务名称。

img/499016_1_En_4_Fig9_HTML.jpg

图 4-9

自定义任务视图

如果您想从控制台执行部署或“取消部署”,也可以使用 Gradle 包装器。转到项目目录,然后输入以下内容:

./gradlew localDeploy
# or
./gradlew localUndeploy

或者,如果您的系统在默认情况下没有使用合适的 Java,请使用这个选项:

JAVA_HOME=/path/to/jdk ./gradlew localDeploy
# or
JAVA_HOME=/path/to/jdk ./gradlew localUndeploy

为了测试 Hello World web 应用,请打开浏览器并输入以下 URL:

http://localhost:8080/HelloWorld

URL 被自动重定向到http://localhost:8080/HelloWorld/mvc/hello,导致呈现登陆页面。

Note

8080是 GlassFish 服务器中 web 应用的默认 HTTP 端口。/HelloWorld来自 WAR 文件的名称(一个特定于服务器的特性),/mvc来自App类,hello来自RootRedirector类。

登陆页面和响应页面如图 4-10 所示。

img/499016_1_En_4_Fig10_HTML.jpg

图 4-10

Hello World web 应用

练习

  • 练习 1: 对还是错?默认情况下,Eclipse Gradle 插件的新 Gradle 项目向导会向项目添加一个 Gradle 包装器。

  • 练习 2: 以下哪些是正确的?(A)Gradle 包装器围绕 Gradle 调用包装操作系统配置。(B)Gradle 包装器在项目文件夹内提供独立的 Gradle 安装。(C)你可以告诉 Gradle 包装使用哪个 JDK。(D)Gradle 包装器将项目添加到操作系统的 Gradle 项目列表中。

  • 练习 3: 对还是错?Gradle 具有在 Jakarta EE 服务器上部署 WAR 文件的内置任务。

  • **练习 4:**Java MVC 支持哪些前端视图模板化技术?

  • 练习 5: 对还是错?Java MVC 模型值必须映射到专用 Java bean 类中的字段。

  • **练习 6:**Java MVC web 应用运行在什么环境下?

  • 练习 7: 对还是错?构建 Java MVC web 应用需要 Gradle。

  • 练习 8:HelloWorld项目中,删除控制器中的models字段,改为添加一个 CDI 托管 bean,如下所示:

  • 提示:您必须给UserData添加javax.enterprise.context.RequestScopedjavax.inject.Named注释。在控制器中,必须添加一个@Inject userData字段。在视图中,您必须使用${userData.name}来访问 bean。

  • 练习 9:HelloWorld示例的响应页面添加一个返回链接。

public class UserData {
    private String name; // + getter / setter
}

摘要

在这一章中,我们讨论了一个 Hello World 风格的 web 应用,它使用 Eclipse 和/或控制台,并使用 Gradle 作为构建框架。在下一章,我们继续从用例的角度来看一些方面,以便提高我们在项目中使用 Java MVC 的技能。

五、开始使用 Java MVC

在我们彻底处理 Java MVC 部分——模型、视图和控制器——之前,我们首先需要讨论一些从用例角度看待 Java MVC 的主题。这介于基本的 Hello World 章节和随后的 Java MVC 实现概念之间。本章的目的是逐步提高你对 Java MVC 开发的熟练程度。具体来说,我们将讨论如何处理来自表单帖子的数据、解析查询参数、转换输入数据类型以及处理异常。

处理表单中的用户输入

在 Java MVC 世界中,前端(浏览器)和控制器之间的数据传输可以通过网页上的<form>元素、前端用户提交发起的POST请求以及控制器类中的方法参数来实现。两个示例参数的相应视图代码如下所示:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
...
<body>
  ...
  <form method="post"
        action="${mvc.uriBuilder(
                'SomeController#someMethod').build()}">
    P1 Parameter: <input type="text" name="p1" />
    P2 Parameter: <input type="text" name="p2" />
    ...
  </form>
  ...
</body>
</html>

这里使用的mvc对象是指mvc,一个自动提供的MvcContext实例(在javax.mvc包中),它的方法uriBuilder()实现了 MVC 项目相关的 URIs/URL 的通用构造。

作为由<form>的动作属性寻址的控制器,我们取一个类似如下的类:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.FormParam;
...

@Path("/abc")
@Controller
public class SomeController {
    ...

    @POST
    @Path("/xyz")
    public Response someMethod(
          @MvcBinding @FormParam("p1") String p1,
          @MvcBinding @FormParam("p2") String p2,
          ...more parameters...
    ) {
        // handle user input ...
        ...
        return Response.ok("responsePage.jsp").build();
    }
}

我们将在本章后面的单独章节中讨论控制器。目前,@Controller注释将该类标识为 Java MVC 控制器,@Path注释用于构建控制器及其方法使用的 URL(子)路径。从@FormParam("p1")开始的p1对应提交的<form>中的一个<input name = "p1" >,相应的一个@FormParam("p2")对应一个<input name = "p2">

也可以避免使用方法参数,而是将用户数据传递给控制器实例字段。这种数据绑定使用以下结构:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.FormParam;
...

@Path("/abc")
@Controller
public class SomeController {
    @MvcBinding @FormParam("p1")
    private String p1;

    @MvcBinding @FormParam("p2")
    private String p2;

    ...

    @POST
    @Path("/xyz")
    public Response someMethod() {
        // handle user input via "p1" and "p2" fields
        ...
       return Response.ok("responsePage.jsp").build();
    }
}

通常最好在方法中声明参数,因为其他方法可能有其他参数,将所有这些参数放在类级别会导致混乱。

对于表单参数,我们知道@FormParam注释直接将方法参数或字段连接到一个<form>输入元素。见图 5-1 。在下一节中,我们将讨论清单中显示的第二个参数注释,@MvcBinding

img/499016_1_En_5_Fig1_HTML.jpg

图 5-1

表单到控制器的连接

Java MVC 中的异常处理

上一节 Java 代码清单中使用的@MvcBinding注释引入了一些关于异常处理的魔法。通常,因为 Java MVC 位于 JAX-RS 之上,所以在输入数据处理期间抛出的异常只能由特殊的异常映射器捕获。这个过程不太适合 Java MVC 世界。我们希望在控制器和表单提交之间有一个明确的关系,一个异常处理映射器类引入了一种额外的“控制器”类型,严格来说,与任何 MVC 概念都没有关系。相反,通过使用@MvcBinding注释,无论是否有错误,都将调用相同的控制器和控制器方法,并且由验证不匹配和转换错误导致的传递错误将被提供给javax.mvc.binding.BindingResult的注入实例。

然后,您可以通过使用BindingResult实例的方法以编程方式检查任何错误:

...
import javax.mvc.binding.MvcBinding;
import javax.mvc.binding.BindingResult;
import javax.ws.rs.FormParam;
import javax.validation.constraints.Size;
...

@Path("/abc")
@Controller
public class SomeController {
    @Named
    @RequestScoped
    public static class ErrorMessages {
      private List<String> msgs = new ArrayList<>();

      public List<String> getMsgs() {
        return msgs;
      }

      public void setMsgs(List<String> msgs) {
        this.msgs = msgs;
      }

      public void addMessage(String msg) {
        msgs.add(msg);
      }
    }

    // Errors while fetching parameters
    // automatically go here:
    private @Inject BindingResult br;

    // We use this to pass over error messages
    // to the response page:
    private @Inject ErrorMessages errorMessages;

    ...

    @POST
    @Path("/xyz")
    public Response someMethod(
          @MvcBinding @FormParam("p1")
           @Size(min=3,max=10)
               String p1,
          @MvcBinding @FormParam("p2")
               String p2)
    {
        // ERROR HANDLING //////////////////////////
        if(br.isFailed()) {
          br.getAllErrors().stream().
                 forEach((ParamError pe) -> {
            errorMessages.addMessage(pe.getParamName() +
                  ": " + pe.getMessage());
          });
    }
    // END ERROR HANDLING //////////////////////

    // handle user input via "p1" and "p2" params
    ...

    // advance to response page
    return Response.ok("responsePage.jsp").build();
  }
}

这里,我们为错误消息使用了一个内部类。当然,您也可以在自己的文件中为消息使用自己的类。还要遵守p1参数的@Size约束。这属于 bean 验证,我们后面会详细讲。这里使用的@Size约束意味着,如果输入的字符串少于三个字符或多于十个字符,验证错误将通过BindingResult键入的br字段提交。

在响应页面上,您可能会呈现如下错误:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
...
<body>
  <div style="color:red">
  <c:forEach var="e" items="${errorMessages.msgs}">
      ${e}
  </c:forEach>
  </div>
...
</body>
</html>

凭借@Named注释,${errorMessages. ...}连接到ErrorMessages的注入实例(第一个字母降低)。

在正常响应页面中显示错误消息的另一种方法是将页面流转移到不同的视图页面。这很容易,因为我们在控制器方法中决定下一步去哪里。因此,我们可以这样写:

...
@POST
@Path("/xyz")
public Response someMethod(...) {
    // ERROR HANDLING //////////////////////////
    if(br.isFailed()) {
      br.getAllErrors().stream().
          forEach((ParamError pe) -> {
        errorMessages.addMessage(pe.getParamName() +
           ": " + pe.getMessage());
    });
    // advance to error page
    return Response.ok("errorPage.jsp").build();
  }
  // END ERROR HANDLING //////////////////////

  // handle user input via "p1" and "p2" params
  ...

  // advance to response page
  return Response.ok("responsePage.jsp").build();
}
...

非字符串 POST 参数

在上一节中,我们只使用了String类型的POST参数。在 Java MVC 中,还可以使用数值类型intlongfloatdoubleBigDecimalBigIntegerboolean ( truefalse)。因此,可以编写以下内容:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
...
<body>
  ...
  <form method="post"
        action="${mvc.uriBuilder(
                'SomeController#someMethod').build()}">
    Int Parameter: <input type="text"
        name="theInt" />
    Double Parameter: <input type="text"
        name="theDouble" />
    Boolean Parameter: <input type="text"
        name="theBoolean" />
    ...
  </form>
  ...
</body>
</html>

在控制器类中,编写以下内容:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.FormParam;
...

@Path("/abc")
@Controller
public class SomeController {
    ...

    @POST
    @Path("/xyz")
    public Response someMethod(
        @MvcBinding @FormParam("theInt")
            int theInt,
        @MvcBinding @FormParam("theDouble")
            double theDouble,
        @MvcBinding @FormParam("theBoolean")
            boolean theBoolean)
    {
        // handle user input via the fields
        ...
        return Response.ok("responsePage.jsp").build();
    }
}

Java MVC 负责将POST参数正确地转换成指定的 Java 类型。

如果无法正确执行转换,可能是因为在theInt输入字段中输入了“x”,例如,可以使用注入的BindingResult(如前一节所述)来捕捉转换错误。

处理查询参数

HTTP 动词包括POSTGETPUTDELETE等。到目前为止,在浏览器到控制器的通信中,我们讨论了通过 HTML <form>元素传输数据的POST请求,以及请求登录页面的GET请求。考虑以下情况:在登录页面上,要求用户输入一些数据,然后提供一个提交按钮,将数据传输到控制器,并前进到响应页面。在响应页面上,我们想要添加一个后退按钮。该按钮需要以下附加功能:在字段中输入的所有数据应该再次显示。我们如何做到这一点?控制器@FormParam字段不能使用,因为它们只适用于表单POST s。

到目前为止,我们还没有使用会话数据存储,这延长了单个请求/响应周期。如果有的话,将用户输入存储在那里,然后用它来预置输入字段将是一种有效的方法。事实上,使用会话是可能的,但是我们将在本章的后面讨论它。此外,不使用会话可以减少内存占用并简化状态管理。

我们能做的和 Java MVC 支持的是查询参数的使用。如果GET打开,例如 http://xyz.com/the-app/start ,查询参数将添加到以?开始并使用&作为分隔符的附加字符串中:

http://xyz.com/the-app/start?name=John&birthday=19971230

要在控制器中获取这样的查询参数,可以使用@QueryParam注释:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("/")
@Controller
public class SomeController {
    private @Inject BindingResult br;

    @GET
    @Path("/start")
    public String someMethod(
        @MvcBinding @QueryParam("name") String name,
        @MvcBinding @QueryParam("birthday") String birthday
  ) {
     if(name != null) {
       // handle "name" parameter
     }
     if(birthday != null) {
       // handle "birthday" parameter
     }

     // advance to page
     return "index.jsp";
  }
  ...
}

同样,与POST参数一样,也可以使用字段来获取查询参数:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("/")
@Controller
public class SomeController {
    private @Inject BindingResult br;

    @MvcBinding @QueryParam("name")
    private String name;

    @MvcBinding @QueryParam("birthday")
    private String birthday);

    @GET
    @Path("/start")
    public String someMethod() {
      if(name != null) {
        // handle "name" parameter
      }
      if(birthday != null) {
        // handle "birthday" parameter
      }

      // advance to page
      return "index.jsp";
    }
    ...
}

在 JSP 页面上,用于发出这种参数化的GET请求的专用元素是一个<a>链接:

<a href="${mvc.uriBuilder('SomeController#someMethod').
          queryParam('name', userData.name).
          queryParam('birthday', userData.birthday).
          build()}">Link</a>

该代码片段使用了一个userData变量,该变量可以作为以下内容的实例注入:

@Named
@RequestScoped
public class UserData {
  private String name;
  private String birthday;
  // Getters, setters...
}

显然,这个对象必须在控制器动作中填充数据,控制器动作最终调用带有<a>链接的页面。

对于请求参数,与POST请求一样,同样的转换规则也适用于非字符串类型的参数。您可以使用类型为string、数字类型为intlongfloatdoubleBigDecimalBigIntegerboolean ( truefalse)的字段或方法参数。被传递的查询参数被适当地转换。同样,因为我们用@MvcBinding标记了参数,所以这里应用了针对POST参数描述的处理异常的相同方法。

创建用先前输入的值填充原始页面的后退链接的详细过程如下:

  1. 在数据输入页面(名为dataInput.jsp)上,值从<form>元素内部发布。

  2. 在相应的控制器类和方法中,我们通过@FormParam注释的字段或方法参数检索数据,并以编程方式将值传输到注入的对象中(用@Named标记)。

  3. 在后续页面(名为responsePage.jsp)上,我们创建了一个反向链接,其中包含来自注入对象的查询参数。

  4. 在相应的控制器类和方法中,我们通过@QueryParam带注释的字段或方法参数检索数据,并以编程方式将值传输到注入的对象中(用@Named标记)。

  5. 我们修改了dataInput.jsp中的<input>元素,增加了value属性:<input ... value = "${injectedObject.field}">,其中injectedObject对应于注入类InjectedObject的字段。

  6. 对于验证和转换错误,我们注入一个BindingResult的实例。我们在控制器方法中使用它来检查错误。为此,我们必须将@MvcBinding添加到所有表单和查询参数中。

Note

可以同时使用表单(POST)参数和查询参数。只需将queryParam( 'name', value )方法调用添加到<form>动作的 URI 生成器中。但是,我们不想把事情搞得太复杂,所以在本书中不进一步考察这种混合。

练习

  • 练习 1: 以下哪一项是正确的?网页上的<form>元素连接到:(A)控制器类的方法userPosts()。(B)由表单的action = "..."属性和控制器类及其方法使用的@Path注释确定的控制器类的某个方法。(C)注入控制器的某个模型元素。

  • 练习 2: 描述 Java 类成为 Java MVC 控制器类的最低要求。

  • **练习 3:**JAX-RS 和 Java MVC 最明显的相似之处是什么?两者最突出的区别是什么?

  • 练习 4:@MvcBinding标注的目的是什么?

  • 练习 5: 将本章描述的错误处理添加到上一章的HelloWorld应用中。

  • 练习 6: 继续上一个练习,添加一个验证约束,确保用户只输入英文字母作为姓名。提示:相应的正则表达式读作[A-Za-z]*

  • 练习 7: 添加一个返回链接到上一章中HelloWorld应用的响应页面。添加用户名作为查询参数,并确保输入的用户名再次出现在起始页的输入字段中。

摘要

在 Java MVC 世界中,前端(浏览器)和控制器之间的数据传输可以通过网页上的<form>元素(和/或查询参数)、前端用户提交(或链接点击)发起的POST(或GET)请求以及控制器类中的方法参数来实现。

@Controller注释将类标识为 Java MVC 控制器,而@Path注释用于构建控制器及其方法使用的 URL(子)路径。

通常,因为 Java MVC 位于 JAX-RS 之上,所以在输入数据处理期间抛出的异常只能由特殊的异常映射器捕获。

这个过程不太适合 Java MVC 世界。相反,通过使用@MvcBinding注释,无论是否有错误,都将调用相同的控制器和控制器方法,并且由于验证不匹配和转换错误导致的传递错误将被提供给javax.mvc.binding.BindingResult的注入实例。

除了在 Java MVC 中发布字符串类型的参数(和/或传递字符串类型的查询参数),还可以使用数值类型intlongfloatdoubleBigDecimalBigIntegerboolean ( truefalse)。Java MVC 负责将POST参数正确地转换成指定的 Java 类型。

如果不能正确执行转换,可能是因为在整型输入字段中输入了“x ”,那么可以使用注入的BindingResult来捕捉转换错误。

在这个更加以用例为中心的 Java MVC 视图之后,我们在下一章继续讨论一个更加以概念为中心的视图,从模型开始,从 Java MVC 的视图和控制器部分开始。

六、深入的 Java MVC

在这一章中,我们彻底地处理了 Java MVC 提供的各种特性。请注意,本章并不能替代官方的 Java MVC 规范(在撰写本书时,最新版本是 1.0),您可以在以下网址找到该规范:

https://download.oracle.com/otndocs/jcp/mvc-1-final-spec

相反,这一章涵盖了你最常遇到的模式,我们也将通过一些例子片段。

模型

对于 Java MVC 的模型部分,不要与数据库模型混淆,MVC 框架的最初想法是相当原始的。模型类只是 Java bean 类(具有字段、getters 和 setters 的类),开发人员会以类似于下面的方式(这是伪代码,不是真正的 Java)以编程方式将它们添加到视图中:

...
// inside some controller
String name = ...; // somehow via form POST
int i1 = ...;    // somehow via form POST

   HttpRequest req = ..; // somehow via framework

   MyBean b = new MyBean(); b.setName(name); b.setSomeInt(i1);

   req.setBean("beanName", b);

   // somehow advance to response page
   ...

在响应视图中,您可能会使用类似下面的表达式来访问模型 beans:

Hello ${beanName.name}

其中beanName对应于来自伪代码的setBean()方法参数,name对应于字段名。

Java MVC 中的 CDI

Java MVC 是一个现代框架,它的模型能力取代了简单引用 beans 的概念。它通过在 CDI 2.0 版本中为 Jakarta EE 8 引入 CDI(上下文和依赖注入)技术来实现这一点。CDI 不是一项小技术——它的规范 PDF 有 200 多页!不用说,我们不能介绍 CDI 的每一个概念,但是我们讨论了最重要的思想,并把我们的调查集中在 Java MVC 使用 CDI 的方式上。

Note

你可以在 https://jakarta.ee/specifications/cdi/2.0/ 找到 CDI 规范。

基本思想是相同的:我们希望实例化 bean 类(主要包含字段及其 getters 和 setters 的数据类),并将这些实例提供给控制器和视图。前 CDI 和 CDI 方式的主要区别在于,我们不自己实例化这样的模型类,而是让 CDI 来做。

为了告诉 Java MVC 我们希望模型类由 CDI 控制并对视图页面可用,我们使用了来自javax.inject包的@Named注释:

import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class UserData {
  private String name;
  private String email;
  // Getters and setters...
}

我们还使用@RequestScoped注释将对象实例的生命周期绑定到一个 HTTP 请求/响应周期。我们将在下一节中更多地讨论作用域。

一旦我们通过@Named向 CDI 框架宣布了一个 bean,在 Java MVC 中就会发生两件事。首先,我们可以使用@Inject(包javax.inject)从任何 Java MVC 控制器内部和任何其他 CDI 控制的类内部引用 bean 实例。其次,我们可以通过使用首字母小写的类名:${userData.name}${userData.email}来使用视图页面中的实例。见图 6-1 。

img/499016_1_En_6_Fig1_HTML.jpg

图 6-1

Java MVC 中的 CDI

如果您想为 CDI beans 使用不同的名称,您可以使用带参数的@Named:

import javax.enterprise.context.RequestScoped;
import javax.inject.Named;
@Named("user")
@RequestScoped
public class UserData {
  private String name;
  private String email;
  // Getters and setters...
}

然后,您可以在视图页面中使用更改后的名称:${user.name}。因为在@Inject中,引用是通过类名而不是注释参数发生的,所以为了注入到 Java 类中,您仍然要使用@Inject private UserName userName;,即使名称已经改变。

模型对象范围

如果您使用 CDI 来管理模型数据,那么模型类实例从属于由 CDI 管理的生命周期控件。这意味着 CDI 决定什么时候构造 beans,什么时候放弃它们。在注入的 beans 中,CDI 控制实例生命周期的方式是通过一个叫做 scope 的特性。在 Java MVC 中,存在以下作用域:

  • 请求范围 : 注入 bean 的实例是在 HTTP 请求期间创建的,并且只在 HTTP 请求和发送给客户端(浏览器)的响应的生命周期内有效。请求范围变量的一个典型使用场景是将POST表单数据或GET查询参数传递给响应中定义的视图层页面。因此,您将@Named请求范围 bean 注入控制器,在那里设置它们的字段,并在视图层使用 bean。因为请求作用域 beans 的生命周期很短,所以它们有助于保持 web 应用的低内存占用并避免内存泄漏。

  • **会话范围:**一个会话绑定到一个浏览器窗口,跨越几个 HTTP 请求/响应周期。每当用户进入 web 应用时,会话就开始,并在超时或显式会话取消时终止。会话范围内的数据对象占主导地位,直到触发某个超时或显式关闭会话。当需要维护生命周期超过一个 HTTP 请求/响应周期的状态时,可以使用会话范围的对象。会话数据简化了状态处理,但是极大地增加了 web 应用消耗内存或造成不稳定的内存泄漏的危险。

  • 重定向范围 : 为了支持POST -redirect- GET设计模式,Java MVC 为 CDI beans 定义了一个重定向范围。如果您想避免浏览器用户在POST动作终止前点击 reload 按钮时的重新发布,您可以使用这种模式。具有重定向范围的 beans 的生命周期跨越了POST和随后的GET(因为浏览器接收重定向代码 303)。在 Java MVC 控制器中,通过从处理POST的方法内部返回一个Response.seeOther( URI.create("response/path" )).build()或一个字符串"redirect:response/path"来启动POST -redirect- GET。流程如下:

    1. 用户在表单中输入数据并提交。Java MVC 控制器被调用。

    2. 控制器通过表单参数工作,最后方法返回Response.seeOther( URI.create("response/path" )).build()"redirect:response/path"

    3. 浏览器自动发送一个重定向到给定的路径。

    4. response/path路径(相应地修改)指向另一个带有GET动词的控制器方法。它前进到一个视图页面,显示对用户请求的适当响应。

重定向范围 CDI beans 的生命周期是从最初的POST请求到随后的GET请求生成的响应,这是两个 HTTP 请求/响应周期。

  • 应用范围 : 任何应用范围内与用户无关的数据都可以使用这个范围。在取消部署 web 应用或停止服务器之前,数据一直有效。

  • 依赖作用域 : 这是伪作用域。这意味着 CDI bean 的作用域与激活它的 bean 的作用域相同。如果没有显式设置范围,则默认为依赖范围。

为了定义注入 bean 的范围,可以使用以下注释之一:

@RequestScoped
@SessionScoped
@ApplicationScoped
@RedirectScoped
@Dependent

都是来自javax.enterprise.context包,除了RedirectScoped是 Java MVC 扩展,属于javax.mvc.annotation包。

简化模型数据容器

除了使用标有@Named注释的 CDI beans,您还可以使用Models的注入实例(在javax.mvc包中)。在控制器中,您可以编写以下内容:

import javax.inject.Inject;
import javax.mvc.Controller;
import javax.mvc.Models;
...

@Path("/abc")
@Controller
public class SomeController {
    @Inject private Models models;

    ...
    // inside any method:
    models.put("name", name);
    ...
}

然后,模型值可从内部视图页面获得,无需前缀:

Hello ${name}

仅在需要处理少量数据时使用Models接口。否则,您将面临无结构、不可理解的代码的风险。

Note

Models数据有请求范围。

如果您需要来自Models对象的模型值(仍然在同一个请求/响应周期内!),可以使用get()方法:

   Object o = models.get("someKey");

   // or, if you know the type
   String s = models.get("someKey", String.class);
}

视图:JSP

Java MVC 的视图部分负责向客户端(浏览器)呈现前端,包括输入和输出。那些连接到控制器方法的 Java MVC 视图文件在WEB-INF/views文件夹中,或者,因为我们使用 Gradle 作为构建框架,所以在src/main/webapp/WEB-INF/views文件夹中。

Java MVC 开箱即用,支持两种视图引擎——JSP(Java server Pages)和 Facelets(JSF 的视图声明语言,JavaServer Faces)。通过设计,基于 CDI 的扩展机制可以包含其他视图引擎。在本节中,我们将讨论 Java MVC 视图的 JSP 变体。

Note

有关 JSP 规范,请参见 https://download.oracle.com/otndocs/jcp/jsp-2_3-mrel2-spec/

JSP 基础

JSP 允许开发人员将静态内容(例如 HTML)和由 JSP 元素表示的动态内容交织在一起。一个 JSP 页面被内部编译成一个继承自Servlet的大 Java 类。包含 JSP 代码的文件以.jsp结尾。

Note

对于 GlassFish,您可以在GLASSFISH_INST/glassfish/domains/domain1/generated/jsp/-[PROJECT-NAME]文件夹中看到生成的 servlets。

指令

JSP 指令为容器提供了方向。表 6-1 给出了指令的描述,表 6-2 具体列出了 JSP page指令。

表 6-2

jsp 指令页

|

名字

|

描述

| | --- | --- | | buffer="..." | 使用它来设置输出缓冲区的大小。可能的值:none(无缓冲),或Nkb,其中N是一个数字,kb代表千字节(例如:8kb)。 | | autoFlush="true"&#124;"false" | 输出缓冲区填满后自动刷新。否则,将引发异常。默认为true。 | | contentType="..." | 设置输出的内容类型。例子:text/htmltext/xml。要指定字符编码,添加;charset=...,如contentType = "text/html;charset=UTF-8"所示 | | errorPage="..." | 指定在引发异常时显示的错误页面。这是一个相对 URL。示例:errorPage = "error.jsp" | | isErrorPage="true"&#124;"false" | 如果true,将该 JSP 视为错误页面。 | | extends="some.pckg.SomeClass" | 使生成的 servlet 扩展给定的类。这样,您可以提供自己的 servlet 实现。 | | import="..." | 工作方式与 Java import语句完全一样。 | | info="..." | 在此添加描述 JSP 的任何文本。 | | isThreadSafe="true"&#124;"false" | 如果false,一次只有一个线程将处理 JSP。默认为true。 | | language="..." | 指示使用的编程语言。这里写java。 | | session="true"&#124;"false" | 如果选择true,将启用会话。默认为true。 | | isELIgnored="true"&#124;"false”" | 如果为true,则表达式语言构造${ ... }不被求值。默认是false。 | | isScriptingEnabled="true"&#124;"false" | 如果true,则启用动态 JSP 脚本。默认为true,设置为false除了真正的静态页面之外,通常没有任何意义。 |

表 6-1

jsp 指令

|

名字

|

描述

| | --- | --- | | <% page ... %> | 依赖于页面的属性。可能的参数如表 6-2 所示(空格分隔列表)。 | | <% include file="relative url" %> | 在此位置包含另一个文件。例如:<% include file = "header1a.jsp" %> | | <% taglib uri="uri" prefix="prefix" %> | 包括标签库。标记库文档中显示了精确的语法。 |

带有最常见指令的基本 JSP 文件头如下所示:

<%@ page language="java"
    contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix = "c"
    uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt"
    uri = "http://java.sun.com/jsp/jstl/fmt" %>

这意味着文本编辑器使用 UTF-8(我假设是这样)。两个taglib指的是 JSTL (JavaServer Pages 标准标记库)标记库。这个taglibcorefmt部分指的是许多 web 应用通用的有用标签。

Note

JSTL 有更多的部件,我们不会用在 Java MVC 中。如果你想了解更多关于 JSTL 的信息,请前往 https://jcp.org/aboutJava/communityprocess/final/jsr052/index.html

静态内容

要生成静态内容,只需将它逐字写入 JSP 文件中:

<%@ page language="java"
    contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix = "c"
    uri = "http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix = "fmt"
    uri = "http://java.sun.com/jsp/jstl/fmt" %>

<html>
<head>
    <meta charset="UTF-8">
    <title>Model And CDI</title>
</head>
<body>
    <%-- The string inside action is dynamic contents --%>
    <form method="post"
        action="${mvc.
                 uriBuilder('ModelAndCdiController#response').
                 build()}">
      Enter your name: <input type="text" name="name" />
      <input type="submit" value="Submit" />
    </form>
</body>
</html>

这段代码将按原样输出,但有三个例外。最上面的指令是包含注释的<%-- ... --%>,以及代表由 JSP 引擎内部的处理步骤处理的表达式的${ ... }

Java Scriptlets 和 Java 表达式

因为 JSP 被转录成 Java 类,所以 JSP 允许 Java 代码和表达式包含在 JSP 页面中。语法如下:

<%=
    Any Java code
    ...
%>

<%=
    Any Java expression (semicolons not allowed)
    ...
%>

第二个构造<%= ... %>,将表达式结果添加到 servlet 的输出流中。

Caution

不要过度使用这些结构。毕竟,Java 是面向对象的语言,而不是前端模板语言。

隐式对象

<%= ... %><% ... %>中,有几个你可以使用的隐式对象:

  • out:JspWriter(扩展java.io.Writer)类型的 servlet 的输出流。

  • request:请求,类型HttpServletRequest

  • response:响应,类型HttpServletResponse

  • session:session,类型HttpSession

  • application:应用,类型ServletContext

  • config:servlet 配置,类型ServletConfig

  • page:servlet 本身,类型Object(运行时类型javax.servlet.http.HttpServlet)。

  • pageContext:页面上下文,类型PageContext

您可以使用这些对象来实现奇特的结果,但是请记住,如果您使用它们,您会以某种方式离开正式的开发模式。这可能会使其他人难以阅读您的代码,并且通过将功能放入视图页面,模型、视图和控制器之间的自然界限被打破。

JavaBeans 组件

带有@Named注释的 CDI beans 被直接提供给 JSP:

@Named
public class UserName {
  private String name;
  // Getters and setters...
}

JSP:
...
Hello ${userName.name}

如果您将模型数据添加到注入的javax.mvc.Models CDI bean 中,您可以直接访问它而无需前缀:

Controller:

import javax.mvc.Models;
...
@Controller
public class SomeController {
  @Inject private Models models;
  ...
  // inside any method:
  models.put("name", name);
  ...
}

JSP:
...
Hello ${name}

在这两种情况下,您都在 JSP 中使用了一个表达式语言构造${ ... }。我们将在下一节讨论表达式语言。

Caution

由于隐式对象,您可以直接从 JSP 内部引用POST或查询参数。然而,这不像 MVC,因为它引入了控制器无法触及的第二个模型层,并且它将控制器的责任转移到视图。所以不要这样做,总是使用注入的 CDI beans 来代替。

表达式语言

${ ... }这样的 JSP 页面中的构造被视为一个表达式,由表达式语言处理程序处理。表达式元素包括:

  • name:直接引用 CDI 托管 bean 或隐式对象。在呈现视图时,表达式导致使用toString()方法来生成输出。例如:${user}

  • value.property:指的是一个value对象的property字段(必须有一个 getter),或者如果value是一个 map,则是一个由property键控的 map 条目。例子:${user.firstName}(在user CDI bean 中必须有一个getFirstName())和${receipt.amount} ( receipt是一个 map,amount是其中的一个 key)。

  • value[property]:指的是一个value对象的字段 value-of- property(必须有一个 getter),或者如果value是一个映射,或者如果property计算为一个int(用于索引)并且如果value是一个列表或数组,则是一个由 value-of- property键控的映射条目。property也可以是字面意思,如421.3'someString'"someString"。示例:${user['firstName']}(与${user.firstName}相同)和${list[2]}(列表或数组中的第三个元素)。

  • unaryOperator value:unaryOperator应用于value。一元运算符有(求反)、not!empty(值为null或空)。

  • value1 binaryOperator value2:binaryOperator应用于value1value2。二元运算符有:

    • 算术:+-*/div%mod(取模)

    • 逻辑:and&&or||

    • 关系:==eq!=ne<lt>gt < =、和le>=ge

  • value1 ternaryOperatorA value2 ternaryOperatorB value3:ternaryOperator应用于value1value2value3。只有一个:a ? b : c评估为b如果a为真;否则,计算结果为c

有几个隐式对象可以在表达式中使用,如表 6-3 所示。

表 6-3

EL 隐式对象

|

名字

|

描述

| | --- | --- | | pageScope | 具有来自page作用域的作用域变量的映射。 | | requestScope | 具有来自request作用域的作用域变量的映射。 | | sessionScope | 具有来自session作用域的作用域变量的映射。 | | applicationScope | 具有来自application作用域的作用域变量的映射。 | | paramValues | 将请求参数作为字符串集合的映射。在 Java MVC 应用中,通常不会通过表达式访问这样的数据,所以不要使用它。 | | param | 将请求参数作为字符串的映射(每个请求参数的第一个)。在 Java MVC 应用中,通常不会通过表达式访问这样的数据,所以不要使用它。 | | headerValues | 将 HTTP 请求头作为字符串集合的映射。 | | header | 将 HTTP 请求头作为字符串的映射(每个头的第一个)。要访问某个标题,你可以写${header["user-agent"]}。 | | initParam | 带有上下文初始化参数的映射。 | | cookie | 将 cookie 名称映射到javax.servlet.http.Cookie的实例。 | | pageContext | 类型为javax.servlet.jsp.PageContext的对象。允许您访问各种对象,如请求、响应和会话。 |

输出

如果您喜欢使用标签进行动态输出,您可以使用如下的<c:out>标签:

Hello <c:out value="${userData.name}" />

<%-- Similar to --%>
Hello ${userData.name}

然而,它们并不完全相同。如果没有额外的escapeXml = "false",标签将例如用>替换>,用<替换<。如果${userData.name}恰好是<John>,你在Hello ${userData.name}的浏览器窗口里什么也看不到。浏览器看到一个<John>,它将其解释为一个(无效的)标签。相反,标签变量输出一个显示为<John><John>

<c:out>的属性如下:

  • escapeXml:是否对特殊 XML 字符进行转义。不需要;默认为true

  • value:要打印的数值。必选。通常你在这里写一个类似于${someBean.someProperty}的表达式。

  • default:值出问题时默认写入。不需要。

变量

使用<c:set>标签,我们可以引入变量,以便在页面上进一步使用。在 Java MVC 世界中,最常见的使用场景是引入别名来提高可读性。像设置会话范围变量这样的任务不应该在 JSP 内部完成,因为这是控制器的责任。

<c:set var="firstName" value=${user.firstName} />
<%-- We can henceforth use 'firstName' in expressions
     Instead of 'user.firstName' --%>
Hi ${firstName}

<c:set>标签的完整属性集如下所示:

  • value:用于新变量(或属性)的值。通常,您在这里编写一个类似于${someBean.someProperty}的表达式。

  • var:存储值的新变量的名称。不是必需的,但是如果没有给出,targetproperty必须使用。

  • scope:在var="..."中给定的变量的范围。默认为page(仅当前渲染的页面)。

  • target:存储值的对象或贴图。不需要。

  • property:如果指定了target,属性(字段)或关键字(用于地图)的名称。不需要。

对于列表或数组上的循环,可以使用<c:forEach>标签(c表示jstl/core taglib):

<c:forEach items="${theList}" var="item">
  ${item} <br/>
</c:forEach}

items="..."中的表达式可以是任何数组或字符串、原语或其他对象的列表。

您将经常在 HTML 表格中使用这样的循环。在控制器中,您构建一个 item 对象列表,每个项目代表表中的一行:

// probably inside a models package:
@Named
@RequestScoped
public class Members {
  private List<Member> list = new ArrayList<>();
  public void add(Member member) {
     list.add(member);
  }
  // Getters, setters...
}

public class Member {
  private int id;
  private String firstName;
  private String lastName;
  // Constructors, getters, setters...
}

// probably inside a controllers package:
@Controller
public class MyController {
    @Inject private Members members;

    // inside a method:
    members.add(new Member(...));
    members.add(new Member(...));
    ...
}

在 JSP 中,我们现在可以通过${members. ...}访问Members对象,并从列表中构建一个表:

<%@ page contentType="text/html;charset=UTF-8"
    language="java" %>
<%@ taglib prefix="c"
    uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
    <meta charset="UTF-8">
    <title>Table</title>
</head>
<body>
  <table>
    <thead>
      <tr>
        <th>ID</th>
        <th>Last Name</th>
        <th>First Name</th>
      </tr>
    </thead>
    <tbody>
      <c:forEach items="${members.list}" var="item">
        <tr>
           <td>${item.id}</td>
           <td>${item.lastName}</td>
           <td>${item.firstName}</td>
        </tr>
      </c:forEach>
    </tbody>
  </table>
</body>
</html>

<c:forEach>标签的所有可能属性如下:

  • items:要迭代的项目。不是必需的,但是如果缺少,循环将迭代一个整数。这是你可能写一个类似于${someBean.someListOrArray}的表达式的地方。

  • var:页面范围变量的名称,该变量将被生成并保存循环中的每一项。不需要。

  • begin:元素开始。不需要;默认为0(第一项)。

  • end:结束元素。不需要;默认值是最后一个元素。

  • step:这一步。不需要;默认为1

  • varStatus:循环状态变量的名称(页面范围)。不需要。该变量将保存一个类型为javax.servlet.jsp.jstl.core.LoopTagStatus的对象。

如果您想在一个整数值范围循环中使用<c:forEach>标记,您不需要指定items属性,而是使用beginend属性:

<c:forEach begin="1" end="10" var="i">
  ${i}<br/>
</c:forEach>

条件分支

对于 JSP 内部的条件分支,您可以使用<c:if><c:choose>标签之一。简单的<c:if>测试允许进行简单的条件检查,无需替代方案和else分支:

<c:if test="${showIncome}">
   <p>Your income is: <c:out value="${income}"/></p>
</c:if>

使用下面的结构可以轻松地实现一个if-else:

<c:if test="${showIncome}">
   <p>Your income is: <c:out value="${income}"/></p>
</c:if><c:if test="${!showIncome}">
   <p>Your income is: ***</p>
</c:if>

然而,对于真正的if-elseif-elseif-...-else<choose>标签是更好的选择:

<c:choose>
    <c:when test="${income <= 1000}">
       Income is not good.
    </c:when>
    <c:when test="${income > 10000}">
       Income is very good.
    </c:when>
    <c:otherwise>
       Income is undetermined...
    </c:otherwise>
</c:choose>

饼干

通过使用隐式的cookie对象,可以直接从 JSP 内部读取 Cookies:

Cookie name: ${cookie.theCookieName.name} <p/>
Cookie value: ${cookie.theCookieName.value} <p/>

其中theCookieName被替换为 cookie 名称。然后,${cookie.theCookieName}引用了一个类型为javax.servlet.http.Cookie的对象。但是,只有名称和值可用。

出于测试目的,您可以在控制器方法中创建一个名为theCookieName的 cookie(随意设置 cookie 属性):

@Controller
@Path("abc")
public class MyController {
  @GET
  public Response myResponse() {
      ...
      // This is a subclass of Cookie:
      NewCookie ck = new NewCookie("theCookieName",
          "cookieValue",
          "the/path",
          "my.domain.com",
          42,
          "Some Comment",
          3600*24*365,
          false);

      return Response.
          ok("responsePage.jsp").
           cookie(ck).
          build();
  }
  ...
}

在响应页面(或稍后的页面)中,您可以编写所示的 JSP 代码来研究 cookie。

Caution

对于本地测试服务器,您必须将localhost设置为 cookie 域。此外,您必须设置适当的路径值,为了简单起见,可能是/(它匹配所有路径)。

视图:Facelets

除了 JSP 之外,Java MVC 支持其他视图技术是 Facelets 。Facelets 是专门为 JSF 创建的模板框架,JSF (JavaServer Faces)是 Jakarta EE 专用的主要前端技术。JSF 是基于组件的,而 Java MVC 是基于动作的。这就是问题出现的地方:Java MVC 在某种程度上是 JSF 的竞争对手,所以 Java MVC 和 Facelets 乍一看似乎不匹配。好消息是,因为 JSF 和 Facelets 是高度解耦的,我们不必使用 JSF 组件,Facelets 作为一个简单的模板引擎也可以用于 Java MVC。这很好,因为与 JSP 相比,Facelets 更适合现代编程风格,JSP 有时被认为是老派的,尽管令人尊敬。

但是,我们并不是无意将 Facelets 作为 Java MVC 的模板引擎放在第二位的。几十年来,JSP 已经被证明是有价值的,它们更接近前端开发人员经常使用的基本编程范例。此外,如果您有一些 JSF 编程经验,使用 Facelets 可以避免尝试使用 Java MVC 的 JSF 特性的危险,这很容易搞乱您的应用设计。相比之下,Facelets 应用了更高程度的抽象,如果由熟练的开发人员使用,它允许更精简和更干净的应用设计。

话虽如此,使用哪种前端技术完全取决于您。本节将向您展示如何使用 Facelets for Java MVC。

Facelets 文件

Java MVC 的 Facelets 文件与 JSP 文件放在同一个文件夹中:文件夹WEB-INF/views,或者,因为我们使用 Gradle 作为构建框架,所以放在文件夹src/main/webapp/WEB-INF/views中。

Facelets 文件是 XML 文件,这可能是 JSP 和 Facelets 之间最显著的区别。Facelets 中没有类似于\ci{<\% ... \%>}的指令,也不能使用不是有效 XML 的遗留 HTML 结构,但是 JSP 允许使用。

Facelets 配置

我们在 JSP 编程中实现的,避免了提供web.xml配置文件的需要,也可以在 Facelets 中实现。首先,我们提供了一个App类来将mvc添加到 URL 上下文路径中:

package any.project.package;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/mvc")
public class App extends Application {
}

该类故意为空;上下文路径元素仅由注释添加。

接下来,我们添加一个重定向器,它允许我们使用基本 URL http://the.server:8080/WarName/ 来启动应用(这是针对 GlassFish 的,WarName需要替换为 WAR 文件名)。重定向器将这样的请求转发给 http://the.server:8080/WarName/mvc/facelets ,我们将使用它作为在控制器类中配置的登录页面的入口点。名字不重要;我们称之为RootRedirector:

package any.project.package;

import javax.servlet.FilterChain;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebFilter(urlPatterns = "/")
public class RootRedirector extends HttpFilter {
  private static final long serialVersionUID =
        7332909156163673868L;

@Override
  protected void doFilter(final HttpServletRequest req,
        final HttpServletResponse res,
        final FilterChain chain) throws      IOException {
      res.sendRedirect("mvc/facelets");
  }
}

剩下要做的就是注意在控制器中,“facelets”路径将导致登录页面上的GET:

import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/facelets")
@Controller
public class MyFaceletsController {
    @GET
    public Response showIndex() {
        return Response.ok("index.xhtml").build();
    }
    ...
}

通过 Facelets 进行模板化

Facelets 允许我们引入参数化的模板 HTML 页面、要包含在页面中的 HTML 片段(组件)、这种片段的占位符,以及修饰器和类似精心制作的列表视图的重复。在接下来的页面中,我们首先使用 Facelets 标记,然后开发一个示例应用来帮助您入门。

要使用 Facelets,必须将 Facelets 名称空间添加到 XHTML 文件中:

<?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html>
<html lang="en"
      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
    <h:head>
        <title>Facelet Title</title>
    </h:head>
<body>
  ...
</body>
</html>

在接下来的小节中,我们将解释可以包含在 XHTML 文件中的 Facelets 标签,以应用或混合模板、包含 XHTML 片段或传递参数。

标签

包括另一个 XHTML 文件,如

<ui:include src="incl.xhtml" />

如果包含的文件包含一个<ui:composition>或者一个<ui:component>,那么只有<ui:composition>或者<ui:component>标签的内部内容会被包含。这使得设计者可以独立于他们后来通过服务器连接在一起的文件来设计包含的文件。

标签,第一个变体

如果使用而不使用??,例如

<ui:composition>
    ...
</ui:composition>

它定义了 HTML 元素的子树(集合)。其背后的思想是,如果您使用<ui:include>并且包含的文件包含一个<ui:composition> ... </ui:composition>,那么只有<ui:composition> ... </ui:composition>的内部内容将被包含。标签本身及其周围的任何内容都将被忽略。因此,您可以让页面设计人员创建一个完全有效的 XHTML 文件,在有趣的部分加上<ui:composition> ... </ui:composition>,并在任何其他 JSF 页面中编写<ui:include>来提取这些部分。

标签,第二个变体

如果它。连用,如在

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

它定义了一组 XHTML 片段,这些片段将被传递到模板文件中的占位符中(对应于template = "..."属性)。

这和没有template="..."<ui:composition>相比是完全不同的使用场景。在模板文件中,你有一个或多个像<ui:insert name="name1" />这样的元素,在带有<ui:composition template="...">的文件中,你在和<ui:composition template="..."> ... </ui:composition>中使用<ui:define>标签

<ui:composition template="templ.xhtml">
    <ui:define name = "someName"> ... </ui:define>
    <ui:define name = "someName2"> ... </ui:define>
    ...
</ui:composition>

定义用于<ui:insert>标签的内容。标签周围的任何东西都将被忽略,所以你可以让设计者使用非 JSF 感知的 HTML 编辑器创建代码片段,然后用 ?? 提取感兴趣的部分来具体化模板文件。

标签

使用它来定义模板文件中的占位符。模板文件中的<ui:insert name="name1"/>标签意味着引用该模板的任何文件都可以定义占位符的内容。这个定义必须发生在<ui:composition><ui:component><ui:decorate><ui:fragment>内部。

通常你不需要在这个标签中提供内容。如果您添加内容,如

<ui:insert name="name1">
    Hello
</ui:insert>

如果没有另外定义占位符,它将被视为默认值。

标签

这个标签声明了将在插入点插入什么:

<ui:define name="theName">
    Contents...
</ui:define>

由于插入点只能存在于模板文件中,<ui:define>标签只能出现在通过<ui:composition template = "...">引用模板文件的文件中

标签

指定一个参数,该参数传递给一个<ci:include>编辑的文件,或者传递给在<ui:composition template = "..."> ...中指定的模板,只需将它作为子元素添加即可,如下所示:

<ui:include src="comp1.xhtml">
    <ui:param name="p1" value="Mark" />
</ui:include>

在引用的文件中,添加#{paramName}以使用参数:

<h:outputText value="Hello #{p1}" />

标签

这与第一个没有模板规范的变体<ui:composition>相同,但是它向 JSF 组件树添加了一个元素。此标签支持以下属性:

  • id:组件树中元素的 ID。不需要;如果不指定,JSF 会自动生成一个 ID。可能是 EL(表达式语言)字符串值。

  • binding:用于将组件绑定到 Java 类(必须从javax.faces.component.UIComponent继承)。不需要。可能是 EL 字符串值(类名)。

  • rendered:是否渲染组件。不需要。可能是 EL 布尔值。

通常的做法是使用<ui:param>将参数传递给组件。例如,您可以告诉组件使用特定的 ID。调用者如下:

<ui:include src="comp1.xhtml">
    <ui:param name="id" value="c1" />
</ui:include>

被叫方(comp1.xhtml)如下:

<ui:component id="#{id}">
    ...
</ui:component>

标签

类似于<ui:composition>,但是这个标签不忽略它周围的 XHTML 代码:

...
I'm written to the output!
<ui:decorate template="templ.xhtml">
    <ui:define name="def1">
        I'm passed to "templ.xhtml", you can refer to
        me in "templ.xhtml" via
        <ui:insert name="def1"/&gth;
    </ui:define>
</ui:include>
...

<ui:composition>相反,带有<ui:decorate>的文件将包含完全有效的 XHTML 代码,包括htmlheadbody,模板文件将被插入到<ui:decorate>出现的地方。因此,它不能包含htmlheadbody。这或多或少是一个扩展的include,其中传递的数据不是由属性给出,而是列在标记体中。

您通常应用<ui:decorate>标签来进一步细化代码片段。您可以将它们包装成更多的<div>来应用更多的样式、添加标签或标题等等。

标签

这个标签与<ui:decorate>相同,但是它在 JSF 组件树中创建了一个元素。它具有以下属性:

  • id:组件树中元素的 ID。不需要;如果不指定,JSF 会自动生成一个 ID。可能是 EL(表达式语言)字符串值。

  • binding:用于将组件绑定到 Java 类(必须从javax.faces.component.UIComponent继承)。不需要。可能是 EL 字符串值(类名)。

  • rendered:组件是否渲染。不需要。可能是 EL 布尔值。

您可以使用它来提取现有的代码片段,并将它们部分转换为组件。例如,考虑以下代码:

<DOCTYPE html>
<html ...><head>...</head>
<h:body>
  ...
  <table>
      Some table|
  </table>
  ...
</h:body></html>

如果我们现在将表提取到一个不同的文件中,名为table1_frag.xhtml:

<!-- Caller: ############################# -->
<!-- original file                        -->
<DOCTYPE html>
<html ...><head>...</head>
<h:body>
  ...
  <ui:include src="table1_frag.xhtml"/>
  ...
</h:body></html>

<!-- Callee: ############################# -->
<!-- table1_frag.xhtml                     -->
<div xmlns:="http://www.w3.org/1999/xhtml"
  xmlns:h="http://xmlns.jcp.org/jsf/html"
xmlns:f="http://xmlns.jcp.org/jsf/core"
xmlns:ui="http://java.sun.com/jsf/facelets"
xmlns:pt="http://xmlns.jcp.org/jsf/passthrough">
  <div>I am the table caption</div>
  <ui:fragment>
    <table>
      [Some table|
    </table>
  </ui:fragment>
  </div>

我们引入了 XHTML(标题)和一个新组件(表格)。

标签

这不一定是与模板相关的标记,但是它被用来在集合或数组中循环。它的属性是:

  • begin:不需要。如果指定了,迭代从列表或数组开始。可能是一个int值表达式。

  • end:不需要。如果指定,迭代在列表或数组中结束。可能是一个int值表达式。

  • step:不需要。如果指定,则在列表或数组内单步执行。可能是一个int值表达式。

  • offset:不需要。如果指定,偏移量将被添加到迭代值中。可能是一个int值表达式。

  • size:不需要。如果指定,它是从集合或数组中读取的最大元素数。不得大于数组大小。

  • value:要迭代的列表或数组。一个Object值表达式。必选。

  • var:保存当前迭代项的表达式语言变量的名称。可能是一个String值表达式。

  • varStatus:不需要。用于保存迭代状态的变量的名称。具有只读值的 POJO:begin(int),end ( int),index ( int),step ( int),even ( boolean),odd ( boolean),first ( boolean)或last ( boolean)。

  • rendered:是否渲染组件。不需要。可能是 EL 布尔值。

Note

JSTL (Java 标准标签库)集合为循环提供了一个<c:forEach>标签。由于观念上的差异,JSF 和 JSTL 并没有很好地合作。在教程和博客中,你会发现很多 JSTL 循环的例子。然而,最好使用<ui:repeat>来避免问题。

标签

在项目的开发阶段将它添加到您的页面中。使用热键,标签将导致 JSF 组件树和其他信息显示在页面上。使用hotkey="x"属性改变热键。然后 Shift+Ctrl+x 将显示组件(注意,默认的d不支持 Firefox 浏览器!).第二个可选属性是rendered="true|false"(你也可以使用一个 EL 布尔表达式)来打开或关闭这个组件。

Note

这个标签只在开发项目阶段有效。在WEB-INF/web.xml里面,你可以添加这个标签:

<context-param>
   <param-name>javax.faces.PROJECT_STAGE</param-name>
   <param-value>Development</param-value>
</context-param>

指定项目阶段(任意一个Development(默认)UnitTestSystemTestProduction)。

Facelets 项目示例

我们用音乐盒数据库构建了一个示例 Facelets 项目,它显示了标题、作曲家和表演者的类似设计页面。我们在 web 应用的每个页面上都有一个页眉、页脚和一个菜单,不管用户当前使用的是哪种功能。Facelets 很好地让我们分离出了常见的页面部分,因此我们只需编写一次代码。见图 [6-2 。

img/499016_1_En_6_Fig2_HTML.jpg

图 6-2

使用 Facelets 进行模板化

在 Eclipse 中启动一个新的 Gradle 项目,并将其命名为MusicBox。使用build.gradle文件并将其内容替换为:

plugins {
    id 'war'
}

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

repositories {
    jcenter()
}

dependencies {
   testImplementation 'junit:junit:4.12'
   implementation 'javax:javaee-api:8.0'
   implementation 'javax.mvc:javax.mvc-api:1.0.0'
   implementation 'org.eclipse.krazo:krazo-jersey:1.1.0-M1'
   implementation 'jstl:jstl:1.2'
   implementation 'com.google.guava:guava:28.0-jre'
}

task localDeploy(dependsOn: war,
             description:">>> Local deploy task") {
  doLast {
    def FS = File.separator
    def glassfish = project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']

    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd}\n"

    def sout = new StringBuilder()
    def serr = new StringBuilder()
    def libsDir =
        "${project.projectDir}${FS}build${FS}libs"
    def proc = """${glassfish}${FS}bin${FS}asadmin
       --user ${user} --passwordfile ${temp.absolutePath}
       deploy --force=true
       ${libsDir}/${project.name}.war""".execute()
    proc.waitForProcessOutput(sout, serr)
    println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)

    temp.delete()
  }
}

task localUndeploy(
             description:">>> Local undeploy task") {
  doLast {
    def FS = File.separator
    def glassfish = project.properties['glassfish.inst.dir']
    def user = project.properties['glassfish.user']
    def passwd = project.properties['glassfish.passwd']

    File temp = File.createTempFile("asadmin-passwd",
        ".tmp")
    temp << "AS_ADMIN_${user}=${passwd}\n"

    def sout = new StringBuilder()
    def serr = new StringBuilder()
    def proc = """${glassfish}${FS}bin${FS}asadmin
        --user ${user} --passwordfile ${temp.absolutePath}
        undeploy ${project.name}""".execute()
    proc.waitForProcessOutput(sout, serr) println "out> ${sout}"
    if(serr.toString()) System.err.println(serr)

    temp.delete()
  }
}

除了依赖处理之外,这个构建文件还引入了两个定制任务,用于在本地服务器上部署和取消部署MusicBox web 应用。番石榴库只是一个简化基本开发需求的有用工具的集合。

为了连接到asadmin工具,我们在项目根目录下创建另一个名为gradle.properties的文件:

glassfish.inst.dir = /path/to/glassfish5.1
glassfish.user = admin
glassfish.passwd =

您应该输入自己的 GlassFish 服务器安装路径。空密码是 Glassfish 的默认设置。如果您对此进行了更改,则必须在该文件中输入密码。

对于musicbox数据,我们创建三个 Java 类。为了简单起见,它们返回静态信息。在现实生活中,您将连接到数据库来获取数据。创建一个名为book.javamvc.musicbox.model的包,并添加以下内容:

// Composers.java:
package book.javamvc.musicbox.model;

import java.io.Serializable;
import java.util.List;

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

import com.google.common.collect.Lists;

@SessionScoped
@Named
public class Composers implements Serializable {
    private static final long serialVersionUID =
        -5244686848723761341L;

    public List<String> getComposers() {
        return Lists.newArrayList("Brahms, Johannes",
              "Debussy, Claude");
    }
}

// Titles.java:
package book.javamvc.musicbox.model;

import java.io.Serializable;
import java.util.List;

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

import com.google.common.collect.Lists;

@SessionScoped
@Named
public class Titles implements Serializable {
    private static final long serialVersionUID =
        -1034755008236485058L;

    public List<String> getTitles() {
        return Lists.newArrayList("Symphony 1",
            "Symphony 2", "Childrens Corner");
    }
}

// Performers.java:
package book.javamvc.musicbox.model;

import java.io.Serializable;
import java.util.List;

import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

import com.google.common.collect.Lists;

@SessionScoped
@Named
public class Performers implements Serializable {
    private static final long serialVersionUID =
        6941511768526140932L;

    public List<String> getPerformers() {
        return Lists.newArrayList(
            "Gewandhausorchester Leipzig",
            "Boston Pops");
    }
}

为了让 CDI 正常工作,创建一个名为src/main/webapp/WEB-INF/beans.xml的空文件。再加一个文件,叫src/main/webapp/WEB-INF/glassfish-web.xml。它应包含以下内容:

<?xml version="1.0" encoding="UTF-8"?>
<glassfish-web-app error-url="">
    <class-loader delegate="true"/>
</glassfish-web-app>

在我们进入视图编码之前,图 6-3 展示了我们想要实现的印象。为了应用 Facelets 功能,我们添加了一个名为src/main/webapp/WEB-INF/frame.xhtml的模板文件:

img/499016_1_En_6_Fig3_HTML.jpg

图 6-3

Musicbox Facelets 应用

<!DOCTYPE html>
<html lang="en"
      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
<head>
  <title>Musicbox</title>
  <link rel="stylesheet" href="../../css/style.css" />
</head>

<body>
  <div class="header-line">
      <ui:insert name="header">
        <h2>Top Section</h2>
      </ui:insert>
  </div>
  <div class="center-line">
      <div class="menu-column">
        <ui:insert name="menu">
          <ul><li>Menu1</li><li>Menu2</li></ul>
        </ui:insert>
      </div>
      <div class="contents-column">
         <ui:insert name="contents">
            Contents
         </ui:insert>
      </div>
    </div>
    <div class="bottom-line">
        <ui:insert name="footer">Footer</ui:insert>
    </div>
</body>
</html>

这个模板文件定义了一个通用的页面结构,并通过<ui:insert>标签声明了几个占位符。我们引用的 CSS 文件叫做style.css,它被发送到src/main/webapp/css/style.css:

body { color: blue; }
.header-line { height: 3em; background-color: #CCF000; }
.bottom-line { clear: both; height: 1.5em; }
.menu-column { float: left; width: 8em;
    background-color: #FFC000; height: calc(100vh - 7em); }
.menu-column ul { margin:0.5em; padding: 0;
    list-style-position: inside; }
.contents-column { float: left; padding: 0.5em;
    background-color: #FFFF99;
    width: calc(100% - 9em); height: calc(100vh - 8em); }
.bottom-line { padding-top: 1em;
    background-color: #CCFFFF; }

对于常见的页面元素,我们在src/main/webapp/common文件夹中定义了几个 XHTML 文件:

<!-- File commonHeader.xhtml -->
<!DOCTYPE html>
<div xmlns:="http://www.w3.org/1999/xhtml"
     xmlns:ui="http://java.sun.com/jsf/facelets">
  <h2>Musicbox</h2>
</div>

<!-- File commonMenu.xhtml -->
<!DOCTYPE html>
<div xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  <ul>
    <li><a href="titles">Titles</a></li>
    <li><a href="composers">Composers</a></li>
    <li><a href="performers">Performers</a></li>
  </ul>
</div>

<!-- File commonFooter.xhtml -->
<!DOCTYPE html>
<div xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">
  (c) The Musicbox company 2019
</div>

commonMenu.xhtml中,我们提供了<a>到标题、作曲家和表演者页面的链接。href属性不直接对应 XHTML 页面;相反,它们指向控制器内部的方法。这是一个在book.javamvc.musicbox.controller包中名为MusicBoxController.java的 Java 类:

package book.javamvc.musicbox.controller;

import java.util.ArrayList;
import java.util.List;

import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import javax.mvc.Controller;
import javax.mvc.binding.BindingResult;
import javax.mvc.binding.MvcBinding;
import javax.mvc.binding.ParamError;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;

import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.NewCookie;
import javax.ws.rs.core.Response;

@Path("/musicbox")
@Controller
public class MusicBoxController {
    private @Inject BindingResult br;

    @GET
    public Response showIndex() {
        return Response.ok("titles.xhtml").build();
    }

    @GET
    @Path("/titles")
    public Response showTitles() {
        return Response.ok("titles.xhtml").build();
    }

    @GET
    @Path("/composers")
    public Response showComposers() {
        return Response.ok("composers.xhtml").build();
    }

    @GET
    @Path("/performers")
    public Response showPerformers() {
        return Response.ok("performers.xhtml").build();
    }

    @POST
    @Path("/response")
    public Response response(
          @MvcBinding @FormParam("name")
          String name) {
        if(br.isFailed()) {
            // ... handle errors
      }

        // ... handle user POSTs

        // ... advance to response page
        return Response.ok("response.xhtml").build();
    }
}

在这个例子中没有实现response()方法。如果您想包含表单,此处显示的内容可以帮助您入门。

src/main/webapp/WEB-INF/views文件夹中的titles.xhtmlcomposers.xhtmlperformers.xhtml三个页面文件是指模板文件和常用页面元素:

<!-- File titles.xhtml ********************** -->
<!DOCTYPE html>
<html lang="en"
      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">

<body>
<ui:composition template="frame.xhtml">

  <ui:define name="header">
    <ui:include src="/common/commonHeader.xhtml" />
  </ui:define>

  <ui:define name="menu">
    <ui:include src="/common/commonMenu.xhtml" />
  </ui:define>

  <ui:define name="contents">
    <h2>Titles</h2>
    <ul>
      <ui:repeat var="t" value="${titles.titles}"
            varStatus="status">
        <li>${t}</li>
      </ui:repeat>
    </ul>
  </ui:define>

  <ui:define name="footer">
    <ui:include src="/common/commonFooter.xhtml" />
  </ui:define>

</ui:composition>

</body>
</html>

<!-- File composers.xhtml ********************** -->
<!DOCTYPE html>
<html lang="en"
      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets">

<body>
<ui:composition template="frame.xhtml">

  <ui:define name="header">
    <ui:include src="/common/commonHeader.xhtml" />
  </ui:define>

  <ui:define name="menu">
    <ui:include src="/common/commonMenu.xhtml" />
  </ui:define>

  <ui:define name="contents">
    <h2>Composers</h2>
    <ul>
      <ui:repeat var="c" value="${composers.composers}"
            varStatus="status">
        <li>${c}</li>
      </ui:repeat>
    </ul>
  </ui:define>

  <ui:define name="footer">
    <ui:include src="/common/commonFooter.xhtml" />
  </ui:define>

</ui:composition>
</body>
</html>

<!-- File performers.xhtml ********************** -->
<!DOCTYPE html>
<html lang="en"

      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jsp/jstl/core">
<body>
<ui:composition template="frame.xhtml">

  <ui:define name="header">
    <c:if test="true">
    <ui:include src="/common/commonHeader.xhtml" />
    </c:if>
  </ui:define>

  <ui:define name="menu">
    <ui:include src="/common/commonMenu.xhtml" />
  </ui:define>

  <ui:define name="contents">
    <h2>Performers</h2>
    <ul>
      <ui:repeat var="p" value="${performers.performers}"
            varStatus="status">
        <li>${p}</li>
      </ui:repeat>
    </ul>
  </ui:define>

  <ui:define name="footer">
    <ui:include src="/common/commonFooter.xhtml" />
  </ui:define>

</ui:composition>
</body>
</html>

您可以看到我们使用了<ui:composition>标签来应用页面模板。

Caution

网页故意不使用任何 JSF 标签。如果你寻找 Facelets 教程,在大多数情况下,他们将包括 JSF 标签。我认为在 Java MVC 项目中使用 Facelets JSF 标签是一种危险的做法。Java MVC(基于动作)和 JSF(基于组件)的不同设计范例很可能会导致难以解决的问题。然而,可以同时使用 Facelets 和 JSTL;请参见下一节。

通过运行 Gradle 任务localDeploy来构建和部署应用。然后将浏览器指向http://localhost:8080/MusicBox以查看应用的运行情况。见图 6-3 。

混合 Facelets 和 JSTL

我们已经指出,对于 Java MVC,出于稳定性原因,我们不想混合 JSF 组件和 Facelets 页面。然而,这导致了功能的严重缺乏,包括一个缺失的if-else构造。在 JSF 世界中,您通过rendered属性打开和关闭组件(或组件子树)。那么,如果我们想使用 Facelets for Java MVC,并且需要在视图页面上进行条件分支,我们该怎么办呢?答案简单得惊人。因为我们不使用 JSF 组件,所以我们可以简单地添加 JSTL 标签库,而没有任何破坏正常页面呈现的危险。然后我们可以使用<c:if><c:choose>标签。

例如,考虑我们想要基于某种条件添加一个消息框。这样就可以写出以下内容:

<!DOCTYPE html>
<html lang="en"
      xmlns:="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:c="http://java.sun.com/jsp/jstl/core">
<head>
  ...
</head>
<body>
    ...
    <c:if test="${pageControl.showMessages}">
      <div class="messages">
        ... the messages ...
      </div>
    </c:if>
    ...
</body>
</html>

因为 JSP 和 JSTL 已经在我们的build.gradle文件中处理好了,我们只需要添加 JSTL 名称空间,以便能够使用 JSTL。

统一表达式

对于 JSF,表达式语言处理已经扩展到使用延迟表达式,用#{ ... }代替${ ... }表示。在 JSF 组件对表单发起的请求做出反应之前,不会对这种延迟表达式进行求值。这样,就有可能使用表达式作为lvalue,也就是说你可以给它们分配用户输入。因此,#{ someBean.someProperty }既可用于输出,也可用于输入。

立即表达式和延迟表达式的组合,更准确地说是增强表达式语言,也称为统一表达式

对于 Java MVC,表单输入由控制器方法专门处理。从设计上来说,没有将表单输入自动连接到 CDI beans 的功能。出于这个原因,我们不需要延迟表达式,为了使事情更清楚,作为一个经验法则,考虑:

Caution

不要在 Java MVC Facelets 视图中使用延迟表达式#{ ... }

控制器

控制器类描述了 Java MVC 应用的动作部分。他们负责准备模型,接受用户请求,更新模型,并决定在请求后显示哪些视图页面。

控制器基础

要将一个类标记为 Java MVC 控制器,向该类添加@Controller注释(在javax.mvc包中)和@Path注释(在javax.ws.rs包中):

...
import javax.mvc.Controller;
import javax.ws.rs.Path;
...

@Path("/controllerPath")
@Controller
public class MyController {
    ...
}

@Path将确保控制器作用于以WEB_APPLICATION_BASE/mvc/controllerPath开始的 URL,其中WEB_APPLICATION_BASE依赖于 Jakarta EE 服务器产品(例如,对于 GlassFish,它是 e http://the.server:8080/TheWarName ),而/mvc被配置为某个类中的应用路径:

...
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
...

@ApplicationPath("/mvc")
public class App extends Application {
}

您不必将controllerPath用于@Path参数;这只是一个例子。

获取页面

对于那些不是的页面,你可以使用GET动词并标记相应的方法:

import javax.mvc.Controller; import javax.ws.rs.GET; import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/controllerPath")
@Controller
public class MyController {
    @GET
    public Response showIndex() {
        return Response.ok("index.jsp").build();
    }

    @GET
    @Path("/b")
    public String showSomeOtherPage() {
        return "page_b.jsp";
    }
}

在这个代码片段中,您可以看到两种可能的返回类型——您返回一个指向 JSP(或 Facelets 页面)的字符串,然后使用后缀.xhtml,或者您返回一个Response对象。虽然返回一个字符串更容易,但是有了Response实例,您有了更多的选择。例如,您可以精确地指定 HTTP 状态代码,并实际指定状态代码(如 OK、服务器错误、已接受、已创建、无内容、未修改、查看其他、临时重定向或不可接受)。您还可以设置编码、缓存控制、HTTP 头、语言、媒体类型、过期时间和上次修改时间,并添加 cookies。详见javax.ws.rs.core.Response类的 API 文档。

触发路径是通过连接 classat @Path注释和 methong@Path注释来计算的,然后在应用的 URL 路径前面加上前缀。例如,如果您在本地 GlassFish 服务器上部署了一个名为TheWAR.war的 WAR,并在端口 8080(默认)上运行了一个 HTTP 连接器,并且在包层次结构中的任意位置添加了这个类:

...
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;
...

@ApplicationPath("/mvc")
public class App extends Application {
}

那么这个控制器:

import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;

@Path("/controllerPath")
@Controller
public class MyController {
    @GET
    public Response showIndex() {
        return Response.ok("index.jsp").build();
    }

    @GET
    @Path("/b")
    public String showSomeOtherPage() {
        return "page_b.jsp";
    }
}

将确保应用以下映射:

http://localhost:8080/TheWAR/mvc/controllerPath
    -> method showIndex()
http://localhost:8080/TheWAR/mvc/controllerPath/b
    -> method showSomeOtherPage()

见图 6-4 。

img/499016_1_En_6_Fig4_HTML.jpg

图 6-4

控制器 URL

准备模型

如果需要为被调用的页面准备模型值,可以在控制器中注入 CDI beans,并从控制器方法内部调整它们的值。

import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.inject.Inject;

@Path("/controllerPath")
@Controller
public class MyController {
    // The controller is under custody of CDI, so
    // we can inject beans.
    @Inject private SomeDataClass someModelInstance;

    @GET
    public Response showIndex() {
        // Preparing the model:
        someModelInstance.setVal(42);
        ...

        return Response.ok("index.jsp").build();
    }

    @GET
    @Path("/b")
    public String showSomeOtherPage() {
        // Preparing the model:
        someModelInstance.setVal(43);

        return "page_b.jsp";
    }
}

然后,可以从调用的视图页面内部使用更新或初始化的模型。我们在前面与视图相关的文本小节中对此进行了描述。

将数据发布到控制器中

为了将用户输入从表单传输到控制器方法,您用@POST注释标记该方法,并添加表单字段作为该方法的参数:

@POST
@Path("/response")
public Response response(
    @MvcBinding @FormParam("name") String name,
    @MvcBinding @FormParam("userId") int userId) {

    // Handle form input, set model data, ...

    return Response.ok("response.jsp").build();
}

参数类型可以选择StringintlongfloatdoubleBigDecimalBigIntegerboolean ( truefalse)。如果您选择除了String之外的任何类型,Java MVC 确保用户输入被适当地转换。

@MvcBinding允许 Java MVC 忽略注入的BindingResult对象中的验证和转换错误。然后,您可以在POST方法中以编程方式处理这些错误:

...
import javax.mvc.binding.MvcBinding;
import javax.mvc.binding.ParamError;
import javax.mvc.binding.BindingResult;
import javax.ws.rs.FormParam;
...

@Path("/controllerPath")
@Controller
public class MyController {

    // Errors while fetching parameters
    // automatically go here:
    private @Inject BindingResult br;

    @POST
    @Path("/response")
    public Response response(
        @MvcBinding @FormParam("name") String name,
        @MvcBinding @FormParam("userId") int userId) {

        // ERROR HANDLING //////////////////////////
        if(br.isFailed()) {
          br.getAllErrors().stream().
               forEach((ParamError pe) -> {
            ...
          });
      }
      // END ERROR HANDLING //////////////////////

      // Handle form input, set model data, ...

      return Response.ok("response.jsp").build();
    }
}

除了将表单输入作为方法参数传递,您还可以使用控制器字段来接收数据:

import javax.mvc.Controller;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Response;
import javax.inject.Inject;

@Path("/controllerPath")
@Controller
public class MyController {
    // Errors while fetching parameters
    // automatically go here:
    private @Inject BindingResult br;

    @MvcBinding @FormParam("name")
    private String name;

    @MvcBinding @FormParam("userId")
    private int userId;

    @POST
    @Path("/response")
    public Response response() {
        // Handle form input, set model data, ...

        return Response.ok("response.jsp").build();
    }
}

通常,建议使用方法参数,因为类实例字段在某种程度上表明参数传递是控制器类的责任,而不考虑使用哪种方法,而实际上取决于方法,哪些参数有意义。

如果您需要让查询参数( http://xyz.com/app?a=3&b=4 中的ab)对控制器方法可用,您基本上可以做与提交参数相同的事情。不同的是,您必须对查询参数使用QueryParam注释,如下所示:

...
import javax.mvc.binding.MvcBinding;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;

@Path("/")
@Controller
public class SomeController {
    private @Inject BindingResult br;
    @GET
    @Path("/start")
    public String someMethod(
          @MvcBinding @QueryParam("name") String name,
          @MvcBinding @QueryParam("birthday") String birthday
    ) {
        if(name != null) {
        // handle "name" parameter
      }
      if(birthday != null) {
        // handle "birthday" parameter
      }

      // advance to page
      return "index.jsp";
    }
    ...
}

这对于@GET@POST带注释的方法是可能的。

练习

  • 练习 1: 在两章前的HelloWorld应用中,删除控制器中的Models字段,代之以添加一个新的请求作用域模型类UserData,它有一个字段name。相应地更新控制器和视图。

  • 练习 2: 哪个是真的?JSP 由一个Servlet处理。或者,每个 JSP 被转换成一个新的Servlet

  • **练习 3:**Facelets 和 JSP 哪个视图技术更新?

  • 练习 4: 对还是错?为了在 Java MVC 中使用 Facelets,您还必须使用 JSF。

摘要

对于 Java MVC 的模型部分,MVC 框架的最初想法是相当不变的。模型类只是 Java bean 类(具有字段、getters 和 setters 的类),开发人员可以通过编程将它们添加到视图中。在响应视图中,您可以使用类似于Hello ${beanName.name}的表达式来访问模型 beans。然而,Java MVC 是一个现代框架,它的模型能力取代了简单引用 beans 的概念。它通过在 CDI 2.0 版本中为 Jakarta EE 8 引入 CDI(上下文和依赖注入)技术来实现这一点。基本思想还是一样的:我们希望实例化 bean 类(主要包含字段及其 getters 和 setters 的数据类),并将这些实例提供给控制器和视图。前 CDI 和 CDI 方式的主要区别在于,我们不自己实例化模型类,而是让 CDI 来做。

为了告诉 Java MVC 我们想要一个模型类由 CDI 控制并对视图页面可用,我们使用了来自javax.inject包的@Named注释。我们还可以添加@RequestScoped注释,将对象实例的生命周期绑定到一个 HTTP 请求/响应周期。

一旦我们通过@Named向 CDI 框架宣布了一个 bean,在 Java MVC 中就会发生两件事。首先,我们可以使用@Inject(在javax.inject包中)从任何 Java MVC 控制器内部和任何其他 CDI 控制的类内部引用 bean 实例。其次,我们可以通过使用首字母小写的类名:${userData.name}${userData.email}来使用视图页面中的实例。

如果您使用 CDI 来管理模型数据,那么模型类实例从属于由 CDI 管理的生命周期控件。这意味着 CDI 决定什么时候构造 beans,什么时候放弃它们。在注入的 beans 中,CDI 控制实例生命周期的方式是通过一个叫做 scope 的特性。在 Java MVC 中,存在以下作用域:请求作用域、会话作用域、重定向作用域和应用作用域。

除了使用标有@Named注释的 CDI beans,还可以使用Models的注入实例(在javax.mvc包中)。模型值可以从内部视图页面获得,没有前缀:Hello ${name}

Java MVC 的视图部分负责向客户端(浏览器)呈现前端,包括输入和输出。那些连接到控制器方法的 Java MVC 视图文件放在WEB-INF/-views文件夹中,或者,因为我们使用 Gradle 作为构建框架,所以放在src/main/webapp/WEB-INF/views文件夹中。

Java MVC 开箱即用,支持两种视图引擎——JSP(Java server Pages)和 Facelets(JSF 视图声明语言,JavaServer Faces)。通过设计,其他视图引擎可以包含在基于 CDI 的扩展机制中。

JSP 允许您交错静态内容,例如 HTML 和由 JSP 元素表示的动态内容。一个 JSP 页面被内部编译成一个继承自Servlet的大 Java 类。包含 JSP 代码的文件以.jsp结尾。JSP 指令<% ... %>提供了容器的方向。要生成静态内容,只需在 JSP 文件中逐字编写即可。因为 JSP 被转录成 Java 类,所以 JSP 允许将 Java 代码和 Java 表达式包含到 JSP 页面中。在<%= ... %><% ... %>中,有几个你可以使用的隐式对象:

  • out:servlet 的类型JspWriter的输出流(扩展java.io.Writer)。

  • request:请求,类型HttpServletRequest

  • response:响应,类型HttpServletResponse

  • session:session,类型HttpSession

  • application:应用,类型ServletContext

  • config:servlet 配置,类型ServletConfig

  • page:servlet 本身,类型Object(运行时类型javax.servlet.http.HttpServlet)。

  • pageContext:页面上下文,类型PageContext

您可以使用这些对象来实现奇特的事情,但是请记住,如果您使用它们,您会以某种方式离开正式的开发模式。这可能会使其他人难以阅读您的代码,并且通过将功能放入视图页面,模型、视图和控制器之间的自然界限被打破。

带有@Named注释的 CDI beans 被直接提供给 JSP:Hello ${userName.name}。如果您将模型数据添加到注入的javax.mvc.Models CDI bean 中,您可以直接访问它而不需要前缀,如在Hello ${name}中所示。

${ ... }这样的 JSP 页面中的构造被视为表达式,由表达式语言处理程序处理。表达式中可以使用几个隐式对象:pageScoperequestScopesessionScopeapplicationScopeparamValuesparamheaderValuesheaderinitParamcookiepageContext

如果您喜欢使用标签进行动态输出,您可以使用如下的<c:out>标签:Hello <c:out value="${userData.name}" />

通过使用<c:set>标签,您可以引入变量以便在页面中进一步使用。

对于列表或数组上的循环,可以使用<c:forEach>标签(c表示jstl/core taglib)。如果您想对一个整数值范围循环使用<c:forEach>标记,您可以使用beginend属性,比如<c:forEach begin="1" end="10" var="i">

对于 JSP 内部的条件分支,您可以使用<c:if><c:choose>标签之一。

通过使用隐式的cookie对象,可以直接从 JSP 内部读取 Cookies。

除了 JSP 之外,Java MVC 支持的另一种视图技术叫做 Facelets 。Facelets 是专门为 JSF 创建的模板框架。Java MVC 的 Facelets 文件与 JSP 文件放在同一个文件夹中,即WEB-INF/views文件夹,或者,因为我们使用 Gradle 作为构建框架,所以放在src/main/webapp/WEB-INF/views文件夹中。Facelets 文件是 XML 文件,这可能是 JSP 和 Facelets 之间最显著的区别。

Facelets 允许您引入参数化的模板 HTML 页面、要包含在页面中的 HTML 片段(组件)、这种片段的占位符,以及诸如精心制作的列表视图之类的装饰器和重复。

对于 Java MVC,出于稳定性原因,我们不想将 JSF 组件混合到 Facelets 页面中。然而,这导致了严重的功能缺失,包括一个缺失的if-else构造。在 JSF 世界中,您通过rendered属性打开和关闭组件(或组件子树)。那么,如果我们想使用 Facelets for Java MVC,并且需要在某个视图页面上进行条件分支,我们该怎么做呢?答案简单得惊人。因为我们不使用 JSF 组件,所以我们可以简单地添加 JSTL 标签库,而不会破坏正常的页面呈现。然后我们可以使用<c:if><choose>标签。

对于 JSF,表达式语言处理已经扩展到使用延迟表达式,用#{ ... }代替${ ... }表示。在 JSF 组件对表单发起的请求做出反应之前,不会对这些延迟表达式进行求值。这样,就有可能使用表达式作为lvalue,也就是说你可以给它们分配用户输入。因此,#{ someBean.someProperty }可用于输出和输入。立即表达式和延迟表达式的组合,更准确地说是增强表达式语言,也被称为统一表达式。对于 Java MVC,表单输入由控制器方法专门处理。从设计上来说,没有将表单输入自动连接到 CDI beans 的功能。由于这个原因,我们不需要延迟表达式。

Caution

不要在 Java MVC Facelets 视图中使用延迟表达式#{ ... }

控制器类描述了 Java MVC 应用的动作部分。他们负责准备模型,接受用户请求,更新模型,并决定在请求后显示哪些视图页面。要将一个类标记为 Java MVC 控制器,向该类添加@Controller注释(在javax.mvc包中)和@Path注释(在javax.ws.rs包中)。

对于那些而非表单发布结果的页面,您可以使用GET动词并用@GET注释标记相应的方法。

在标有@GET@POST的控制器方法中,要么返回一个指向 JSP(或 Facelets 页面)的字符串,然后使用后缀.xhtml,要么返回一个Response对象。虽然返回一个字符串更容易,但是有了Response实例,你有了更多的选择。例如,您可以精确地指定 HTTP 状态代码,并实际指定状态代码(如 OK、服务器错误、已接受、已创建、无内容、未修改、查看其他、临时重定向或不可接受)。您还可以设置编码、缓存控制、HTTP 头、语言、媒体类型、过期时间和上次修改时间,并添加 cookies。

触发路径是通过连接类的'@Path注释和方法的@Path注释,然后在应用的 URL 路径前面加上前缀来计算的。

如果需要为被调用的页面准备模型值,可以在控制器中注入 CDI beans,并在控制器方法中调整它们的值。然后,可以从调用的视图页面内部使用更新或初始化的模型。

为了将用户输入从表单传输到控制器方法,您用一个@POST注释来标记该方法,并添加表单字段作为该方法的参数。参数类型可以选择StringintlongfloatdoubleBigDecimalBigIntegerboolean ( truefalse)。如果您选择除了String之外的任何类型,Java MVC 确保用户输入被适当地转换。

@MvcBinding允许 Java MVC 忽略注入的BindingResult对象中的验证和转换错误。然后,您可以在POST方法中以编程方式处理这些错误。

如果您需要让查询参数( http://xyz.com/app?a=3&b=4 中的ab)对控制器方法可用,您基本上可以做与提交参数相同的事情。不同的是,您必须为查询参数使用QueryParam注释。这对于@GET@POST带注释的方法是可能的。

在下一章,我们将讨论 Java MVC 更高级的主题。*