高级组件

4.高级组件

本篇的主要内容如下:

  1. ribbon 基础
  2. Ribbon 自带的负载规则(自学) 
  3. Feign 基础
  4. 雪崩效应与 Hystrix
  5. 配置中心 Config
  6. config-server-jdbc(暂不可用)
  7. config-git 与动态刷新
  8. zuul 网关
  9. 网关请求过滤(自学)
Ribbon 基础

负载均衡是分布式架构的重点,负载均衡机制将决定着整个服务集群的性能与稳定。根据前面章节可知,Eureka 服务实例可以进行集群部署,每个实例都均衡处理服务请求,那么这些请求是如何被分摊到各个服务实例中的?本章将讲解 Netflix 的负载均衡项目 Ribbon。

客户端负载均衡

负载均衡在系统架构中是一个非常重要,并且是不得不去实施的内容。因为负载均衡是对系统的高可用、网络压力的缓解和处理能力扩容的重要手段之一。我们通常所说的负载均衡都指的是服务端负载均衡,其中分为硬件负载均衡和软件负载均衡。硬件负载均衡主要通过在服务器节点之间安装专门用于负载均衡的设备,比如 F5 等;而软件负载均衡则是通过在服务器上安装一些具有均衡负载功能或模块的软件来完成请求分发工作,比如 Nginx 等。

spring cloud 中使用 Ribbon

创建 maven 项目 ribbon,将 Eureka 章节的eureka-server、eureka-service-invoker、eureka-service-provider模块拷贝到ribbon下

启动eureka-server后需要启动两个 eureka-service-provider,端口分别为8080和8888


Ribbon 主要在调用者上实现负载均衡,下面是重点部分

@RestController
@Configuration
public class InvokerController {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

@RequestMapping(value = "/router/{personId}")
public String router(@PathVariable Integer personId) {
    RestTemplate restTpl = getRestTemplate();
    // 根据应用名称调用服务
    String json = restTpl.getForObject(
            "http://eureka-service-provider/person/" + personId, String.class);
    return json;
}

}

RestTemplate 本是 spring-web 项目中的一个 REST 客户端访问类,它遵循 REST 的设计原则,提供简单的 API 让调用去访问 HTTP 服务器。RestTemplate 本身不具有负载均衡的功能,该类也与 Spring Cloud 没有关系,但为何加入@LoadBalanced 注解后,一个
RestTemplate 实例就具有负载均衡的功能呢?实际上这要得益于 RestTemplate 的拦截器功能。

在 Spring Cloud 中,使用@LoadBalanced 修饰的 RestTemplate,在 Spring 容器启动
时,会为这些被修饰过的 RestTemplate 添加拦截器,拦截器中使用了 LoadBalancerClient
来处理请求,LoadBalancerClient 本来就是 Spring 封装的负载均衡客户端,通过这样间接处理,使得 RestTemplate 就拥有了负载均衡的功能。


Ribbon 自带的负载规则(自学) 

在面试或笔试中 经常考查 Ribbon 自带的负载规则,如下所示:

RoundRobinRule:系统默认的规则,通过简单的轮询服务列表来选择服务器,其他的规则在很多情况下,仍然使用 RoundRobinRule。 

AvailabilityFilteringRule:该规则会忽略以下服务器:

无法连接的服务器:在默认情况下,如果 3 次连接失败,该服务器将会被置为 “短路”的状态,该状态将持续 30 秒,如果再次连接失败,“短路”状态的持续时间将会以几何级增加。可以通过修改niws.loadbalancer.<clientName>.connectionFailureCountThreshold 属性,来配置连接失败的次数。

并发数过高的服务器:如果连接到该服务器的并发数过高,也会被这个规则忽略,可以通过修改<clientName>.ribbon.ActiveConnectionsLimit 属性来设定最高并发数。

WeightedResponseTimeRule:为每个服务器赋予一个权重值,服务器的响应时间越长,该权重值就是越少,这个规则会随机选择服务器,这个权重值有可能会决定服务器的选择。

ZoneAvoidanceRule:该规则以区域、可用服务器为基础,进行服务器选择。使用 Zone 对服务器进行分类,可以理解为机架或者机房。

BestAvailableRule:忽略“短路”的服务器,并选择并发数较低的服务器。

RandomRule:顾名思义,随机选择可用的服务器。

