4.2 使用Java创建Web服务

我们将使用Java以测试驱动的方法创建一个简单的Web服务。为此,我们将采取如下步骤:

  • 在Eclipse IDE中创建项目;
  • 修改Gradle构建文件;
  • 编写后端类;
  • 编写Web服务。

4.2.1 在Eclipse IDE中新建Gradle项目

我们将创建一个Gradle项目并查看生成的项目。

如果还没有启动Eclipse IDE,现在就启动它。如果必要,确认工作空间目录——Eclipse将在这个目录中创建和查找项目。

接下来,将出现Eclipse IDE Welcome选项卡。鉴于这个选项卡中没有新建Gradle项目的快捷方式,我们先不管它。为创建Gradle项目,请执行如下操作。

  • 选择菜单File>New>Project…(千万不要选择Java Project,否则将不会使用Gradle来构建项目):

4.2 使用Java创建Web服务 - 图1

  • 在向导窗口New Project中,展开选项Gradle并选择Gradle Project,再单击Next按钮。然后将出现一个欢迎窗口,请仔细阅读其中的文本。如果愿意,可取消选择复选框Show the welcome page the next time the wizard appears。单击Next按钮:

4.2 使用Java创建Web服务 - 图2

  • 在New Gradle Project窗口中,指定项目名——JavaWebservice,确保同意将项目存储到默认位置,再单击Finish按钮。第一次使用时,插件可能需要一段时间来下载并安装Gradle。最终,这个窗口将自动关闭。

4.2 使用Java创建Web服务 - 图3

请注意,在这个屏幕截图中,调整了窗口的大小,你看到的窗口将包含更多选项。

探索生成的项目

Eclipse IDE窗口左边将出现一个包含Package Explorer选项卡的窗口,请展开该选项卡中的项目JavaWebservice。我们来简单地看看这个生成的项目。

Gradle构建插件生成了如下项目条目。

  • src/main是一个快捷方式,指向主程序源代码所在的目录。
  • src/test是一个快捷方式,指向单元测试脚本所在的目录。
  • JRE System Library显示了运行程序所需的Java平台文件。
  • Project and External Dependencies显示了程序所需的附加库。当前,Gradle默认加载的单元测试框架JUnit 4需要多个库。
  • 目录gradle包含Gradle wrapper所需的文件。这个脚本确保你能够在没有安装Gradle的系统中运行项目,并确保在构建项目期间下载并使用正确的Gradle版本。
  • src显示了源代码目录的完整内容。当前,它包含子目录main和test,这在本书前面讨论过。
  • 最后,在项目的根目录中,有一些与Gradle相关的文件,其中最重要的是build.gradle,Gradle将使用这个构建文件来编译和构建项目以及运行单元测试。

4.2.2 修改Gradle构建文件

对于这个项目,我们将使用框架SparkJava。第1章简要讨论过SparkJava,请不要将其与Apache的大数据平台混为一谈。SparkJava是一个让你能够轻松创建快速、独立Web应用程序的框架。你可手动从官方网站下载这个库,并将必要的文件放到正确的目录中,但让构建工具去处理这些事情要容易得多。

4.2 使用Java创建Web服务 - 图4 很多著名的库都依赖于众多其他的依赖项,而这些依赖项也有自己的依赖项。对现代JVM软件开发来说,能够下载库的构建工具是必不可少的。

双击Package Explorer中的文件build.gradle将其打开。最新的Gradle版本使用基于Groovy的领域特定语言(domain-specific language,DSL);以后的版本将支持基于Kotlin的DSL。你将在第11章看到,Groovy对语法的要求没有Java严格,因此在Gradle构建文件中,分号和小括号通常并不是必不可少的。找到dependencies块:

  1. dependencies {
  2. ....
  3. }

如果其中包含以compile打头的行,请将它们删除,但务必保留以testCompile打头的语句。在dependencies块开头添加如下条目:

  1. compile 'org.slf4j:slf4j-simple:1.7.21'
  2. compile 'com.sparkjava:spark-core:2.5.4'
  3. compile 'com.fasterxml.jackson.core:jackson-databind:2.8.5'

