使用Sprint Boot创建微服务

jopen 9年前

来自:http://www.infoq.com/cn/articles/boot-microservices


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

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

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

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

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

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

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

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

Spring Boot

Sprint 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标注就可以实现。

设计API

对于“产品信息”这个微服务来说,提供简单的CRUD式功能或许就已经足够了,但也许它还需要提供一些扩展功能,例如分页的结果集和数据过滤。可以通过一个简单的控制器(controller)实现这个操作数据集的API,Spring会将该控制器映射到某个HTTP的路由上。下方的列表1.6中的代码示例可以作为一个起点,这个API暴露了create与findAll方法,通过它可以实现之前那个原型中所演示的代码功能。

import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.*;  @RestController  @RequestMapping("/products")  public class ProductDetailController {      private final ProductDetailRepository repository;      @Autowired      public ProductDetailController(ProductDetailRepository repository) {          this.repository = repository;      }      @RequestMapping(method = RequestMethod.GET)      public Iterable findAll() {          return repository.findAll();      }      @RequestMapping(method = RequestMethod.POST)      public ProductDetail create(@RequestBody ProductDetail detail) {          return repository.save(detail);      }  }

列表 1.6 —— Product Detail控制器类

Spring中提供的@RestController标注将通知该框架,让框架为我们实现数据序列化与数据绑定的大部分繁重工作。此外,对于那些将为这个微服务生成数据的服务来说,我们只需为create()方法的参数标注为@RequestBody,Spring就能够自动为我们生成该对象的内容。随后就可以使用系统自动装配的ProductDetailRepository对象保存相应的ProductDetail对象。Boot为Spring中内置提供的这些功能加入了一些额外的数据转换器,它们将通过Jackson类库,将ProductDetail对象序列化为JSON格式,以便微服务的API的调用者进行操作。在列表1.6中的控制器示例的基础上,如果该服务的/products终结点收到了一个JSON格式的请求,那么该服务就会创建一个新的产品信息项,正如列表1.7中所描述的那样。

{      "productId": "DEF0000",      "productName": "MakerBot",      "shortDescription": "A product that makes other products",      "longDescription": "This is an extended description for a makerbot, which is basically a product that makes other products.",      "inventoryId": "00854321"  }

列表 1.7 —— 用于表现某个产品的JSON结构

通过对/products这个地址进行一个HTTP GET请求,可以刷新产品的详细信息,并显示新创建的产品细节内容。

在微服务的create()中基本上只有一个用例,就是进行数据绑定并保存到repository中。但在某些情况下,该服务还需要执行一些较复杂的业务逻辑,以确保保存到产品信息中的数据的准确性。我们可以通过使用Spring中内置的校验框架,在进行数据绑定时确认产品信息中的数据符合微服务的业务逻辑。在列表1.8中的代码展现了对ProductDetail校验逻辑的一种实现,它将调用另一个微服务中的方法,以确定所提供的库存ID的有效性。

import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Component;  import org.springframework.validation.*;  @Component  public class ProductDetailValidator implements Validator {      private final InventoryService inventoryService;      @Autowired      public ProductDetailValidator(InventoryService inventoryService) {          this.inventoryService = inventoryService;      }      @Override      public boolean supports(Class<?>clazz) {          return ProductDetail.class.isAssignableFrom(clazz);      }      @Override      public void validate(Object target, Errors errors) {          ProductDetail detail = (ProductDetail)target;          if (!inventoryService.isValidInventory(detail.getInventoryId())) {              errors.rejectValue("inventoryId", "inventory.id.invalid", "Inventory ID is invalid");          }      }  }

列表1.8 ——ProductDetail的校验逻辑

这段示例代码中的InventoryService中的逻辑有些生硬,但不难看出这种进行数据校验的机制具有固有的灵活性,这也利益于该服务能够对其它微服务进行查询调用,以获得其它微服务对于整个数据领域中某些子数据的信息。

