深度长文<1>:使用Spring Boot创建微服务

过去几年以来,“微服务架构”的概念已经在软件开发领域获得了一个稳定的基础。作为“面向服务架构”(SOA)的一个继任者,微服务同样也可以被归类为“分布式系统”这一类,并且进一步发扬了SOA中的许多概念与实践。不过,它们在不同之处在于每个单一服务所应承担的责任范围。在SOA中,每个服务将负责处理广范围的功能与数据领域,而微服务的一种通用指南则认为,它所负责的部分是管理一个单独的数据领域,以及围绕着该领域的相关功能。使用分布式系统方式的目的是将整体性的服务基础设施解耦为个别的可扩展子系统,可以通过垂直分片的方式将这些子系统组织在一起,并通过一种通用的传输方式将它们进行相关连接。

在整体性的基础设施中,构成系统的服务在逻辑上是在相同的代码基础与部署单元中组织的。这就能够通过相同的运行时对多个服务之间的相互依赖进行管理,同时也意味着在系统的多个组件中能够共享通用的模型与资源。在整体性基础设施中的子系统之间的相互连接性意味着,通过抽象与功能性函数,可以实现对业务逻辑与数据领域的极大的重用性。虽然这种重用通常是通过紧耦合的方式实现的,但它也存在着一个潜在的好处,就是易于确定某个单一的变更将会对整个系统带来怎样的影响。但为了实现这种便利性,要付出的代价就是牺牲了整个基础设施中单个组织的可伸缩性,同时也意味着整个系统的能力受限于其可伸缩性最薄弱的环节。

在分布式系统中,整体性系统的组件被解耦为个别的部署单元,这些部署单元能够独立地根据可伸缩性的需求自行升级,而不必理会其它子系统的情况。这也意味着整个系统的资源能够被更加有效地利用,并且由于组件之间的相互依赖性不再由运行时环境进行管理,因此它们之间可以通过相对灵活的契约进行相互交互。在传统的SOA架构中,服务的边界之内可以封装有关某个业务逻辑的大量功能,并且可以潜在地将大量数据领域集中在一起。而微服务架构不仅继承了系统分布式的概念,同时也承诺只对一个单一的业务功能和数据领域进行管理,这意味着从逻辑上控制某个子系统将变得非常容易。同时也意味着管理子系统的文档化与测试的范围也将变得更简单,因此在这两方面的涵盖程度理应有所提高。

与SOA架构一样,微服务架构也必须通过某种通用的传输方式进行相互连接,而这些年以来,HTTP已经被证明是完成这一任务的一样强大的手段。除此之外还存在着多种选择,例如二进制传输协议以及消息代理,微服务架构中并没有明显地倾向于其中任何一种方式,主要的选择依据是看那些能够在服务之间建立互通信的类库的成熟度与可用性。作为一种成熟的传输协议,几乎每种编程语言与框架都提供了HTTP的客户端类库,因此它作为服务间互通信的协议是一个优秀的选择。微服务架构对于与服务交互的无状态性这一方面有着特别的要求,无论采用了哪种底层协议,微服务都应该保持通信的无状态性,并且遵循RESTful范式以求实现这一点,这在业界基本已经达成了很好的共识。这就意味着对于某个微服务的每个请求与响应必须保证所调用的方法中的状态必须始终保持可用。说得更明白一点,就是指该服务不能够根据之前的交互行为对于每个请求中所需的数据进行任何假设。保证了正确的REST实现,也就意味着微服务本质上就是为了大规模而设计的,这也确保了对于任何一个服务的后续部署能够将停机时间减至最低、甚至做到无停机时间。

要充分了解如何切分一个整体性的架构,并创建微服务可能会存在一些困难,尤其在遗留的代码中,服务边界之间的数据领域通常是紧耦合的。根据经验来看,可以根据某个特定业务功能的边界对基础设施进行垂直切分。多个微服务能够在某个垂直分片的上下文中以协作方式一起运行。举例来说,设想某个电子商务网站的功能,从登陆页面开始,到客户与某个产品进行交互的页面,再到客户购买某个产品的页面,这一连串的业务功能之间存在着清晰的界线。可以将这一套流程分解为多个垂直分片,包括查看产品详细信息、将某个产品加入“购物车”、以及对一个或多个产品下订单。在客户查看产品信息的这个业务上下文中,可能会存在多个微服务,用于处理获取某个特定产品并将其详细信息展现给用户的流程。再举一个例子,在网站的登陆页面中,可能会显示大量产品的名称、图片以及价格。该页面可以从两个后台微服务中获取这些细节信息:一个微服务用于提供产品信息,另一个用于获取每个产品的价格。当用户选中某个特定的产品后,网站可以调用另外两个微服务,它们将用于为用户提供产品的评分与客户的评价。因此,为了提供用于“查看产品详细信息”业务功能在架构上的垂直分片,这个分片或许要通过四种后台微服务的结合才得以实现。

