编写Spring应用

因为我们刚刚开始,所以首先为 Taco Cloud 应用做一些小的变更,但是这些变更会展现 Spring 的很多优点。在刚开始的时候,比较合适的做法是为 Taco Cloud 应用添加一个主页。在添加主页时,我们将会创建两个代码构件:

  • 一个控制器类,用来处理主页相关的请求;

  • 一个视图模板,用来定义主页看起来是什么样子。

测试是非常重要的,所以我们还会编写一个简单的测试类来测试主页。但是,要事优先,我们需要先编写控制器。

处理Web请求

Spring 自带了一个强大的 Web 框架,名为 Spring MVC。Spring MVC 的核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的 HTML。在第 2 章中,我们将会学习更多关于 Spring MVC 的知识。现在,我们会编写一个简单的控制器类以处理来自根路径(如 “/”)的请求,并将这些请求转发至主页视图,在这个过程中不会填充任何的模型数据。程序清单1.4展示了这个简单的控制器类。

在第 2 章中,我们将会学习更多关于 Spring MVC 的知识。现在,我们会编写一个简单的控制器类以处理来自根路径(如 “/”)的请求,并将这些请求转发至主页视图,在这个过程中不会填充任何的模型数据。程序清单1.4展示了这个简单的控制器类。

程序清单1.4 主页控制器
package tacos;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller  ⇽---控制器
public class HomeController {
  @GetMapping("/")  ⇽---处理对根路径“/ ”的请求
  public String home() {
    return "home";  ⇽---返回视图名
  }
}

可以看到,这个类带有 @Controller 注解。就其本身而言,@Controller 并没有做太多的事情。它的主要目的是让组件扫描将这个类识别为一个组件。因为 HomeController 带有 @Controller 注解,所以 Spring 的组件扫描功能会自动发现它,并创建一个 HomeController 实例作为 Spring 应用上下文中的 bean。

实际上,有一些其他的注解与 @Controller 有着类似的目的(包括 @Component、@Service 和 @Repository)。你可以为 HomeController 添加上述的任意其他注解,其作用是完全相同的。但是,在这里选择使用 @Controller 更能描述这个组件在应用中的角色。

home() 是一个简单的控制器方法。它带有 @GetMapping 注解,表明如果针对 “/” 发送 HTTP GET 请求,那么将会由这个方法来处理请求。该方法所做的只是返回 String 类型的 home 值。

这个值将会解析为视图的逻辑名。视图如何实现取决于多个因素,但是 Thymeleaf 位于类路径中,使得我们可以使用 Thymeleaf 来定义模板。

为何使用Thymeleaf?

你可能会想:为什么要选择 Thymeleaf 作为模板引擎?为何不使用 JSP?为何不使用 FreeMarker?为何不选择其他的几个可选方案呢?

简单来说,我必须要做出选择,我喜欢 Thymeleaf,相对于其他的方案,我会优先使用它。即便 JSP 是更加显而易见的选择,但是组合使用 JSP 和 Spring Boot 需要克服一些挑战。我不想脱离第 1 章的内容定位,所以就此打住。在第 2 章中,我们会看到其他的模板方案,其中也包括 JSP。

模板名称是由逻辑视图名派生而来的,再加上 “/templates/” 前缀和 “.html” 后缀。最终形成的模板路径将是 “/templates/home.html”。所以,我们需要将模板放到项目的 “/src/main/resources/templates/home.html” 中。现在,就让我们来创建这个模板。

定义视图

为了让主页尽可能简单,主页除了欢迎用户访问站点之外,不会做其他的任何事情。程序清单1.5展现了基本的 Thymeleaf 模板,定义了 Taco Cloud 的主页。

<!DOCTYPE html>
<html xmlns = "http://www.w3.org/1999/xhtml"
      xmlns:th = "http://www.thymeleaf.org">
  <head>
    <title>Taco Cloud</title>
  </head>

  <body>
    <h1>Welcome to...</h1>
    <img th:src = "@{/images/TacoCloud.png}"/>
  </body>
</html>

这个模板并没有太多需要讨论的。唯一需要注意的是用于展现 Taco Cloud Logo 的 <img> 标签。它使用了 Thymeleaf 的 th:src 属性和 @{...} 表达式,以便于引用相对于上下文路径的图片。除此之外,这个主页就是一个扮演 “Hello World” 角色的页面。

我们再讨论一下这个图片。我将定义 Taco Cloud Logo 的工作留给你,但是你需要将它放到应用的正确位置。