将修改后的文件存盘。这个文件告诉Gradle,为编译这个项目,需要Simple Logging Facade for Java(SLF4J)、SparkJava和Jackson's JSON handler框架。在依赖项中包含版本号通常是个不错的主意,因为更新的版本可能导致既有代码无法正确运行。构建项目时, Gradle将搜索流行的仓库网站,下载指定版本的依赖项及其依赖项,并将它们放到正确的目录中。还将设置classpath,让你无需手动修改它就能运行项目。

4.2 使用Java创建Web服务 - 图5 别忘了,较新版本的库和框架能修复重要的安全bug,这对生产环境来说尤其重要。在任何情况下,紧跟你依赖的框架的发展步伐都是明智的。

4.2.3 构建项目

在Eclipse IDE右下方的窗口中,找到并单击标签Gradle Tasks。展开条目build,再双击其中的build任务,如下图所示。

4.2 使用Java创建Web服务 - 图6

这将切换到Gradle Executions选项卡,其中显示了插件Gradle运行的任务的进度和状态。如果一切顺利,每项任务旁边都将出现绿点,如下图所示。

4.2 使用Java创建Web服务 - 图7

如果不太顺利,且只有任务:compileJava旁边为红点,请切换到选项卡Console,其中包含Gradle的输出。请向下滚动,看看能否找到这样的错误消息,即未能找到tools.jar。如果有这样的错误消息,就意味着你需要将Java安装位置告诉插件Gradle。为此,可按如下步骤做。

(1) 切换到选项卡Gradle Tasks。

(2) 右击任务build并选择Open Gradle Run Configuration…。

(3) 这将打开Edit configuration对话框。切换到选项卡Java Home并单击按钮Browse。切换到JDK安装目录,并单击OK按钮关闭对话框;再单击OK按钮关闭对话框Edit configuration。

(4) 双击任务build以再次执行它。现在选项卡Gradle Executions中将只有绿点。

Eclipse插件Buildship Gradle Integration的最新版存在的一个问题是,添加、更新或删除依赖后,你可能必须手动刷新项目。请在Package Explorer中展开条目Project and External Dependencies,如果其中没有列出JAR文件spark-core,请右击项目JavaWebservice并选择Gradle>Refresh Gradle Project。这样做后,将列出其他很多作为依赖的JAR文件。

4.2 使用Java创建Web服务 - 图8

4.2.4 编写后端类

这里将创建一个可重用的通用后端类,它对使用的Web服务技术一无所知。用于处理HTTP请求的代码将使用这个后端类来生成JSON响应。我们将使用测试驱动开发(test-driven development,TDD)方法,并涵盖如下主题:

  • 后端类的业务规则;
  • 创建方法的哑实现(dummy implementation);
  • 创建测试用例类并编写其第一个单元测试;
  • 实现输入有效性检查;
  • 编写第二个单元测试;
  • 实现业务逻辑;
  • 创建Web服务。

  • 后端类的业务规则

我们来创建一个简单的Web服务,它返回发送给它的字符串中各个字符出现的次数。

计算字符串中各个字符出现的次数的方法必须满足如下需求。

  • 它所在的类必须放在chapter03.backend包中,同时必须名为CharacterCounter且是公有的。
  • 这个方法必须名为countCharacters,它将一个String作为输入值且是公有的。
  • 这个方法必须返回一个实现了泛型接口Map的类的实例。这是一个将Character实例映射到Integer实例的映射。
  • 返回的Map对象必须将输入字符串包含的每个UTF-16字符映射到一个整数,而这个整数指出了这个字符在字符串中出现的次数。
  • 传入的字符串不能是空的,否则将引发IllegalArgumentException异常。
    下面是一个输入/输出示例:
  1. "A!Ba?!?!" --> {'A': 1, 'B':1, 'a':1, '?': 2, '!': 3}
  • 创建方法的哑实现

要编写单元测试,必须先编写符合签名要求(输入和输出)的方法,但我们只是让这个方法返回null。这样做后,就可编写单元测试,并在确定测试失败后提供方法的正确实现,以确保测试成功。