RetryRule:含有重试的选择逻辑,如果使用 RoundRobinRule 选择服务器无法连接,那么将会重新选择服务器。


Feign 基础

Feign 是一个 Github 上一个开源项目,目的是为了简化 Web Service 客户端的开发。在使用 Feign 时,可以使用注解来修饰接口,被注解修饰的接口具有访问 Web Service 的 能力,这些注解中既包括了 Feign 自带的注解,也支持使用第三方的注解。除此之外,Feign 还支持插件式的编码器和解码器,使用者可以通过该特性,对请求和响应进行不同的封装与解析。

Spring Cloud 将 Feign 集成到 netflix 项目中,当与 Eureka、Ribbon 集成时,Feign 就具有负载均衡的功能。Feign 本身在使用上的简便性,加上与Spring Cloud 的高度整合,使 用该框架在 Spring Cloud 中调用集群服务,将会大大降低开发的工作量。

创建maven项目feign,将Eureka章节的eureka-server、eureka-service-invoker、eureka-service-provider模块拷贝到 feign 下

在服务提供者的控制器中加入一个方法

@RequestMapping(value = "/hello")
public String hello() {
    return "hello feign!";
}

修改ribbon的依赖为feign的依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

在服务调用者的启动类中,打开 Feign 开关

@EnableFeignClients

接下来,编写客户端接口

@FeignClient("eureka-service-provider") //声明调用的服务名称
public interface PersonClient {
    @RequestMapping("/hello")
    String hello();
@RequestMapping(value = "/person/{personId}")
Map&lt;String, Object&gt; findPerson(@PathVariable Integer personId);

}

@FeignClient 注解声明了需要调用的服务名称。

另外,除了方法的@RequestMapping 注解外 , 默认还支持 @RequestParam 、 @RequestHeader、@PathVariable 这 3 个参数注解,也就是说,在定义方法时,可以使用 方式定义参数: 

@RequestMapping(method = RequestMethod.GET, value = "/hello/{name}")
String hello(@PathVariable("name") String name);

在 InvokerController 加入

@Autowired
private PersonClient personClient;

@RequestMapping(value = "/invokeHello")
public String invokeHello() {
return personClient.hello();
}

依次启动服务后,在浏览器中输入:http://localhost:9000/invokeHello,可以看到服务提供者的 “/hello”服务被调用。


雪崩效应与 Hystrix

在微服务架构中,我们将系统拆分成了很多服务单元,各单元的应用间通过服务注册与发现的方式互相依赖。由于每个单元都在不同的进程中运行,依赖通过远程调用的方式执行,这样就有可能因为网络原因或是依赖服务自身问题出现调用故障或延迟,而这些问题会直接导致调用方的对外服务也出现延迟,若此时调用方的请求不断增加,最后就会因等待出现故障的依赖方响应形成任务积压,最终导致自身服务的瘫痪。

举个例子,在一个电商网站中,我们可能会将系统拆分成用户、订单、库存、积分、评论等一系列服务单元。用户创建一个订单的时候,客户端将调用订单服务的创建订单接口,此时创建订单接口又会向库存服务来请求出货(判断是否有足够库存来出货)。此时若库存服务因自身处理逻辑等原因造成响应缓慢,会直接导致创建订单服务的线程被挂起,以等待库存申请服务的响应,在漫长的等待之后用户会因为请求库存失败而得到创建订单失败的结果。如果在高并发情况之下,因这些挂起的线程在等待库存服务的响应而未能释放,使得后续到来的创建订单请求被阻塞,最终导致订单服务也不可用。

在微服务架构中,存在着那么多的服务单元,若一个单元出现故障,就很容易因依赖关系而引发故障的蔓延,最终导致整个系统的瘫瘓,这样的架构相较传统架构更加不稳定。为了解决这样的问题,产生了断路器等一系列的服务保护机制。

在分布式架构中,断路器模式的作用也是类似的,当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延。

针对上述问题,Spring Cloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能。它也是基于Netlix的开源框架Hystrix 实现的,该框架的目标在于通过控制那些访问远程系统、服务和第三方库的节点,从而对延迟和故障提供更强大的容错能力。Hystrix 具备服务降级、服务熔断、线程和信号隔离、请求缓存、请求合并以及服务监控等强大功能。