为了在数据绑定时能够使用ProductDetailValidator的功能,需要在Spring的数据绑定器中进行注册,而注册时机是特定于控制器的。在下方的列表1.9中对控制器的代码进行了改动,展现了如何在控制器中对校验器进行自动装配,并通过initBinder()方法将其注册进行数据绑定的过程。@InitBinder这个标注将通过Spring,我们将对这个类的默认数据绑定器进行自定义。此外,请注意thecreate()方法中的ProductDetail对象参数现在加上了一个@Valid标注,该标注的作用是通知数据绑定器,我们需要在数据绑定时对请求体进行校验。而Spring中内置的校验器也将提供JSR-303 与JSR-349这两种数据校验规范(Bean校验)的字段级标注。

import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.WebDataBinder;  import org.springframework.web.bind.annotation.*;  import javax.validation.Valid;  @RestController  @RequestMapping("/products")  public class ProductDetailController {      private final ProductDetailRepository repository;      private final ProductDetailValidator validator;      @Autowired      public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator) {          this.repository = repository;          this.validator = validator;      }      @InitBinder      protected void initBinder(WebDataBinder binder) {          binder.addValidators(validator);      }      @RequestMapping(method = RequestMethod.GET)      public Iterable findAll() {          return repository.findAll();      }      @RequestMapping(method = RequestMethod.POST)      public ProductDetail create(@RequestBody @Valid ProductDetail detail) {          return repository.save(detail);      }  }

列表1.9 —— 经过修改后的Product Detail控制器,现在加入了校验器

如果该API的调用者在POST提交的JSON结构中没有包含一个有效的库存ID,Spring将会产生一个校验失败的错误,并且为调用者返回一个“400 – Bad Request”的HTTP状态码。由于控制器的定义使用了RestController这个标注,因此Spring能够将校验失败的信息进行正确地格式化,让调用者能够理解其内容。作为这个微服务的开发者,实现这一功能无需进行任何额外的配置。

对于电子商务网站这个示例来说,一个仅包含简单的CRUD REST API的产品详细信息微服务没有什么太大的作用。这个服务还需要提供对产品信息结果列表进行分页以及排序的功能,并且提供某种程序上的搜索功能。为了实现第一个需求,需要对ProductDetailController中的findAll()这个控制器action方法进行修改,让它能够接受由API使用者所定义的数据范围所对应的查询参数,然后该方法就可以使用Spring Data中内置的PagingAndSortingRepositorytype类,在findAll()方法中对repository进行调用时提供分页及排序的参数。我们需要修改ProductDetailRepository,让它继承自这个新的类型,如列表1.10中的代码所示。

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

列表1.10 —— 修改后的ProductDetailRepository提供了对分页与排序的支持

列表1.11中的代码展现了经过修改后的findAll这个控制器方法,它能够利用repository中新的分页与排序功能。如果某个对/products这个终结点的API调用提供了?page=0&count=20这个查询字符串,该方法就能够返回数据库中的前20条结果。在这个示例中的代码还利用了Spring的功能,为查询参数赋予了默认值,因此这些参数中的大部分都成为可选参数了。

@RequestMapping(method = RequestMethod.GET)  public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,  @RequestParam(value = "count", defaultValue = "10", required = false) int count,  @RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,  @RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {   Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));      return result.getContent();  }

列表1.11 ——ProductDetailController中修改后的findAll方法,现在能够支持分页及排序功能

当该电子商务网站的用户进行登陆页面时,该网页会通过贪婪查询方式加载10条或20条结果,随后当滚动条到达页面上的某个位置,或是经过一段时间后,通过延迟加载的方式获取之后的50条结果。通过这个内置的分页功能,调用者就能够控制每次调用需要返回的数据量。列表1.12中描述了ProductDetailController的完整实现。