在“产品”这个垂直分片上的每个微服务都是对于“产品”这个领域中不同部分的实现,而每个微服务都具备根据系统的需求进行自我伸缩,并为系统所用的能力。可以想象,负责提供登陆页面用户体验的服务需要应对的请求数量,要远远大于那些提供某个产品详细信息的服务所应对的请求。它们甚至可能是基于不同的技术决策所设计的,例如缓存策略,而在展示产品评分与客户评论的服务中就不会用到这种技术。因为微服务能够根据功能选择适当的技术决策,因此能够更高效地利用资源。而在整体性的架构中,产品评分与客户评价服务则不得不屈从于产品信息与价格服务对于可伸缩性与可用性的需求。

不过,微服务的复杂度与代码的大小没有任何联系。有一种常见的误解认为,微服务的代码量也应该遵循“微”这个概念,但这种说法并不成立,只要你考虑一下微服务构架所试图实现的目标就知道。这个目标是将服务分解为一种分布式系统,而每个服务的复杂度所需的代码量完全于它本身。“微”这个术语表示了这种将职责分散在不同的子系统中的模式,而不是指代码量。不过,由于一个微服务的职责只限制在系统的某个垂直分片中的某个单一功能,因此它的代码通常比较简洁、易于理解、并且能够通过较小的部署单元进行发布。对于微服务有一种推荐的模式,就是将这些服务与运行它们所需的资源一起发布。这也意味着微服务的可部署单元通常包含了它们自己的运行时,并且能够单独运行,这大大减少了与部署相关的运维工作。

过去,部署Java web应用程序的方式往往包括一些笨重的、经过预先配置的应用服务器,这些服务器将把档案文件进行解压缩,部署在一个规定的、并且通常是有状态的运行时环境中。为了解压缩某个档案文件,并且开始运行新的应用程序代码,这些应用服务器有可能会产生几十分钟的停机时间,这就造成对更新的迭代变得十分困难,并且从运维的角度来看,也很难接受对某个系统进行多个部署的流程。随着各种各样的框架开始不断进化,以支持微服务的开发,对于代码进行打包以实现部署的流程也在不断改变。在如今的Java世界中,基于微服务的web应用程序能够很容易地将它们自身所需的运行时环境打包到一个可以运行的档案文件中。现代的嵌入时运行时,例如Tomcat和Jetty,是它们前身的应用服务器所对应的轻量级版本,它们通常都能够做到在几秒钟之内迅速启动。所有安装了Java的系统都能够直接运行部署的程序,这也简化了部署新变更的流程。

Spring Boot

Spring Boot这个框架在经历了不断的演变之后,如今已经能够用于开发Java微服务了。Boot是基于Spring框架进行开发的,也继承了Spring的成熟性。它通过一些内置的固件封装了底层框架的复杂性,以帮助使用者进行微服务的开发。Spring Boot的一大优点是提高开发者的生产力,因为它已经提供了许多通用的功能,例如RESTful HTTP以及嵌入式的web应用程序运行时,因此很容易进行装配及使用。在许多方面上,它也是一种“微框架”,允许开发者选择在整个框架中他们所需的那部分,而无需使用庞大的、或是不必要的运行时依赖。这也让Boot应用程序能够被打包为多个小单元以进行部署,并且该框架还能够使用构建系统生成可部署文件,例如可运行的Java档案包。

Spring Boot团队提供了一种便利的机制,让开发者能够简单地上手创建应用程序,也就是所谓的Spring Initializr。这个页面的作用是引导基于Boot的web应用程序的构件配置,并且允许开发者在多个分类中选择在项目中需要使用的类库。开发者只需要输入项目的一些元数据、选择所需的依赖项、并且单击“生成项目”按钮,就能够生成一个基于Maven或Gradle的Spring Boot项目的压缩文件了。文件里提供了用于开始设计项目的脚手架代码,对于首次使用这个框架的开发者来说是个绝佳的起点。

