gradle构建Java web应用

Posted by Raymond on August 28, 2017

这篇文章译自Building Java Web Applications

介绍

Gradle有一个用于构建Java web应用的war插件,并且社区提供了一个叫gretty的优秀插件,用于在Jetty或Tomcat上测试和部署Web应用。本文演示如何构建一个简单的Web应用并将其部署在Jetty上。您还将学习如何使用Mockito框架为servlet编写单元测试,以及如何使用gretty和Selenium为Web应用编写功能测试。

构建什么

你将用gradle默认的项目结构创建一个web应用,添加Servlet API的依赖,添加gretty插件,然后构建和测试应用。

需要什么

  • 大约30分钟
  • 文本编辑器或IDE
  • Java7或以上
  • Gradle4.0或以上

创建web应用的目录结构

Gradle的war插件记录在web应用快速入门和用户手册的WAR插件章节中。war插件扩展了Java插件,增加对web应用的支持。默认情况下,它用src/main/webapp文件夹存放web相关的资源。

因此,新建webdemo项目,并创建以下目录结构:

示例项目的目录结构

webdemo/
    src/
        main/
            java/
            webapp/
        test
            java/

任何servlets或其他Java文件放在src/main/java,测试的代码放在src/test/java,而其他web资源放在src/main/webapp

添加Gradle构建文件

在项目的根目录创建build.gradle文件,其内容如下:

build.gradle

plugins {
    id 'java'
    id 'war'
}

repositories {
    jcenter()
}

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

war插件添加了configurations组的providedCompileprovidedRuntime插件,类似于常见Java应用程序中的compileruntime,来表示这些依赖在本地需要但不添加到生成的webdemo.war文件里。
这些插件的语法是用在javawar插件中。不需要版本,因为它们已经包含在Gradle的发布版里。

建议通过执行wrapper任务生成Gradle包装:

$ gradle wrapper --gradle-version=4.0
:wrapper

这会产生gradlewgradlew.bat脚本和包含包装器的jar的gradle文件夹,详细查看用户手册的wrapper章节

如果你使用的是Gradle4.0或以上,则你可能看到比本文更少的控制台输出。本文使用--console-plain命令行参数来调整输出信息。这是为了显示Gradle正在执行的任务。

向项目添加servlet和元数据

定义Web应用的元数据有两个方式。在servlet3.0规范之前,元数据在项目的WEB-INF/web.xml中描述。3.0之后,元数据可以用注解来定义。

在src/main/java文件夹下创建一个包文件夹org/gradle/demo。添加一个servlet文件HelloServlet.java,内容如下:

src/main/java/com/gradle/demo/HelloServlet.java

package org.gradle.demo;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(name = "HelloServlet", urlPatterns = {"hello"}, loadOnStartup = 1) 
public class HelloServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        response.getWriter().print("Hello, World!");  
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException {
        String name = request.getParameter("name");
        if (name == null) name = "World";
        request.setAttribute("user", name);
        request.getRequestDispatcher("response.jsp").forward(request, response); 
    }
}

该servlet使用@WebServlet注释进行配置。doGet方法通过将“Hello,World!”字符串输出到输出字符流来响应HTTP GET请求。它通过请求参数name作为新的请求参数user的值,然后转发到response.jsp页面来响应HTTP POST请求。

提示:war插件支持使用较旧的web.xml,默认放在src/main/webapp下的WEB-INF文件夹中。可以使用它替代基于注解的方法。

你现在有一个简单的servlet,来响应GET和POST请求。

增加JSP页面到demo

在应用的根目录src/main/webapp下创建首页index.html,内容如下:

<html>
<head>
  <title>Web Demo</title>
</head>
<body>
<p>Say <a href="hello">Hello</a></p> 

<form method="post" action="hello">  
  <h2>Name:</h2>
  <input type="text" id="say-hello-text-input" name="name" />
  <input type="submit" id="say-hello-button" value="Say Hello" />
</form>
</body>
</html>

index.html用一个链接提交HTTP GET请求,用表单提交HTTP POST请求。表单的name文本字段就是doPost接受的参数。

doPost方法里,servlet将请求转发到另一个JSP页面response.jsp。所以要在src/main/webapp里定义那个文件,内容如下:

src/main/webapp/response.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
    <head>
        <title>Hello Page</title>
    </head>
    <body>
        <h2>Hello, ${user}!</h2>
    </body>