接下来,我们就从一个简单示例开始对 Spring Cloud Hystrix 的学习与使用。

spring cloud 中使用 hystrix

创建 maven 项目 hystrix ,将 Eureka 章节的 eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 hystrix 下,按照如下顺序启动。

  1. 启动 eureka-server
  2. 启动 eureka-service-provider 在 8888 端口和 8886 端口
  3. 启动 eureka-service-invoker

关闭 8888 端口的服务后刷新 http://localhost:9000/router/123 地址,发现地址有时报错如下:


在 eureka-service-invoker(消费者)中加入依赖

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

在启动类上添加注解 @EnableCircuitBreaker 开启断路器功能。

注意:这里还可以使用 Spring Cloud 应用中的 @SpringCloudApplication 注解来修饰应用主类,该注解的具体定义如下所示。可以看到,该注解中包含了上述我们所引用的三个注解,这也意味着一个 Spring Cloud 标准应用应包含服务发现以及断路器。

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public @interface SpringCloudApplication {
}

改造服务消费者,新增 errorFallback 方法,该方法参数和原方法参数列表和返回类型必须一致,在 router 函数上增加 @HystrixCommand 注解来指定错误时回调方法:

@RestController
@Configuration
public class InvokerController {

@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
    return new RestTemplate();
}

@RequestMapping(value = "/router/{personId}")
@HystrixCommand(fallbackMethod = "errorFallback")
public String router(@PathVariable Integer personId) {
    RestTemplate restTpl = getRestTemplate();
    // 根据应用名称调用服务
    String json = restTpl.getForObject(
            "http://eureka-service-provider/person/" + personId, String.class);
    return json;
}
public String errorFallback(Integer integer){
    System.out.println(integer);
    return "Hystrix Error!";
}

}

当报错时,返回指定字符串。


上面简单的演示了断路器调用保存时处理办法,但丝毫不能体现断路器的熔断作用。原因是演示断路器的熔断和关闭需要在高并发情况下。下面我们从理论上来看断路器的工作流程。Hystrix 整个工作流如下:

  1. 构造一个 HystrixCommand 或 HystrixObservableCommand 对象,用于封装请求,并在构造方法配置请求被执行需要的参数;
  2. 执行命令,Hystrix 提供了 4 种执行命令的方法,主要用于方法执行顺序的线程控制;
  3. 判断是否使用缓存响应请求,若启用了缓存,且缓存可用,直接使用缓存响应请求。Hystrix 支持请求缓存,但需要用户自定义启动;
  4. 判断熔断器是否打开,如果打开,跳到第 8 步;
  5. 判断线程池/队列/信号量是否已满,已满则跳到第 8 步;
  6. 执行 HystrixObservableCommand.construct() 或 HystrixCommand.run(),如果执行失败或者超时,跳到第 8 步;否则,跳到第 9 步;
  7. 统计熔断器监控指标;
  8. 走 Fallback 备用逻辑
  9. 返回请求响应

从流程图上可知道,第 5 步线程池/队列/信号量已满时,还会执行第 7 步逻辑,更新熔断器统计信息,而第 6 步无论成功与否,都会更新熔断器统计信息。  


配置中心 Config

Spring Cloud Config 是 Spring Cloud 团队创建的一个全新项目,用来为分布式系统中的基础设施和微服务应用提供集中化的外部配置支持,它分为服务端与客户端两个部分。其中服务端也称为分布式配置中心,它是一个独立的微服务应用,用来连接配置仓库并为客户端提供获取配置信息、加密/解密信息等访问接口;而客户端则是微服务架构中的各个微服务应用或基础设施,它们通过指定的配置中心来管理应用资源与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。Spring Cloud Config 实现了对服务端和客户端中环境变量和属性配置的抽象映射,所以它除了适用于 Spring 构建的应用程序之外,也可以在任何其他语言运行的应用程序中使用。由于 Spring Cloud Config 实现的配置中心默认采用 Git 来存储配置信息,所以使用 Spring Cloud Config 构建的配置服务器,天然就支持对微服务应用配置信息的版本管理,并且可以通过 Git 客户端工具来方便地管理和访问配置内容。当然它也提供了对其他存储方式的支持,比如 SVN 仓库、本地化文件系统。接下来,我们从一个简单的入门示例开始学习 Spring Cloud Config 服务端以及客户端的详细构建与使用方法。