图片是使用相对于上下文的 “/images/TacoCloud.png” 路径来引用的。回忆一下我们的项目结构,像图片这样的静态资源位于 “/src/main/resources/static” 文件夹。这意味着,在项目中 Taco Cloud Logo 的图片路径必须为 “/src/main/resources/static/images/TacoCloud.png”。

现在,我们有了一个处理主页请求的控制器和渲染主页的模板,基本就可以启动应用来看一下它的效果了。但是,在此之前,我们先看一下如何为控制器编写测试。

测试控制器

在测试 Web 应用时,对 HTML 页面的内容进行断言是比较困难的。幸好,Spring 对测试提供了强大的支持,这使得测试 Web 应用变得非常简单。对于主页来说,我们所编写的测试在复杂性上与主页本身差不多。测试需要针对根路径 “/” 发送一个 HTTP GET 请求并期望得到成功结果,其中视图名称为 home 并且结果内容包含 “Welcome to…​”。程序清单1.6就能够完成该任务。

package tacos;

import static org.hamcrest.Matchers.containsString;
import static
     org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static
     org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

@WebMvcTest(HomeController.class)  ⇽---针对HomeController的Web测试
public class HomeControllerTest {

  @Autowired
  private MockMvc mockMvc;  ⇽---注入MockMvc

  @Test
  public void testHomePage() throws Exception {
    mockMvc.perform(get("/"))  ⇽---发起对“/”的GET请求
      .andExpect(status().isOk())  ⇽---期望得到HTTP 200
      .andExpect(view().name("home"))  ⇽---期望得到home视图
      .andExpect(content().string(  ⇽---期望包含“Welcome to...”
          containsString("Welcome to...")));
  }

}

对于这个测试,首先注意到的可能就是它使用了与 TacoCloudApplicationTests 类不同的注解。HomeControllerTest 没有使用 @SpringBootTest 标记,而是添加了 @WebMvcTest 注解。这是 Spring Boot 提供的一个特殊测试注解,让这个测试在 Spring MVC 应用的上下文中执行。更具体来讲,在本例中,它会将 HomeController 注册到 Spring MVC 中,这样一来,我们就可以向它发送请求了。

@WebMvcTest 同样会为测试 Spring MVC 应用提供了 Spring 环境的支持。尽管可以启动一个服务器来进行测试,但是对于我们的场景来说,仿造一下 Spring MVC 的运行机制就可以。测试类被注入了一个 MockMvc,能够让测试实现 mockup。

通过 testHomePage() 方法,我们定义了针对主页想要执行的测试。它首先使用 MockMvc 对象对 “/”(根路径)发起 HTTP GET 请求。对于这个请求,我们设置了如下的预期:

  • 响应应该具备 HTTP 200 (OK) 状态;

  • 视图的逻辑名称应该是 home;

  • 渲染后的视图应该包含文本 “Welcome to…​.”。

我们可以在所选的 IDE 中运行测试,也可以使用如下的 Maven 命令:

$ mvnw test

如果在 MockMvc 对象发送请求之后,上述预期没有全部满足,那么这个测试会失败。但是,我们的控制器和视图模板在编写时都满足了这些预期,所以测试应该能够通过,并且带有成功的图标——至少能够看到一些绿色的背景,表明测试通过了。

控制器已经编写好了,视图模板也已经创建完毕,而且我们还通过了测试。看上去,我们已经成功实现了主页。但是,尽管测试已经通过了,但是如果能够在浏览器中看到结果,会更有成就感。毕竟,这才是 Taco Cloud 的客户所能看到的效果。接下来,我们构建应用并运行它。

构建和运行应用

就像初始化 Spring 应用有多种方式一样,运行 Spring 应用也有多种方式。你如果愿意,可以翻到本书附录部分,了解运行 Spring Boot 应用的一些通用方式。

因为我们选择了使用 Spring Tool Suite 来初始化和管理项目,所以可以借助名为 Spring Boot Dashboard 的便捷功能来帮助我们在 IDE 中运行应用。Spring Boot Dashboard 的表现形式是一个 Tab,通常会位于 IDE 窗口的左下角附近。图1.7展现了一个带有标注的 Spring Boot Dashboard 截屏。

image 2024 03 11 10 46 38 012
Figure 1. 图1.7 Spring Boot Dashboard的重点功能

