1.3 编写Spring应用

1.3 编写Spring应用

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

  • 一个控制器类,用来处理主页相关的请求;
  • 一个视图模板,用来定义主页看起来是什么样子。

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

1.3.1 处理Web请求

Spring自带了一个强大的Web框架,名为Spring MVC。Spring MVC的核心是控制器(controller)的理念。控制器是处理请求并以某种方式进行信息响应的类。在面向浏览器的应用中,控制器会填充可选的数据模型并将请求传递给一个视图,以便于生成返回给浏览器的HTML。

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

程序清单1.4 主页控制器

1
2
3
4
5
6
7
8
9
10
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.3.2 定义视图

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

程序清单1.5 Taco Cloud主页模板

1
2
3
4
5
6
7
8
9
10
11
<!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”。

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

1.3.3 测试控制器

在测试Web应用时,对HTML页面的内容进行断言是比较困难的。幸好Spring对测试提供了强大的支持,这使得测试Web应用变得非常简单。

对于主页来说,我们所编写的测试在复杂性上与主页本身差不多。测试需要针对根路径“/”发送一个HTTP GET请求并期望得到成功结果,其中视图名称为home并且结果内容包含“Welcome to…”。程序清单1.6能够完成该任务。

程序清单1.6 针对主页控制器的测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
@RunWith(SpringRunner.class)
@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对象对“/”(根路径)发起HTTPGET请求。对于这个请求,我们设置了如下的预期:

  • 响应应该具备HTTP 200 (OK)状态;
  • 视图的逻辑名称应该是home;
  • 渲染后的视图应该包含文本“Welcome to…”。

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

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

1.3.4 构建和运行应用

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

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

epub_29101559_14

图1.7 Spring Boot Dashboard的重点功能

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

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

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

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

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

epub_29101559_15

图1.8 Taco Cloud主页

看上去似乎并不太美观,但这不是一本关于平面设计的书。目前,略显简陋的主页外观已经足够了,它为我们学习Spring打下了一个良好的开端。

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

1.3.5 了解Spring Boot DevTools

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

  • 代码变更后应用会自动重启;
  • 当面向浏览器的资源(如模板、JavaScript、样式表)等发生变化时,会自动刷新浏览器;
  • 自动禁用模板缓存;
  • 如果使用H2数据库的话,内置了H2控制台。

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

应用自动重启

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

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

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

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

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

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

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

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

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

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

LiveReload有针对Google Chrome、Safari和Firefox的浏览器插件(要对Internet Explorer和Edge粉丝说声抱歉)。请访问LiveReload官网,以了解如何为你的浏览器安装LiveReload。

内置的H2控制台

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

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

1.3.6 回顾一下

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

  • 使用Spring Initializr创建初始的项目结构;
  • 编写控制器类处理针对主页的请求;
  • 定义了一个视图模板来渲染主页;
  • 编写了一个简单的测试类来验证工作符合预期。

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

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

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

但是这又是如何运行起来的呢?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,看看在我们的路途中都会有哪些地标。