如果你详细研究业务规则,将发现必须编写一个这样的方法,即接受一个输入并生成一个响应。这个方法必须是自给自足的——看起来不需要任何类实例变量和方法。有鉴于此,我们将编写一个静态的类方法。在编写单元测试期间,我们就能判断这样的选择是否是糟糕的。如果是糟糕的,我们总是可对其进行修改。

4.2 使用Java创建Web服务 - 图9 TDD的一大优点是,允许你改变错误的设计决策。

下面来创建类chapter03.backend.CharacterCounter

(1) 在Package Explorer中,右击条目src/main/java并选择New>Class…。

(2) 在文本框Package中,输入chapter03.backend

(3) 在文本框Name中,输入CharacterCounter

(4) 确保选择了限定符public

(5) 单击Finish按钮生成这个类,如下图所示。

4.2 使用Java创建Web服务 - 图10

在上述屏幕截图中,调整了窗口的大小;在你看到的窗口中,显示的选项更多。向导生成的类如下:

  1. package chapter03.backend;
  2. public class CharacterCounter {
  3. }

下面来编写这个方法的哑实现。将光标放到这个类中,并执行如下操作。

(1) 按Tab键缩进一层。

(2) 输入必不可少的访问限定符和非访问限定符(public static),再添加一个空格。

(3) 输入单词Map,再按住Ctrl和空格键。之后将出现一个窗口,其中显示了几个与你输入的名称匹配的类。选择Map - java.util并按回车,如下图所示:

4.2 使用Java创建Web服务 - 图11

  Eclipse将编写相应的导入语句和Map,并将光标放在表示映射键K上。

(4) 输入Char并再次按住Ctrl和空格键,再选择Character - java.lang并按回车,如下图所示:

4.2 使用Java创建Web服务 - 图12

(5) 按Tab键移到字符V上,输入Int并按住Ctrl和空格键,再选择Integer - java.lang并按回车。这指定了映射中值的类型。

4.2 使用Java创建Web服务 - 图13

(6) 将光标移到行尾并输入countCharacters(String text),再在当前行输入{,并在下一行输入}

现在代码应类似于下面这样:

  1. package chapter03.backend;
  2. import java.util.Map;
  3. public class CharacterCounter {
  4. public static Map<Character, Integer> countCharacters(String text) {
  5. }
  6. }

Eclipse IDE指出这个类无法通过编译。将鼠标指向带红色下划线的方法名或左边的红色X图标,将出现一个工具提示,其中包含内容“This method must return a result of type Map”。Eclipse提供了两种自动解决方案:Add return statement(添加return语句)和Change return type to 'void'(将返回类型改为void),如下图所示:

4.2 使用Java创建Web服务 - 图14

这里选择第一种解决方案;为此,选择Add return statement。Eclipse将自动编写如下代码:

  1. return null;

至此,我们编写了一个能够通过编译的后端类。下面来编写一个单元测试,以便检查我们的API是否正确。如果对结果满意,就可编写有效的实现,并检查它是否像预期的那样工作。

  • 创建测试用例类并编写其第一个单元测试

前面的业务规则明确地指出,如果传入的是null而不是String实例,这个类将引发IllegalArgumentException异常。下面来测试传入null时这个类是否会引发这种异常。为此,先来创建一个供测试框架JUnit使用的类(所有的测试都将包含在这个类中)。

(1) 在Eclipse IDE中,右击条目src/test/java并选择New>JUnit Test Case。

(2) 在文本框Package中,输入chapter03.backend

(3) 在文本框Name中,输入CharacterCounterTests

(4) 单击Finish按钮生成这个类。

生成的类如下(出于简洁考虑删除了一些空行):

  1. package chapter03.backend;
  2. import static org.junit.Assert.*;
  3. import org.junit.Test;
  4. public class CharacterCounterTests {
  5. @Test
  6. public void test() {
  7. fail("Not yet implemented");
  8. }
  9. }

注解@Test告诉框架JUnit,这是一个包含单元测试的方法。稍后你将看到,它可包含可选参数。包含单元测试的方法不能返回任何值,因此使用了关键字void来指定其返回类型。下面来重命名方法test():将光标放在第一个字符上,再按Alt + Shift + R,将方法名改为testNullInput并按回车。

4.2 使用Java创建Web服务 - 图15