import com.fasterxml.jackson.databind.ObjectMapper;  import org.springframework.beans.MutablePropertyValues;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.data.domain.*;  import org.springframework.http.*;  import org.springframework.validation.DataBinder;  import org.springframework.web.bind.WebDataBinder;  import org.springframework.web.bind.annotation.*;  import javax.servlet.http.HttpServletRequest;  import javax.validation.Valid;  import java.io.IOException;  @RestController  @RequestMapping("/products")  public class ProductDetailController {      private final ProductDetailRepository repository;      private final ProductDetailValidator validator;      private final ObjectMapper objectMapper;      @Autowired      public ProductDetailController(ProductDetailRepository repository, ProductDetailValidator validator,                                     ObjectMapper objectMapper) {          this.repository = repository;          this.validator = validator;          this.objectMapper = objectMapper;      }      @InitBinder      protected void initBinder(WebDataBinder binder) {          binder.addValidators(validator);      }      @RequestMapping(method = RequestMethod.GET)      public Iterable findAll(@RequestParam(value = "page", defaultValue = "0", required = false) int page,     @RequestParam(value = "count", defaultValue = "10", required = false) int count,     @RequestParam(value = "order", defaultValue = "ASC", required = false) Sort.Direction direction,    @RequestParam(value = "sort", defaultValue = "productName", required = false) String sortProperty) {    Page result = repository.findAll(new PageRequest(page, count, new Sort(direction, sortProperty)));          return result.getContent();      }      @RequestMapping(value = "/{id}", method = RequestMethod.GET)      public ProductDetail find(@PathVariable String id) {          ProductDetail detail = repository.findOne(id);          if (detail == null) {              throw new ProductNotFoundException();          } else {              return detail;          }      }      @RequestMapping(method = RequestMethod.POST)      public ProductDetail create(@RequestBody @Valid ProductDetail detail) {          return repository.save(detail);      }      @RequestMapping(value = "/{id}", method = RequestMethod.PUT)      public HttpEntity update(@PathVariable String id, HttpServletRequest request) throws IOException {          ProductDetail existing = find(id);          ProductDetail updated = objectMapper.readerForUpdating(existing).readValue(request.getReader());          MutablePropertyValues propertyValues = new MutablePropertyValues();          propertyValues.add("productId", updated.getProductId());          propertyValues.add("productName", updated.getProductName());          propertyValues.add("shortDescription", updated.getShortDescription());          propertyValues.add("longDescription", updated.getLongDescription());          propertyValues.add("inventoryId", updated.getInventoryId());          DataBinder binder = new DataBinder(updated);          binder.addValidators(validator);          binder.bind(propertyValues);          binder.validate();          if (binder.getBindingResult().hasErrors()) {              return new ResponseEntity<>(binder.getBindingResult().getAllErrors(), HttpStatus.BAD_REQUEST);          } else {              return new ResponseEntity<>(updated, HttpStatus.ACCEPTED);          }      }      @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)      public HttpEntity delete(@PathVariable String id) {          ProductDetail detail = find(id);          repository.delete(detail);          return new ResponseEntity<>(HttpStatus.ACCEPTED);      }      @ResponseStatus(HttpStatus.NOT_FOUND)      static class ProductNotFoundException extends RuntimeException {      }  }

列表1.12 —— ProductDetailController的完整实现

毫无疑问,除了数据的分页与排序之外,这个电子商务网站还需要提供一些类似于搜索引擎一样的功能。由于在垂直分片中的每个微服务都对自己的数据领域子集进行维护,因此它也理应负责自身的搜索功能。这也让调用者能够异步地对整个数据领域中很大一部分的属性进行搜索。

Spring Data允许对repository的接口附加某个方法签名,在其中加入自定义的查询。这表示repository能够使用一种预先确定的JPA查询,它将对每个产品信息对象中的一个属性子集进行查询,这就让微服务能够具备一些原始的搜索功能。在列表1.13中所描述的代码中对ProductDetailRepository进行了修改,其中加入了一个search()方法,它能够接受查询语句,并尝试对productName或longDescription字段进行大小写无关的匹配,并将一个结果列表返回给调用 者。

import org.springframework.data.jpa.repository.Query;  import org.springframework.data.repository.PagingAndSortingRepository;  import org.springframework.stereotype.Repository;  import java.util.List;  @Repository  public interface ProductDetailRepository extends PagingAndSortingRepository<ProductDetail, String> {      @Query("select p from ProductDetail p where UPPER(p.productName) like UPPER(?1) or " +              "UPPER(p.longDescription) like UPPER(?1)")      List search(String term);  }

列表1.1.3 ——ProductDetailRepository中的自定义查询

为了公开这个搜索功能,我们将创建另一个RestController,并将它映射到/search这个终结点,如列表1.1.4中所示。