图1.7包含了一些有用的细节,但是我不想花太多时间介绍 Spring Boot Dashboard 支持的所有功能。对我们来说,现在最重要的事情是需要知道如何使用它来运行 Taco Cloud 应用。确保 taco-cloud 应用程序在项目列表中能够显示(这是图1.7中显示的唯一应用),然后单击启动按钮(最左边的按钮,也就是带有绿色三角形和红色正方形的按钮)。应用程序应该就能立即启动。

在应用启动的过程中,你会在控制台看到一些 Spring ASCII 码,随后会是描述应用启动各个步骤的日志条目。在控制台输出的最后,你将会看到一条日志显示 Tomcat 已经在 port(s): 8080 (http) 启动,这意味着此时可以打开 Web 浏览器并导航至主页,看到我们的劳动成果。

稍等一下!刚才说启动 Tomcat?我们是什么时候将应用部署到 Tomcat Web 服务器的呢?

Spring Boot 应用的习惯做法是将所有它所需要的东西都放到一起,没有必要将其部署到某种应用服务器中。在这个过程中,我们根本没有将应用部署到 Tomcat 中——Tomcat 是我们应用的一部分!(在1.3.6小节,我会详细描述 Tomcat 是如何成为我们应用的一部分的。)

现在,应用已经启动起来了,打开 Web 浏览器并访问 http://localhost:8080(或者在 Spring Boot Dashboard 中点击地球样式的按钮),你将会看到如图1.8所示的界面。如果你设计了自己的 Logo 图片,显示效果可能会有所不同。但是,跟图1.8相比,应该不会有太大的差异。

image 2024 03 11 10 49 06 481
Figure 2. 图1.8 Taco Cloud主页

看上去,似乎并不太美观,但本书不是关于平面设计的,略显简陋的主页外观已经足够了。

到现在为止,我一直没有提及 DevTools。在初始化项目的时候,我们将其作为一个依赖添加了进来。在最终生成的 pom.xml 文件中,它表现为一个依赖项。甚至 Spring Boot Dashboard 都显示项目启用了 DevTools。那么,DevTools 到底是什么,又能为我们做些什么呢?接下来,让我们快速浏览一下 DevTool 最有用的一些特性。

了解Spring Boot DevTools

顾名思义,DevTools 为 Spring 开发人员提供了一些便利的开发期工具和特性,其中包括:

  • 代码变更后应用会自动重启;

  • 当面向浏览器的资源(如模板、JavaScript、样式表)等发生变化时,会自动刷新浏览器;

  • 自动禁用模板缓存;

  • 如果使用 H2 数据库,则内置了 H2 控制台。

需要注意,DevTools 并不是 IDE 插件,也不需要你使用特定的 IDE。在 Spring Tool Suite、IntelliJ IDEA 和 NetBeans 中,它都能很好地运行。另外,因为它的用途仅仅是开发,所以它能够很智能地在生产环境中把自己禁用掉。我们将会在第 18 章讨论它是如何做到这一点的。现在,我们主要关注 Spring Boot DevTools 最有用的特性,那么先从应用的自动重启开始吧。

应用自动重启

如果将 DevTools 作为项目的一部分,那么你可以看到,当对项目中的 Java 代码和属性文件作出修改后,这些变更稍后就能发挥作用。DevTools 会监控变更,在看到变化的时候自动重启应用。

更准确地说,当 DevTools 启用的时候,应用程序会加载到 Java 虚拟机(Java Virtual Machine, JVM)中的两个独立的类加载器中。其中一个类加载器会加载 Java 代码、属性文件,以及项目的 “src/main/” 路径下几乎所有的内容。这些条目很可能会经常发生变化。另外一个类加载器会加载依赖的库,这些库不太可能经常发生变化。

当探测到变更的时候,DevTools 只会重新加载包含项目代码的类加载器,并重启 Spring 的应用上下文,在这个过程中,另外一个类加载器和 JVM 会原封不动。这个策略非常精细,但能减少应用启动的时间。

这种策略的一个不足之处就是自动重启无法反映依赖项的变化。这是因为包含依赖库的类加载器不会自动重新加载。这意味着每当在构建规范中添加、变更或移除依赖的时候,为了让变更生效,都要重新启动应用。

浏览器自动刷新和禁用模板缓存

默认情况下,像 Thymeleaf 和 FreeMarker 这样的模板方案在配置时,会缓存模板解析的结果,这样一来,在为每个请求提供服务的时候,模板就不用重新解析了。在生产环境中,这是一种很好的方式,因为它会带来一定的性能收益。