</html>

response页面从request访问 user参数并渲染到h2标签里。

添加gretty插件并运行应用

gretty插件是一款很出色的社区支持的插件,可以再Gradle仓库https://plugins.gradle.org/plugin/org.akhikhl.gretty找到它。它使得很容易地用Jetty或Tomcat运行或测试web应用。

要将它加入到我们的项目,需要将下面几行代码加入到build.gradleplugins的模块里。

plugins {
    id 'java'
    id 'war'
    id 'org.akhikhl.gretty' version '1.4.2' 
}

gretty插件往应用加入了很多任务,对于在Jetty或Tomcat环境中运行或测试非常有用。现在你可以用appRun任务构建和部署应用到默认的容器(Jetty)。

$ ./gradlew appRun
:prepareInplaceWebAppFolder
:createInplaceWebAppFolder UP-TO-DATE
:compileJava
:processResources UP-TO-DATE
:classes
:prepareInplaceWebAppClasses
:prepareInplaceWebApp
:appRun
12:25:13 INFO  Jetty 9.2.15.v20160210 started and listening on port 8080
12:25:13 INFO  webdemo runs at:
12:25:13 INFO    http://localhost:8080/webdemo
Press any key to stop the server.
> Building 87% > :appRun

BUILD SUCCESSFUL

你现在可以在__http://localhost:8080/webdemo__访问web应用或者点击链接执行GET请求或者提交表单执行POST请求。

虽然输出的信息说Press any key to stop the server,但是通常用ctrl-C中断停止进程。

使用Mockito对servlet进行单元测试

开源的Mockito framework可以很容易地对Java应用程序进行单元测试。将Mockito的依赖项添加到build.gradletestCompile配置里。

添加Mockito包到build.gradle

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:2.7.19'  
}

要对servlet进行单元测试,先要在src/test/java下创建一个包目录org.gradle.demo,并且创建测试类HelloServletTest.java,内容如下:

package org.gradle.demo;

import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;

import javax.servlet.RequestDispatcher;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.io.StringWriter;

import static org.junit.Assert.assertEquals;
import static org.mockito.Mockito.*;

public class HelloServletTest {
    @Mock private HttpServletRequest request;
    @Mock private HttpServletResponse response;
    @Mock private RequestDispatcher requestDispatcher;

    @Before
    public void setUp() throws Exception {
        MockitoAnnotations.initMocks(this);
    }

    @Test
    public void doGet() throws Exception {
        StringWriter stringWriter = new StringWriter();
        PrintWriter printWriter = new PrintWriter(stringWriter);

        when(response.getWriter()).thenReturn(printWriter);

        new HelloServlet().doGet(request, response);

        assertEquals("Hello, World!", stringWriter.toString());
    }

    @Test
    public void doPostWithoutName() throws Exception {
        when(request.getRequestDispatcher("response.jsp"))
            .thenReturn(requestDispatcher);

        new HelloServlet().doPost(request, response);

        verify(request).setAttribute("user", "World");
        verify(requestDispatcher).forward(request,response);
    }

    @Test
    public void doPostWithName() throws Exception {
        when(request.getParameter("name")).thenReturn("Dolly");
        when(request.getRequestDispatcher("response.jsp"))
            .thenReturn(requestDispatcher);

        new HelloServlet().doPost(request, response);

        verify(request).setAttribute("user", "Dolly");
        verify(requestDispatcher).forward(request,response);
    }
}

这次测试创建了HttpServletRequestHttpServletResponseRequestDispatcher类的模拟对象。在doGet测试里,用StringWriter创建出PrintWriter,当getWriter被执行,模拟的请求对象则会返回。在调用doGet方法后,测试将会检查返回的字符串是否正确。

对于post请求,模拟请求被设置伟返回一个指定的名字,否则返回null。getRequestDispatcher方法则返回关联的模拟对象。调用doPost方法并执行请求。然后Mockito验证setAttribute方法是在模拟请求中用正确的参数调用的,并且在request dispatcher上调用了forward方法。

你现在可以用Gradle的test任务(或者其他依赖它的任务,例如build)测试servlet。

$ ./gradlew build
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:war
:assemble
:compileTestJava
:processTestResources UP-TO-DATE
:testClasses
:test
:check
:build

BUILD SUCCESSFUL

测试结果可以在build/reports/tests/test/index.html里看。

test-result

新增功能测试

