24 服务测试:如何使用 Spring 测试 Web 服务层组件?

23 讲我们介绍了对数据访问层进行测试的方法,这一讲将着重介绍对三层架构中的另外两层(Service 层和 Controller 层)的测试方法。

与位于底层的数据访问层不同,这两层的组件都依赖于它的下一层组件,即 Service 层依赖于数据访问层,而 Controller 层依赖于 Service 层。因此,对这两层进行测试时,我们将使用不同的方案和技术。

使用 Environment 测试配置信息

在《定制配置:如何创建自定义的配置信息?》中,我们介绍了自定义配置信息的实现方式。而在 Spring Boot 应用程序中,Service 层通常依赖于配置文件,所以我们也需要对配置信息进行测试。

配置信息的测试方案分为两种,第一种依赖于物理配置文件,第二种则是在测试时动态注入配置信息。

第一种测试方案比较简单,在 src/test/resources 目录下添加配置文件时,Spring Boot 能读取这些配置文件中的配置项并应用于测试案例中。在介绍具体的实现过程之前,我们有必要先来了解一下 Environment 接口,该接口定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface Environment extends PropertyResolver {



String[] getActiveProfiles();



String[] getDefaultProfiles();



boolean acceptsProfiles(String... profiles);



}

在上述代码中我们可以看到,Environment 接口的主要作用是处理 Profile,而它的父接口 PropertyResolver 定义如下代码所示:

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
30
31
32
33
34
35
36
37
38
39
40
41
public interface PropertyResolver {



boolean containsProperty(String key);



String getProperty(String key);



String getProperty(String key, String defaultValue);



<T> T getProperty(String key, Class<T> targetType);



<T> T getProperty(String key, Class<T> targetType, T defaultValue);



String getRequiredProperty(String key) throws IllegalStateException;



<T> T getRequiredProperty(String key, Class<T> targetType) throws IllegalStateException;



String resolvePlaceholders(String text);



String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;



}

显然,PropertyResolver 的作用是根据各种配置项的 Key 获取配置属性值。

现在,假设 src/test/resources 目录中的 application.properties 存在如下配置项:

1
springcss.order.point = 10

那么,我们就可以设计如下所示的测试用例了。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RunWith(SpringRunner.class)



@SpringBootTest



public class EnvironmentTests{







@Autowired



public Environment environment;







@Test



public void testEnvValue(){



Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point")));



}



}

这里我们注入了一个 Environment 接口,并调用了它的 getProperty 方法来获取测试环境中的配置信息。

除了在配置文件中设置属性,我们也可以使用 @SpringBootTest 注解指定用于测试的属性值,示例代码如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RunWith(SpringRunner.class)



@SpringBootTest(properties = {" springcss.order.point = 10"})



public class EnvironmentTests{







@Autowired



public Environment environment;







@Test



public void testEnvValue(){



Assert.assertEquals(10, Integer.parseInt(environment.getProperty("springcss.order.point")));



}



}

使用 Mock 测试 Service 层

正如这一讲开篇时提到,Service 层依赖于数据访问层。因此,对 Service 层进行测试时,我们还需要引入新的技术体系,也就是应用非常广泛的 Mock 机制。

接下来,我们先看一下 Mock 机制的基本概念。

Mock 机制

Mock 的意思是模拟,它可以用来对系统、组件或类进行隔离。

在测试过程中,我们通常关注测试对象本身的功能和行为,而对测试对象涉及的一些依赖,仅仅关注它们与测试对象之间的交互(比如是否调用、何时调用、调用的参数、调用的次数和顺序,以及返回的结果或发生的异常等),并不关注这些被依赖对象如何执行这次调用的具体细节。因此,Mock 机制就是使用 Mock 对象替代真实的依赖对象,并模拟真实场景来开展测试工作。

使用 Mock 对象完成依赖关系测试的示意图如下所示:

图片1.png

Mock 对象与依赖关系测试示意图

从图中可以看出,在形式上,Mock 是在测试代码中直接 Mock 类和定义 Mock 方法的行为,通常测试代码和 Mock 代码放一起。因此,测试代码的逻辑从测试用例的代码上能很容易地体现出来。

下面我们一起看一下如何使用 Mock 测试 Service 层。

使用 Mock

23 讲中我们介绍了 @SpringBootTest 注解中的 SpringBootTest.WebEnvironment.MOCK 选项,该选项用于加载 WebApplicationContext 并提供一个 Mock 的 Servlet 环境,内置的 Servlet 容器并没有真实启动。接下来,我们针对 Service 层演示一下这种测试方式。

