读书笔记《gradle-essentials》构建Web应用程序
同样,我们将使我们的应用程序尽可能小,并创建我们在上一章中开发的应用程序的支持网络的版本。该应用程序将为用户提供一个表单来输入他们的姓名和一个提交按钮。当用户点击 Submit 按钮时,将显示问候语。
该应用程序将基于 Servlet 3.1 规范。我们将重用我们在上一章中开发的 GreetService
。该表单将由一个静态 HTML 文件提供服务,该文件可以将数据发布到我们的 servlet。 servlet 将创建问候消息并将其转发到 JSP 以进行呈现。
Note
有关 Servlet 规范 3.1 的更多详细信息,请转到 https://jcp.org/aboutJava/communityprocess/final/jsr340/index.html。
让我们将项目的根目录创建为hello-web
。该结构类似于我们在一个简单的 Java 应用程序中看到的结构,只是增加了一个,即 Web 应用程序根。默认情况下,Web 应用程序根位于 src/main/webapp
。熟悉 Maven 的人会立即注意到,这与 Maven 使用的路径相同。
Web 应用程序根 (webapp
) 包含运行 Web 应用程序所需的所有公共资源,其中包括 JSP 等动态页面或其他视图模板所需的文件 Thymeleaf、FreeMarker、Velocity等引擎;以及静态资源,例如 HTML、CSS、JavaScript 和图像文件;和其他配置文件,如 web.xml
在名为 WEB-INF
的特殊目录中。 WEB-INF
中存储的文件不能被客户端直接访问;因此,它是存储受保护文件的理想场所。
我们将从创建最终应用程序的目录结构开始:
然后,执行以下步骤:
让我们首先将上一章中熟悉的
GreetingService
添加到我们的源代码中。我们可能会注意到,复制 Java 源文件并不是重用的正确方法。有更好的方法来组织这种依赖关系。其中一种方法是使用多模块项目。我们将在 Chapter 5,Multiprojects Build。现在,将以下内容添加到
index.html
文件中:该文件以 HTML 5
doctype
声明开始,这是我们可以使用的最简单的doctype
。然后,我们创建一个将发布到greet
端点的表单(它是页面的相对路径)。现在,在 这个应用程序的核心,有一个响应发布请求的
GreetServlet
:在前面的代码中,
WebServlet
注释的值将此 servlet 映射到相对于应用程序上下文的/greet
路径。然后,GreetService
的实例在这个 servlet 中可用。重写的方法doPost
从request
对象中提取名称,生成问候消息,将此消息设置回request
作为属性,以便在 JSP 中可访问,然后最终将请求转发到位于greet.jsp
文件代码类="literal">/WEB-INF/greet.jsp。这个 将我们带到
greet.jsp
文件,该文件保存在WEB-INF
以便它不能直接访问,并且请求必须始终通过设置正确请求属性的 servlet 来:这个 JSP 只打印请求属性中可用的
message
。
现在让我们尝试理解这个文件:
第一行将
war
插件应用于项目。这个插件为项目添加了一个war
任务。有人可能想知道为什么我们不需要应用java
插件来编译类。这是因为war
插件扩展了java
插件;因此,除了war
任务之外,我们应用java
插件时可用的所有任务仍然可供我们使用。接下来是
repositories
部分,它配置我们的构建以查找 Maven 中央存储库中的所有依赖项。
最后,在 dependencies
块中,我们添加 servlet-api< /code> 到
providedCompile
配置(范围)。这告诉 Gradle 不要将 servlet API 与应用程序打包在一起,因为它已经在部署应用程序的容器上可用。 providedCompile
配置由 war
插件添加(它还添加了 providedRuntime
)。如果我们有任何其他依赖项需要与我们的应用程序一起打包,它会使用编译配置声明。例如,如果我们的应用程序依赖于 Spring 框架,那么依赖项部分可能如下所示:
如果感觉像 repositories
、configurations
和 dependencies
的详细信息,请不要担心代码>有点粗略。我们很快将在本章后面更详细地再次看到它们。
现在我们的源文件已经准备好构建文件,我们必须构建可部署的WAR文件。让我们使用以下命令验证可用于我们的构建的任务:
我们会注意到那里的 war
任务,它依赖于 classes
(任务)。我们不需要显式编译和构建 Java 源代码,这由 classes
任务自动处理。所以我们现在需要做的就是,使用以下命令:
构建完成后,我们将看到目录结构类似于以下结构:
战争文件 在 /build/libs/hello-web.war
创建。
Note
war
文件只不过是具有不同文件扩展名的 ZIP 文件。 .ear
或 .jar
文件也是如此。我们也可以使用标准的 zip/unzip 工具或使用 JDK 的 jar
实用程序对这些文件执行各种操作。要列出 WAR 的内容,请使用 jar -tf build/libs/hello-web.war
。
让我们检查一下这个 WAR 文件的内容:
…
完美的。编译后的类进入 WEB-INF/classes
目录。 servlet API 的 JAR 不包含在 providedCompile
范围内。
在创建网络应用方面取得了长足的进步。但是,要使用它,必须将其部署到 servlet 容器。可以通过复制servlet容器指定目录下的.war
文件(如webapps
中的.war
文件经典部署到servlet容器中Tomcat 的情况)。或者,可以使用更新的技术将 Servlet 容器嵌入到 Java 应用程序中,该应用程序被打包为 .jar
并像任何其他 java –jar
命令。
Web 应用程序通常以三种模式运行,开发、功能测试和生产。这三种模式的主要特点不同如下:
在开发模式下运行 Web 的关键特征是更快的部署(最好是热重载)、快速的服务器启动和关闭、非常低的服务器占用空间等等。
在功能测试中,我们通常为整个测试套件的运行部署一次
web-app
。我们需要尽可能地模仿应用程序的生产行为。我们需要设置和销毁 Web 应用程序的状态(例如数据库),使用轻量级数据库(最好是内存中的)进行所有测试。我们还需要模拟外部服务。而在生产部署中,应用服务器的(无论是独立的还是嵌入式的)配置、安全性、应用程序优化、缓存等优先级更高,很少使用热重载部署等功能;更快的启动时间优先级较低。
我们将仅介绍本章中的开发场景。我们将从传统的方式开始突出它的问题,然后转向 Gradle 的方式。
现在,如果我们需要手动部署战争。我们可以选择任何 Java servlet 容器,例如 Jetty 或 Tomcat 来运行我们的 Web 应用程序。在这个例子中,让我们使用 Tomcat。假设 Tomcat 安装在 ~/tomcat
或 C:\tomcat
(基于我们使用的操作系统):
如果服务器正在运行,理想情况下我们应该停止它。
将 WAR 文件复制到 Tomcat 的
webapp
(~/tomcat/webapps
) 目录。然后,使用
~/tomcat/bin/startup.sh
或C:\tomcat\bin\startup.bat
。
然而,这种部署在 Gradle 时代已经过时了。尤其是在开发 web-app的时候,我们要不断的把应用打包成war
,复制容器的最新版本,然后重新启动容器以运行最新的代码。当我们说构建自动化时,它隐含地意味着不应该期望人工干预并且事情应该一键运行(或者在 Gradle 的情况下是一个命令)。此外,幸运的是,有很多选择可以实现这种自动化水平。
开箱即用,Gradle 不支持现代 servlet 容器。然而,这正是 Gradle 架构的美妙之处。创新和/或实施不一定来自创建 Gradle 的少数人。在插件 API 的帮助下,任何人都可以创建功能丰富的插件。我们将使用一个名为 Gretty 的插件来部署我们的 web 应用程序的开发时间,但您还应该查看其他插件以了解最适合您的插件。
Note
Gradle 附带了一个 jetty
插件。但是,它并没有积极更新;因此,它官方只支持 Jetty 6.x(在撰写本文时)。因此,如果我们的 Web 应用程序基于 Servlet 2.5 或更低的规范,我们就可以使用它。
可以在 Gradle 插件门户中找到 Gretty 插件(查看下面的参考资料)。该插件为构建添加了许多任务,并支持各种版本的 Tomcat 和 Jetty。安装它再简单不过了。此代码使用与上一节相同的 hello-web
源代码,但更新了 build.gradle
文件。该示例的完整源代码可以在本书示例代码的chapter-03/hello-gretty
目录中找到。
只需在 build.gradle
的第一行包含以下内容:
就是这样——我们完成了。这是在 Gradle 2.1 中添加的用于将插件应用于构建的相对较新的语法。这对于应用第三方插件特别有用。与调用 apply
方法来应用插件不同,我们从第一行的插件块开始。然后,我们指定插件的 ID。要应用外部插件,我们必须使用完全限定的插件 ID 和版本。我们可以在这个块中包含 war
插件的应用程序。对于内部插件,我们不需要指定版本。它将如下所示:
如果我们现在运行 gradle tasks
,我们必须在 Gretty
appRun
任务代码>组。这个组还有很多任务,都是 Gretty 插件添加的。如果我们运行 appRun
任务,而不显式配置插件,那么默认情况下,Jetty 9 将在 http://localhot:8080 上运行
。我们可以打开浏览器进行验证。
插件公开了许多配置,用于控制服务器版本、端口号等方面。在 build.gradle
文件中添加 gretty
块,如下所示:
如果我们想在端口 8080 上使用 Tomcat 8,我们将添加以下代码行:
如果我们想在 9080 上使用 Jetty 9,我们将添加以下代码行:
Gretty 中还有更多可用的配置选项;我们建议您查看 Gretty 的在线文档。请参阅参考资料部分中的 Gretty 链接。
以下是正在运行的应用程序的外观:
在现实 生活中,我们处理的应用程序比我们刚刚看到的要复杂得多。此类应用程序依赖于其他专用组件来提供某些功能。例如,企业 Java 应用程序的构建可能依赖于各种组件,例如 Maven 中心中的开源库、内部开发和托管的库,甚至(可能)依赖于另一个子项目。这些依赖项本身位于不同的位置,例如本地 Intranet、本地文件系统等。它们需要被解析、下载并带入适当的配置(如compile
、testCompile
等)的构建。
Gradle 在定位和使依赖项在适当的 classpath
中可用并在需要时打包方面做得很好。让我们从最常见的依赖类型开始——外部库。
几乎 所有现实世界的项目都依赖于外部库 来重用经过验证和测试的成分。此类依赖项包括语言实用程序、数据库驱动程序、Web 框架、XML/JSON 序列化库、ORM、日志实用程序等等。
项目的依赖关系在构建文件的 dependencies
部分中声明。
Gradle 提供了一种非常简洁的语法来声明工件的坐标。它通常采用 group:name:version
的形式。请注意,每个值都用冒号分隔(:
)。
例如,可以使用以下代码引用 Spring Framework 的核心库:
我们还可以指定多个依赖项,如下所示:
其中configurationName
代表compile
、testCompile
等配置,我们很快就会看到在这种情况下是什么配置。
我们依赖项的 版本会不时更新。此外,当我们处于开发阶段时,我们不想继续手动检查是否有新版本可用。
在这种情况下,我们可以添加一个 +
来表示上面提到的版本,给定工件的数量。例如,org.slf4j:slf4j-nop:1.7+
声明任何高于 1.7 的 SLF4J 版本。让我们将其包含在 build.gradle
文件中,并检查 Gradle 为我们带来了什么。
我们在 build.gradle
文件中运行以下代码:
然后,我们运行 dependencies
任务:
我们看到 Gradle 选择了 1.7.7 版本,因为它是撰写本书时可用的最新版本。如果你观察第二行,它告诉我们 slf4j-nop
依赖于 slf4j-api
;因此,它是我们项目的传递依赖。
这里需要注意的是,始终使用 +
进行小版本升级(例如前面示例中的 1.7+
)。让主版本自动更新(比如just image is spring自动从3更新到4,compile 'org.springframework:spring-core:+'
)无非就是一个赌。动态依赖解析是一个不错的功能,但应谨慎使用。理想情况下,它应该只在项目的开发阶段使用,而不是用于发布候选。
每当依赖项的版本更新到与我们的应用程序不兼容的版本时,我们就会得到一个不稳定的构建。我们应该针对可重现的构建,这样的构建应该产生完全相同的工件,无论是今天还是一年后。
Gradle 提供了一种非常优雅的方式来声明构建不同组所需的依赖项项目构建的各个阶段的资源。
Tip
这些源组称为 源集。最简单且易于理解的源集示例是 main
和 test
。 main
源集包含将被编译和构建为 JAR 文件的文件,这些文件将被部署到某个地方或发布到某个存储库。另一方面,test
源集包含将由 JUnit 等测试工具执行但不会投入生产的文件。现在,两个源集对依赖项、构建、打包和执行都有不同的要求。我们将在 Chapter 7,Testing and Reporting with 中了解如何添加新的源集Gradle,用于集成测试。
正如我们在一个源集中定义了相关源的组,依赖关系也被定义为一个称为 的组配置。每个配置都有其名称,例如 compile
、testCompile
等。各种配置中包含的依赖关系也不同。配置按依赖项的特征分组。例如,以下是 java
和 war
插件添加的配置:
compile
:这是 由java
插件添加的.向此配置添加依赖项意味着编译源需要依赖项。在war
的情况下,这些也会被复制到WEB-INF/lib
中。此类依赖项的示例是诸如 Spring Framework、Hibernate 等库。runtime
:这是由java
插件添加的.默认情况下,这包括compile
依赖项。编译的源代码在运行时需要此组中的依赖项,但编译它不需要它们。诸如 JDBC 驱动程序之类的依赖项只是运行时依赖项。我们不需要在类路径中使用它们来编译源代码,因为我们针对 JDK 中可用的标准 JDBC API 接口进行编码。但是,为了使我们的应用程序正常运行,我们需要在运行时实现特定的驱动程序。例如,runtime 'mysql:mysql-connector-java:5.1.37'
包含 MySQL 驱动程序。testCompile
:这个是由java
插件添加的.默认情况下,这包括compile
依赖项。添加到此配置的依赖项仅可用于测试源。示例是测试库,如 JUnit、TestNG 等,或任何由测试源专门使用的库,如 Mockito。对于主源集,它们既不需要编译,也不需要在运行时。在构建web-app
的情况下,它们不会包含在war
中。testRuntime
:这个是由java
插件添加的.默认情况下,这包括testCompile
和runtime
依赖项。此配置中的依赖项仅需要在运行时(即运行测试时)测试源。因此,它们不包含在测试的编译类路径中。这就像运行时配置,但仅适用于测试源。providedCompile
:这个是由war
插件添加的. servlet API 等依赖项由应用服务器提供,因此不需要打包在我们的war
中。我们期望已经包含在服务器运行时中的任何内容都可以添加到此配置中。但是,它必须在编译源代码时出现。因此,我们可以将这样的依赖声明为providedCompile
。示例是 servlet API 和在服务器运行时可用的任何 Java EE 实现。war
中不包含此类依赖项。providedRuntime
:这个是由war
插件添加的.服务器和应用程序将在应用程序运行时提供的依赖项在编译时不需要包括在内,因为没有对实现的直接引用。可以将此类库添加到此配置中。此类依赖项不会包含在war
中。因此,我们应该确保在应用程序运行时中有可用的实现。
我们知道,当我们应用war
插件时,java
插件也被应用。这就是我们在构建 Web 应用程序时所有六种配置都可用的原因。可以通过插件添加更多配置,或者我们可以在构建脚本中自己声明它们。
有趣的是,配置不仅包括依赖关系,还包括此配置产生的工件。
repositories 部分配置 Gradle 将在其中查找依赖项的存储库。 Gradle 将依赖项下载到自己的缓存中,这样就不需要在每次运行 Gradle 时都进行下载。我们可以配置多个存储库,如下所示:
支持 Maven、Ivy 和平面目录(文件系统)等存储库用于依赖解析和上传工件。有一些更具体的便利方法可用于常用的 Maven 存储库,例如 mavenCentral()
、jcenter()
和 mavenLocal()
。但是,可以使用以下语法轻松配置更多 Maven 存储库:
在中央存储库之前,项目用于管理文件系统上的库,这些库大多与源代码一起签入。有些项目仍然这样做;尽管我们不鼓励这样做,但人们有理由这样做,Gradle 没有理由不支持。
重要的是要记住,Gradle 不会自动假定要搜索和 从中下载依赖项的任何存储库。我们必须在 repositories
块中明确配置至少一个存储库,Gradle 将在其中搜索工件。
在本章中,我们首先使用 Gradle 开发了一个 Web 应用程序。我们通过构建应用程序生成 WAR 工件,然后将其部署到本地 Tomcat。然后,我们学习了一些关于 Gradle 中的依赖管理、配置和支持的存储库的基础知识。
Note
读者应该花更多时间在 Gradle 的官方文档中详细阅读这些概念:https ://docs.gradle.org/current/userguide/userguide 。
现在,我们应该可以使用 Gradle 构建最常见的 Java 应用程序了。在下一章中,我们将尝试理解 Gradle 提供的 Groovy DSL,同时也理解基本的项目模型。