vlambda博客
学习文章列表

读书笔记《gradle-essentials》构建Web应用程序

Chapter 3. Building a Web Application

既然我们已经看到使用 Gradle 构建命令行 Java 应用程序的便利性,我们应该不会惊讶地发现,使用 Gradle 构建基于 Java servlet 规范的 Web 应用程序也同样容易。

在本章中,我们将首先构建一个简单的 Web 应用程序,它以 WAR 文件的形式分发,可以部署到任何 servlet 容器中。然后,我们将看看如何在构建文件中配置依赖项和存储库。

Building a simple Java web project


同样,我们将使我们的应用程序尽可能小,并创建我们在上一章中开发的应用程序的支持网络的版本。该应用程序将为用户提供一个表单来输入他们的姓名和一个提交按钮。当用户点击 Submit 按钮时,将显示问候语。

该应用程序将基于 Servlet 3.1 规范。我们将重用我们在上一章中开发的 GreetService。该表单将由一个静态 HTML 文件提供服务,该文件可以将数据发布到我们的 servlet。 servlet 将创建问候消息并将其转发到 JSP 以进行呈现。

Note

有关 Servlet 规范 3.1 的更多详细信息,请转到 https://jcp.org/aboutJava/communityprocess/final/jsr340/index.html

Creating source files

让我们将项目的根目录创建为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 中存储的文件不能被客户端直接访问;因此,它是存储受保护文件的理想场所。

我们将从创建最终应用程序的目录结构开始:

hello-web
├── build.gradle
└── src
    └── main
        ├── java// source root
        │ └── com
        │     └── packtpub
        │         └── ge
        │             └── hello
        │                 ├── GreetingService.java
        │                 └── GreetingServlet.java
        └── webapp// web-app root
            ├── WEB-INF
            │ └── greet.jsp
            └── index.html

然后,执行以下步骤:

  1. 让我们首先将上一章中熟悉的 GreetingService 添加到我们的源代码中。我们可能会注意到,复制 Java 源文件并不是重用的正确方法。有更好的方法来组织这种依赖关系。其中一种方法是使用多模块项目。我们将在 Chapter 5Multiprojects Build

  2. 现在,将以下内容添加到 index.html 文件中:

    <!doctype html>
    <html>
      <head>
        <title>Hello Web</title>
      </head>
      <body>
        <form action="greet" method="post">
          <input type="text" name="name"/>
          <input type="submit"/>
        </form>
      </body>
    </html>

    该文件以 HTML 5 doctype 声明开始,这是我们可以使用的最简单的 doctype。然后,我们创建一个将发布到 greet 端点的表单(它是页面的相对路径)。

  3. 现在,在 这个应用程序的核心,有一个响应发布请求的GreetServlet

    package com.packtpub.ge.hello;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebServlet;
    import javax.servlet.http.*;
    import java.io.IOException;
    
    @WebServlet("/greet")
    public class GreetingServlet extends HttpServlet {
    
      GreetingService service = new GreetingService();
    
      @Override
      public void doPost(HttpServletRequest request,
                         HttpServletResponse response)
        throws ServletException, IOException {
    
        String name = request.getParameter("name");
        String message = service.greet(name);
        request.setAttribute("message", message);
    
        RequestDispatcher dispatcher = getServletContext()
          .getRequestDispatcher("/WEB-INF/greet.jsp");
    
        dispatcher.forward(request, response);
      }
    
    }

    在前面的代码中,WebServlet 注释的值将此 servlet 映射到相对于应用程序上下文的 /greet 路径。然后,GreetService 的实例在这个 servlet 中可用。重写的方法 doPostrequest 对象中提取名称,生成问候消息,将此消息设置回 request 作为属性,以便在 JSP 中可访问,然后最终将请求转发到位于 greet.jsp 文件代码类="literal">/WEB-INF/greet.jsp。

  4. 这个 将我们带到 greet.jsp 文件,该文件保存在 WEB-INF 以便它不能直接访问,并且请求必须始终通过设置正确请求属性的 servlet 来:

    <!doctype html>
    <html>
      <head>
        <title>Hello Web</title>
      </head>
      <body>
        <h1>${requestScope.message}</h1>
      </body>
    </html>

    这个 JSP 只打印请求属性中可用的 message

