微服务架构是当下构建互联网应用的主流架构。在Spring家族中,专门有着用于构建微服务架构的Spring Cloud框架。而Spring Cloud框架本身则是构建在Spring Boot之上。
在本节中,我们将讨论Spring Boot与SpringCloud的关系,并给出Spring微服务架构的案例分析。
微服务架构是一种架构模式,区别于其他系统架构的构建方式和技术方案,微服务架构具有其固有特点。微服务架构的提出者Martin Fowler在其文章“Microservices”中定义了服务组件化、去中心化、基础设施自动化等多个微服务架构特点,正是这些特点为我们使用微服务架构进行系统设计提供了主要的切入点。
从实施角度来讲,我们可以基于这些微服务架构的特点提炼出构建微服务架构三大要素,即服务建模、技术体系和研发过程。
微服务架构设计首要的切入点在于服务建模,因为微服务架构与传统SOA等技术架构的本质区别就是其服务的粒度不同,以及服务本身有面向业务和组件化的特性。针对服务建模,我们首先需要明确服务的类别以及服务与业务之间的关系,尽可能明确领域的边界。对服务建模,推荐使用领域驱动设计(DomAIn Driven Design,DDD)方法,通过识别领域中的各个子域、判断这些子域是否独立、考虑子域与子域的交互关系,明确各个限界上下文(Boundary Context)之间的边界。
微服务架构的第二大要素就是它的技术体系,这也是我们学习微服务架构的主体内容。同样,不同的开发技术和框架都会基于自身的设计理念给出各自的技术体系类型及其实现方式。一个完整的微服务架构,通常需要包括服务通信、服务治理、服务路由、服务容错、服务网关、服务配置、服务安全和服务监控等核心技术体系,如图13-3所示。
图13-3 微服务架构的核心技术体系
过程转变与技术体系建设有密切关联,所以对于微服务架构而言,最后一个要素就是研发过程。Martin Fowler在介绍微服务架构时,同样也提出了“围绕业务功能组织团队”的研发管理理念。
本书关注Spring Boot及其生态系统,因此,我们的重点还是如何基于Spring家族来开发微服务架构。在Spring家族中,为开发人员提供了开发微服务架构整套解决方案的就是Spring Cloud框架。
Spring Cloud具备一个天生的优势,它是Spring家庭的一员,而Spring在JAVA EE开发领域的强大地位给Spring Cloud起到很好的推动作用。同时,Spring Cloud基于Spring Boot构建,而Spring Boot已经成为Java EE领域中最流行的开发框架,用来简化Spring应用程序的框架搭建和开发过程。
在微服务架构中,我们将通过Spring Boot来开发单个微服务。同样作为Spring家族的新成员,Spring Boot提供了令人兴奋的特性,这些特性主要体现在开发过程的简单化,包括支持快速构建项目、不依赖外部容器独立运行、开发部署效率高以及与云平台天然集成等,我们已经在前面的章节中对这些功能特性做了详细的介绍。而在微服务架构中,Spring Cloud构建在Spring Boot之上,继承了Spring Boot简单配置和快速开发的特点,让原本复杂的架构工作变得相对容易上手。
技术组件的完备性是Spring Cloud的主要优势。Spring Cloud是一系列框架的有序集合。它利用Spring Boot开发的便利性巧妙地简化了微服务系统基础设施的开发过程,如服务发现注册、API网关、配置中心、消息总线、负载均衡、熔断器、数据监控等都可以使用Spring Boot的开发风格做到一键启动和部署。
在对微服务的各项技术组件进行设计和实现的过程中,Spring Cloud也有一些自己的特色。一方面,它对微服务架构开发所需的技术组件进行了抽象,提供了符合开发需求的独立组件,包括用于配置中心的Spring CloudConfig、用于API网关的Spring Cloud Gateway等。另一方面,Spring Cloud也没有重复造轮子,它将目前各家公司比较成熟、经得起实践考验的服务框架组合起来,使用Spring Boot开发风格进行了再次封装。这部分主要指的是Spring Cloud.NETflix组件。Spring Cloud Netflix基于Spring Boot集成了Netflix OSS中的诸多核心组件,其中与服务治理相关的除了用于服务注册和发现的Eureka之外,实际上还有用于实现客户端负载均衡的Ribbon和用于实现声明式REST客户端的Feign等。Netflix OSS、Spring Boot、Spring CloudNetflix以及Spring Cloud之间的关系如图13-4所示。
图13-4 Spring Cloud、Spring Cloud Netflix、Spring Boot与Netflix OSS之间的关系
Spring Cloud屏蔽了微服务架构开发所需的复杂的配置和实现过程,最终给开发者提供了一套易理解、易部署和易维护的开发工具包。SpringCloud中的组件非常多,本书不对图13-4中的所有组件详细讲解,而是通过一个案例来展示Spring Cloud的使用方法,这就是接下来的内容。
本节将基于Spring Boot和Spring Cloud来演示微服务系统的构建过程,我们将结合案例重点介绍注册中心、配置中心以及API网关这三个核心的微服务技术组件,以及如何在这三个技术组件的基础上完成整个微服务系统的实现。
1. 案例业务场景和服务拆分
现在,让我们构建一个精简但又完整的系统来展示微服务架构的设计理念和常见实现技术。本案例的业务场景是订单管理,需要对互联网应用中最常见的商品、订单业务做抽象。现实环境中订单业务可以非常复杂,该案例的目的在于介绍系统实现的技术体系,不在于介绍具体业务逻辑,所以在业务领域建模上做了高度抽象和简化。
按照实施微服务架构的基本思路,服务识别和拆分是案例分析的第一步。案例包含的业务场景比较简单,主要针对用户的下单操作。在提交订单的过程中,我们需要对商品和用户信息进行验证。因此,我们可以把该过程对应的系统分成三个服务,即商品服务(goods-service)、订单服务(order-service)和用户服务(user-service)。
以上三个微服务构成了案例的业务服务,而若要构建一个完整的微服务系统,我们还需要引入其他基础设施类服务,这些服务从不同的角度为实现微服务架构提供支持。在本书中,我们重点介绍注册中心、配置中心和API网关这三个基础设施类服务。无论是业务服务、还是基础设施类服务,本质上它们都是一个Spring Boot应用程序。
2. 注册中心
基于Spring Cloud实现注册中心的方式有多种。在本文中,我们将采用Spring Cloud Netflix中的Eureka来构建用于服务发现和服务注册的注册中
心。Eureka同时具备服务器端组件和客户端组件,其中客户端组件内嵌在各个业务微服务中;而服务器端组件则是独立的,所以需要构建一个Eureka服务,并将这个服务命名为eureka-server。
我们创建一个Spring Boot应用程序,并在该应用程序的pom文件中添加Eureka服务器端组件依赖包,如代码清单13-23所示。
代码清单13-23 Eureka服务器端依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka
server</artifactId>
</dependency>
引入Maven依赖之后就可以创建Spring Boot的启动类,在示例代码中,我们把该启动类命名为EurekaServerApplication,如代码清单13-24所示。
代码清单13-24 EurekaServerApplication类代码
@SpringBootApplication
@EnableEurekaServerpublic class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
请注意,在上面的代码中,我们在启动类上加了一个@EnableEurekaServer注解。在Spring Cloud中,包含@EnableEurekaServer注解的服务意味着就是一个Eureka服务器组件。
构建eureka-server的另一件事情是对Spring Boot的配置文件进行处理,并添加如代码清单13-25所示的配置项。
代码清单13-25 eureka-server配置
eureka:
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://localhost:8761
在这些配置项中,我们看到了三个以eureka.client开头的客户端配置项,分别是register-WithEureka、fetchRegistry和serviceUrl。从配置项的命名上不难看出,registerWithEureka用于指定是否把当前的客户端实例注册到Eureka服务器中,而fetchRegistry则指定是否从Eureka服务器上拉取服务注册信息。这两个配置项默认都是true,但这里都将其设置为false。而最后的serviceUrl配置项用于设置服务地址。
3. 配置中心
与Eureka一样,基于Spring Cloud Config构建的配置中心同样存在服务器端组件和客户端组件,其服务器端组件也需要构建一个独立的配置服务。我们将这个服务命名为config-server,并在pom文件中引入spring-cloudconfig-server和
spring-cloud-starter-config这两个Maven依赖,其中前者包含了用于构建配置服务器的各种组件,如代码清单13-26所示。
代码清单13-26 Spring Cloud Config服务器依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
接下来我们在新建的config-server工程中添加一个Bootstrap类ConfigServerApplication,如代码清单13-27所示。
代码清单13-27 ConfigServerApplication类代码
@SpringCloudApplication
@EnableConfigServer
public class ConfigServerApplication {
public static void main(String[] args) {
SpringApplication.run(ConfigServerApplication.class, args);
}
}
除了熟悉的@SpringCloudApplication注解之外,我们还看到这里添加了一个新的注解@EnableConfigServer。有了这个注解,配置服务器就可以将所存储的配置信息转化为RESTful接口数据供各个业务微服务在分布式环境下使用。
同样,在config-server工程的配置文件中,我们也需要添加与配置中心相关的配置项,如代码清单13-28所示。
代码清单13-28 config-server配置
spring:
cloud:
config:
server:
native:
searchLocations: classpath: config/
classpath: config/userservice,
classpath: config/goodsservice,
classpath: config/orderservice
可以看到,这里通过searchLocations配置项设置了Spring CloudConfig从本地文件系统中读取配置文件的目录,分别是config目录下的userservice、goodsservice和orderservice这三个子目录。请注意这三个子目录的名称必须与各个服务自身的名称完全一致。然后我们在这三个子目录下面都放入以服务名称命名的针对不同运行环境的.yml配置文件。
4. API网关
对于API服务而言,无论是使用Spring Cloud Netflix中的Zuul还是使用Spring自建的Spring Cloud Gateway,都需要构建一个独立的服务来承接路由、安全和监控等各种功能。这里,我们以Spring Cloud Gateway为例构建一个独立的gateway-server服务。在技术上,Spring Cloud Gateway基于最新的Spring 5和Spring Boot 2,以及用于响应式编程的Project Reactor框架,提供响应式、非阻塞式I/O模型,所以Spring Cloud Gateway为开发人员提供了更好的性能支持。要想在微服务架构中引入Spring Cloud Gateway,我们同样需要构建一个独立的Spring Boot应用程序,并在Maven中添加如代码清单13-29所示的依赖项。
代码清单13-29 Spring Cloud Gateway依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
按照约定,我们把这个独立的微服务命名为gateway-server,然后在作为Bootstrap类的GatewayApplication上添加@EnableDiscoveryClient注解即可,如代码清单13-30所示。
代码清单13-30 GatewayApplication类代码
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
Spring Cloud Gateway中的核心概念有两个,一个是过滤器,一个是谓词(Predicate)。Spring Cloud Gateway中的过滤器用于在响应HTTP请求之前或之后修改请求本身及对应的响应结果。而所谓谓词,本质上是一种判断条件,用于将HTTP请求与路由进行匹配。接下来,我们通过一个完整的路由配置来展示谓词和过滤器规则的配置方法,如代码清单13-31所示。
代码清单13-31 gateway-server配置
spring:
cloud:
gateway:
discovery:
locator:
enabled: true
routes:
- id: user-route
uri: lb://userservice
predicates:
- Path=/user/**
- id: goods-route
uri: lb://goodsservice
predicates:
- Path=/good/**
- id: order-route
uri: lb://orderservice
predicates:
- Path=/order/**
在上述配置中,有几个注意点。首先我们使用id配置项为三个业务服务指定了路由信息的规则编号,分别为user-route、goods-route和orderroute。而uri配置项中的lb代表负载均衡LoadBalance,也就是说在访问URL指定的服务名称时需要集成负载均衡机制。请注意lb配置项中指定的服务名称需要与保存在Eureka中的服务名称完全一致。然后我们使用了谓词来对请求路径进行匹配,例如这里的Path=/order/**代表所有以/order开头的请求都将被路由到这条路径中。
5. 业务服务整合
接下来,我们基于前面构建的基础设施服务来实现各个业务服务。我们知道,各个业务服务本质上都是一个Spring Boot应用程序。在这些SpringBoot应用程序中,首先需要引入Spring Cloud组件,如代码清单13-32所示。
代码清单13-32 Spring Cloud依赖包
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
同时,作为微服务系统中的单个微服务,这些Spring Boot应用程序都应该注册到Eureka注册中心,并完成与配置中心的集成。因此,在pom文件中,我们也需要添加如代码清单13-33所示的依赖项。
代码清单13-33 注册中心和配置中心客户端依赖包
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka
client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>
<dependency> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId>
</dependency>
对应地,在配置文件中,我们也需要分别添加对注册中心和配置中心的引用,如代码清单13-34所示。
代码清单13-34 注册中心和配置中心客户端配置项
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
spring:
cloud:
config:
enabled: true
uri: http://localhost:8888
现在,order-service、user-service和goods-service这三个业务服务都融入了微服务架构的体系中,接下来要做的就是根据业务逻辑实现业务服务之间的整合。在案例中,我们以最典型的下订单业务流程为例,给出在微服务架构下业务服务之间整合的实现过程。
通常,在用户下订单的过程中,我们需要传入用户信息以及商品信息,然后系统通过校验用户信息和商品信息的正确性来决定是否生成一个合法的订单。在这个过程中,order-service需要基于REST API完成与user-service和goods-service之间的远程交互,如图13-5所示。
图13-5 业务服务交互关系图
在order-service中,我们将暴露一个用于生成订单的HTTP端点,如代码清单13-35所示。
代码清单13-35 OrderController中的HTTP端点
@RestController
@RequestMapping(value = "orders")
public class OrderController {
@Autowired
OrderService orderService;
@GetMapping(value = "/{userId}/{goodsCode}")
public Order addOrder(@PathVariable("userId") String userId,
@PathVariable("goodsCode") String goodsCode) {
return orderService.generateOrder(userId, goodsCode);
}
}
可以看到,在上述addOrder()方法中,我们传入用户的ID以及商品的编号,并调用OrderService来生成订单。在OrderService中,负责具体生成订单信息的generateOrder()方法如代码清单13-36所示。
代码清单13-36 OrderService中的generateOrder()方法实现代码
public Order generateOrder(String userId, String goodsCode) {
Order order = new Order();
//获取远程Goods信息
GoodMapper goods = getGoodsFromRemote(goodsCode);
if (goods == null) {
return order;
}
//获取远程User信息
UserMapper user = getUserFromRemote(userId);
if (user == null) {
return order;
}
order.setOrderNumber("DemoOrderNumber");
order.setDeliveryAddress("DemoDeliveryAddress");
order.setUserId(userId);
order.setGoodsName(goods.getName());
order.setCreateTime(new Date());
orderRepository.save(order);
return order;
}
上述方法的执行流程非常明确,我们分别通过getGoodsFromRemote()和getUserFromRemote()方法远程获取商品信息和用户信息。这个过程就涉及微服务之间的相互调用。这里以getUserFromRemote()方法为例,来看对应的UserRestTemplateClient远程访问工具类的实现过程,如代码清单13-37所示。
代码清单13-37 UserRestTemplateClient类实现代码
@Service
public class UserRestTemplateClient {
@Autowired RestTemplate restTemplate;
public UserMapper getUserById(String userId) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://userservice/users/{userId}",
HttpMethod.GET, null, UserMapper.class, userId);
return result.getBody();
}
public UserMapper getUserByUserName(String userName) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://userservice/users/userName/{userName}",
HttpMethod.GET, null, UserMapper.class, userName);
UserMapper user = result.getBody();
return user;
}
}
这里我们通过RestTemplate发起HTTP请求并获取响应结果,整个远程调用过程与第4章介绍的内容完全一致。唯一的区别在于这里请求URL使用的并不是一个具体的IP地址,而是对应微服务的服务名userservice。能够实现这一效果的原因是Spring Cloud可以对默认的RestTemplate做改造,如代码清单13-38所示。
代码清单13-38 添加了@LoadBalanced注解的RestTemplate
@Bean
@LoadBalanced
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
可以看到,我们在创建的RestTemplate上添加了一个@LoadBalanced注解。@LoadBalanced注解用于修饰发起HTTP请求的RestTemplate工具类,在该工具类中自动与注册中心集成,并嵌入客户端负载均衡功能。这样,开发人员不需要针对负载均衡做任何特殊的开发或配置,就能实现基于服务名进行远程方法调用。
请注意,上述实现方式中我们并没有集成API网关功能。想要发挥SpringCloud Gateway所引入的服务路由机制,基于前面配置的路由规则,我们可以采用如代码清单13-39所示的实现方式。
代码清单13-39 通过API网关执行远程服务调用的实现代码
public UserMapper getUserById(String userId) {
ResponseEntity<UserMapper> result =
restTemplate.exchange("http://gatewayservice/user/users/{userId}",
HttpMethod.GET, null, UserMapper.class, userId);
return result.getBody();
}