gretty插件与Gradle结合起来,使向Web应用程序添加功能测试变得容易。为此,将以下配置添加到build.gradle文件中:

gretty {
    integrationTestTask = 'test'  
}

// ... rest from before ...

dependencies {
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    testCompile 'junit:junit:4.12'
    testCompile 'org.mockito:mockito-core:2.7.19'
    testCompile 'io.github.bonigarcia:webdrivermanager:1.6.1' 
    testCompile 'org.seleniumhq.selenium:selenium-java:3.3.1' 
}

gretty插件需要知道哪个任务需要先执行,通常是你自己的任务,但为了保持简单,只使用现有的测试任务。

Selenium是用于编写功能测试的流行的开源API。2.0版基于WebDriver API。最近的版本要求测试者为他们的浏览器下载和安装一个WebDriver版本,这个版本可能很乏味而且很难自动化。 WebDriverManager项目可以让Gradle轻松地为你处理这个过程。

添加功能测试到你的项目,并在src/test/java目录下:

src/test/java/org/gradle/demo/HelloServletFunctionalTest.java

package org.gradle.demo;

import io.github.bonigarcia.wdm.ChromeDriverManager;
import org.junit.After;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;

import static org.junit.Assert.assertEquals;

public class HelloServletFunctionalTest {
    private WebDriver driver;

    @BeforeClass
    public static void setupClass() {
        ChromeDriverManager.getInstance().setup(); 
    }

    @Before
    public void setUp() {
        driver = new ChromeDriver();               
    }

    @After
    public void tearDown() {
        if (driver != null)
            driver.quit();                         
    }

    @Test
    public void sayHello() throws Exception {
        driver.get("http://localhost:8080/webdemo");

        driver.findElement(By.id("say-hello-text-input")).sendKeys("Dolly");
        driver.findElement(By.id("say-hello-button")).click();

        assertEquals("Hello Page", driver.getTitle());
        assertEquals("Hello, Dolly!", driver.findElement(By.tagName("h2")).getText());
    }
}

WebDriverManager会先检查最新的版本,如果版本不存在则下载并安装。然后sayHello测试方法会用Chrome访问应用的根路径,填写文本字段,点击按钮,并验证目标页面的标题,并且h2标签包含期望的字符串。

WebDriverManager系统支持Chrome, Opera, Internet Explorer, Microsoft Edge, PhantomJS, and Firefox。查看文档获取更多信息。

运行功能测试

$ ./gradlew test
:prepareInplaceWebAppFolder UP-TO-DATE
:createInplaceWebAppFolder UP-TO-DATE
:compileJava UP-TO-DATE
:processResources UP-TO-DATE
:classes UP-TO-DATE
:prepareInplaceWebAppClasses UP-TO-DATE
:prepareInplaceWebApp UP-TO-DATE
:compileTestJava UP-TO-DATE
:processTestResources UP-TO-DATE
:testClasses UP-TO-DATE
:appBeforeIntegrationTest
12:57:56 INFO  Jetty 9.2.15.v20160210 started and listening on port 8080
12:57:56 INFO  webdemo runs at:
12:57:56 INFO    http://localhost:8080/webdemo
:test
:appAfterIntegrationTest
Server stopped.

BUILD SUCCESSFUL

gretty插件在默认端口启动一个嵌入式版本的Jetty 9,执行测试并关闭服务器。如果你在看,你会看到Selenium系统打开一个新的浏览器,访问网站,填写表格,点击按钮,检查新的页面,最后关闭浏览器。

集成测试通常是通过创建一个独立的源代码集和专用任务来处理的,但这不在本指南的范围之内。有关详细信息,请参阅Gretty文档

总结

在本指南中,你学习了如何:

  • 在Gradle构建中使用war插件来定义一个web应用程序
  • 将一个servlet和JSP页面添加到一个Web应用程序
  • 使用gretty插件来部署应用程序
  • Mockito框架单元测试servlet
  • 使用grettySelenium功能测试Web应用程序

下一步

Gretty是非常强大的API. 更多信息请查阅[Gretty documentation]。有关Selenium的更多详细信息可以在Selenium website上找到,有关WebDriverManager系统的更多信息可以在[WebdriverDriverManager GitHub repository]上找到。

如果您对功能测试感兴趣,请查看开源的[Geb,它提供了一个强大的Groovy DSL,用于浏览器自动化,位于Selenium和WebDriver之上。