Creating a build file

最后,让我们在项目的根:

apply plugin: 'war'

repositories {
    mavenCentral()
}

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}

现在让我们尝试理解这个文件:

  • 第一行将 war 插件应用于项目。这个插件为项目添加了一个 war 任务。有人可能想知道为什么我们不需要应用 java 插件来编译类。这是因为 war 插件扩展了 java 插件;因此,除了 war 任务之外,我们应用 java 插件时可用的所有任务仍然可供我们使用。

  • 接下来是 repositories 部分,它配置我们的构建以查找 Maven 中央存储库中的所有依赖项。

最后,在 dependencies 块中,我们添加 servlet-api< /code> 到 providedCompile 配置(范围)。这告诉 Gradle 不要将 servlet API 与应用程序打包在一起,因为它已经在部署应用程序的容器上可用。 providedCompile 配置由 war 插件添加(它还添加了 providedRuntime )。如果我们有任何其他依赖项需要与我们的应用程序一起打包,它会使用编译配置声明。例如,如果我们的应用程序依赖于 Spring 框架,那么依赖项部分可能如下所示:

dependencies {
    compile 'org.springframework:spring-context:4.0.6.RELEASE'
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}

如果感觉像 repositoriesconfigurationsdependencies 的详细信息,请不要担心代码>有点粗略。我们很快将在本章后面更详细地再次看到它们。

Building the artifact

现在我们的源文件已经准备好构建文件,我们必须构建可部署的WAR文件。让我们使用以下命令验证可用于我们的构建的任务:

$ gradle tasks --all
…
war - Generates a war archive with all the compiled classes, the web-app content and the libraries. [classes]
…

我们会注意到那里的 war 任务,它依赖于 classes (任务)。我们不需要显式编译和构建 Java 源代码,这由 classes 任务自动处理。所以我们现在需要做的就是,使用以下命令:

$ gradle war

构建完成后,我们将看到目录结构类似于以下结构:

hello-web
├── build
│ ├── classes
│ │ └── main
│ │     └── com
│ │         └── packtpub
│ │             └── ge
│ │                 └── hello
│ │                     ├── GreetService.class
│ │                     └── GreetServlet.class
│ ├── dependency-cache
│ ├── libs
│ │ └── hello-web.war
│ └── tmp
│     └── war
│         └── MANIFEST.MF
…

战争文件 /build/libs/hello-web.war 创建。

Note

war 文件只不过是具有不同文件扩展名的 ZIP 文件。 .ear.jar 文件也是如此。我们也可以使用标准的 zip/unzip 工具或使用 JDK 的 jar 实用程序对这些文件执行各种操作。要列出 WAR 的内容,请使用 jar -tf build/libs/hello-web.war

让我们检查一下这个 WAR 文件的内容:

├── META-INF
│ └── MANIFEST.MF
├── WEB-INF
│ ├── classes
│ │ └── com
│ │     └── packtpub
│ │         └── ge
│ │             └── hello
│ │                 ├── GreetService.class
│ │                 └── GreetServlet.class
│ └── greet.jsp
└── index.html

完美的。编译后的类进入 WEB-INF/classes 目录。 servlet API 的 JAR 不包含在 providedCompile 范围内。

Tip

锻炼

dependencies 部分添加 compile 'org.springframework:spring-context:4.0.6.RELEASE' 然后执行 < code class="literal">gradle war 文件并查看创建的 WAR 的内容。

Running the web application

