使用 Gradle 进行 Web 开发

在 Java 中,企业版 (Java EE) 的服务器端 Web 组件提供了动态扩展功能,用于在 Web 容器或应用程序服务器中运行应用程序。 正如 Servlet 名称所示,它为客户端请求提供服务并构建响应。 它充当模型-视图-控制器 (MVC) 架构中的控制器组件。 Servlet 的响应由视图组件 — Java 服务器页面 (JSP) 呈现。 图 3.4 说明了 Java Web 应用程序上下文中的 MVC 架构模式。

WAR(Web 应用程序存档)文件用于捆绑 Web 组件、编译的类和其他资源文件,例如部署描述符、HTML、JavaScript 和 CSS 文件。 它们一起构成了一个 Web 应用程序。 要运行 Java Web 应用程序,需要将 WAR 文件部署到服务器环境(Web 容器)。

Gradle 提供了开箱即用的插件,用于组装 WAR 文件并将 Web 应用程序部署到本地 Servlet 容器。 在我们了解如何应用和配置这些插件之前,您需要将独立的 Java 应用程序转换为 Web 应用程序。 接下来我们将重点关注将要介绍的 Web 组件以及它们如何相互交互。

添加 Web 组件

Java 企业环境由各种 Web 框架主导,例如 Spring MVC 和 Tapestry。 Web 框架旨在抽象标准 Web 组件并减少样板代码。 尽管有这些好处,但 Web 框架在引入新概念和 API 时可能会带来陡峭的学习曲线。 为了使示例尽可能简单易懂,我们将坚持使用标准 Java 企业 Web 组件。

在进入代码之前,让我们看看添加 Web 组件如何改变上一节中现有类之间的交互。 您要创建的 Servlet 类称为 ToDoServlet。 它负责接受 HTTP 请求,执行映射到 URL 端点的 CRUD 操作,并将请求转发到 JSP。 为了向用户提供流畅、舒适的体验,您将把待办事项列表实现为单页应用程序。 这意味着您只需编写一个 JSP,将其命名为 todo-list.jsp。 该页面知道如何动态呈现待办事项列表,并提供用于启动 CRUD 操作的按钮和链接等 UI 元素。 图 3.5 显示了检索和呈现所有待办事项用例的新系统的流程。

image 2024 03 08 15 47 07 276
Figure 1. 图 3.5 查找所有待办事项用例:用户通过浏览器发出 HTTP 请求,该请求由 Servlet 提供服务并通过 JSP 呈现结果。

如图所示,您可以重用 ToDoItem 类来表示模型,并重用 InMemoryToDoRepository 类来存储数据。 这两个类都可以与控制器和视图组件无缝协作。 让我们看看控制器组件的内部工作原理。

控制器Web组件

为了使事情变得简单和集中,您将为要向客户端公开的所有 URL 端点编写一个入口点。 以下代码片段显示了控制器 Web 组件(ToDoServlet 类)中最重要的部分:

package com.manning.gia.todo.web;
import com.manning.gia.todo.model.ToDoItem;
import com.manning.gia.todo.repository.InMemoryToDoRepository;
import com.manning.gia.todo.repository.ToDoRepository;
import javax.servlet.*;
import java.io.IOException;
import java.util.List;

public class ToDoServlet extends HttpServlet {
    private ToDoRepository toDoRepository = new InMemoryToDoRepository();

    @Override
    protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        // 检索请求的 URL 的路径; 路径以 / 字符开头
        String servletPath = request.getServletPath();
        String view = processRequest(servletPath, request);
        RequestDispatcher dispatcher = request.getRequestDispatcher(view);
        // 将请求从 Servlet 转发到 JSP
        dispatcher.forward(request, response);
    }

    // 为每个映射的 URL 实现 CRUD 操作
    private String processRequest(String servletPath, HttpServletRequest request) {
        if(servletPath.equals("/all")) {
            List<ToDoItem> toDoItems = toDoRepository.findAll();
            request.setAttribute("toDoItems", toDoItems);
            return "/jsp/todo-list.jsp";
        }
        else if(servletPath.equals("/delete")) {
            (...)
        }
        (...)
        // 如果传入请求 URL 与任何处理不匹配,则重定向到 /all URL
        return "/all";
    }
}

对于每个传入请求,您获取 Servlet 路径,根据确定的 CRUD 操作在方法 processRequest 中处理请求,并使用 javax.servlet.RequestDispatcher 实例将其转发到 JSP todo-list.jsp。

就是这样; 您将任务管理程序转换为 Web 应用程序。 在示例中,我只涉及了代码中最重要的部分。 为了更深入地理解,我鼓励您浏览完整的源代码。 接下来,我们将使用 Gradle。