作为一个框架,Boot中内建了一些聚合模块,通常称为“启动者”。这些启动模块中是一些类库的已知的、良好的、具备互操作性的版本的组合,这些类库能够为应用程序提供某些方面的功能。Boot能够通过应用程序的配置对这些类库的进行设置,这也为整个开发周期中带来了配置胜于约定的便利性。这些启动模块中有许多是专门用于进行微服务架构开发的,它们为应用程序的开发者带来了一些免费的关键功能。在Spring Boot中实现一个基于HTTP的RESTful微服务,只需简单地加入actuator与web启动模块就足够了。web模块将提供嵌入式的运行时,而且能够让使用者基于RESTful HTTP控制器进行微服务API的开发,而actuator模块则为对外暴露的试题、配置参数和内部组件的映射提供了基本功能与RESTful HTTP终结点,因而使微服务能够正常运转,同时也为调试提供了极大的便利。

作为一个微服务框架,Boot的很大一部分价值在于它能够无缝地为基于Maven和Gradle的项目提供各种构建工具。通过使用Spring Boot插件,就能够利用该框架的能力,将项目打包为一个轻量级的、可运行的部署包,而除此之外几乎不需要进行任何额外的配置。在列表1中的代码展示了一个Gradle的构建脚本,可作为运行某个Spring Boot微服务的起点。此外,也可在Spring Initializr网站上选择使用较繁琐的Maven POM的示例,同时需要将应用程序的启动类的地址告诉该插件。而在使用Gradle时则无需进行这方面的配置,因为插件本身就能够找到这个类的地址。

buildscript {
  repositories {
    jcenter()
  }
  dependencies { 
   classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'
  }
}
apply plugin: 'spring-boot'
repositories { jcenter()
}
dependencies {
  compile "org.springframework.boot:spring-boot-starter-actuator"
  compile "org.springframework.boot:spring-boot-starter-web"
}

列表 1 – Gradle的构建脚本

如果选择使用Spring Initializr上的项目,就需要让项目结构符合常规的需求,只需遵循Maven风格的项目结构就能够实现这一点。代码必须被保存在src/main/java文件夹下,这样才能够正确地编译。该项目随后还要提供一个应用程序的入口点。在Spring Initializr的脚手架代码中有一个名为DemoApplication.java的文件,它的作用正是该项目的main类。可以随意对这个类进行重命名,通常来说将其命名为“Main”就可以了。列表1.1的示例描述了开始开发一个微服务所需的最少代码。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class);
    }
}

列表 1.1 - Spring Boot应用

通过在Main类中使用“EnableAutoConfiguration”标注,该框架就能够进行行为的配置,以引导应用程序的启动与运行。这些行为很大程度上是通过约定用于配置的方式确定的,为此Boot将对classpath进行扫描,以确定微服务中需要具备哪些功能。在上面的示例中,该微服务选择了actuator与web这两个启动模块,因此该框架能够确定这个项目是一个微服务,引导某个嵌入的Tomcat容器的启动,并通过某个预先配置的终结点提供该服务。在该示例中的代码并没有进行太多工作,但只需简单地启动该示例,就能够使actuator模块所暴露的终结点开始运行。只需将该项目导入任何IDE,随后为“Main”类创建一个“作为Java应用程序运行”的配置,就能够启动这个微服务了。此外,也可以选择在命令行中运行gradle bootRun这个Gradle任务,或是针对Maven的mvn spring-boot:run命令,也能够启动该应用程序,具体的命令取决于你选择了哪种项目配置。

操作数据

接下来我们要实现之前所说的那个“产品的垂直分片”,考虑一下“产品详细信息”这个服务,它与“产品价格”这个服务一起提供了登录页面体验的详细信息。至于微服务的职责,它的数据领域应当是与某个“产品”相关的属性的子集,包括产品名称、简短描述、详细描述、以及一个库存id。可以使用Java bean对这些信息进行建模,正如列表1.2中的代码所描述的一样。