在创建网络应用方面取得了长足的进步。但是,要使用它,必须将其部署到 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 安装在 ~/tomcatC:\tomcat (基于我们使用的操作系统):

  1. 如果服务器正在运行,理想情况下我们应该停止它。

  2. 将 WAR 文件复制到 Tomcat 的 webapp (~/tomcat/webapps) 目录。

  3. 然后,使用 ~/tomcat/bin/startup.shC:\tomcat\bin\startup.bat

然而,这种部署在 Gradle 时代已经过时了。尤其是在开发 web-app的时候,我们要不断的把应用打包成war,复制容器的最新版本,然后重新启动容器以运行最新的代码。当我们说构建自动化时,它隐含地意味着不应该期望人工干预并且事情应该一键运行(或者在 Gradle 的情况下是一个命令)。此外,幸运的是,有很多选择可以实现这种自动化水平。

Plugins to the rescue

开箱即用,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 的第一行包含以下内容:

plugins {
  id "org.akhikhl.gretty" version "1.2.4"
}

就是这样——我们完成了。这是在 Gradle 2.1 中添加的用于将插件应用于构建的相对较新的语法。这对于应用第三方插件特别有用。与调用 apply 方法来应用插件不同,我们从第一行的插件块开始。然后,我们指定插件的 ID。要应用外部插件,我们必须使用完全限定的插件 ID 和版本。我们可以在这个块中包含 war 插件的应用程序。对于内部插件,我们不需要指定版本。它将如下所示:

plugins {
  id "org.akhikhl.gretty" version "1.2.4"
  id "war"
}

如果我们现在运行 gradle tasks,我们必须在 Gretty appRun 任务代码>组。这个组还有很多任务,都是 Gretty 插件添加的。如果我们运行 appRun 任务,而不显式配置插件,那么默认情况下,Jetty 9 将在 http://localhot:8080 上运行 。我们可以打开浏览器进行验证。

插件公开了许多配置,用于控制服务器版本、端口号等方面。在 build.gradle 文件中添加 gretty 块,如下所示:

  • 如果我们想在端口 8080 上使用 Tomcat 8,我们将添加以下代码行:

    gretty {
      servletContainer = 'tomcat8'
      port = 8080
    }
  • 如果我们想在 9080 上使用 Jetty 9,我们将添加以下代码行:

    gretty {
      servletContainer = 'jetty9'
      port = 9080
    }

Gretty 中还有更多可用的配置选项;我们建议您查看 Gretty 的在线文档。请参阅参考资料部分中的 Gretty 链接。

以下是正在运行的应用程序的外观:

读书笔记《gradle-essentials》构建Web应用程序

一旦 提交按钮被按下,我们将得到以下结果:

读书笔记《gradle-essentials》构建Web应用程序

References


对于 Gradle,请参考以下 URL:

Gretty,参考以下网址:

有各种插件可用于自动化部署。其中一些列在这里:

Project dependencies


在现实 生活中,我们处理的应用程序比我们刚刚看到的要复杂得多。此类应用程序依赖于其他专用组件来提供某些功能。例如,企业 Java 应用程序的构建可能依赖于各种组件,例如 Maven 中心中的开源库、内部开发和托管的库,甚至(可能)依赖于另一个子项目。这些依赖项本身位于不同的位置,例如本地 Intranet、本地文件系统等。它们需要被解析、下载并带入适当的配置(如compiletestCompile等)的构建。

Gradle 在定位和使依赖项在适当的 classpath 中可用并在需要时打包方面做得很好。让我们从最常见的依赖类型开始——外部库。

External libraries

几乎 所有现实世界的项目都依赖于外部库 来重用经过验证和测试的成分。此类依赖项包括语言实用程序、数据库驱动程序、Web 框架、XML/JSON 序列化库、ORM、日志实用程序等等。

项目的依赖关系在构建文件的 dependencies 部分中声明。

Gradle 提供了一种非常简洁的语法来声明工件的坐标。它通常采用 group:name:version 的形式。请注意,每个值都用冒号分隔(:)。