4.2 使用Java创建Web服务 - 图16 在这里,按Alt+Shift+R好像有点傻。但如果这个方法位于大型程序中,这样做将把修改应用于整个程序,因此务必尽可能这样做。用得越多,越容易记住键盘快捷键。

将光标放在fail语句所在的行,并按Ctrl + D将整行删除。将整个方法修改成下面这样(别忘了按Ctrl和空格键来输入expectedIllegalArgumentExceptionCharacterCounter,以减少键击次数):

  1. @Test(expected=IllegalArgumentException.class)
  2. public void testNullInput() {
  3. CharacterCounter.countCharacters(null);
  4. }

这些代码告诉JUnit,仅当引发了IllegalArgumentException异常时,这个测试才是成功的。如果类没有引发这个异常,这个测试就被视为失败的。现在正是运行这个测试的大好时机。为此按F11,Gradle将构建和编译代码并运行测试。

4.2 使用Java创建Web服务 - 图17

测试失败了,这符合预期。现在切换到选项卡Package Explorer,以便修复这个测试。

  • 实现输入有效性检查

打开src/main/java中的类CharacterCounter。在这个类的方法countCharacters中,在代码行return null上方输入如下代码:

  1. if (text == null) {
  2. throw new IllegalArgumentException("text must not be null");
  3. }

这些代码的含义几乎是不言自明的。由于IllegalArgumentExceptionRuntimeException的子类,因此这个方法无需向编译器指出它可能引发这种异常。

4.2 使用Java创建Web服务 - 图18 为验证这一点,可双击类名IllegalArgumentException,再按F4,也可右击它并选择Open Type Hierarchy。之后,请单击标签Package Explorer返回到包资源管理器。

现在按F11运行这个测试,如下图所示:

4.2 使用Java创建Web服务 - 图19

这次测试成功了。祝贺你!高兴之余别忘了再次单击标签Package Explorer。

  • 编写第二个单元测试

下面再创建一个测试,对主业务逻辑进行测试。为添加新的单元测试,请打开src/test/java中的CharacterCounterTests类,在方法testNullInput()后面添加一个空行,再添加如下方法:

  1. @Test
  2. public void testStringInput() {
  3. Map<Character, Integer> map = CharacterCounter
  4. .countCharacters("!a!A!");
  5. assertEquals(map.size(), 3);
  6. assertEquals(map.get('a').intValue(), 1);
  7. assertEquals(map.get('!').intValue(), 3);
  8. assertEquals(map.get('A').intValue(), 1);
  9. }

我们传入一个字符串,并核实返回的映射包含的字符数未超过预期;另外,我们还测试了每个字符出现的次数。下面是一些注意事项。

  • Character类是一个基本类型包装类,包装了基本类型char值。在Java中,将char值(单个UTF-16 Unicode字符)放在单引号中,而将String值放在双引号中。如果我们传入String "A"而不是字符'A',映射的get方法将返回null。
  • 映射的方法get返回包装类Integer的一个实例,其中包含指定字符出现的次数。我们将它转换为基本类型值,以便能够轻松地使用JUnit方法assertEquals的重载版本assertEquals(int, int)。这个方法有很多重载版本,如果我们不使用前面说的版本,就必须执行一些类型转换操作,以遵循Java的重载规则。
    按F11,你将看到这个测试因异常NullPointerException而失败。之所以会引发这种异常,是因为返回的映射为null,导致调用map.size()失败。
  • 实现业务逻辑

对API感到满意后,我们来实现业务逻辑,以便让刚才的测试通过。切换到Package Explorer,打开src/main/java中的CharacterCounter类,并在检查输入是否为null的语句后面添加如下代码:

  1. Map<Character, Integer> map = new HashMap<>();
  2. for (char c: text.toCharArray()) {
  3. if (!map.containsKey(c)) {
  4. map.put(c, 1);
  5. } else {
  6. int curValue = map.get(c);
  7. map.put(c, ++curValue);
  8. }
  9. }
  10. return map;