首先,我们来看一种简单场景,在 customer-service 中存在如下 CustomerTicketService 类:

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
30
31
32
33
34
35
36
37
38
39
40
41
@Service



public class CustomerTicketService {







@Autowired



private CustomerTicketRepository customerTicketRepository;







public CustomerTicket getCustomerTicketById(Long id) {



return customerTicketRepository.getOne(id);



}







}

这里我们可以看到,以上方法只是简单地通过 CustomerTicketRepository 完成了数据查询操作。

显然,对以上 CustomerTicketService 进行集成测试时,还需要我们提供一个 CustomerTicketRepository 依赖。

下面,我们通过以下代码演示一下如何使用 Mock 机制完成对 CustomerTicketRepository 的隔离。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@RunWith(SpringRunner.class)



@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)



public class CustomerServiceTests {







@MockBean



private CustomerTicketRepository customerTicketRepository;







@Test



public void testGetCustomerTicketById() throws Exception {



Long id = 1L;



Mockito.when(customerTicketRepository.getOne(id)).thenReturn(new CustomerTicket(1L, 1L, "Order00001", "DemoCustomerTicket1", new Date()));



CustomerTicket actual = customerTicketService.getCustomerTicketById(id);







assertThat(actual).isNotNull();



assertThat(actual.getOrderNumber()).isEqualTo("Order00001");



}



}

首先,我们通过 @MockBean 注解注入了 CustomerTicketRepository;然后,基于第三方 Mock 框架 Mockito 提供的 when/thenReturn 机制完成了对 CustomerTicketRepository 中 getCustomerTicketById() 方法的 Mock。

当然,如果你希望在测试用例中直接注入真实的CustomerTicketRepository,这时就可以使用@SpringBootTest 注解中的 SpringBootTest.WebEnvironment.RANDOM_PORT 选项,示例代码如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
@RunWith(SpringRunner.class)



@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)



public class CustomerServiceTests {







@Autowired



private CustomerTicketRepository customerTicketRepository;







@Test



public void testGetCustomerTicketById() throws Exception {



Long id = 1L;



CustomerTicket actual = customerTicketService.getCustomerTicketById(id);







assertThat(actual).isNotNull();



assertThat(actual.getOrderNumber()).isEqualTo("Order00001");



}



}

运行上述代码后就会以一个随机的端口启动整个 Spring Boot 工程,并从数据库中真实获取目标数据进行验证。

以上集成测试的示例中只包含了对 Repository 层的依赖,而有时候一个 Service 中可能同时包含 Repository 和其他 Service 类或组件,下面回到如下所示的 CustomerTicketService 类:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Service



public class CustomerTicketService {







@Autowired



private OrderClient orderClient;







private OrderMapper getRemoteOrderByOrderNumber(String orderNumber) {







return orderClient.getOrderByOrderNumber(orderNumber);



}







}

这里我们可以看到,在该代码中,除了依赖 CustomerTicketRepository 之外,还同时依赖了 OrderClient。

请注意:以上代码中的 OrderClient 是在 customer-service 中通过 RestTemplate 访问 order-service 的远程实现类,其代码如下所示:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
@Component



public class OrderClient {







@Autowired



RestTemplate restTemplate;







public OrderMapper getOrderByOrderNumber(String orderNumber) {







ResponseEntity<OrderMapper> restExchange = restTemplate.exchange(



"http://localhost:8083/orders/{orderNumber}", HttpMethod.GET, null,



OrderMapper.class, orderNumber);







OrderMapper result = restExchange.getBody();







return result;



}



}

CustomerTicketService 类实际上并不关注 OrderClient 中如何实现远程访问的具体过程。因为对于集成测试而言,它只关注方法调用返回的结果,所以我们将同样采用 Mock 机制完成对 OrderClient 的隔离。

对 CustomerTicketService 这部分功能的测试用例代码如下所示,可以看到,我们采用的是同样的测试方式。

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Test



public void testGenerateCustomerTicket() throws Exception {



Long accountId = 100L;



String orderNumber = "Order00001";



Mockito.when(this.orderClient.getOrderByOrderNumber("Order00001"))



.thenReturn(new OrderMapper(1L, orderNumber, "deliveryAddress"));



Mockito.when(this.localAccountRepository.getOne(accountId))



.thenReturn(new LocalAccount(100L, "accountCode", "accountName"));







CustomerTicket actual = customerTicketService.generateCustomerTicket(accountId, orderNumber);







assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);



}

这里提供的测试用例演示了 Service 层中进行集成测试的各种手段,它们已经能够满足一般场景的需要。