在本节中,我们将演示如何构建一个基于本地化文件系统的分布式配置中心,同时对配置的详细规则进行讲解,并在客户端中演示如何通过配置指定微服务应用的所属配置中心,并让其能够从配置中心获取配置信息并绑定到代码中的整个过程。

Spring Cloud Config 搭建

创建 maven 项目 config,将 Eureka 章节的 eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 config 下

创建 config-server 模块,并勾选 Spring Cloud Config->Config Server 和 Spring Cloud Discovery->Eureka Server

启动类加 @EnableConfigServer @EnableEurekaClient 注解启用配置。

配置文件

server:
port: 1201
spring:
application:
name: config-server
cloud:
config:
server:
native:
search-locations: classpath:/properties/
profiles:
active: native
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

在 resources 下创建 properties 文件夹用于存储其他应用的配置文件。

eureka-service-invoker-pro.yml
server:
port: 9000

eureka-service-provider-pro.yml
user: hhh

修改 eureka-service-provider 配置文件为 bootstrap.properties

spring.application.name=eureka-service-provider
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.discovery.enabled=true
spring.cloud.config.profile=pro
spring.cloud.config.label=master

<pre宋体';font-size:9.0pt;"></pre宋体';font-size:9.0pt;">

修改 eureka-service-invoker 配置文件为 bootstrap.properties

spring.application.name=eureka-service-invoker
eureka.client.serviceUrl.defaultZone=http://localhost:8761/eureka/
spring.cloud.config.discovery.serviceId=config-server
spring.cloud.config.discovery.enabled=true
spring.cloud.config.profile=pro
spring.cloud.config.label=master

这里需要格外注意,”上面这些属性必须配置在 bootstrap.properties 中,这样 config-server 中的配置信息才能被正确加载。bootstrap.yml(bootstrap.properties)用来在程序引导时执行,应用于更加早期配置信息读取,如可以使用来配置 application.yml 中使用到参数等 application.yml(application.properties)应用程序特有配置信息,可以用来配置后续各个模块中需使用的公共参数等。bootstrap.yml 先于 application.yml 加载。

在 provider 的 controller 中获取 user 属性

@RefreshScope
@RestController
public class FirstController {
@Value("${user}")
private String user;

@RequestMapping(value = "/person/{personId}")
public Map&lt;String, Object&gt; findPerson(@PathVariable Integer personId, HttpServletRequest request) {
    HashMap&lt;String, Object&gt; map = new HashMap&lt;&gt;();
    map.put("id", personId);
    map.put("name", user);
    map.put("age", 18);
    map.put("url", request.getRequestURL());
    return map;
}

}

依次启动 eureka-server、config-sever、eureka-service-provider:8888、eureka-service-provider:8886、eureka-service-invoker

访问路径后发现可以获取到配置文件 user 的值,而且 eureka-service-invoker 也在 9000 端口正常启动。

应用启动时,根据 bootstrap.properties 中配置的应用名 {application}、环境名 {profile}、分支名 {label},向 Config Server 请求获取配置信息。

需要在服务提供者和服务调用者加入依赖。

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-config</artifactId>
</dependency>



config-server-jdbc(暂不可用)

很多时候为了修改方便,我们常常将配置文件存入数据库,以下为配置。

创建数据库和表