使用 War 和 Jetty 插件

Gradle 为构建和运行 Web 应用程序提供广泛的支持。 在本节中,我们将介绍两个用于 Web 应用程序开发的插件:War 和 Jetty。 War 插件通过添加 Web 应用程序开发约定和组装 WAR 文件的支持来扩展 Java 插件。 在本地计算机上运行 Web 应用程序应该很容易,可以实现快速应用程序开发 (RAD),并提供快速启动时间。 理想情况下,它不应该要求您安装 Web 容器运行时环境。 Jetty 是一个流行的、轻量级的、开源的 Web 容器,支持所有这些功能。 它通过向您的应用程序添加 HTTP 模块来实现嵌入式实现。 Gradle 的 Jetty 插件扩展了 War 插件,提供将 Web 应用程序部署到嵌入式容器的任务,并运行您的应用程序。

替代嵌入式容器插件

Jetty 插件非常适合本地 Web 应用程序开发。 但是,您可以在生产环境中使用不同的 Servlet 容器。 为了在软件开发生命周期的早期提供运行时环境之间的最大兼容性,请寻找替代的嵌入式容器实现。 与 Gradle 的标准 Jetty 扩展非常相似的可行解决方案是第三方 Tomcat 插件。

您已经了解了上一节的练习内容。 首先,您将应用插件并使用默认约定,然后自定义它们。 我们首先关注 War 插件。

War插件

我已经提到 War 插件扩展了 Java 插件。 实际上,这意味着您不必再在构建脚本中应用 Java 插件。 它是由 War 插件自动引入的。 请注意,即使您也应用了 Java 插件,也不会对您的项目产生任何副作用。 应用插件是幂等操作,因此仅针对特定插件执行一次。 创建 build.gradle 文件时,请使用如下插件:

apply plugin: 'war'

这对您的项目到底意味着什么? 除了 Java 插件提供的约定之外,您的项目还会了解 Web 应用程序文件的源目录,并知道如何组装 WAR 文件而不是 JAR 文件。 Web 应用程序源的默认约定是目录 src/main/webapp。 将所有 Web 资源文件放置在正确的位置后,您的项目布局应如下所示:

.
├── buildxxx.gradle
└── src
    └── main
        ├── java
        │   └── com
        │       └── manning
        │           └── gia
        │               └── todo
        │                   ├── model
        │                   │     └── ToDoItem.java
        │                   ├── repository
        │                   │     ├── InMemoryToDoRepository.java
        │                   │     └── ToDoRepository.java
        │                   └── web
        │                        └── ToDoServlet.java
        └── webapp
            ├── WEB-INF
            │   └── web.xml
            ├── css
            │   ├── base.css
            │   └── bg.png
            └── jsp
                ├── index.jsp
                └── todo-list.jsp

您借助不属于 Java 标准版的类(例如 javax.servlet.HttpServlet)实现了 Web 应用程序。 在运行构建之前,您需要确保声明这些外部依赖项。 War 插件引入了两个新的依赖配置。 您将用于 Servlet 依赖项的配置是providedCompile。 它用于编译所需但由运行时环境提供的依赖项。 本例中的运行环境是 Jetty。 因此,标记为 “已提供” 的依赖项不会与 WAR 文件一起打包。 编译过程不需要像 JSTL 库这样的运行时依赖项,但在运行时需要。 它们将成为 WAR 文件的一部分。 以下依赖关系闭包声明了应用程序所需的外部库:

dependencies {
    providedCompile 'javax.servlet:servlet-api:2.5'
    runtime 'javax.servlet:jstl:1.1.2'
}

构建项目

在 Gradle 中构建 Web 应用程序就像构建独立的 Java 应用程序一样简单。 运行命令 gradle build 后,可以在 build/libs 目录中找到编译好的 WAR 文件。 通过将项目的性质从独立应用程序更改为 Web 应用程序,任务 jar 被任务 war 取代,如以下输出所示:

$ gradle build
:compileJava
:processResources UP-TO-DATE
:classes
:war  // War插件提供的用于组装WAR文件的任务
:assemble
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:test
:check
:build

War 插件确保组装的 WAR 文件遵循 Java EE 规范定义的标准结构。 war 任务将默认 Web 应用程序源目录 src/main/webapp 的内容复制到 WAR 文件的根目录,而不修改结构。 编译后的类最终位于目录 WEB-INF/classes 中,而通过依赖关系闭包定义的运行时库则位于 WEB-INF/lib 中。 以下目录结构显示了运行 jar tf todo-webapp-0.1.war 后组装的 WAR 文件的内容:

.
├── META-INF
│   └── MANIFEST.MF
├── WEB-INF
│   ├── classes
│   │   └── com
│   │       └── manning
│   │           └── gia
│   │               └── todo
│   │                   ├── model
│   │                   │   └── ToDoItem.class
│   │                   ├── repository
│   │                   │   ├── InMemoryToDoRepository.class
│   │                   │   └── ToDoRepository.class
│   │                   └── web
│   │                       └── ToDoServlet.class
│   ├── lib
│   │   └── jstl-1.1.2.jar
│   └── web.xml
├── css
│   ├── base.css
│   └── bg.png
└── jsp
    ├── index.jsp
    └── todo-list.jsp

默认情况下,WAR 文件名源自项目的目录名。 即使您的项目不遵守 Gradle 的标准约定,该插件也可用于构建 WAR 文件。 让我们看看一些自定义选项。

定制War插件

您已经看到使 Java 项目适应自定义项目结构是多么容易。 对于非常规的 Web 项目布局也是如此。 在下面的示例中,我们假设所有静态文件都位于 static 目录中,并且所有 Web 应用程序内容都位于 webfiles 目录下:

.
├── buildxxx.gradle
├── src
│   └── main
│       └── java
│           └── ...
├── static
│   └── css
│       ├── base.css
│       └── bg.png
└── webfiles
    ├── WEB-INF
    │   └── web.xml
    └── jsp
        ├── index.jsp
        └── todo-list.jsp

以下代码片段显示了如何配置约定属性。 War 插件公开了约定属性 webAppDirName。 通过分配新值,可以轻松地将默认值 src/main/webapp 切换为 webfiles。 可以通过调用 from 方法有选择地将目录添加到 WAR 文件中,如下所示:

webAppDirName = 'webfiles' // 更改 Web 应用程序源目录

war {
    from 'static' // 将目录 css 和 jsp 添加到 WAR 文件存档的根目录
}

前面的示例仅显示了 War 插件配置选项的摘录。 您可以轻松包含其他外部 JAR 文件、使用非标准目录中的 Web 部署描述符或将另一个文件集添加到 WEB-INF 目录。 如果您正在寻找配置参数,最好的检查位置是 War 插件 DSL 指南。

您已经了解了如何从具有标准结构或自定义目录布局的 Web 项目构建 WAR 文件。 现在是时候将文件部署到 Servlet 容器了。 在下一部分中,您将启动 Jetty 以在本地开发计算机上运行应用程序。

在嵌入式 Web 容器中运行

在您提供 Web 应用程序的确切类路径和相关源目录之前,嵌入式 Servlet 容器对您的应用程序一无所知。 通常,您会以编程方式执行此操作。 在内部,Jetty 插件会为您完成所有这些工作。 由于 War 插件公开了所有这些信息,因此可以通过 Jetty 插件在运行时访问它。 这是一个插件通过 Gradle API 使用另一个插件配置的典型示例。 在您的构建脚本中,使用如下插件:

apply plugin: 'jetty'

您将用来运行 Web 应用程序的任务是 jettyRun。 它将启动 Jetty 容器,甚至无需创建 WAR 文件。 在命令行上运行任务的输出应类似于以下内容:

$ gradle jettyRun
:compileJava
:processResources UP-TO-DATE
:classes
> Building > :jettyRun > Running at http://localhost:8080/todo-webapp-jetty

在输出的最后一行,插件为您提供了 Jetty 侦听传入请求的 URL。 打开您喜欢的浏览器并输入 URL。 最后,您可以看到正在运行的待办事项 Web 应用程序。 Gradle 将使应用程序保持运行状态,直到您通过按 Ctrl + C 停止它为止。Jetty 如何知道运行应用程序时使用哪个端口和上下文? 再说一次,这是惯例。 Jetty 插件运行的 Web 应用程序的默认端口是 8080,上下文路径 todo-webapp-jetty 源自您的项目名称。 当然,所有这些都是可配置的。

快速应用开发

对应用程序代码所做的每一次更改都必须重新启动容器,这既麻烦又耗时。 Jetty 插件允许您动态更改静态资源和 JSP 文件,而无需重新启动容器。 此外,可以配置 JRebel 等字节码交换技术来执行类文件更改的热部署。

自定义 Jetty 插件

假设您对 Jetty 插件提供的默认值不满意。 另一个应用程序已在端口 8080 上运行,并且您厌倦了输入长上下文路径。 只需提供以下配置:

jettyRun {
    httpPort = 9090
    contextPath = 'todo'
}

太好了,你实现了你想要的。 使用此配置启动应用程序将公开 URL http://localhost:9090/todo 。 还有更多用于配置 Jetty 插件的选项。 一个很好的起点是插件的 API 文档。 这将帮助您了解所有可用的配置选项。