测试 Controller 层

对 Controller 层进行测试之前,我们先来提供一个典型的 Controller 类,它来自 customer-service,如下代码所示:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@RestController



@RequestMapping(value="customers")



public class CustomerController {



@Autowired



private CustomerTicketService customerTicketService;



@PostMapping(value = "/{accountId}/{orderNumber}")



public CustomerTicket generateCustomerTicket( @PathVariable("accountId") Long accountId,



@PathVariable("orderNumber") String orderNumber) {



CustomerTicket customerTicket = customerTicketService.generateCustomerTicket(accountId, orderNumber);



return customerTicket;



}



}

关于上述 Controller 类的测试方法,相对来说比较丰富,比如有 TestRestTemplate、@WebMvcTest 注解和 MockMvc 这三种,下面我们逐一进行讲解。

使用 TestRestTemplate

Spring Boot 提供的 TestRestTemplate 与 RestTemplate 非常类似,只不过它专门用在测试环境中。

如果我们想在测试环境中使用 @SpringBootTest,则可以直接使用 TestRestTemplate 来测试远程访问过程,示例代码如下:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@RunWith(SpringRunner.class)



@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)



public class CustomerController2Tests {







@Autowired



private TestRestTemplate testRestTemplate;







@MockBean



private CustomerTicketService customerTicketService;







@Test



public void testGenerateCustomerTicket() throws Exception {



Long accountId = 100L;



String orderNumber = "Order00001";



given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))



.willReturn(new CustomerTicket(1L, accountId, orderNumber, "DemoCustomerTicket1", new Date()));







CustomerTicket actual = testRestTemplate.postForObject("/customers/" + accountId+ "/" + orderNumber, null, CustomerTicket.class);



assertThat(actual.getOrderNumber()).isEqualTo(orderNumber);



}



}

上述测试代码中,首先,我们注意到 @SpringBootTest 注解通过使用 SpringBootTest.WebEnvironment.RANDOM_PORT 指定了随机端口的 Web 运行环境。然后,我们基于 TestRestTemplate 发起了 HTTP 请求并验证了结果。

特别说明:这里使用 TestRestTemplate 发起请求的方式与 RestTemplate 完全一致,你可以对《服务调用:如何使用 RestTemplate 消费 RESTful 服务?》的内容进行回顾。

使用 @WebMvcTest 注解

接下来测试方法中,我们将引入一个新的注解 @WebMvcTest,该注解将初始化测试 Controller 所必需的 Spring MVC 基础设施,CustomerController 类的测试用例如下所示:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@RunWith(SpringRunner.class)



@WebMvcTest(CustomerController.class)



public class CustomerControllerTestsWithMockMvc {







@Autowired



private MockMvc mvc;







@MockBean



private CustomerTicketService customerTicketService;







@Test



public void testGenerateCustomerTicket() throws Exception {



Long accountId = 100L;



String orderNumber = "Order00001";



given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))



.willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));







this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());



}



}

以上代码的关键是 MockMvc 工具类,所以接下来我们有必要对它进一步展开说明。

MockMvc 类提供的基础方法分为以下 6 种,下面一一对应来看下。

  • Perform:执行一个 RequestBuilder 请求,会自动执行 SpringMVC 流程并映射到相应的 Controller 进行处理。
  • get/post/put/delete:声明发送一个 HTTP 请求的方式,根据 URI 模板和 URI 变量值得到一个 HTTP 请求,支持 GET、POST、PUT、DELETE 等 HTTP 方法。
  • param:添加请求参数,发送 JSON 数据时将不能使用这种方式,而应该采用 @ResponseBody 注解。
  • andExpect:添加 ResultMatcher 验证规则,通过对返回的数据进行判断来验证 Controller 执行结果是否正确。
  • andDo:添加 ResultHandler 结果处理器,比如调试时打印结果到控制台。
  • andReturn:最后返回相应的 MvcResult,然后执行自定义验证或做异步处理。

执行该测试用例后,从输出的控制台日志中我们不难发现,整个流程相当于启动了 CustomerController 并执行远程访问,而 CustomerController 中使用的 CustomerTicketService 则做了 Mock。

显然,测试 CustomerController 的目的在于验证其返回数据的格式和内容。在上述代码中,我们先定义了 CustomerController 将会返回的 JSON 结果,然后通过 perform、accept 和 andExpect 方法模拟了 HTTP 请求的整个过程,最终验证了结果的正确性。

使用 @AutoConfigureMockMvc 注解

请注意 @SpringBootTest 注解不能和 @WebMvcTest 注解同时使用。