import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.*;  import java.util.ArrayList;  import java.util.List;  @RestController  @RequestMapping("/search")  public class ProductDetailSearchController {      private final ProductDetailRepository repository;      @Autowired      public ProductDetailSearchController(ProductDetailRepository repository) {          this.repository = repository;      }      @RequestMapping(method = RequestMethod.GET)      public List search(@RequestParam("q") String queryTerm) {          List productDetails = repository.search("%"+queryTerm+"%");          return productDetails == null ? new ArrayList<>() : productDetails;      }  }

列表1.14 —— 用于对ProductDetail进行搜索的控制器 Search controller for ProductDetails

将来还可以进一步增加这个ProductDetailSearchController的功能,可以让它在查询时实现与ProductDetailController相同的分布及排序功能。

配置

Spring Boot中丰富的应用程序配置能够让创建的微服务具有强大的能力,而且在某些场合下完全不需要修改这些配置。当准备将服务进行部署时,也许要根据部署环境或某些外部的影响的结果对某些配置指令进行调整,例如在哪个端口上运行内嵌的容器。Boot为微服务的开发者提供了多种方式以重写这些默认的配置,而且该框架也支持由多个不同的因素决定实际的配置。

在进行微服务的配置时,要仔细考虑某个重要的因素,即该服务的运行时环境。如果服务是部署在某个静态的基础设施中,那么对某些配置项进行预定义或许是可行的。为了更加清晰地说明这个问题,再来看看上面的那个示例,这个微服务的数据源只是一个简单的内嵌的h2数据库。而在生产环境中,该微服务将指向某个持久化的数据源,例如某个MySQL或Oracle数据库,因此应用程序的配置必须包含正确的JDBC URL、用户名、密码,并且使用适当的JDBC驱动类。在静态基础设施中,可以对这些配置进行预定义,并直接打包在应用程序中。而Boot自身就提供了从Java属性文件、XML配置文件或YAML配置文件中解析配置的功能,并相应地在classpath的根目录中寻找名为application.properties、application.xml或application.yml(或是application.yaml)的配置文件。列表1.15中的配置文件展现了如何使用配置指令覆盖默认的数据源配置。

spring.datasource.url=jdbc:mysql://prod-mysql/product  spring.datasource.username=root  spring.datasource.password=  spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表1.15 —— 在配置文件中指定数据源

Boot中的配置机制有一个很重要的能力,它能够在启动时使用Java系统属性对配置进行重写。在JVM启动中提供的配置将覆盖在classpath中指定的application.properties中的内容。这就说明运行时环境能够基于某些在对微服务进行打包时未知的信息对配置进行自定义。举例来说,如果该微服务运行在某个非静态环境,例如云端部署环境中,那么也许要根据VM或容器的地点来决定数据库的托管。应用程序可以通过系统环境变量访问这些信息。可以通过JVM的启动参数,或是直接在配置中方便地调用这些环境变量。在后一种方式里,可以使用Spring中的属性占位符获取某个配置指令的引用。在列表1.16中展现的配置文件对列表1.15进行了一些修改,它使用了属性占位符,并且包含一个默认值。