CREATE TABLE properties (
id int(11) NOT NULL AUTO_INCREMENT,
key varchar(50) DEFAULT NULL,
value varchar(500) DEFAULT NULL,
application varchar(50) DEFAULT NULL,
profile varchar(50) DEFAULT NULL,
label varchar(50) DEFAULT NULL,
remark varchar(50) CHARACTER SET utf8 DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=MyISAM AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

insert into properties(id,key,value,application,profile,label,remark) values (1,'user','hhh','eureka-service-provider','pro','master','自定义属性');
insert into properties(id,key,value,application,profile,label,remark) values (2,'server.port','9000','eureka-service-invoker','pro','master','应用服务端口号');

创建config-server-jdbc模块,并勾选Spring Cloud Config->Config Server和Spring Cloud Discovery->Eureka Server和jdbc

启动类加@EnableConfigServer @EnableEurekaClient注解启用配置

配置文件

spring:
profiles:
active: jdbc
application:
name: config-server
datasource:
url: jdbc:mysql://127.0.0.1:3306/config-jdbc?characterEncoding=utf-8&useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
cloud:
config:
label: master
server:
jdbc:
sql: SELECT 'properties_key','properties_value' FROM properties WHERE application=? AND PROFILE=? AND label=?label=?
server:
port: 1201
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

启动后和存于本地系统功能相同。


config-server-git

本文使用开源中国的码云来创建我们的 Git 仓库,当然你也可以选择其他的 Github 或者阿里云 Git 等创建自己的 Git 仓库。

点击导航栏中的“+”按钮==>新建仓库,填入仓库信息,完成创建。

点击 克隆/下载 ==>复制,将 Git 仓库地址复制下来备用。基本配置如下:

spring:
application:
name: config-server
cloud:
config:
label: master
server:
git:
uri: https://gitee.com/wpfhhh/config-server
search-paths: springcloud
username:
password:
server:
port: 1201
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/

动态刷新配置

有时候,我们需要对配置内容做一些实时更新,这时就需要 Spring Boot Actuator。它可以帮助你监控和管理 Spring Boot 应用,比如健康检查、审计、统计和 HTTP 追踪等。Actuator 同时还可以与外部应用监控系统整合,比如 Prometheus, Graphite, DataDog, Influx, Wavefront, New Relic 等。这些系统提供了非常好的仪表盘、图标、分析和告警等功能,使得你可以通过统一的接口轻松的监控和管理你的应用。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

SpringCloud2.0 以后,没有 /refresh 手动调用的刷新配置地址

2.0 之前只要加入依赖:spring-boot-starter-actuator 并且在类上,变量类上打上@RefreshScope 的注解,在启动的时候,都会看到RequestMappingHandlerMapping : Mapped "{/refresh,methods=[post]}"

也就是 SpringCloud 暴露了一个接口 /refresh 来给我们去刷新配置,但是 SpringCloud 2.0.0 以后,需要在 bootstrap.yml 里面加上需要暴露出来的地址

management.endpoints.web.exposure.include=refresh,health

现在的地址也不是 /refresh 了,而是 /actuator/refresh


zuul 网关

通过前几章的介绍,我们对于 Spring Cloud Nettlix下的核心组件已经了解了一大半。这些组件基本涵盖了微服务架构中最为基础的几个核心设施,利用这些组件我们已经可以构建起一个简单的微服务架构系统,比如,通过使用 Spring Cloud Eureka 实现高可用的服务注册中心以及实现微服务的注册与发现;通过Spring Cloud Ribbon 或 Feign 实现服务间负载均衡的接口调用;同时,为了使分布式系统更为健壮,对于依赖的服务调用使用 Spring Cloud Hystrix 来进行包装,实现线程隔离并加入熔断机制,以避免在微服务架构中因个别服务出现异常而引起级联故障蔓延。

在传统模式中,我们的服务集群包含内部服务 Service A 和 Service B,它们都会向 Eureka Server 集群进行注册与订阅服务,而 Open Service 是一个对外的RESTful API 服务,它通过 F5、Nginx 等网络设备或工具软件实现对各个微服务的路由与负载均衡,并公开给外部的客户端调用。

该模式有以下的几个缺点:

1.当消费者数量增加,或更换 ip 时,我们不得不修改 F5 或 nginx 的配置文件,使其均衡到该消费者上。

2.权限验证模块不得不因为不同的消费者使用相同的逻辑,增加代码的冗余。

为了解决上面这些常见的架构问题,API 网关的概念应运而生。API 网关是一个更为智能的应用服务器,它的定义类似于面向对象设计模式中的 Facade (外观)模式,它的存在就像是整个微服务架构系统的门面一样,所有的外部客户端访问都需要经过它来进行调度和过滤。它除了要实现请求路由、负载均衡、校验过滤等功能之外,还需要更多能力,比如与服务治理框架的结合、请求转发时的熔断机制、服务的聚合等一系列高级功能。

首先,对于路由规则与服务实例的维护问题。Spring Cloud Zuul 通过与 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从Eureka 中获得了所有其他微服务的实例信息。这样的设计非常巧妙地将服务治理体系中维护的实例信息利用起来,使得将维护服务实例的工作交给了服务治理框架自动完成,不再需要人工介入。而对于路由规则的维护,Zuul 默认会将通过以服务名作为 ContextPath 的方式来创建路由映射,大部分情况下,这样的默认设置已经可以实现我们大部分的路由需求,除了一些特殊情况(比如兼容一些老的 URL )还需要做一些特别的配置。但是相比于之前架构下的运维工作量,通过引入 Spring Cloud Zuul 实现 API 网关后,工作量已经能够大大减少了。

其次,对于类似签名校验、登录校验在微服务架构中的冗余问题。理论上来说,这些校验逻辑在本质上与微服务应用自身的业务并没有多大的关系,所以它们完全可以独立成一个单独的服务存在,只是它们被剥离和独立出来之后,并不是给各个微服务调用,而是在 API 网关服务,上进行统一调用来对微服务接口做前置过滤,以实现对微服务接口的拦截和校验。Spring Cloud Zuul 提供了一套过滤器机制,它可以很好地支持这样的任务。开发者可以通过使用 Zuul 来创建各种校验过滤器,然后指定哪些规则的请求需要执行校验逻辑,只有通过校验的才会被路由到具体的微服务接口,不然就返回错误提示。通过这样的改造,各个业务层的微服务应用就不再需要非业务性质的校验逻辑了,这使得我们的微服务应用可以更专注于业务逻辑的开发,同时微服务的自动化测试也变得更容易实现。

使用 idea 创建名为 zuul 的maven项目,将Eureka章节的eureka-server、eureka-service-invoker、eureka-service-provider 模块拷贝到 zuul 下

用springboot 创建 zuul-server模块,勾选Spring Cloud Routing->Zuul [Maintenance],Spring Cloud Discovery->Eureka Server

在启动类上使用@EnableZuulProxy注解开启路由功能,使用@EnableEurekaClient注解开启eureka注册功能

配置文件加入基本信息

spring:
application:
name: zuul-server
server:
port: 5555
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
zuul:
routes:
api-1:
path: /api-1/**
serviceId: eureka-service-invoker
api-2:
path: /api-2/**
serviceId: eureka-service-provider

我们这样就完成了配置

打开浏览器访问

http://localhost:9000/router/123和http://localhost:5555/api-1/router/123

http://localhost:8888/person/123和http://localhost:5555/api-2/person/123

发现获取到的内容一样

通过上面的面向服务的配置方式,我们不需要再为各个路由维护微服务应用的具体实例的位置,而是通过简单的path与serviceld的映射组合,使得维护工作变得非常简单。这完全归功于Spring Cloud Eureka的服务发现机制,它使得API网关服务可以自动化完成服务实例清单的维护,完美地解决了对路由映射实例的维护问题。

注意:如果在项目的配置文件没有加入 zuul.routes... 的配置,zuul 会自动为每个应用根据名字创建对应的路径。类似于前面开发中的项目名。但是前提条件是所有项目的 contentPath 都只是斜杠。



网关请求过滤(自学)

在实现了请求路由功能之后,我们的微服务应用提供的接口就可以通过统一的 API 网关入口被客户端访问到了。但是,每个客户端用户请求微服务应用提供的接口时,它们的访问权限往往都有一定的限制,系统并不会将所有的微服务接口都对它们开放。然而,目前的服务路由并没有限制权限这样的功能,所有请求都会被毫无保留地转发到具体的应用并返回结果,为了实现对客户端请求的安全校验和权限控制,最简单和粗暴的方法就是为每个微服务应用都实现一套用于校验签名和鉴别权限的过滤器或拦截器。不过,这样的做法并不可取,它会增加日后系统的维护难度,因为同一个系统中的各种校验逻辑很多情况下都是大致相同或类似的,这样的实现方式会使得相似的校验逻辑代码被分散到了各个微服务中去,冗余代码的出现是我们不希望看到的。所以,比较好的做法是将这些校验逻辑剥离出去,构建出一个独立的鉴权服务。在完成了剥离之后,有不少开发者会直接在微服务应用中通过调用鉴权服务来实现校验,但是这样的做法仅仅只是解决了鉴权逻辑的分离,并没有在本质上将这部分不属于冗余的逻辑从原有的微服务应用中拆分出,冗余的拦截器或过滤器依然会存在。

对于这样的问题,更好的做法是通过前置的网关服务来完成这些非业务性质的校验。由于网关服务的加入,外部客户端访问我们的系统已经有了统一入口,既然这些校验与具体业务无关,那何不在请求到达的时候就完成校验和过滤,而不是转发后再过滤而导致更长的请求延迟。同时,通过在网关中完成校验和过滤,微服务应用端就可以去除各种复杂的过滤器和拦截器了,这使得微服务应用接口的开发和测试复杂度也得到了相应降低。

为了在API网关中实现对客户端请求的校验,我们将继续介绍 Spring Cloud Zuul 的另外一个核心功能:请求过滤。Zuul 允许开发者在 API 网关上通过定义过滤器来实现对请求的拦截与过滤,实现的方法非常简单,我们只需要继承 ZuulFilter 抽象类并实现它定义的 4 个抽象函数就可以完成对请求的拦截和过滤了。

创建一个类 AccessFilter

public class AccessFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}

@Override
public int filterOrder() {
    return 0;
}

@Override
public boolean shouldFilter() {
    return true;
}

@Override
public Object run() throws ZuulException {
    RequestContext currentContext = RequestContext.getCurrentContext();
    HttpServletRequest request = currentContext.getRequest();
    String token = request.getParameter("token");
    if (StringUtils.isEmpty(token)) {
        System.out.println("token is empty");
        currentContext.setSendZuulResponse(false);
        currentContext.setResponseStatusCode(401);
        return null;
    }
    return null;
}

}

在上面实现的过滤器代码中,我们通过继承ZuulFilter抽象类并重写下面4个方法来实现自定义的过滤器。这4个方法分别定义了如下内容。

●filterType:过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。这里定义为 pre,代表会在请求被路由之前执行。

●filterOrder:过滤器的执行顺序。当请求在一个阶段中存在多个过滤器时,需要根据该方法返回的值来依次执行。

●shouldFilter:判断该过滤器是否需要被执行。这里我们直接返回了 true,因此该过滤器对所有请求都会生效。实际运用中我们可以利用该函数来指定过滤器的有效范围。

●run:过滤器的具体逻辑。这里我们通过 ctx.setSendZuulResponse(false) 令 zuul 过滤该请求,不对其进行路由,然后通过 ctx.setResponseStatusCode(401) 设置了其返回的错误码,当然也可以进一步优化我们的返回,比如,通过 ctx.setResponseBody(body) 对返回的 body 内容进行编辑等。

在实现了自定义过滤器之后,它并不会直接生效,我们还需要为其创建具体的 Bean 才能启动该过滤器。

访问 http://localhost:5555/api-1/router/123 发现报错。地址加上?token=12 后能正常访问了。

到这里,对于 API 网关服务的快速入门示例就完成了。相信经过上面的学习我们很快认识到 API 网关服务对微服务架构的重要性了,就目前掌握的 API 网关知识,我们可以将具体总结如下:

  • 它作为系统的统一入口,屏蔽了系统内部各个微服务的细节。
  • 它可以与服务治理框架结合,实现自动化的服务实例维护以及负载均衡的路由转发。
  • 它可以实现接口权限校验与微服务业务逻辑的解耦。
  • 通过服务网关中的过滤器,在各生命周期中去校验请求的内容,将原本在对外服务层做的校验前移,保证了微服务的无状态性,同时降低了微服务的测试难度,让服务本身更集中关注业务逻辑的处理。

章节练习:

1.在大型商城项目开发中项目分模块开发和部署是非常常用的事,创建一个项目 mall,里面有如下几个模块商品模块(mall-product)、订单模块(mall-order)营销模块(mall-market),会员模块(mall-members)。在订单模块中创建一个服务接口(findOrderById),在商品模块中调用该服务接口。同时为所有模块使用统一的网关调用。



项目考核:商城项目(9天)

该项目分为商城前台,商城后台,APP 端三个模块,根据班级人数进行适当安排,每小组 5~8 人。

商城前台:

1.参照智原商城,完成数据库设计和整个购买流程。


商城后台:

1.参照智原商城,完成数据库设计和整个商品管理流程。


APP 端

1.参考 uni 官方文档,和模板项目,完成 APP 整个购买流程。


提交内容:

由于所有操作都是在码云和云文档上,故不需要提交内容。


posted @ 2021-10-04 02:49  柠檬色的橘猫  阅读(65)  评论(0)    收藏  举报