但是,在开发期,缓存模板就不太友好了。在应用运行的时候,如果缓存模板,刷新浏览器就无法看到模板变更的效果了。即便我们对模板做了修改,在应用重启之前,缓存的模板依然会有效。

spring.thymeleaf.cache=false

DevTools 通过禁用所有模板缓存解决了这个问题。你可以对模板进行任意数量的修改,只需刷新一下浏览器就能看到结果。

如果你像我一样,连浏览器的刷新按钮都懒得点,希望在对代码做出变更之后马上就能在浏览器中看到结果,那么很幸运,DevTools 有一些特殊的功能可以供我们使用。

DevTools 会和你的应用程序一起,自动启动一个 LiveReload 服务器。LiveReload 服务器本身并没有太大的用处。但是,当它与 LiveReload 浏览器插件结合起来的时候,就能够在模板、图片、样式表、JavaScript等(实际上,几乎涵盖为浏览器提供服务的所有内容)发生变化的时候,自动刷新浏览器。

LiveReload 有针对 Google Chrome、Safari 和 Firefox 的浏览器插件(这里要对 Internet Explorer 和 Edge 的支持者说声抱歉)。请访问 LiveReload 网站的 Extensions 页面了解如何为你的浏览器安装 LiveReload 。

内置的H2控制台

虽然我们的项目还没有使用数据库,但是这种情况在第 3 章中就会发生变化。如果你使用 H2 数据库进行开发,DevTools 将会自动启用 H2 控制台,这样一来,我们可以通过 Web 浏览器进行访问。只需要让浏览器访问 http://localhost:8080/h2-console ,就能看到应用所使用的数据。

此时,我们已经编写了一个非常简单却很完整的 Spring 应用。在本书接下来的章节中,我们将会不断扩展它。但现在,要回过头来看一下我们都完成了哪些工作、Spring 发挥了什么作用。

回顾一下

回想一下我们是怎样完成这一切的。简短来说,在构建基于 Spring 的 Taco Cloud 应用的过程中,我们执行了如下步骤:

  • 使用 Spring Initializr 创建初始的项目结构;

  • 编写控制器类处理针对主页的请求;

  • 定义了一个视图模板来渲染主页;

  • 编写了一个简单的测试类来验证工作符合预期。

这些步骤都非常简单直接,对吧?除了初始化应用的第一个步骤之外,我们所做的每一个操作都专注于生成主页的目标。

实际上,我们所编写的每行代码都致力于实现这个目标。除了 Java import 语句之外,我只能在控制器中找到两行 Spring 相关的代码,而在视图模板中,一行 Spring 相关的代码都没有。尽管测试类的大部分内容都使用了 Spring 对测试的支持,但是它在测试的运行环境中,似乎没有那么强的侵入性。

这是使用 Spring 进行开发的一个重要优势。你可以只关注满足应用需求的代码,无须考虑如何满足框架的需求。尽管我们偶尔还是需要编写一些框架特定的代码,但是它们通常只占整个代码库很小的一部分。正如我在前文所述,Spring(以及 Spring Boot)可以视为感受不到框架的框架(frameworkless framework)。

但是,这一切到底是如何运行起来的呢?Spring 在幕后做了些什么来保证应用的需求能够得到满足?要理解 Spring 到底做了些什么,我们首先来看一下构建规范。

在 pom.xml 文件中,我们声明了对 Web 和 Thymeleaf starter 的依赖。这两项依赖会传递引入大量其他的依赖,包括:

  • Spring 的 MVC 框架;

  • 嵌入式的 Tomcat;

  • Thymeleaf 和 Thymeleaf 布局方言。

它还引入了 Spring Boot 的自动配置库。当应用启动的时候,Spring Boot 的自动配置将会探测到这些库,并自动完成如下功能:

  • 在 Spring 应用上下文中配置 bean 以启用 Spring MVC;

  • 在 Spring 应用上下文中配置嵌入式的 Tomcat 服务器;

  • 配置 Thymeleaf 视图解析器以便于使用 Thymeleaf 模板渲染 Spring MVC 视图。

简言之,自动配置功能完成了所有的脏活累活,让我们能够集中精力编写实现应用功能的代码。如果你问我的观点,我认为这是一个很好的安排!

我们的 Spring 之旅才刚刚开始。Taco Cloud 应用程序只涉及了 Spring 所提供功能的一小部分。在开始下一步之前,我们先整体了解一下 Spring,看看在我们的路途中都会有哪些地标。