在使用 @SpringBootTest 注解的场景下,如果我们想使用 MockMvc 对象,那么可以引入 @AutoConfigureMockMvc 注解。

通过将 @SpringBootTest 注解与 @AutoConfigureMockMvc 注解相结合,@AutoConfigureMockMvc 注解将通过 @SpringBootTest 加载的 Spring 上下文环境中自动配置 MockMvc 这个类。

使用 @AutoConfigureMockMvc 注解的测试代码如下所示:

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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
@RunWith(SpringRunner.class)



@SpringBootTest



@AutoConfigureMockMvc



public class CustomerControllerTestsWithAutoConfigureMockMvc {







@Autowired



private MockMvc mvc;







@MockBean



private CustomerTicketService customerTicketService;







@Test



public void testGenerateCustomerTicket() throws Exception {



Long accountId = 100L;



String orderNumber = "Order00001";



given(this.customerTicketService.generateCustomerTicket(accountId, orderNumber))



.willReturn(new CustomerTicket(1L, 100L, "Order00001", "DemoCustomerTicket1", new Date()));







this.mvc.perform(post("/customers/" + accountId+ "/" + orderNumber).accept(MediaType.APPLICATION_JSON)).andExpect(status().isOk());



}



}

在上述代码中,我们使用了 MockMvc 工具类完成了对 HTTP 请求的模拟,并基于返回状态验证了 Controller 层组件的正确性。

Spring Boot 中的测试注解总结

通过前面内容的学习,相信你已经感受到了各种测试注解在测试 Spring Boot 应用程序的过程中所发挥的核心作用。

如下所示表格,我们罗列了一些经常使用的测试注解及其描述。

图片8.png

小结与预告

对于一个 Web 应用程序而言,Service 层和 Web 层组件的测试是核心关注点。这一讲我们通过 Mock 机制实现了 Service 层的测试,并引入了三种不同的方法对 Controller 层组件完成验证。

这里给你留一道思考题:在使用 Spring Boot 测试 Web 应用程序时,你知道常见的测试注解有哪些?欢迎在留言区进行互动、交流。

讲完测试组件之后,我们将进入本专栏的最后一讲。在结束语中,我们将对 Spring Boot 进行总结,并对它的后续发展进行展望。

结束语 以终为始:Spring Boot 总结和展望

终于到了课程的最后一讲啦,这一讲我们将对整个 Spring Boot 课程进行总结。

Spring Boot 不仅是 Spring 家族的重要组成成员,也是一款业界领先的应用程序开发框架。通过 Spring Boot 提供的多项核心功能,我们能够快速构建一个完整的 Web 服务开发解决方案。

作为一门针对 Spring Boot 的系统化课程,在课程最后,我们先总结一下整个课程中介绍的 Spring Boot 核心功能,然后梳理一下写作过程中的一些思考和心得,最后对 Spring Boot 的未来发展进行展望。

Spring Boot 的创新性

Spring Boot 是 Spring 家族中由 Pivotal 团队提供的核心开发框架,它基于 Spring4.0 设计,目的是简化新 Spring 应用程序的初始搭建过程和开发过程。

自这个框架诞生以来,它就是快速应用开发领域(Rapid Application Development,RAD)界的领导者。通过使用 Spring Boot 内置的自动配置机制,使得开发人员再也不需要像传统的 Spring 框架那样,使用复杂和重复的配置构建和运行整个 Web 应用程序了,大大简化了开发工作流程。从这点上讲,Spring Boot 无疑是一款具有创新性的开发框架。

Spring Boot 不仅继承了 Spring 框架原有的优秀特性,同时它还添加了很多新的功能,包含但不限于以下五点:

  • 可以创建独立的 Spring 应用程序,这些应用程序可以以 JAR 包的形式直接运行;
  • 内嵌 Tomcat、Jetty 等 Servlet 容器;
  • 提供自动配置的“starter”项目以简化外部组件依赖管理;
  • 尽可能把配置 Spring 容器的工作自动化;
  • 提供开箱即用的特性,如指标、健康检查和外部化配置。

除此之外,Spring Boot 还是 “约定优于配置(Convention over configuration)”设计理念的倡导者和实践者。

约定优于配置是一种软件设计范式,旨在帮助软件开发人员获得已经默认的内置功能组件,从而减少做决定的次数。在日常开发过程中,开发人员只需要指定自己所开发的应用程序中与约定存在差异的那部分内容即可,剩余的全部交给框架处理。

当然,Spring Boot 框架的强大之处还体现在集成性上。