spring.datasource.url=${JDBC_URL:jdbc:mysql://prod-mysql/product}  spring.datasource.username=${JDBC_USER:root}  spring.datasource.password=${JDBC_PASS:}  spring.datasource.driver-class-name=com.mysql.jdbc.Driver

列表1.16 —— 修改后的配置文件使用了带有默认值的系统变量

Boot也将对文件系统进行查找,以寻找一个相对于启动路径的,名为“config”的目录,并在其中寻找与之前所述相同的一系列配置文件,如果一旦找到,那么在应用classpath中找到的任何配置之前,它会首先应用这些配置。而spring.config.location这个Java系统属性也能够告诉Spring配置文件的地址。比方说,如果微服务的配置文件地址是/etc/spring/boot.yml,那么通过 –D spring.config.location=/etc/spring/boot.yml指令,其中的配置就能够覆盖文件系统中的配置信息。通过同样的方式也可以使用classpath中定义的资源,只需在属性的值前面加上classpath:前缀即可。

利用同样的配置机制,通过server.port这个键,还可以自定义内嵌的容器的服务器端口。如果在某种PaaS云端环境,例如Heroku中运行微服务时,这一点尤为重要。这个键将通过某个环境变量映射端口的范围,并将其暴露给应用程序。在列表1.16中的配置指令也能够用于映射PORT环境变量。列表1.17就展现了这种配置的方式。

server.port=${PORT:8080}

列表1.17 —— 将启动端口映射为某个环境变量的配置 Configuration to map the startup port to an environment var

打包

当微服务的部署已经准备就绪后,就可以使用Boot中提供的构建系统的工具,以生成一个轻量级的、可运行的部署文件。正如本文之前所说的一样,Boot为Gradle和 Maven提供了插件,因此可以通过它们创建一个可运行的JAR文件用于发布。只需使用在前文中提到的Gradle构建脚本,就能够通过调用项目的构造任务gradle build,简单地生成JAR文件。Boot将对jar任务进行拦截,并将常规方式生成的文件进行重写打包,在其中加入所有的依赖项,以生成所谓的“fat”或“uber”JAR文件。而在Maven项目的配置中,Boot插件也能够拦截打包过程,并进行相同的重新打包操作。

Boot的Gradle插件还有一个额外的优点,就是它能够与应用程序的插件进行交互,它将生成一个可发布的tarball文件,其中已经重新打包了所有依赖,以及在多种Unix及Windows上的启动脚本。这种打包方式对于部署来说非常理想,因为微服务中所有的启动脚本都已经写入包里了。只需在目标服务器上将tarball解压缩,就能够在bin文件夹中以项目名称命名的脚本中直接启动微服务了。

虽然对于微服务来说,使用单一的可部署文件是受推荐的方式,也是一种接受度最高的可部署单元,但绝不是说必须强制使用单一的部署文件。Boot的功能更进一步,它能够将应用程序打包为WAR文件,并部署到某个应用程序容器中。为了使用war插件,需要对Gradle的构建脚本进行一些修改,如列表1.18所示。与之前的示例相似,该构建任务将生成web文件。

buildscript {    repositories {      jcenter()    }    dependencies {      classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.2.0.RELEASE'    }  }  apply plugin: 'spring-boot'  apply plugin: 'war'  repositories {    jcenter()  }  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 'mysql:mysql-connector-java:5.1.34'  }

列表1.18 —— Gradle的构建脚本,使用了Boot以及War插件

在Maven项目中,可以通过修改项目的pom.xml文件中的打包配置实现war打包。列表1.19中的片段展现了经过修改后的配置。

<?xml version="1.0" encoding="UTF-8"?>  <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"      xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>      <groupId>com.infoq</groupId>      <artifactId>sb-microservices</artifactId>      <version>0.1.0</version>      <packaging>war</packaging>      <!-- ...remaining omitted for brevity... -->  </project>

列表1.19 —— 可以用于War打包的Maven pom.xml的起始部分代码

网关API

在之前的章节中,我们已经深入地探讨了产品信息这个微服务的开发,这套模板同样也能够以相似的方式应用于这个电子商务网站的垂直分片中的其它每一种服务中。当系统的每个垂直分片中的各个组件被分解为一系列微服务的集合之后,该系统就可被视为一种完全分布式的微服务架构。但对于外部调用者,例如该电子商务网站的页面来说,这种方式也会产生一些复杂性,因为这些调用者从系统中获取的数据可能会横跨多个不同的微服务。如果不通过某种机制,将这些服务综合地重新组织为一种看起来具有整体性的API,那么每个客户端的调用者将不得不承担这种职责,它们将分别调用这些不同的数据集,并将它们重新组织为一种可重用的结构。这种方式对于调用者产生了很大的负责,因为他们必须建立大量的HTTP连接,以实现对某些数据集的聚合。这也意味着如果某个服务不可用或掉线,那么每个调用者将负责对数据缺失这一场景进行适当的处理。

用于微服务基础设施中的某种模式正在逐渐浮现,这种模式体现了一种网关API服务的概念,这种服务处于各个不同的后端服务的前方,并为调用者提供一种全面的、易于使用的API。继续以电子商务网站的例子进行讲解,当网站的某个访问者打算查看某个产品的详细信息时,为了生成产品信息视图的数据,需要四种服务参与其中。该网页不再对这些服务分别进行调用,而是访问该网关服务的某个聚合API的终结点,网关服务会代为调用底层服务,并将结果集进行合并,返回页面进行显示。从网页的角度来看,它只是发送了一个调用请求,就获得了显示页面所必须的完整数据。

这种方式还具有一个额外的好处,就是可以在调用者与后台服务之间更好地管理数据的传输。比方说,该网关服务在它的服务层可以实现某些逻辑,当对某个产品的信息的访问量很大的时候,它将不会在每个请求中都去调用相应的产品信息微服务,而是选择在某个预定义的时间段之内直接返回缓存中的数据。这种方式能够显著地提升性能,并减少网络负载。

同样重要的一点还在于对后台服务的可用性进行抽象。一旦发生某个后台服务不可用的情况下,网关服务能够明智地决定应该提供怎样的数据。现实这一点的方式有多种,而在网关服务中确保分布式系统的持久性这方面最引人注目的一种机制是由Netflix开发的名为Hystrix的类库。在Hystrix中有许多功能能够确保对故障的适应性,并且对于海量请求提供了性能方面的优化,但其中最吸引人的特性大概要数它对断路设计模式的实现了。具体来说,Hystrix能够观察到对某个后端服务的连接断开,在这种情况下它不会选择持续访问这个下线的服务,因为这会造成网络阻塞以及等待超时,而是打开这个服务的回路,将后续的请求委托给某个“后备”方法,让它接管这些调用。在底层,Hystrix会间隔式地检查该连接,查看该后台服务是否已经恢复了正常操作状态,如果服务已经恢复,那么它就会重新建立通信连接。

当回路打开的期间,网关服务能够任意选择返回给调用者的响应。可以使用某些“最后一次正常运行时”的正确数据集,也可以返回一个空响应,在头信息中告知调用者后端回路已经打开,或是以上两者的某种结合。Hystrix提供的适应性在任何一个具有一定复杂性的分布式系统中都是一种关键的组件。为了更直白地理解Hystrix的能力,我们再回头来看看这个电子商务中的产品垂直分片,其中必须调用四种服务,以获得产品信息视图中对应的数据。列表1.20展现了使用网关API服务的ProductService的代码。

import com.netflix.hystrix.*;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.cache.annotation.Cacheable;  import org.springframework.stereotype.Service;  import java.util.*;  import java.util.concurrent.*;  @Service  public class ProductService {      private static final String GROUP = "products";      private static final int TIMEOUT = 60000;      private final ProductDetailService productDetailService;      private final ProductPricingService productPricingService;      private final ProductRatingService productRatingService;      private final ProductReviewService productReviewService;      @Autowired      public ProductService(ProductDetailService productDetailService, ProductPricingService productPricingService,                            ProductRatingService productRatingService, ProductReviewService productReviewService) {          this.productDetailService = productDetailService;          this.productPricingService = productPricingService;          this.productRatingService = productRatingService;          this.productReviewService = productReviewService;      }      public Map<String, Map<String, Object>> getProductSummary(String productId) {          List<Callable<AsyncResponse>> callables = new ArrayList<>();          callables.add(new BackendServiceCallable("details", getProductDetails(productId)));          callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));          return doBackendAsyncServiceCall(callables);      }      public Map<String, Map<String, Object>> getProduct(String productId) {          List<Callable<AsyncResponse>> callables = new ArrayList<>();          callables.add(new BackendServiceCallable("details", getProductDetails(productId)));          callables.add(new BackendServiceCallable("pricing", getProductPricing(productId)));          callables.add(new BackendServiceCallable("ratings", getProductRatings(productId)));          callables.add(new BackendServiceCallable("reviews", getProductReviews(productId)));          return doBackendAsyncServiceCall(callables);      }      private static Map<String, Map<String, Object>> doBackendAsyncServiceCall(List<Callable<AsyncResponse>> callables) {          ExecutorService executorService = Executors.newFixedThreadPool(4);          try {              List<Future<AsyncResponse>> futures = executorService.invokeAll(callables);              executorService.shutdown();              executorService.awaitTermination(TIMEOUT, TimeUnit.MILLISECONDS);              Map<String, Map<String, Object>> result = new HashMap<>();              for (Future<AsyncResponse> future : futures) {                  AsyncResponse response = future.get();                  result.put(response.serviceKey, response.response);              }              return result;          } catch (InterruptedException|ExecutionException e) {              throw new RuntimeException(e);          }      }      @Cacheable      private HystrixCommand<Map<String, Object>> getProductDetails(String productId) {          return new HystrixCommand<Map<String, Object>>(                  HystrixCommand.Setter                          .withGroupKey(HystrixCommandGroupKey.Factory.asKey(GROUP))                          .andCommandKey(HystrixCommandKey.Factory.asKey("getProductDetails"))                          .andCommandPropertiesDefaults(                                  HystrixCommandProperties.Setter()  .withExecutionIsolationThreadTimeoutInMilliseconds(TIMEOUT)                          )          ) {              @Override              protected Map<String, Object> run() throws Exception {                  return productDetailService.getDetails(productId);              }              @Override              protected Map getFallback() {                  return new HashMap<>();              }          };      }      private HystrixCommand<Map<String, Object>> getProductPricing(String productId) {          // ... snip, see getProductDetails() ...      }      private HystrixCommand<Map<String, Object>> getProductRatings(String productId) {          // ... snip, see getProductDetails() ...      }      private HystrixCommand<Map<String, Object>> getProductReviews(String productId) {          // ... snip, see getProductDetails() ...      }      private static class AsyncResponse {          private final String serviceKey;          private final Map<String, Object> response;          AsyncResponse(String serviceKey, Map<String, Object> response) {              this.serviceKey = serviceKey;              this.response = response;          }      }      private static class BackendServiceCallable implements Callable<AsyncResponse> {          private final String serviceKey;          private final HystrixCommand<Map<String, Object>> hystrixCommand;          public BackendServiceCallable(String serviceKey, HystrixCommand<Map<String, Object>> hystrixCommand) {              this.serviceKey = serviceKey;              this.hystrixCommand = hystrixCommand;          }          @Override          public AsyncResponse call() throws Exception {              return new AsyncResponse(serviceKey, hystrixCommand.execute());          }      }  }

列表1.20 – 某个异步网关API服务的示例,其中使用了Hystrix

以上示例中的服务可以用于RESTful HTTP客户端,这些客户端可以基于Spring中的RestTemplate进行创建,或是使用其它的某种HTTP客户框架,例如Retrofit。getProductSummary()方法对后端服务发起了一个异步调用,该服务将用于获取登陆页面所需的产品信息。与之类似,getProduct()方法将从所有相关的后端服务中获取某个产品的详细信息,并将信息进行合并,以便API的调用者使用。在这个示例中,产品的信息很少会发生变化,该网关服务也应该尽量减少对于后端服务的调用次数,因此getProductDetails()方法利用了Spring提供的@Cacheable标注,在一段合理的时间之内对调用进行缓存。网关服务随后将通过某个映射到/products路由上的RestController获取综合的产品信息。对于微服务架构中的其它垂直分片,也可以采用类似的方式设计终结点,系统API的调用者也能够以一种在更传统的整体性应用程序中相同的方式来访问新的终结点。

结论

Spring Boot很早就意识到将整体性服务分解为分布式微服务所带来的优点,它的设计宗旨是让开发与创建微服务成为一种节省资源的,专注于开发者的流程。通过框架中所提供的启动模块以启用自动配置机制,应用程序就能够方便地充分利用强大的功能子集,否则开发者将不得不进行明确的配置,并通过编程方式进行组装。这些自动配置的模块可以作为开发一个完整的微服务架构的基础,其中已经内置了一种网关API服务。

关于作者

使用Sprint Boot创建微服务 Daniel Woods是一位技术狂热者,尤其是在企业级的Java、Groovy,和Grails开发方面。他在JVM软件开发方面已经具有超过十年以上的经验,并且通过对GrailsRatpack web框架这样的开源项目进行贡献的方式分享他的经验。Dan也是Gr8conf和SpringOne 2GX会议上的演讲者,他在会议上展现了他在JVM的企业级应用架构上的专业知识。

 

查看英文原文:Building Microservices with Spring Boot