例如,可以使用以下代码引用 Spring Framework 的核心库:

dependencies {
  compile 'org.springframework:spring-core:4.0.6.RELEASE'
}

Note

对于那些不喜欢简洁的人,可以以更具描述性的格式(称为映射格式)来引用依赖关系。

compile group:'org.springframework', name:'spring-core', version:'4.0.6.RELEASE'

我们还可以指定多个依赖项,如下所示:

configurationName dep1, dep2, dep3…. 

其中configurationName代表compiletestCompile等配置,我们很快就会看到在这种情况下是什么配置。

The dynamic version

我们依赖项的 版本会不时更新。此外,当我们处于开发阶段时,我们不想继续手动检查是否有新版本可用。

在这种情况下,我们可以添加一个 + 来表示上面提到的版本,给定工件的数量。例如,org.slf4j:slf4j-nop:1.7+ 声明任何高于 1.7 的 SLF4J 版本。让我们将其包含在 build.gradle 文件中,并检查 Gradle 为我们带来了什么。

我们在 build.gradle 文件中运行以下代码:

runtime 'org.slf4j:slf4j-nop:1.7+'

然后,我们运行 dependencies 任务:

$ gradle dependencies
…
+--- org.slf4j:slf4j-nop:1.7+ -> 1.7.7
|    \--- org.slf4j:slf4j-api:1.7.7
…

我们看到 Gradle 选择了 1.7.7 版本,因为它是撰写本书时可用的最新版本。如果你观察第二行,它告诉我们 slf4j-nop 依赖于 slf4j-api;因此,它是我们项目的传递依赖。

这里需要注意的是,始终使用 + 进行小版本升级(例如前面示例中的 1.7+)。让主版本自动更新(比如just image is spring自动从3更新到4,compile 'org.springframework:spring-core:+')无非就是一个赌。动态依赖解析是一个不错的功能,但应谨慎使用。理想情况下,它应该只在项目的开发阶段使用,而不是用于发布候选。

每当依赖项的版本更新到与我们的应用程序不兼容的版本时,我们就会得到一个不稳定的构建。我们应该针对可重现的构建,这样的构建应该产生完全相同的工件,无论是今天还是一年后。

Transitive dependencies

默认情况下,Gradle 非常智能地解决传递依赖关系,优先考虑最新的冲突版本(如果有)。然而,出于某种原因,如果我们想禁用传递依赖,我们只需要在依赖声明中提供一个额外的块:

    runtime ('org.slf4j:slf4j-nop:1.7+') {
        transitive = false
    }

现在,如果我们检查 dependencies 任务的输出,我们会看到不再包含其他依赖项:

\--- org.slf4j:slf4j-nop:1.7.2

我们还可以强制一个给定版本的库,这样即使是相同的工件,后面的版本也可以通过传递依赖来实现;我们强制的版本将获胜:

    runtime ('org.slf4j:slf4j-nop:1.7.2') {
        force = true  
    }

现在运行依赖项任务将产生:

+--- org.slf4j:slf4j-api:1.7.2
\--- org.slf4j:slf4j-nop:1.7.7
     \--- org.slf4j:slf4j-api:1.7.7 -> 1.7.2

这表明旧版本的 slf4j-api 获胜,即使传递依赖项可以获取更高版本。

Dependency configurations

Gradle 提供了一种非常优雅的方式来声明构建不同组所需的依赖项项目构建的各个阶段的资源。

Tip

这些源组称为 源集。最简单且易于理解的源集示例是 maintestmain 源集包含将被编译和构建为 JAR 文件的文件,这些文件将被部署到某个地方或发布到某个存储库。另一方面,test 源集包含将由 JUnit 等测试工具执行但不会投入生产的文件。现在,两个源集对依赖项、构建、打包和执行都有不同的要求。我们将在 Chapter 7Testing and Reporting with 中了解如何添加新的源集Gradle,用于集成测试。