在使用传统的 Spring 框架时,我们经常会遇到因为各个依赖的组件之间版本不一致导致应用程序无法启动的情况。而 Spring Boot 通过集成大量的外部框架,使得依赖包的版本冲突、引用的不稳定性等问题得到了很好的解决。在这点上,同样也体现了 Spring Boot 的创新性。

Spring Boot 课程总结

总结完 Spring Boot 的各项核心功能及所具备的创新性,我们再来总结一下整个课程的讲解特色和与其他课程之间的差异。

这里,我整理了本课程的三大亮点。

第一大亮点:完整介绍了 Spring Boot 开发技术体系。通过学习本课程,你可以全面梳理基于 Spring Boot 的 Web 应用程序开发技术组件,其中包括配置体系、数据访问、Web 服务、消息通信、系统安全、系统监控、系统测试等专项主题。这些技术组件涵盖了 Java EE 应用程序开发涉及的方方面面,具有广泛的应用场景。

第二大亮点:提供了实用的应用程序开发案例。这个案例系统足够简单,可以让你从零开始理解和掌握其中的各项知识点。同时,这个案例系统又足够完整,涉及的各个核心功能都为我们提供了相关的配置项和示例代码,在日常开发过程中可以进行参考。

第三大亮点:提供了深入的功能组件原理分析过程。这些分析过程能帮助你在掌握 Spring Boot 框架应用的基础上,深入理解自动配置、数据访问、远程调用等核心技术组件的实现原理,做到知其然而知其所以然。

整个课程从平时的积累到酝酿,再到启动,最后到上线经历了小半年的时间,伴随着这个过程,我把 Spring Boot 的部分源代码系统梳理了一遍,也对内部的设计思想和实现原理做了一些提炼和总结。

总体而言,Spring Boot 是一款代码质量非常高的开源框架,其中关于 Spring Boot 和 Spring 框架的集成、内置的自动配置机制,以及数据访问、远程调用等诸多功能都给我留下了深刻的印象,使我受益良多,相信坚持学习到今天的你也是如此。

Spring Boot 的发展和演进

最后,我们来对 Spring Boot 的发展做一些展望。

首先,我们来看一下 Spring Boot 与 Spring 框架的演进过程。

目前 Spring 已经演进到 5.X 版本,随着 Spring 5 的正式发布,我们迎来了响应式编程(Reactive Programming)的全新发展时期。

Spring 5 中内嵌了与数据管理相关的响应式数据访问、与系统集成相关的响应式消息通信,以及与 Web 服务相关的响应式 Web 框架等多种响应式组件,从而极大简化了响应式应用程序的开发过程和难度。

以支持响应式 Web 的 Spring WebFlux 为例,这里我们给出它的架构图,如下图所示:

Drawing 0.png

Spring WebFlux 架构图(来自 Spring 官网)

在图中我们可以看到,上图左侧为基于 Spring Webflux 的技术栈,右侧为基于 Spring MVC 的技术栈。我们知道传统的 Spring MVC 是在 Java EE 的 Servlet 标准之上进行构建的,该标准本身就是阻塞式和同步式。而 Spring WebFlux 基于响应式流进行构建,因此我们可以使用它来构建异步非阻塞的服务。

随着 WebFlux 等响应式编程技术的兴起,它为构建具有即时响应性和回弹性的应用程序提供了一个很好的技术基础。

我们知道一个分布式系统中,可能存在数十乃至数百个独立的 Web 应用程序,它们之间互相通信以完成复杂的业务流程,而这个过程势必涉及大量的 I/O 操作。

一旦涉及 I/O 操作,尤其是阻塞式 I/O 操作将会整体增加系统的延迟并降低吞吐量。如果我们能够在复杂的流程中集成非阻塞、异步通信机制,就可以高效处理跨服务之间的网络请求。针对这种场景,WebFlux 也是一种非常有效的解决方案。

下面我们再来看一下 Spring Boot 2 的另一张官网架构图,如下图所示:

Drawing 1.png

Spring Boot 2 架构图(来自 Spring 官网)

从图中我们可以看到,上图底部将 Spring Data 明确划分为两大类型:一类是支持 JDBC、JPA 和部分 NoSQL 的传统 Spring Data Repository,另一类则是支持 Mongo、Cassandra、Redis、Couchbase 等的响应式 Spring Data Reactive Repository。

这张图背后的意义在于,Spring Boot 可以帮助我们构建从 Web 服务层到数据访问层的全栈式响应式编程技术,从而确保系统的各个环节都具备即时响应性。

未来,让我们一起期待响应式编程技术与 Spring Boot 框架之间更加紧密的整合吧。