对上述代码说明如下。

  • 变量map指向一个HashMap实例,后者将一个Character实例(映射的键)映射到一个Integer实例(映射的值)。
  • 数据类型String没有实现接口Iterable,无法用于改进的for循环中。我们必须调用其方法toCharArray()来返回一个char数组,而数组总是可用于改进的for循环中。
  • 我们在这些代码中使用了基本类型char值。当这些char值被用于映射的方法中时,Java将自动把它们为Character实例,这都是拜自动装箱功能所赐。别忘了,泛型要求将类作为参数。
  • 如果在映射中没有找到当前字符,就将其加入映射中,并将键设置为当前字符,而值设置为1。
  • 如果在映射中找到了当前字符,就从映射中获取其当前值。
  • 请注意代码行map.put(c, ++curValue);。它首先将值加1,再将键和修改后的值存储到映射中。作为练习,请读者尝试将++curValue改为curValue++,看看测试是否会失败。
    按F11再次运行这两个测试,现在它们都应该成功。
  • 创建可执行的应用程序任务

这里不会使用SparkJava的单元测试功能,而创建一个Gradle能够运行的可执行程序。为此,最简单的方法是使用Gradle的插件application在这个Gradle项目中添加一个run任务。下面先来创建启动Web服务的类。

在Package Explorer中,添加一个名为Webservice的类,并将其放在chapter03.main包中。为此,可右击src/main/java并选择New>Class。在这个类中,添加向控制台打印简单字符串的方法main(),此时这个类应类似于下面这样:

  1. package chapter03.main;
  2. public class WebService {
  3. public static void main(String[] args) {
  4. System.out.println("The program is running!");
  5. }
  6. }

使用Package Explorer打开文件build.gradle,并在apply plugin:'java'下方新增一行内容,如下所示:

  1. apply plugin: 'java'
  2. apply plugin: 'application'

添加一个空行,再添加如下内容:

  1. mainClassName = "chapter03.main.WebService"

这个条目使用全限定名指定了插件application将运行的包含方法main()的类。在选项卡Gradle Tasks中,双击build > build来构建这个项目。如果一切顺利,请单击选项卡Gradle Tasks中右侧的第四个图标,其工具提示为“Refresh Tasks for All Projects”。这将添加插件application在这个Gradle项目中新增的任务。

4.2 使用Java创建Web服务 - 图20

现在可以在选项卡Gradle Tasks中选择任务application>run了。如果必要,将自动编译项目。为查看控制台输出,必须切换到选项卡Console。如果你不希望Eclipse自动切换到选项卡Gradle Execution,可禁用这项功能,为此需执行下面的步骤。

(1) 切换到选项卡Gradle Tasks。

(2) 展开条目JavaWebservice>application,右击其中的条目run并选择Open Gradle Run Configuration…。

(3) 取消选中复选框Show Execution View,再单击OK按钮关闭打开的对话框。

现在当你选择任务application>run时,将自动切换到选项卡Console,你将能够看到方法main打印到控制台的消息。

  • 创建Web服务

下面来将方法main()转换为一个这样的程序,即设置一条Spark路由以处理HTTP GET请求。为此,打开chapter03.main.Webservice类,并在package语句下方添加如下import语句:

  1. package chapter03.main;
  2. import java.util.Map;
  3. import spark.Spark;
  4. import com.fasterxml.jackson.databind.ObjectMapper;
  5. import chapter03.backend.CharacterCounter;

WebService类中,添加private static变量mapper,它是一个ObjectMapper实例。这个类来自Jackson库,将负责对方法countCharacters的输出(一个Map 实例)进行转换。换而言之,这个Map对象将把从Character键映射到Integer值的Map实例映射到JSON:

  1. private static ObjectMapper mapper = new ObjectMapper();

现在可以编写方法main()了。为此可修改原来的main()方法,但别忘了在编写代码时使用前面提到的组合键:

  1. public static void main(String[] args) {
  2. Spark.get("/main", (req, res) -> {
  3. res.type("application/json");
  4. try {
  5. String value = req.queryMap("value").value();
  6. value = (value == null ? "" : value);
  7. Map<Character, Integer> map = CharacterCounter
  8. .countCharacters(value);
  9. return mapper.writeValueAsString(map);
  10. } catch (Exception e) {
  11. e.printStackTrace();
  12. return "{}";
  13. }
  14. });
  15. }