正如我们在一个源集中定义了相关源的组,依赖关系也被定义为一个称为 的组配置。每个配置都有其名称,例如 compiletestCompile 等。各种配置中包含的依赖关系也不同。配置按依赖项的特征分组。例如,以下是 javawar 插件添加的配置:

  • 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插件添加的.默认情况下,这包括 testCompileruntime 依赖项。此配置中的依赖项仅需要在运行时(即运行测试时)测试源。因此,它们不包含在测试的编译类路径中。这就像运行时配置,但仅适用于测试源。

  • providedCompile:这个是由war插件添加的. servlet API 等依赖项由应用服务器提供,因此不需要打包在我们的 war 中。我们期望已经包含在服务器运行时中的任何内容都可以添加到此配置中。但是,它必须在编译源代码时出现。因此,我们可以将这样的依赖声明为 providedCompile。示例是 servlet API 和在服务器运行时可用的任何 Java EE 实现。 war 中不包含此类依赖项。

  • providedRuntime:这个是由war插件添加的.服务器和应用程序将在应用程序运行时提供的依赖项在编译时不需要包括在内,因为没有对实现的直接引用。可以将此类库添加到此配置中。此类依赖项不会包含在 war 中。因此,我们应该确保在应用程序运行时中有可用的实现。

我们知道,当我们应用war插件时,java 插件也被应用。这就是我们在构建 Web 应用程序时所有六种配置都可用的原因。可以通过插件添加更多配置,或者我们可以在构建脚本中自己声明它们。

有趣的是,配置不仅包括依赖关系,还包括此配置产生的工件。

Repositories

repositories 部分配置 Gradle 将在其中查找依赖项的存储库。 Gradle 将依赖项下载到自己的缓存中,这样就不需要在每次运行 Gradle 时都进行下载。我们可以配置多个存储库,如下所示:

repositories {
  mavenCentral()  // shortcut to maven central
  mavenLocal()    // shortcut to maven local (typically ~/.m2)
  jcenter()       // shortcut to jcenter
  maven {
    url "http://repo.company.com/maven"
  }
  ivy {
    url "http://repo.company.com/ivy"
  }
  flatDir {       // jars kept on local file system
    dirs 'libDir'
  }
}

支持 Maven、Ivy 和平面目录(文件系统)等存储库用于依赖解析和上传工件。有一些更具体的便利方法可用于常用的 Maven 存储库,例如 mavenCentral()jcenter()mavenLocal()。但是,可以使用以下语法轻松配置更多 Maven 存储库:

maven {
  url"http://intranet.example.com/repo"
}

在中央存储库之前,项目用于管理文件系统上的库,这些库大多与源代码一起签入。有些项目仍然这样做;尽管我们不鼓励这样做,但人们有理由这样做,Gradle 没有理由不支持。

重要的是要记住,Gradle 不会自动假定要搜索和 从中下载依赖项的任何存储库。我们必须在 repositories 块中明确配置至少一个存储库,Gradle 将在其中搜索工件。

Tip

锻炼

包含 Apache Commons Lang 库以使用以下方法将消息转换为标题大小写:

WordUtils.capitalize(String str)

将字符串中所有以空格分隔的单词大写。

Summary


在本章中,我们首先使用 Gradle 开发了一个 Web 应用程序。我们通过构建应用程序生成 WAR 工件,然后将其部署到本地 Tomcat。然后,我们学习了一些关于 Gradle 中的依赖管理、配置和支持的存储库的基础知识。

Note

读者应该花更多时间在 Gradle 的官方文档中详细阅读这些概念:https ://docs.gradle.org/current/userguide/userguide

现在,我们应该可以使用 Gradle 构建最常见的 Java 应用程序了。在下一章中,我们将尝试理解 Gradle 提供的 Groovy DSL,同时也理解基本的项目模型。