...
 
Commits (7)
    https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/4dc34f10dbb9195ee4631e0853a6e1997a90ffa8 2021-10-07 15:07:45 2021-10-07T15:07:45+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/48df14f2d59ca711117eb55e1eaa4b3047200b7d 2021-10-07 15:09:35 2021-10-07T15:09:35+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/b84c66b97a54e9cb70e588517757381913f7543b 2021-10-07 15:11:27 2021-10-07T15:11:27+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/4725fe314b61066ac425f9e1286da0c474e411cb 2021-10-07 15:13:06 2021-10-07T15:13:06+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/e98e37c4b00b79295cb9d65b68d4231add6db92f 2021-10-07 15:13:46 2021-10-07T15:13:46+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/d655d053b87c16d366aea16b0f18c6aa70b6966d 2021-10-07 22:37:08 2021-10-07T22:37:08+08:00 wizardforcel 562826179@qq.com https://gitcode.net/OpenDocCN/apachecn-java-zh/-/commit/e3197aed94c601fb711be6680d4843ffbe4876aa 2021-10-07 22:37:31 2021-10-07T22:37:32+08:00 wizardforcel 562826179@qq.com
# 前言
今天的企业发展如此之快,以至于他们正在借助云的弹性来提供一个平台来构建和部署其高度可扩展的应用程序。这意味着开发人员现在面临着构建云本地应用程序的挑战。为此,他们需要了解他们编码所针对的环境、工具和资源
本书首先解释了云应用的驱动因素,并向您展示了云部署与标准数据中心上常规应用程序部署的区别。您将了解特定于云中运行的应用程序的设计模式,并了解如何使用 RESTAPI 在 JavaSpring 中构建微服务。
然后,您将深入了解以最大自动化程度构建、测试和部署应用程序的生命周期,以缩短部署周期时间。逐渐地,您将继续配置 AWS 和 Azure 平台,并使用它们的 API 部署您的应用程序。最后,您将了解 API 设计关注点及其最佳实践。您还将学习如何将现有的单片应用程序迁移到分布式云本地应用程序中。
最后,您将了解如何构建和监控始终可用且容错的可伸缩、弹性和健壮的云本机应用程序。
# 这本书是给谁的
希望构建针对基于云部署的弹性、健壮和可伸缩应用程序的 Java 开发人员会发现本书很有帮助。关于 Java、Spring、web 编程和公共云提供商(AWS 和 Azure)的一些知识应该足以让您读完这本书。
# 充分利用这本书
1. 这本书从介绍开始,然后以一个简单的服务为基础,一步一步地,通过章节。因此,读者将受益于遵循书的流程,除非他们正在寻找一个特定的主题。
2. 下载代码并运行它总是很诱人的。但是,当您键入代码时,尤其是在最初的章节中,您将受益更多。这本书的写作方式使得重要的概念和代码出现在这一章中,从而阻止了您回头看源代码。
3. 话虽如此,请尝试代码示例并运行它们。它使原则更具体,更容易掌握
4. 我希望您已经投资了一台好的台式机/笔记本电脑,因为您将在您的机器上运行容器和虚拟机,这需要资源,所以最好有一台强大的设备来运行
5. 参考章节中提到的文档链接,以扩展本书中讨论的框架和技术的知识。
6. 云是一种变化非常迅速的技术。因此,本书强调概念,并通过代码演示它们。例如,CQRS 作为一个概念很重要,因此我们展示了 MongoDB 和 Elasticsearch 的实现。但是,您可以在任何其他数据库集上尝试该模式。
# 下载示例代码文件
您可以从您的账户[www.packtpub.com](http://www.packtpub.com)下载本书的示例代码文件。如果您在其他地方购买了本书,您可以访问[www.packtpub.com/support](http://www.packtpub.com/support)并注册,将文件通过电子邮件直接发送给您。
您可以通过以下步骤下载代码文件:
1. 登录或注册[www.packtpub.com](http://www.packtpub.com/support)
2. 选择“支持”选项卡。
3. 点击代码下载和勘误表。
4. 在搜索框中输入图书名称,然后按照屏幕上的说明进行操作。
下载文件后,请确保使用以下最新版本解压或解压缩文件夹:
* WinRAR/7-Zip for Windows
* 适用于 Mac 的 Zipeg/iZip/UnRarX
* 适用于 Linux 的 7-Zip/PeaZip
该书的代码包也托管在 GitHub 上的[https://github.com/PacktPublishing/Cloud-Native-Applications-in-Java](https://github.com/PacktPublishing/Cloud-Native-Applications-in-Java) 。我们的丰富书籍和视频目录中还有其他代码包,请访问**[https://github.com/PacktPublishing/](https://github.com/PacktPublishing/)** 。看看他们!
# 下载彩色图像
我们还提供了一个 PDF 文件,其中包含本书中使用的屏幕截图/图表的彩色图像。您可以在这里下载:[https://www.packtpub.com/sites/default/files/downloads/CloudNativeApplicationsinJava_ColorImages.pdf](https://www.packtpub.com/sites/default/files/downloads/CloudNativeApplicationsinJava_ColorImages.pdf)
# 使用的惯例
本书中使用了许多文本约定。
`CodeInText`:表示文本中的码字、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 句柄。下面是一个例子:`CrudRepository`接口附带了一组默认方法来实现最常见的操作
代码块设置如下:
```java
-- Adding a few initial products
insert into product(id, name, cat_Id) values (1, 'Apples', 1)
insert into product(id, name, cat_Id) values (2, 'Oranges', 1)
insert into product(id, name, cat_Id) values (3, 'Bananas', 1)
insert into product(id, name, cat_Id) values (4, 'Carrot', 2)
```
当我们希望提请您注意代码块的特定部分时,相关行或项目以粗体显示:
```java
public class Product implements Serializable {
```
任何命令行输入或输出的编写方式如下:
```java
mongoimport --db masterdb --collection product --drop --file D:datamongoscriptsproducts.json
```
**粗体**:表示一个新术语、一个重要单词或您在屏幕上看到的单词。例如,菜单或对话框中的单词出现在文本中,如下所示。下面是一个示例:“接下来,我们单击左侧的 Deployment credentials 链接。”
警告或重要提示如下所示。
提示和技巧如下所示。
# 联系
我们欢迎读者的反馈。
**一般反馈**:发送电子邮件`feedback@packtpub.com`并在邮件主题中提及书名。如果您对本书的任何方面有疑问,请发送电子邮件至`questions@packtpub.com`
**勘误表**:尽管我们已尽一切努力确保内容的准确性,但还是会出现错误。如果您在本书中发现错误,如果您能向我们报告,我们将不胜感激。请访问[www.packtpub.com/submit-errata](http://www.packtpub.com/submit-errata),选择您的书籍,点击 errata 提交表单链接,然后输入详细信息。
**盗版**:如果您在互联网上发现我们作品的任何形式的非法复制品,请您提供我们的位置地址或网站名称,我们将不胜感激。请通过`copyright@packtpub.com`与我们联系,并提供该材料的链接。
**如果您有兴趣成为一名作家**:如果您对某个主题有专业知识,并且您有兴趣撰写或贡献一本书,请访问[authors.packtpub.com](http://authors.packtpub.com/)
# 评论
请留下评论。一旦你阅读并使用了这本书,为什么不在你购买它的网站上留下评论呢?然后,潜在读者可以看到并使用您的无偏见意见做出购买决策。此外,我们在 Packt 可以理解您对我们产品的看法,我们的作者可以看到您对他们的书的反馈。非常感谢。
有关 Packt 的更多信息,请访问[packtpub.com](https://www.packtpub.com/)
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
# 测试云本地应用程序
在本章中,我们将深入研究测试云本机应用程序。使用各种测试工具、策略和模式,从手动测试到自动化测试,测试已经成熟了很多。这种方法的好处是可以以故障保护的方式频繁地进行测试,这对云开发非常重要。
本章将介绍以下主题:
* 测试概念,如**行为驱动开发(BDD)****测试驱动开发(TDD)**
* 测试模式,如 A/B 测试和双重测试
* 测试工具,如 JUnit、Cucumber、JaCoCo 和 Spring 测试
* 测试类型,如单元测试、集成测试、性能测试和压力测试
* 将 BDD 和集成测试的概念应用到我们在[第 2 章](02.html)中开发的产品服务中,*编写您的第一个云本机应用程序*,并在[第 4 章](04.html)中增强,*扩展您的云本机应用程序*
# 在开发之前编写测试用例
在本书中,我们在[第 2 章](02.html)*中开始开发一个简单的服务,在春季启动时编写您的第一个云本机应用程序*,让您对云开发感到兴奋。然而,真正的开发遵循不同风格的最佳实践。
# TDD
一个项目从理解需求和编写验证需求的测试用例开始。由于此时代码不存在,测试用例将失败。然后,编写通过测试用例的代码。这个过程不断迭代,直到完成测试用例和所需的代码,以实现业务功能。Kent Beck 有一本关于这个主题的好书,*通过示例*进行测试驱动开发。在下一节中,我们将使用本章的原则,重做[第 4 章](04.html)中的产品服务*扩展您的云本机应用程序*。但在此之前,让我们看看另一个重要的概念,BDD。
# BDD
借鉴敏捷开发原则和用户案例,BDD 鼓励我们将开发视为一组场景,在这些场景中,给定特定条件,系统以特定的、可预测的方式对设置的刺激进行行为。如果这些场景、条件和操作可以在业务和 IT 团队之间用一种易于理解的通用语言捕获,这将为开发带来很多清晰性,并减少犯错误的机会。这是一种编写易于测试的规范的方法。
在本章中,我们将介绍我们的产品服务,并使用 Cucumber 工具对其应用 BDD。
# 测试模式
为云测试大型互联网应用程序需要一种严格的方法,其中一些模式很方便。
# A/B 测试
A/B 测试也被称为**拆分测试**,其初衷是为了通过实验找出几个选定用户对具有相同功能的两个不同网页的响应。如果与其他模式相比,用户对某个模式的响应更为积极,则选择该模式。
这一概念可以扩展到分阶段引入新功能。向一组受控用户介绍功能、活动、布局或新服务,并测量响应:
![](img/8e04a14e-0ee4-454f-ab59-77b7b52500fa.jpg)
测试窗口结束后,对结果进行汇总,以规划更新功能的有效性。
这种测试的策略是,对于选定的用户集,使用 HTTP`302`(临时重定向)将用户从常规网站切换到新设计的网站。这将需要在测试期间运行网站或功能服务的变体。一旦测试成功,该功能将慢慢扩展到更多用户,并合并到主网站/代码库中。
# 双打
通常,被测试的功能依赖于由其他团队独立开发的组件和 API,这有以下缺点:
* 在开发功能时,它们可能不可用于测试
* 它们可能并不总是可用的,并且使用测试各种情况所需的数据进行设置
* 每次使用实际组件可能会较慢
因此,双重测试的概念变得流行起来。测试替身(就像电影中的特技替身)是一个组件/API,它替换实际组件并模仿其行为。test double 组件通常是一个轻量级且易于更改的组件,由构建功能的团队控制,与可能是依赖项或外部流程的真实组件不同。
There are many types of test doubles, such as Dummy, Fakes, Test Stubs, and Mocks.
# 测试存根
当下游组件返回改变被测系统行为的响应时,测试存根很有用;例如,如果我们的产品服务要调用其输出决定产品服务行为的引用数据服务。参考数据服务的测试存根可以模拟导致产品服务行为变化的各种响应类型:
![](img/adc5e088-75aa-4700-a19e-18a933b4725f.jpg)
# 模拟对象
下一种类型的 test-double 是一个模拟对象,它记录系统如何使用它,然后呈现记录以供验证。例如,一个模拟数据库组件可以检查一个产品是否没有被调用,而该产品应该从缓存层而不是数据库得到响应。
以下是模拟周围生态系统的基本示意图:
![](img/4475e788-f329-4f3f-b7ae-dad19dbd83a7.jpg)
# 模拟 API
在云开发中,您将构建一个依赖于其他服务或主要依赖于访问服务的 API 的服务。通常,其他服务无法立即用于测试。但是你不能阻止你的发展。在这里,模拟或添加虚拟服务是测试服务的有用模式。
服务模拟模拟模拟真实服务的所有契约和行为。[WireMock.org](http://wiremock.org/)[Mockable.io](https://www.mockable.io/)等示例帮助我们模拟 API,测试主要情况、边缘情况和故障条件。
# 确保代码审查和覆盖
自动代码审查工具增加了对代码的手动审查。这有助于识别代码中任何可能的错误,并确保覆盖范围完整,并且测试所有路径。
稍后我们将介绍代码覆盖工具 JaCoCo。
# 测试类型
我们在本章后面讨论的各种类型的测试在云计算普及之前就已经知道了。使用**持续集成****CI****持续开发****CD**的敏捷开发原则使得自动化这些类型的测试变得非常重要,以便在每次代码签入和构建时都执行这些测试。
# 单元测试
单元测试的目的是测试每个类或代码组件,并确保其按预期执行。JUnit 是用于单元测试的流行 Java 框架。
使用模拟对象模式和测试存根,可以隔离被测试服务的依赖组件,以便测试集中在被测试的系统上,即服务。
JUnit 是执行单元测试最流行的工具。
# 集成测试
组件测试的目的是检查组件(如产品服务)是否按预期运行。
`spring-boot-test`等组件帮助运行测试套件,并在整个组件上运行测试。我们将在本章中看到这一点。
# 负载测试
负载测试涉及将大量并发请求推送到被测系统一段时间,并观察其影响,如系统的响应时间和错误率。如果添加更多的服务实例使系统能够处理额外的负载,则系统称为水平可伸缩。
JMeter 和 Gatling 是涵盖此维度的常用工具。
# 回归测试
在引入新功能时,现有功能不应中断。回归测试涵盖了这一点。
Selenium 是一种基于 web 浏览器的开源工具,在该领域流行,用于执行回归测试。
# 测试产品服务
让我们将我们学到的测试原则应用到我们迄今为止一直在构建的产品服务中。我们从用户的角度出发,进行验收测试。
# 黄瓜中的 BDD
第一步是回顾我们产品和服务的规格。在[第 4 章](04.html)*扩展您的云本机应用程序*中,我们在产品服务上构建了一些功能,允许我们获取、添加、修改和删除产品,并获取给定产品类别的产品 ID 列表。
让我们将其表示为 Cucumber 中的特性。
# 为什么是黄瓜?
Cucumber 允许用一种简单的类似英语的语言表达行为,称为**小黄瓜**。这使得领域驱动的设计术语中的语言无处不在,因此业务、开发和测试之间的通信是无缝的,并且可以很好地理解。
# 黄瓜是如何工作的?
让我们了解 Cumber 的工作原理:
1. Cucumber 中的第一步是用场景和`Given`-`When`-`Then`条件将用户故事表达为功能:
* `Given`:设置行为的前提条件
* `When`:改变系统状态的触发器,例如,向服务发出请求
* `Then`:服务应该如何响应
2. 使用`cucumber-spring`转换层将这些转换为自动测试用例,以便执行。
让我们从一个简单的`getProduct`验收测试用例开始。我们将在 Gherkin 中编写一个简单的特性,如果产品 ID 存在,则获取产品;如果找不到产品 ID,则返回一个错误。
让我们以真正的 BDD 样式实现以下特性。产品服务上的`get`API 返回产品详细信息,例如给定产品 ID 的描述和类别 ID。如果找不到产品,它还可以返回错误,例如 404。让我们在小黄瓜特性文件中将这两种行为表示为两种不同的场景。
**特征**`getProduct`
获取给定产品 ID 的产品详细信息。
**场景 1**:产品 ID 有效且存在。将返回其所属的产品名称和类别:
1. `Given`产品服务正在运行
2. `When`使用现有产品 ID`1`调用 get product 服务
3. `Then`我们应该得到一个 HTTP 状态码为`200`的响应
4. `And`退货产品明细,名称`Apples`和类别`1`
**场景 2**:产品 ID 无效或不存在。应返回一个错误:
1. `Given`产品服务正在运行
2. `When`使用不存在的产品 ID`456`调用 get product 服务
3. `Then`返回 404 未找到状态
4. `And`返回错误消息`No product for ID 456`
场景 1 是一个成功的场景,返回数据库中存在的产品 ID 并根据其进行验证。
场景 2 检查数据库中不存在的 ID 的故障情况。
每个场景分为多个部分。对于快乐之路场景:
* `Given`设定了一个前提条件。在我们的例子中,很简单:产品服务应该运行。
* `When`更改系统的状态,在我们的例子中,它通过提供产品 ID 向服务发出请求。
* `Then``And`是系统预期的结果。在这种情况下,我们希望服务返回`200`成功代码以及给定产品的有效描述和类别代码。
正如您可能已经注意到的,这是我们服务的文档,业务和测试团队以及开发人员都可以理解。它是技术不可知论;也就是说,如果通过 SpringBoot、Ruby 或.NET 微服务实现,则不会改变。
在下一节中,我们将把服务映射到我们开发的 Spring 引导应用程序。
# 使用 JaCoCo 的代码覆盖率
JaCoCo 是由 EclEmma 团队开发的代码覆盖率库。JaCoCo 在 JVM 中嵌入了一个代理,它扫描遍历的代码路径并创建一个报告。
此报告可以导入到更广泛的 DevOps 代码质量工具(如 SonarQube)中。SonarQube 是一个平台,它通过大量插件帮助管理代码质量,并与 DevOps 进程很好地集成(我们将在后面的章节中看到)。它是开源的,但也有商业版。它是一个平台,因为它有多个组件,例如服务器(计算引擎服务器、Web 服务器和 Elasticsearch)、数据库和特定于语言的扫描仪。
# 弹簧启动试验
Spring 引导测试扩展并简化了 Spring 框架提供的 Spring 测试模块。让我们看一下编写验收测试的基本要素,然后我们可以在本章后面的部分重新讨论细节:
1. 复制我们在[第 4 章](04.html)中创建的项目*使用 HSQLDB 和 Hazelcast 扩展您的云本机应用程序*,作为本章的新项目。
2. 在 Maven POM 文件中包含对 Spring 的依赖:
```java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
```
您可能已经注意到,`scope`已更改为`test`。这意味着我们所定义的依赖关系不是正常运行时所必需的,只是用于编译和测试执行。
3. 向 Maven 添加另外两个依赖项。我们正在下载 Cucumber 及其 Java 翻译的库,以及`spring-boot-starter-test`
```java
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-spring</artifactId>
<version>1.2.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>info.cukes</groupId>
<artifactId>cucumber-junit</artifactId>
<version>1.2.5</version>
<scope>test</scope>
</dependency>
```
`CucumberTest`类是启动 Cumber 测试的主类:
```java
@RunWith(Cucumber.class)
@CucumberOptions(features = "src/test/resources")
public class CucumberTest {
}
```
`RunWith`告诉 JUnit 使用 Spring 的测试支持,然后使用 Cucumber。我们给出了`.feature`文件的路径,其中包含前面讨论的小黄瓜中的测试用例。
`Productservice.feature`文件是包含小黄瓜语言场景的文本文件,如前所述。我们将在这里介绍两个测试用例。此文件存在于`src/test/resources`文件夹中。
`CucumberTestSteps`类包含将小黄瓜中的步骤转换为等效的 Java 代码。每个步骤对应一个方法,并且根据小黄瓜文件中的场景构造调用这些方法。让我们讨论与一个用例相关的所有步骤:
```java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ContextConfiguration
public class CucumberTestSteps {
@Autowired
private TestRestTemplate restTemplate;
private ResponseEntity<Product> productResponse;
private ResponseEntity<String> errResponse;
@Given("(.*) Service is running")
public void checkServiceRunning(String serviceName) {
ResponseEntity<String> healthResponse = restTemplate.getForEntity("/health",String.class, new HashMap<>());
Assert.assertEquals(HttpStatus.OK, healthResponse.getStatusCode());
}
@When("get (.*) service is called with existing product id (\d+)$")
public void callService(String serviceName, int prodId) throws Throwable {
productResponse = this.restTemplate.getForEntity("/"+serviceName+"/" + prodId, Product.class, new HashMap<>());
}
@Then("I should get a response with HTTP status code (.*)")
public void shouldGetResponseWithHttpStatusCode(int statusCode) {
Assert.assertEquals(statusCode, productResponse.getStatusCodeValue());
}
@And("return Product details with name (.*) and category (\d+)$")
public void theResponseShouldContainTheMessage(String prodName, int categoryId) {
Product product = productResponse.getBody() ;
Assert.assertEquals(prodName, product.getName());
Assert.assertEquals(categoryId, product.getCatId());
}
```
`@SpringBootTest`注释告诉 Spring Boot 框架它是一个测试类。`RANDOM_PORT`表示测试服务在随机端口上启动 Tomcat 进行测试。
我们注入了一个自动连线的`restTemplate`,它将帮助访问 HTTP/REST 服务并接收将要测试的响应。
现在,请注意带有注释`@Given``@When``@Then`的方法。每个方法都使用正则表达式(从功能文件)提取变量,并将其用于方法中的断言。我们通过以下方式对此进行了系统测试:
1. 首先通过访问`/health`检查服务是否正在运行(正如我们在[第 2 章](02.html)中对 Spring Boot Actuator 所做的那样,*编写您的第一个云本机应用程序*
2. 使用产品 ID 调用服务。
3. 检查返回码是否为`200`,响应的描述和类别是否与预期结果相符。
4. 运行测试。
5. 右键单击`CucumberTest.java`文件并选择运行方式| JUnit 测试:
![](img/aeb8a942-20e4-4ae4-b5c2-f4ab4aca8499.png)
您将看到控制台发出启动消息。最后,JUnit 将反映测试结果,如下所示:
![](img/99043fcd-d743-4357-bab9-11aa1fdb232b.png)
作为练习,尝试将测试用例添加到`ProductService`类中的插入、更新和删除产品方法中。
# 集成 JaCoCo
让我们将 JaCoCo 整合到现有项目中:
1. 首先,在 POM 文件中包含包含 JaCoCo 的插件:
```java
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.7.9</version>
</plugin>
```
The second and third step is to include pre-executions and post-executions into the preceding plugin.
2. 预执行为要配置并添加到命令行的代理做好准备。
3. 执行后确保在输出文件夹中创建报告:
```java
<executions>
<execution>
<id>pre-unit-test</id>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</destFile>
<propertyName>surefireArgLine</propertyName>
</configuration>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/coverage-reports/jacoco-ut.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-ut</outputDirectory>
</configuration>
</execution>
</executions>
```
4. 最后,创建的命令行更改必须插入到`maven-surefire-plugin`中,如下所示:
```java
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<!-- Sets the VM argument line used when unit tests are run. -->
<argLine>${surefireArgLine}</argLine>
<excludes>
<exclude>**/IT*.java</exclude>
</excludes>
</configuration>
</plugin>
```
5. 现在,我们都准备好运行覆盖率报告了。右键单击项目并选择 Run As | Maven test 以测试程序,如以下屏幕截图所示:
![](img/74d4a555-4b66-497f-8f7e-6ff1f20354f2.png)
6. 当控制台中充满 Spring 引导启动时,您将看到以下行:
```java
2 Scenarios ([32m2 passed[0m)
8 Steps ([32m8 passed[0m)
0m0.723s
Tests run: 10, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 26.552 sec - in com.mycompany.product.CucumberTest......Results :Tests run: 10, Failures: 0, Errors: 0, Skipped: 0[INFO] [INFO] --- jacoco-maven-plugin:0.7.9:report (post-unit-test) @ product ---[INFO] Loading execution data file D:AppswkNeonch5-producttargetcoverage-reportsjacoco-ut.exec[INFO] Analyzed bundle 'product' with 6 classes
```
7. 这告诉我们使用`8 Steps`执行了两个场景(与之前一样)。但除此之外,`coverage-reports`被生成并放置在`target`目录中:
![](img/2927cb7b-c5d5-4c0a-bca2-d78fe6dd75bf.png)
8.`site`文件夹中,点击`index.html`;您将看到以下报道:
![](img/c10de181-60d7-476e-aa69-57ce441e6297.png)
9. 通过对`product`包的调查,您可以看到`ProductService`只覆盖了`24%`,如下图所示:
![](img/a3cb7b70-a1fb-4bc9-88cd-612d0f4af2bf.png)
10. 原因是我们在服务中只涉及了`getProduct`API。`insertProduct``updateProduct`未涵盖。以下屏幕截图中的向下钻取报告显示了这一点:
![](img/5d010c06-257b-4370-b8ea-fa9de6bc6d7c.png)
11.`getProduct`方法中,覆盖是完整的。这是因为,在两个场景中,我们讨论了快乐路径和错误条件:
![](img/bb6b3bad-be68-4019-b44f-c45e330552ea.png)
12. 另一方面,您会发现我们没有涵盖`ExceptionHandler`类中的分支,如下所示:
![](img/508a5194-6c30-4d13-8780-6d7da4326ab2.png)
# 总结
在接下来的章节中,我们将把覆盖率报告与 DevOps 管道集成在一起,并看到它在 CI 和 CD 期间工作。但是,首先,让我们看看部署机制。
\ No newline at end of file
# 云本地应用程序部署
云本机应用程序最独特的一点是它们的部署。在传统的应用程序部署中,团队通过登录到服务器并安装应用程序来部署应用程序。但在云中,通常有许多服务器,登录到每个服务器并手动安装应用程序是不可行的,并且可能非常容易出错。为了解决这些问题,我们使用云资源调配工具来自动部署云本地应用程序。
在本章中,我们将深入探讨微服务的部署模型,包括如何将您的应用程序打包为 Docker 容器,如何设置 CI/CD 管道,以及如何保护您的服务免受安全攻击,例如**分布式拒绝服务****DDoS**)。我们将介绍以下内容:
* 部署模型、打包和集装箱化(使用 Docker)
* 部署模式(蓝绿色、金丝雀色和深色)
* DDoS
* CI/CD
# 部署模型
我们将介绍用于在云环境中部署应用程序的部署模型。
# 虚拟化
云的基本构建块是虚拟机(从现在起称为 VM),它相当于用户可以登录、安装或维护应用程序的物理服务器(或主机)。不同之处在于,可以在单个主机上托管多个 VM,从而提高资源利用率。这是通过使用虚拟化实现的,虚拟机监控程序安装在主机上,然后可以将物理服务器上的可用资源(如计算、内存、存储和网络)分配给托管在其上的不同虚拟机。可以使用以下策略在此类虚拟机上部署云本机应用程序:
* 每个虚拟机有几个应用程序
* 每个虚拟机一个应用程序
当每个 VM 运行多个应用程序时,一个应用程序可能占用 VM 上的所有可用资源,并耗尽其他应用程序。另一方面,每个 VM 运行一个应用程序可以确保应用程序是隔离的,这样它们就不会相互影响,但这种部署的缺点是浪费资源,因为每个应用程序可能并不总是消耗所有可用的资源。
# 帕斯
PaaS 或平台即服务是部署云本机应用程序的另一个流行选项。PaaS 提供了补充云本地应用程序的开发、扩展和维护的附加服务。通过构建包实现的自动化构建和部署等服务极大地减少了设置支持这些活动的附加基础架构所花费的时间。PaaS 还提供一些基本的基础设施服务,如监控、日志聚合、机密管理和开箱即用的负载平衡。CloudFoundry、GoogleAppEngine、Heroku 和 OpenShift 都是 PaaS 的一些例子。
# 容器
为了提供独立运行所需的隔离级别,同时节约资源利用,容器技术得到了发展。通过利用 Linux 内核的特性,容器在进程级别提供 CPU、内存、存储和网络隔离。下图显示了虚拟化之间的区别:
![](img/8c53dec6-cf46-4c95-be5b-36c11e62e444.jpg)
容器消除了对来宾操作系统的需求,从而大大增加了可以运行的容器数量,而不是同一主机上的虚拟机数量。容器的占用空间也较小,以 MB 为单位,而虚拟机很容易超过几 GB。
容器在所需的 CPU 和内存量方面也非常节省资源,因为它们不必支持运行成熟操作系统时必须支持的许多外围系统:
![](img/48a52590-2ddf-490d-b0fd-2d9b256d9448.jpg)
上图显示了云本机应用程序部署策略的演变,旨在提高应用程序的资源利用率和隔离度。堆栈顶部是主机上运行的虚拟机中运行的容器。这允许应用程序按两个角度扩展:
* 增加 VM 中容器的数量
* 增加运行容器的虚拟机的数量
# 码头工人
Docker 是一个已经非常流行的容器运行时,并且已经证明自己是部署云本地应用程序的健壮平台。Docker 可用于所有主要平台,如 Windows、Mac 和 Linux。因为容器需要 Linux 内核,所以在 Linux 环境中运行 Docker 引擎更容易。但是,有几种资源可用于在 Windows 和 Mac 环境中舒适地运行 Docker 容器。我们将演示如何部署到 Docker 容器为止我们一直在开发的服务,包括连接到在其自身容器中运行的外部数据库。
在我们的示例中,我们将使用 Docker 工具箱和 Docker Machine 创建一个 VM,Docker 引擎将在其中运行。我们将使用 Docker 命令行客户端连接到此引擎,并使用提供的各种命令。
# 构建 Docker 图像
我们将开始将当前项目作为一组 Docker 容器进行容器化。我们将完成每个项目的步骤。
# 尤里卡服务器
1.`$WORKSPACE/eureka-server/.dockerignore`中增加一个包含以下内容的`.dockerignore`文件:
```java
.*
target/*
!target/eureka-server-*.jar
```
2.`$WORKSPACE/eureka-server/Dockerfile`中添加具有以下内容的 Dockerfile:
```java
FROM openjdk:8-jdk-alpine
RUN mkdir -p /app
ADD target/eureka-server-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8761
ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar" ]
```
3. 构建 runnable JAR,该 JAR 将在目标文件夹中可用:
```java
mvn package
```
4. 构建 Docker 容器:
```java
docker build -t cloudnativejava/eureka-server .
```
上述命令的输出显示在以下屏幕截图中:
![](img/42200017-a61d-439d-b4dc-428fa2f4682a.png)
5. 在运行容器之前,我们需要创建一个网络,在这个网络上不同的容器可以自由地相互通信。可以通过运行以下命令来创建此命令:
```java
docker network create app_nw
```
上述命令的输出显示在以下屏幕截图中:
![](img/9e39ed0d-67a3-4a5e-9b93-9b5d9bbe7c21.png)
6. 运行名为`eureka`的容器并将其连接到先前创建的网络:
```java
docker run -d --network app_nw --name eureka cloudnativejava/eureka-server
```
上述命令的输出显示在以下屏幕截图中:
![](img/82772713-3df6-42a4-974e-5bd487f590b5.png)
# 产品 API
接下来,我们将研究产品 API 项目:
1. 通过将以下内容附加到现有文件中,在`application.yml`中添加一个新的弹簧外形`docker`
```java
---
spring:
profiles: docker
eureka:
instance:
preferIpAddress: true
client:
serviceUrl:
defaultZone: http://eureka:8761/eureka/
```
2. 构建 Spring 引导 JAR 以反映对`application.yml`的更改:
```java
mvn clean package
```
3. 增加一个`.dockerignore`文件,内容如下:
```java
.*
target/*
!target/product-*.jar
```
4. 添加包含以下内容的 Dockerfile:
```java
FROM openjdk:8-jdk-alpine
RUN mkdir -p /app
ADD target/product-0.0.1-SNAPSHOT.jar /app/app.jar
EXPOSE 8080
ENTRYPOINT [ "/usr/bin/java", "-jar", "/app/app.jar", "--spring.profiles.active=docker" ]
```
5. 构建 Docker 容器:
```java
docker build -t cloudnativejava/product-api .
```
上述命令的输出显示在以下屏幕截图中:
![](img/1936e083-c0fd-458d-bf95-2182c3abbe95.png)
6. 启动几个 Docker 容器:
```java
docker run -d -p 8011:8080 \
--network app_nw \
cloudnativejava/product-api
docker run -d -p 8012:8080 \
--network app_nw \
cloudnativejava/product-api
```
以下屏幕截图显示了上述命令的输出:
![](img/a3cc2622-9d4b-405e-98cb-d34ef758054c.png)
产品 API 将在以下 URL 上提供:
* `http://<docker-host>:8011/product/1`
* `http://<docker-host>:8012/product/1`
# 连接到外部 Postgres 容器
要将`product`API 连接到外部数据库而不是内存数据库,请首先创建一个容器映像,其中包含已填充的数据:
1. 创建一个文件`import-postgres.sql`,包含以下内容:
```java
create table product(id serial primary key, name varchar(20), cat_id int not null);
begin;
insert into product(name, cat_id) values ('Apples', 1);
insert into product(name, cat_id) values ('Oranges', 1);
insert into product(name, cat_id) values ('Bananas', 1);
insert into product(name, cat_id) values ('Carrots', 2);
insert into product(name, cat_id) values ('Beans', 2);
insert into product(name, cat_id) values ('Peas', 2);
commit;
```
2. 创建一个包含以下内容的`Dockerfile.postgres`
```java
FROM postgres:alpine
ENV POSTGRES_USER=dbuser
POSTGRES_PASSWORD=dbpass
POSTGRES_DB=product
EXPOSE 5432
RUN mkdir -p /docker-entrypoint-initdb.d
ADD import-postgres.sql /docker-entrypoint-initdb.d/import.sql
```
3. 现在构建 Postgres 容器映像,该映像将使用`import-postgres.sql`的内容初始化数据库:
```java
docker build -t cloudnativejava/datastore -f Dockerfile.postgres .
```
上述命令的输出显示在以下屏幕截图中:
![](img/0f5dd173-6cab-407f-b09f-fe8ea3dcd8c3.png)
4. 通过将以下内容附加到现有文件,将新的弹簧外形`postgres`添加到`application.yml`
```java
---
spring:
profiles: postgres
datasource:
url: jdbc:postgresql://<docker-host>:5432/product
username: dbuser
password: dbpass
driver-class-name: org.postgresql.Driver
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: none
```
确保用适合您环境的值替换`<docker-host>`
5. 构建 Spring 引导 JAR 以反映对`application.yml`的更改:
```java
mvn clean package
```
6. 构建 Docker 容器:
```java
docker build -t cloudnativejava/product-api .
```
上述命令的输出显示在以下屏幕截图中:
![](img/1089a595-eaec-4cd2-a7e2-f88448bee18d.png)
7. 如果您已经有容器从旧映像运行,则可以停止并删除它们:
```java
old_ids=$(docker ps -f ancestor=cloudnativejava/product-api -q)
docker stop $old_ids
docker rm $old_ids
```
8. 启动数据库容器:
```java
docker run -d -p 5432:5432
--network app_nw
--name datastore
cloudnativejava/datastore
```
上述命令的输出显示在以下屏幕截图中:
![](img/c281d1ed-3aae-4f73-ae03-1ffdc9694a6e.png)
9. 为产品 API 启动几个 Docker 容器:
```java
docker run -d -p 8011:8080
--network app_nw
cloudnativejava/product-api
--spring.profiles.active=postgres
docker run -d -p 8012:8080
--network app_nw
cloudnativejava/product-api
--spring.profiles.active=postgres
```
上述命令的输出显示在以下屏幕截图中:
![](img/c1ba1fe6-d65b-48e3-a52d-493e594b36bf.png)
产品 API 将在以下 URL 上提供:
* `http://<docker-host>:8011/product/1`
* `http://<docker-host>:8012/product/1`
# 部署模式
在介绍了云本机应用程序的打包和部署模型之后,我们现在将介绍用于部署云本机应用程序的模式。传统上,应用程序部署在多个环境中,如开发、测试、登台、预生产等,而这些环境中的每一个都可能是最终生产环境的缩小版本。应用程序经过一系列预生产环境,最终部署到生产环境。然而,一个显著的区别是,尽管在所有其他环境中都可以容忍停机,但生产部署中的停机可能会导致严重的业务后果。
使用云本机应用程序,可以零停机时间发布软件。这是通过在应用程序的开发、测试和部署的各个方面严格应用自动化来实现的。我们将在后面的章节中介绍**持续集成****CI**)/**持续部署****CD**),但这里我们将介绍一些能够快速部署应用程序的模式。所有这些模式都依赖于路由器组件的存在,与负载平衡器不同,路由器组件可以将请求路由到特定的应用程序实例集。在某些情况下,应用程序本身是使用隐藏在功能标志后面的功能构建的,这些功能标志可以通过更改应用程序配置来启用。
# 蓝绿部署
蓝绿色部署是一种分三个阶段进行的模式。部署的初始状态如下图所示。应用程序的所有流量都路由到现有实例,这些实例被视为蓝色实例。蓝绿部署的表示如下:
![](img/f7c9ea7f-4822-4601-b318-9d079e137d2f.jpg)
在 blue-green 部署的第一阶段,将提供一组具有新版本应用程序的新实例并使其可用。在此阶段,新的绿色应用程序实例对最终用户不可用,部署将在内部进行验证。如下所示:
![](img/3337e8ca-4e65-4196-b35b-125c16a298c0.jpg)
在部署的下一个阶段,路由器上会抛出一个象征性的交换机,它现在开始将所有请求路由到绿色实例,而不是旧的蓝色实例。旧的蓝色实例会保留一段时间进行观察,如果检测到任何关键问题,我们可以根据需要快速回滚部署到应用程序的旧实例:
![](img/180fc891-da2a-4ec2-8bb1-ca2ac06e6c4f.jpg)
在部署的最后阶段,应用程序的较旧的蓝色实例将被停用,绿色实例将成为下一个稳定的生产版本:
![](img/21619477-0763-44a9-83d3-f945bb33dd27.jpg)
在应用程序的两个稳定版本之间切换时,蓝绿色部署是有效的,而快速恢复是由回退环境的可用性保证的。
# 金丝雀部署
金丝雀部署也是蓝绿色部署的一种变体。Canary 部署解决了在同时运行两个生产实例(尽管持续时间很短)时调配的浪费资源。在金丝雀部署中,绿色环境是蓝色环境的缩小版本,依赖路由器的能力将一小部分请求一致地路由到新的绿色环境,而大部分请求路由到蓝色环境。下图描述了这一点:
![](img/76b22168-fae1-4ec4-a902-9b3f0cc10699.jpg)
这在发布应用程序的新功能时特别有用,这些功能需要与几个 beta 测试用户一起测试,然后根据该用户组向所有用户发布的反馈进行测试。一旦确定绿色环境已准备好完全展开,绿色环境中的实例将逐渐增加,同时蓝色环境中的实例将逐渐减少。下面的一系列图表最好地说明了这一点:
![](img/b668e3dc-825b-46c1-b9ed-85e2d32535f6.jpg)
这样就避免了必须运行两个生产级环境的问题,并且可以从一个版本平稳地过渡到另一个版本,同时还可以轻松地回退到旧版本。
# 暗释放
另一种用于部署云本机应用程序的流行部署模式是黑暗发布模式。在这里,新功能隐藏在功能标志后面,并为选定的用户组启用,或者在某些情况下,当应用程序模仿用户的行为并练习应用程序的隐藏功能时,用户完全不知道该功能。一旦认为该功能已准备就绪并稳定,可向所有用户推出,则可通过切换功能标志来启用该功能。
# 应用 CI/CD 实现自动化
云本机应用程序部署的一个核心方面依赖于有效地自动化和构建软件交付管道的能力。这主要是通过使用 CI/CD 工具来实现的,这些工具可以从源存储库获取源代码,运行测试,构建可部署的工件,并将它们部署到目标环境。大多数现代 CI/CD 工具(如 Jenkins)都支持配置构建管道,这些管道可用于基于脚本形式的配置文件构建多个构件。
我们将以 Jenkins 管道脚本为例,演示如何配置简单的构建管道。在我们的示例中,我们将简单地构建两个工件,即`eureka-server``product-api`可运行 JAR。新增一个名为`Jenkinsfile`的文件,内容如下:
```java
node {
def mvnHome
stage('Preparation') { // for display purposes
// Get some code from a GitHub repository
git 'https://github.com/...'
// Get the Maven tool.
// ** NOTE: This 'M3' Maven tool must be configured
// ** in the global configuration.
mvnHome = tool 'M3'
}
stage('Eureka Server') {
dir('eureka-server') {
stage('Build - Eureka Server') {
// Run the maven build
if (isUnix()) {
sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
} else {
bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/)
}
}
stage('Results - Eureka Server') {
archiveArtifacts 'target/*.jar'
}
}
}
stage('Product API') {
dir('product') {
stage('Build - Product API') {
// Run the maven build
if (isUnix()) {
sh "'${mvnHome}/bin/mvn' -Dmaven.test.failure.ignore clean package"
} else {
bat(/"${mvnHome}binmvn" -Dmaven.test.failure.ignore clean package/)
}
}
stage('Results - Product API') {
junit '**/target/surefire-reports/TEST-*.xml'
archiveArtifacts 'target/*.jar'
}
}
}
}
```
管道脚本执行以下操作:
1. 从 GitHub 签出源代码
2. 配置 Maven 工具
3. 通过在签出的源存储库的两个目录中运行 Maven 构建来构建两个工件
4. 存储测试结果和生成的 JAR
在 Jenkins 中创建新的管道作业:
![](img/56901c5c-41af-48b3-b314-6315c6e06ff0.png)
在管道配置中,指定 GitHub 存储库和该 Git 存储库中的`Jenkinsfile`路径:
![](img/f9c530a7-f018-4e4e-ac6a-8a4f1e0d0f25.png)
运行构建时,应生成两个构件:
![](img/ecbf5ebc-5677-4959-9b5f-6d59d4cfedc0.png)
管道脚本可以扩展以构建 Docker 容器,我们在本章前面使用 Jenkins 的 Docker 插件手工构建了 Docker 容器。
# 总结
在本章中,我们了解了可用于部署云本机应用程序的各种部署模式,以及如何使用 Jenkins 等持续集成工具来自动化构建和部署。我们还学习了如何使用 Docker 容器构建和运行示例云本机应用程序。
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。