对这些代码说明如下。

  • 通过调用方法Spark.get,Spark库设置了一个处理程序,这个处理程序将在用户通过HTTP GET请求指定的URL时做出响应。定义HTTP处理程序后,Spark将确保HTTP服务器在程序初始化完毕后自动启动。
  • 方法Spark.get()的第二个参数是一个lambda表达式。这里的lambda表达式接受两个参数:请求和响应。这两个参数都将被传入的lambdab表达式使用:请求(req)被用来读取HTTP请求的查询参数,而响应(res)被用来将Web服务的输出格式设置为JSON。这个lambda表达式将在用户通过HTTP GET请求URL /main时执行。
  • 这里使用了一个以前没见过的“if”条件,其中?前面的部分为表达式。如果这个表达式的结果为true,就返回?后面的第一部分(这里为""),否则就返回:后面的那部分。在这里,如果从查询参数中获取的值为null,就将其改为空字符串,否则返回查询参数的值。
  • 静态变量mapper是一个Jackson类ObjectMapper的实例,负责将Map map(将Character键映射到Integer值的Map)转换为有效的JSON表示。如果引发了异常,将向控制台打印错误消息,并返回一个空的JSON对象。
    • 运行这个Web服务

要通过Eclipse IDE顶部的工具栏中的绿色run图标运行这个Web服务,请单击这个图标旁边的箭头:

4.2 使用Java创建Web服务 - 图21

这里列出了通过这个按钮可执行的所有Gradle任务,包括选项build和run,它们分别启动构建过程和程序。请选择选项run,这样当你按Ctrl + F11或单击run图标时,Eclipse将启动应用程序。Eclipse IDE将一刻不耽误,马上启动应用程序。

过段时间后,应用程序将开始向选项卡Console中打印输出,其中的最后一条消息应包含Started以及SparkJava 在内部使用的服务器的类名,即org.eclipse.jetty.server.Server

4.2 使用Java创建Web服务 - 图22

SparkJava使用的HTTP服务器默认使用端口4567。请启动浏览器,并访问http://localhost:4567/main?value=Test

得到的输出应类似于下面这样:

4.2 使用Java创建Web服务 - 图23

4.2 使用Java创建Web服务 - 图24 输出的顺序可能与这里显示的不同,原因是HashMap类不以任何有意义的方式排列元素。

要停止这个应用程序,可单击Console选项卡中红色的停止按钮。

4.2 使用Java创建Web服务 - 图25

HTTP服务器可能需要过段时间才会停止,但最终这个按钮将变成灰色,而应用程序本身将随之停止。每个Gradle操作都会打开一个新的控制台。要在这些控制台之间切换,可单击工具提示为“Display Selected Console”的按钮,也可单击这个按钮右边的箭头,这将列出所有打开的控制台:

4.2 使用Java创建Web服务 - 图26

要关闭所有非活动窗口,可单击工具提示为“Remove All Terminated Gradle Consoles”的按钮。在你在开发项目期间启动了大量Gradle任务时,这个按钮提供了极大的方便:

4.2 使用Java创建Web服务 - 图27

作为练习,在CharacterCounter类中,尝试将java.util.HashMap实例替换为java.util.TreeMap实例。TreeMap类会根据插入的键排列元素,同时由于它像HashMap类一样实现了接口Map,因此这样替换后程序依然能够正确运行。在实际工作中,通过使用接口来隐藏实现细节确实是个很好的主意。

  • 创建Javadoc文档

现在是创建一些文档的绝佳时机。在选项卡Gradle Tasks中,单击任务Documentation> Javadoc。你必须在Package Explorer中刷新项目。然后,打开选项卡Navigator,并选择项目的构建文件——build>docs>javadocs>index.html,再右击它并选择Open With>System Editor,这将打开默认浏览器。

要给类或方法添加文档,可在类或方法的定义前面输入/**并按回车。

在Java中,常规多行注释放在/**/之间(与C和C++一样),而单行注释以//打头。Javadoc注释以/**打头,并以*/结尾(与常规多行注释一样)。Eclipse会自动创建一个分别以/***/打头和结束的块,并添加一些属性。文档必须由你来提供,同时别忘了你需要编写HTML,因此在使用某些字符时必须进行转义,例如,使用>来表示>