import javax.persistence.Entity;
import javax.persistence.Id;
@Entity
public class ProductDetail {
    @Id
    private String productId;
    private String productName;
    private String shortDescription;
    private String longDescription;
    private String inventoryId;
    public String getProductId() {
        return productId;
    }
    public void setProductId(String productId) {
        this.productId = productId;
    }
    public String getProductName() {
        return productName;
    }
    public void setProductName(String productName) {
        this.productName = productName;
    }
    public String getShortDescription() {
        return shortDescription;
    }
    public void setShortDescription(String shortDescription) {
        this.shortDescription = shortDescription;
    }
    public String getLongDescription() {
        return longDescription;
    }
    public void setLongDescription(String longDescription) {
        this.longDescription = longDescription;
    }
    public String getInventoryId() {
        return inventoryId;
    }
    public void setInventoryId(String inventoryId) {
        this.inventoryId = inventoryId;
    }
}

列表1.2 —— 产品详细信息的POJO对象

在ProductDetail这个Java bean中有一点要特别注意,这个类使用了JPA标注,以表示它是一个实体。Spring Boot中专门提供了一个可用于JPA实体与关系型数据库数据源的启动模块。考虑一下列表1中的构建脚本,我们可以在其中的“依赖”一节中加入这个Boot的启动模块,以用于持久化数据集,如列表1.3中的代码所示。

dependencies {
compile “org.springframework.boot:spring-boot-starter-actuator”
compile “org.springframework.boot:spring-boot-starter-web”
compile “org.springframework.boot:spring-boot-starter-data-jpa”
compile ‘com.h2database:h2:1.4.184’
}
列表 1.3 —— 在构建脚本中设置Spring Boot的依赖

出于演示与原型的目的,该项目中现在还包括了内嵌的h2数据库类型。Boot的自动配置机制能够检测到classpath中存在h2,随后为ProductDetail实体生成必要的表结构。在内部,Boot会调用Spring Data进行对象实体映射操作,有了它之后,我们就可以利用它的约定和机制与数据库打交道了。Spring Data中提供了一个便捷的抽象,也就是“repository”的概念,它本质上就是一种数据访问对象(DAO),该对象在启动时会由框架为我们自动装配。为了实现ProductDetail实体的CRUD功能,我们只需要创建一个接口,扩展在Spring Data中内置的CrudRepository即可,正如列表1.4中的代码所示。

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface ProductDetailRepository extends CrudRepository <ProductDetail, String>{
}

列表 1.4 —— 产品信息的数据访问对象(Spring Data Repository)

在接口定义中的@Repository标注将通知Spring,这个类的作用是一个DAO。这个标注也是一种特别的机制,我们可以通过这种机制通知框架,让框架自动将其进行装配,并分配到微服务的配置中,从而让我们可以使用依赖注入的方式访问它。为了在Spring中应用这一特性,我们还必须在列表1.1中定义的Main类上添加@ComponentScan这个额外的标注。当微服务启动之后,Spring将会对项目的classpath进行扫描以寻找各种组件,并且将这些组件作为应用程序中需要自动装配的备选组件。

为了展现微服务的新能力,请仔细阅读列表1.5中的代码,这里我们利用了一个先决条件,就是Boot会在main()方法中为我们提供一个指向Spring的ApplicationContext的引用。

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.ComponentScan;
@ComponentScan
@EnableAutoConfiguration
public class Main {
    public static void main(String[] args) {
        ApplicationContext ctx = SpringApplication.run(Main.class);
        ProductDetail detail = new ProductDetail();
        detail.setProductId("ABCD1234");
        detail.setProductName("Dan's Book of Writing");
        detail.setShortDescription("A book about writing books.");
        detail.setLongDescription("In this book about writing books, Dan will show you how to write a book.");
        detail.setInventoryId("009178461");
        ProductDetailRepository repository = ctx.getBean(ProductDetailRepository.class);
        repository.save(detail);
        for (ProductDetail productDetail : repository.findAll()) {
            System.out.println(productDetail.getProductId());
        }
    }
}

列表 1.5 —— 展现加载数据的功能

在这个简单的示例中,我们为一个ProductDetail对象加载了某些数据,我们通过调用ProductDetailRepository的方法将产品信息进行保存,随后再次调用这个repository对象,从数据库中取回产品的信息。到目前为止,对于在微服务使用持久化数据,没有进行任何额外的配置。我们可以使用列表1.5中的这个原型代码作为定义RESTful HTTP API契约的基础,通过Spring中提供的@RestController标注就可以实现。

未完待续……

本文转载自infoq,原文链接:http://www.infoq.com/cn/articles/boot-microservices/