SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)

SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)

继续接上一篇~

1、服务网关

1.1、Zuul

说道服务网关,也是微服务架构中非常重要的一个组件!我们的网关是所有微服务的入口,几乎所有微服务架构都需要服务网关,让网关统一的挡在"前面",帮助我们做一些日志记录、限流、权鉴、安全架构等工作。例如医院的门诊室,先由门诊室进行初步诊断,再分发到各个科室具体医治。

然而关于Zuul组件,netflix公司也发现了存在一些问题,目前已经停更!想推出Zuul2,但是由于开发人员的变动和技术上选型的分歧,迟迟未能推出Zuul2。市面上使用的企业也逐渐在下降,因此按Spring官网的推荐我们应该把学习成本放在新一代的网关组件Gateway身上。

1.2、Gateway

gateway网关组件是Spring社区自研的全新网关组件,也是由于Zuul的停更,Zuul2迟迟未推出,导致SpringCloud没有更好的网关组件代替,因此社区吸收了Zuul优秀的理念,并且建立在Spring Boot 2.x、Spring WebFluxProject Reactor之上,自研了一套网关组件Spring Cloud Gateway。更加符合Spring未来发展趋势。

官网:https://spring.io/projects/spring-cloud-gateway

1.2.1、Gateway概述

gateway就是原zuul 1.x版的代替。是在Spring生态系统之上构建的API网关服务,基于Spring 5、Spring Boot 2Project Reactor等技术。

gateway旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第1张图片
官网介绍说为了提高网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层使用的是高性能的Reactor模式通信框架NettySpringCloud Gateway的目标是提供统一的路由方式且基于Filter链的方式提供了网关基本的功能,例如:安全、监控/指标、限流等。

1.2.2、为什么选择Gateway

一方面因为Zuul 1.0已经进入了维护阶段,而且GatewaySpringCloud团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul都没有用起来而到了Gateway这里非常的简单便捷。

Gateway是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix也发布了最新的Zuul 2,但SpringCloud貌似没有整合的计划,而且Netflix相关组件都宣布进入维护期,前景堪忧!

多方面综合考虑Gateway是很理想的网关组件选择。

最重要的一点,Spring Cloud Gateway具有如下特性:

  • 基于Spring Framework 5,Project Reactor 和 Spring Boot 2 进行构建
  • 动态路由,能够匹配任何请求属性
  • 可以对路由指定Predicate(断言)Filter(过滤器),并且易于编写
  • 集成Hystrix的断路器功能
  • 集成SpringCloud服务发现功能
  • 请求限流
  • 支持路径重写

更多关于ZuulGateway的区别与联系请观看视频讲解:https://www.bilibili.com/video/BV18E411x7eT?p=67

1.2.3、Gateway的三大核心概念

Route(路由):路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true则匹配该路由。

Predicate(断言):参考的是Java8的java.util.function.Predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由

Filter(过滤):指的是Spring框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。

总结下来就是,我们的web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。Predicate就是我们的匹配条件,而Filter,就可以理解为一个无所不能的拦截器。有了拦截器。有了这两个元素,再加上目标URI,就可以实现一个具体的路由了。

核心逻辑:路由转发 + 执行过滤器链
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第2张图片
如上图所示!客户端向Spring Cloud Gateway发出请求。然后再Gateway Handler Mapping中找到与请求相匹配的路由。将其发送到Gateway Web Handler

Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在代理请求之前pre或之后post执行业务逻辑。

Filterpre类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在post类型的过滤器中可以做响应内容、响应头修改、日志的输出、流量监控等有着非常重要的作用。

1.2.4、实验环境搭建

了解完理论,最终还是要回到代码中!落地实现才是重点!

1、使用Maven或者SpringBoot的初始化向导新建cloud_gateway9527模块pom文件依赖如下

pom.xml


<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">
    <parent>
        <artifactId>cloudartifactId>
        <groupId>com.laizhenghua.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>
    <artifactId>cloud_gateway9527artifactId>
    <dependencies>
        <dependency>
            <groupId>com.laizhenghua.springcloudgroupId>
            <artifactId>cloud_api_commonsartifactId>
            <version>${project.version}version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-gatewayartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
    dependencies>
project>

还是和以前一样,先引入这些坐标!以后需要用到什么我们在引入什么即可。

2、编写application.yml文件

server:
  port: 9527
spring:
  application:
    name: gateway-service
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

3、编写主程序

/**
 * @description: 主程序
 * @date: 2022/2/16 20:56
 */
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class GatewayMain {
    public static void main(String[] args) {
        SpringApplication.run(GatewayMain.class, args);
    }
}

启动eureka注册中心,启动8001服务!注意8001服务提供的地址有

  1. http://127.0.0.1:8001/payment/testError
  2. http://127.0.0.1:8001/payment/testOk
  3. http://127.0.0.1:8001/payment/circuitBreaker/{id}

那么网关如何做路由映射呢?我们目前不想暴露8001端口,希望在8001外面套一层9527!完成路由的匹配与配置断言规则我们一般在application.yml文件中配置(也可以编写配置类进行配置),例如:

server:
  port: 9527
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      routes:
        - id: paymentCircuitBreakerRoute  # 路由的ID,没有固定的规则但要求唯一,建议配合服务名
          uri: http://127.0.0.1:8001  # 匹配后提供服务的路由地址(目标地址)
          predicates:
            - Path=/payment/circuitBreaker/**  # 断言,路径相匹配的进行路由
        - id: paymentRoute
          uri: http://127.0.0.1:8001
          predicates:
            - Path=/payment/**
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

启动Gateway服务,启动集成Spring Cloud Gateway的服务坑比较多!除了最常见的spring-boot-starter-webspring-boot-starter-webflux依赖冲突!更多的是SpringCloud的版本适配原因。推荐版本:

<properties>
	<spring-boot.version>2.1.1spring-boot.version>
	<spring-cloud.version>Hoxton.SR1spring-cloud.version>
	<com-alibaba-cloud.version>2.2.1.RELEASEcom-alibaba-cloud.version>
properties>

在这里插入图片描述
4、测试

未添加网关组件之前
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第3张图片
添加网关组件之后(我们可以只暴露9527这个端口,完成更多高级的操作)
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第4张图片

1.2.5、Gateway配置路由的两种方式

1、在配置文件application.yml中配置,详见上一章节!

2、代码中注入RouteLocator的Bean实例。这里重点介绍。

定个目标:我们要实现的效果就是通过9527网关访问到外网的百度新闻网站(http://news.baidu.com/)

GatewayConfig.java

/**
 * @description: SpringCloud Gateway Config
 * @date: 2022/2/16 22:55
 */
@Configuration
public class GatewayConfig {
    @Bean
    public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
        RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
        routes.route("newsRoute", r -> r.path("/news").uri("http://news.baidu.com/")).build();
        return routes.build();
    }
}

测试(访问:http://127.0.0.1:9527/news):
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第5张图片
推荐还是在配置文件中编写,方便后面维护(不需要修改代码!)。

1.2.6、Gateway配置动态路由

前面我们介绍的配置方式,存在一定的局限性!就是把路由写死了。不方便服务提供者扩容(增加集群数量),例如前面介绍的Riibon组件,调用服务时自带负载均衡,而现在有了网关后,我们只暴露一个端口(9527),而集群服务有多个端口,如何让网关按照特定的策略(负载均衡、随机等)路由到某个端口,便成了一大问题!解决方案:根据服务名配置动态路由。

Gateway的动态路由:默认情况下Gateway会根据注册中注册的服务列表,以注册中心上的服务名为路径创建动态路由进行转发,从而事项动态路由的功能

准备环境:一个7001Eureka注册中心 + 两个服务提供者8001/8002
在这里插入图片描述
准备好环境后,我们需要修改网关服务的配置文件(开启从注册中心动态创建路由的功能等)

application.yml

server:
  port: 9527
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: paymentCircuitBreakerRoute  # 路由的ID,没有固定的规则但要求唯一,建议配合服务名
          uri: http://127.0.0.1:8001  # 匹配后提供服务的路由地址(目标地址)
          predicates:
            - Path=/payment/circuitBreaker/**  # 断言,路径相匹配的进行路由
        - id: paymentRoute
          # uri: http://127.0.0.1:8001
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/**
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://eureka7001.com:7001/eureka/

注意查看路由ID = paymentRouteuri的配置。启动9527Gateway网关服务进行测试!
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第6张图片
这样就实现了网关的动态路由功能,以服务名为路径创建动态路由并进行负载均衡转发。

1.2.7、predicates常用配置

我们在启动网关服务时,控制台会打印出如下日志:
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第7张图片
关于以上列举的断言配置规则,官网上也有详细介绍

官网地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#gateway-request-predicates-factories

After Route Predicate:采用一个参数(datetime是一个javaZoneDateTime)在指定日期时间之后才会进行路由转发,配置示例:

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: paymentRoute
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/**
            - After=2022-02-19T11:17:52.335+08:00[Asia/Shanghai]

如何获取指定的时间格式呢?

@Test
public void getSysDate() {
    ZonedDateTime zonedDateTime = ZonedDateTime.now(); // 默认时区
    System.out.println(zonedDateTime); // 2022-02-19T11:17:52.335+08:00[Asia/Shanghai]
}

Before Route PredicateBetween Route Predicate与上面类似,见名知意,略。

Cookie Route Predicate:需要配置两个参数分别是Cookie的Cookie name与Java的一个正则表达式,路由规则会通过获取对应的Cookie name值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行,配置示例:

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: paymentRoute
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/**
            - Cookie=username,Alex

使用curl命令进行测试

curl http://127.0.0.1:9527/payment/getPaymentById/1 --cookie "username=Alex"

在这里插入图片描述
Header Route Predicate:需要配置两个参数一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行,配置示例:

spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: paymentRoute
          # uri: http://127.0.0.1:8001
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/**
            # - After=2022-02-19T11:17:52.335+08:00[Asia/Shanghai]
            # - Cookie=username, Alex
            - Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性并且值为整数的正则表达式

重启测试:
在这里插入图片描述
Query Route Predicate:需要配置两个参数,一个是属性名,一个是属性值,属性值可以是正则表达式。配置示例:

server:
  port: 9527
spring:
  application:
    name: gateway-service
  cloud:
    gateway:
      discovery:
        locator:
          enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
      routes:
        - id: paymentRoute
          # uri: http://127.0.0.1:8001
          uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
          predicates:
            - Path=/payment/**
            - Query=age, \d+ # 要有参数名age并且还要是整数才能路由

重新启动网关服务进行测试

curl http://127.0.0.1:9527/payment/getPaymentById/1?age=18

SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第8张图片
小结:说白了,Predicate就是为了实现一组匹配规则,让请求过来找到对应的Route进行处理。

1.2.8、过滤器(GatewayFilter)

前面我们也介绍了SpringCloud Gateway的三大核心概念,过滤器就是其中之一。也是SpringCloud Gateway的特色功能。

据官网介绍得知:SpringCloud Gateway内置了多种路由过滤器,他们都是由GatewayFilter的工厂类来产生。路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应。路由过滤器只能指定路由进行使用。

SpringCloud Gateway内置的过滤器(局部的和全局的):
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第9张图片
内置的过滤器局部的总共有30多个全局的也有10多个,也不一定要全部梳理一遍,用到那个去官网查询配置示例即可。这里重要的是我们要学会自定义全局的GatewayFilter,来完成项目上所需的功能例如全局日志记录、统一网关鉴权等

自定义过滤器GatewayFilter需要实现两个接口分别是GlobalFilterOrdered。例如:

/**
 * @description: 自定义的 GatewayFilter 主要实现功能全局日志记录
 * @date: 2022/2/19 20:23
 */
@Slf4j
@Component
public class LogGatewayFilter implements GlobalFilter, Ordered {

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("come in LogGatewayFilter -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
        String username = exchange.getRequest().getQueryParams().getFirst("username");
        if (username == null) {
            log.info("username is null");
            exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
            return exchange.getResponse().setComplete();
        }
        return chain.filter(exchange);
    }

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

重新启动网关服务测试带和不带username请求参数。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第10张图片
小结:官网上内置的过滤器其实对于我们来说应用场景不多,更多的是使用我们自定义的过滤器,去完成某个业务!

2、服务的配置中心

服务的配置中心在分布式微服务架构中也非常重要,它主要解决服务的配置文件统一管理与修改!例如前面我们从服务的注册与发现学到服务的配置中心,已经搭建了10多个服务模块!每个模块都有自己的application.yml文件,越来越臃肿,导致后面维护非常不方便(假如需要修改4个服务的数据库连接信息,需要手动一个个找出来修改并重新启动服务,非常的麻烦与容易出错)这也是分布式微服务面临的一大问题。

2.1、SpringCloud Config

微服务意味着将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少缺少的。

SpringCloud提供了configServer来解决这个问题。

2.1.1、SpringCloud Config概述

SpringCloud Config为微服务架构的微服务提供了集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置

在配置中心可以配置公用的和私用的配置,功能非常的强大,比如上述所述修改数据库连接信息问题,我们就可以把连接信息配到公用的配置文件,修改方便,并且不用重启服务。

SpringCloud Config分为服务端客户端两部分。

  1. 服务端也称为配置中心,它是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取获取配置信息,加密/解密信息等访问接口。
  2. 客户端则是通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。配置服务器默认采用git来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。

总结下来Spring Cloud Config的功能有:

  • 集中管理配置文件
  • 不同环境不同配置,动态化的配置更新,分环境部署比如dev/test/prod
  • 运行期间动态调整配置,不再需要在每个服务部署的机器上编写或更改配置文件,服务会向配置中心统一拉取配置自己的信息
  • 当配置发生变动时,服务不需要重启即可感知到配置的变化并应用新的配置
  • 将配置信息以REST接口的形式暴露
2.1.2、配置中心服务端的搭建

前面我们已经介绍了,Spring Cloud Config服务端的配置信息是使用git来存储的,因此我们需要与GitHub整合配置!

1、使用自己的GitHub账号新建一个名为springcloud-config的新Repository
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第11张图片
2、由上一步获取Repository 地址:https://github.com/laizhenghua2/springcloud-config.git

3、本地电脑目录下新建git仓库并clone,并新建如下文件(yml文件以UTF-8格式保存)
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第12张图片
4、提交至仓库

git add .
git commit -m "init .yml config file"
git push -u origin master

# 踩坑记录:使用命令 ssh-keygen -t rsa -C "[email protected]" 生成的公钥,使用notepad++打开后复制,会损坏公钥,导致 SSH keys 添加不成功
clip < ~/.ssh/id_rsa.pub
# 需要使用此命令直接把公钥复制到剪贴板

至此GitHub这边已经准备完毕!接下来就是搭建config server

1、使用maven或初始化向导新建cloud_config_center3344模块,pom.xml依赖坐标如下


<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">
    <parent>
        <artifactId>cloudartifactId>
        <groupId>com.laizhenghua.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloud_config_center_3344artifactId>
    <dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-config-serverartifactId>
        dependency>

project>

暂时先添加这些,后面使用到什么添加什么即可。

2、编写application.yml配置文件

server:
  port: 3344
spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
          uri: https://github.com/laizhenghua2/springcloud-config.git # GitHub HTTPS 仓库地址
          search-paths:
            - spring-cloud-config # 搜索目录
          force-pull: true
          username: xxxxx # GitHub 账号
          password: xxxxx # GitHub 密码
      label: main # 读取的分支
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/

3、编写主程序

/**
 * @description: 主程序
 * @date: 2022/2/20 10:24
 */
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain {
    public static void main(String[] args) {
        SpringApplication.run(ConfigCenterMain.class, args);
    }
}

启动服务,测试通过config微服务是否可以从GitHub上获取配置内容。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第13张图片
OK,成功实现了用SpringCloud Config通过GitHub获取配置信息。

2.1.3、配置读取规则

关于配置读取在官网SpringCloud config章有详细文档,包括前面整合GitHub会出现的问题以及解决方案等都有详细的文档。

在这里主要整理下常用的配置读取规则:

1、/{label}/{application}-{profile}.yml:label就是分支,application是文件名,profile是环境名。读取示例:

http://127.0.0.1:3344/main/config-dev.yml
# 因此我们编写外部配置文件时,一般文件名与环境名使用 - 连接 

2、/{application}-{profile}.yml:默认找config服务配置的分支,读取示例:

http://127.0.0.1:3344/config-test.yml

3、/{application}/{profile}/{label},读取示例:

http://127.0.0.1:3344/main/config/prod/master
2.1.4、配置中心客户端的搭建

前面我们也说了,SpringCloud Config分为客户端与服务端,因此还需要搭建一个客户端模块(个人感觉好麻烦,还是学Nacos比较香)。

1、使用maven或初始化向导新建cloud_config_client_3355模块,pom.xml依赖坐标如下


<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">
    <parent>
        <artifactId>cloudartifactId>
        <groupId>com.laizhenghua.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>

    <artifactId>cloud_config_client_3355artifactId>

    <dependencies>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-configartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
    dependencies>

project>

2、编写配置文件(编写bootstrap.yml系统级配置文件),为了配置文件的加载顺序和分级管理这里编写bootstrap.yml

application.yml:是用户级的资源配置项

bootstrap.yml:是系统级的,优先级更高

Spring Cloud会创建一个Bootstrap Context,作为Spring应用的Application Context父上下文。初始化的时候,Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment

Bootstrap属性有更高的优先级,默认情况下,他们不会被本地配置覆盖。Boorstrap ContextApplication Context有着不同的约定,所以增加了一个bootstrap.yml文件、保证Bootstrap ContextApplication Context的配置分离。

因此要将client模块下的application.yml文件改为bootstrap.yml这是很关键的一步。

server:
  port: 3355
spring:
  application:
    name: config-client
  cloud:
    config:
      label: main # 分支名称
      name: config # 配置文件名称
      profile: dev
      uri: http://127.0.0.1:3344 # 配置中心的地址
      # 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/

3、编写主程序

/**
 * @description:
 * @date: 2022/2/20 19:55
 */
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain {
    public static void main(String[] args) {
        SpringApplication.run(ConfigClientMain.class, args);
    }
}

4、编写业务方法进行测试

/**
 * @description:
 * @date: 2022/2/20 19:58
 */
@Slf4j
@RestController
@RequestMapping(path = "/config")
public class ConfigController {
    @Value(value = "${config.info}")
    private String configInfo;

    @RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
    public R getConfigInfo() {
        return R.ok().put("data", configInfo);
    }
}

5、启动3355服务进行测试
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第14张图片
至此,成功实现了客户端3355访问SpringCloud Config 3344通过GitHub获取配置信息。

当然还有一些问题我们没有解决,例如:在GitHub中修改配置文件,config服务端确定实时同步过来,而config客户端却不可以!

服务端
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第15张图片
客户端(version:0.1)
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第16张图片
向这种情况我们重启config客户端就能同步过来!但是每次修改后都要重启或重新加载,Spring Cloud Config显然不可能这么设计!因此Spring Cloud Config提供了动态刷新功能,避免每次更新配置都要重启客户端服务3355

2.1.5、动态刷新

1、首先需要添加actuator依赖,主要目的是自己模块发生变化后其他模块要能监控到。

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

2、bootstrap.yml添加新的配置,暴露监控的端点

management:
  endpoints:
    web:
      exposure:
        include: "*" # 暴露监控端点

3、controller类添加@RefreshScope注解,开启刷新功能

/**
 * @description: ConfigController
 * @date: 2022/2/20 19:58
 */
@Slf4j
@RefreshScope
@RestController
@RequestMapping(path = "/config")
public class ConfigController {
    @Value(value = "${config.info}")
    private String configInfo;

    @RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
    public R getConfigInfo() {
        return R.ok().put("data", configInfo);
    }
}

4、重新启动config客户端服务进行测试。注意:我们改了这么多配置,动态刷新配置还是没有生效!这是因为需要发送一个POST请求才能刷新config客户端服务。

curl -X POST "http://127.0.0.1:3355/actuator/refresh"

在这里插入图片描述
现在我们的动态刷新功能就生效了,比起重新启动服务好了那么一点。弊端就是每次修改配置文件,都需要发一次这样的请求,才能生效。并且最重要的一点如果有多个config客户端,每个客户端都要手动发一次POST请求,还是很麻烦。

因此对于自动刷新功能我们还有优化的空间,通过广播的方式,一次通知处处生效。实现分布式自动刷新配置功能。而广播涉及到了服务间的通信问题,需要一个新的组件Spring Cloud Bus来完成。

2.2、Nacos

Nacos属于重点组件!需要认真准备。未完待续~

https://blog.csdn.net/m0_46357847/article/details/123142004

3、服务总线

什么是总线?

在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个共同的消息主题,并让系统中的所有微服务实例都连接上来。由于该主题中生产的消息会被所有实例监听和消费,所以称它为消息总线。在总线上的各个实例,都可以方便的广播一些需要让其他连接在该主题上的实例都知道的消息。

基本原理

ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus)。当一个服务刷新数据的时候,它会吧这个消息放入到topic中,这样其他监听同一个topic的服务就能得到通知,然后去更新自身的配置。

3.1、Spring Cloud Bus

前面我们也引出了可以通过广播的方式通知各个Spring Cloud Config客户端服务,让各个服务刷新配置!所以Spring Cloud ConfigSpring Cloud Bus可以说是相辅相成!一起使用才能发挥出更多的优势。

3.1.1、Spring Cloud Bus概述

再次声明:Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新。

Spring Cloud Bus是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第17张图片
Spring Cloud Bus支持两种消息代理:RabbitMQKafka

Spring Cloud Bus的具体功能:管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。

3.1.2、RabbitMQ环境搭建

直接使用Docker进行构建,拆箱即用省去一切安装烦恼!

# 1.拉取镜像,注意一定要下标签带类似 3.8.14-management 有 management(管理界面)的,方便我们从web端查看
docker pull rabbitmq:3.8.14-management

# 2.启动 RabbitMQ 容器
[root@laizhenghua /]# docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq ee045987e252
32b2eb19574db54e977254665b78f2ab769e5fb594a395253bb96c4547a99b22

# 3.此时就可以通过 15672 端口访问 rabbitMQ 的管理界面

SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第18张图片
注意:账号密码都是guest

简单吧!真的是一键部署,我们后面的广播演示等,都必须要有良好的RabbitMQ环境!另外为了更好的看到实验效果,在上面配置中心的环境下我们新增cloud_config_client_3366服务模块。前面搭建步骤已经写过很多了,这里就不重复啰嗦了。

1、pom.xml


<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">
    <parent>
        <artifactId>cloudartifactId>
        <groupId>com.laizhenghua.springcloudgroupId>
        <version>1.0-SNAPSHOTversion>
    parent>
    <modelVersion>4.0.0modelVersion>
    <artifactId>cloud_config_client_3366artifactId>
    <dependencies>
        <dependency>
            <groupId>com.laizhenghua.springcloudgroupId>
            <artifactId>cloud_api_commonsartifactId>
            <version>${project.version}version>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-configartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
        
        <dependency>
            <groupId>org.springframework.cloudgroupId>
            <artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-actuatorartifactId>
        dependency>
    dependencies>
project>

2、bootstrap.yml

server:
  port: 3366
spring:
  application:
    name: config-client
  cloud:
    config:
      label: main # 分支名称
      name: config # 配置文件名称
      profile: dev
      uri: http://127.0.0.1:3344 # 配置中心的地址
      # 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/
management:
  endpoints:
    web:
      exposure:
        include: "*" # 暴露监控端点

3、修改业务方法,区分是哪一个服务

/**
 * @description: ConfigController
 * @date: 2022/2/20 19:58
 */
@Slf4j
@RefreshScope
@RestController
@RequestMapping(path = "/config")
public class ConfigController {

    @Value(value = "${config.info}")
    private String configInfo;

    @Value(value = "${server.port}")
    private String serverPort;

    @RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
    public R getConfigInfo() {
        return R.ok().put("data", configInfo).put("port", serverPort);
    }
}

SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第19张图片

3.1.3、动态刷新之全局广播

至此,我们的环境已经搭建完毕。前面也说了Spring Cloud Bus配合Spring Cloud Config使用可以实现配置的动态刷新,那么为什么要这样设计呢?实现这一功能的思想又是什么呢?

1、利用消息总线触发一个客户端ConfigClient的/bus/refresh,而刷新所有客户端的配置。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第20张图片
2、利用消息总线触发一个服务端ConfigServer的/bus/refresh,而刷新所有客户端的配置。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第21张图片
两种广播方式,应该选择第二种,由ConfigServer/bus/refresh更合适因为:

  • 第一种打破了微服务的单一职责性,拆分出来的微服务应该只关注业务本身,不应该承担配置刷新的职责
  • 第一种也破坏了微服务各节点的对等性
  • 由客户端来通知刷新,也有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动刷新,就会增加更多的修改。

因此我们在ConfigServer-3344服务模块中配置全局广播的支持,实现自动刷新配置。

1、新增RabbitMQ的stater坐标依赖以及actuator的坐标依赖


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

2、修改application.yml文件,新增RabbitMQactuator的一些配置

server:
  port: 3344
spring:
  application:
    name: cloud-config-center
  cloud:
    config:
      server:
        git:
          uri: https://github.com/laizhenghua2/springcloud-config.git # GitHub HTTPS 仓库地址
          search-paths:
            - spring-cloud-config # 搜索目录
          force-pull: true
          username: xxxx # GitHub 账号
          password: xxxx # GitHub 密码
      label: main # 读取的分支
  # RabbitMQ的配置
  rabbitmq:
    host: 180.76.238.29
    port: 5672
    username: guest
    password: guest
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/

# RabbitMQ相关配置,暴露bus刷新配置的端点
management:
  endpoints:
    web:
      exposure:
        include: 'bus-refresh'

3、给cloud_config_client_3355cloud_config_client_3366客户端添加消息总线的支持

pom.xml


<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-actuatorartifactId>
dependency>

bootstrap.yml

server:
  port: 3366
spring:
  application:
    name: config-client
  cloud:
    config:
      label: main # 分支名称
      name: config # 配置文件名称
      profile: dev
      uri: http://127.0.0.1:3344 # 配置中心的地址
      # 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
  rabbitmq:
    host: 180.76.238.29
    port: 5672
    username: guest
    password: guest
eureka:
  client:
    register-with-eureka: true
    fetch-registry: true
    service-url:
      defaultZone: http://127.0.0.1:7001/eureka/
management:
  endpoints:
    web:
      exposure:
        include: "*" # 暴露监控端点

4、启动服务,进行测试,一次修改,广播通知,处处生效。

curl -X POST "http://127.0.0.1:3344/actuator/bus-refresh"

4、消息驱动 Spring Cloud Stream

4.1、Spring Cloud Stream 概述

SpringCloud Stream的产生绝非偶然,传统的消息中间件有ActiveMQ / RabbitMQ / RocketMQ / Kafka等,当项目到达一定规模的时候,有可能存在两种MQ!例如项目后台使用RabbitMQ,大数据这边使用Kafka,此时也带来了一些痛点,切换、开发、维护非常的不方便。

因此出现了一种新的技术SpringCloud Stream,官方定义是:一个构建消息驱动微服务的框架。让我们不再关注具体的MQ的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ内切换。就像Hibernate一样统一提供sesstionAPI,无需关注各数据库厂商的语法。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第22张图片
如上图蓝色部分所示,SpringCloud Stream的核心就是Binder,应用程序通过inputs或者outputs来与Spring Cloud Stream中的Binder对象交互,通过我们配置来binding(绑定),而Spring Cloud Stream 的Binder对象负责与消息中间件交互,所以我们只需搞清楚如何与Spring Cloud Stream 交互就可以方便使用消息驱动的方式。

另外Spring Cloud Stream为一些供应商的消息代理中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。但是目前只支持RabbitMQKafka

一句话总结就是:屏蔽底层消息中间件的差异,降低切换成本,统一消息的变成模型。

4.2、Stream的设计思想

在传统的MQ中,我们知道生产者 / 消费者之间是靠消息媒介传递消息内容(Message),消息也必须走特定的通道(Message Channel),因此我们更多关注的是通道里的消息如何被消费?谁来负责收发处理?

消息通道Message Channel的子接口Subscribale Channel,由MessageHandler消息处理器所订阅。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第23张图片
而到了Stream这里,设定一个目标绑定器(Destination Binder)负责在消息通道里的消息收发处理!

在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间加进行信息交互的时候,由于消息中间件构建的初衷不同,他们的实现细节上也会有较大的差异,通过定义绑定器作为中间层,完美的实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel通道,使得应用程序不需要再考虑各种不同的消息中间件的实现。

即:通过定义绑定器Bander作为中间层,实现了应用程序与消息中间件细节之间的隔离。

而且Stream中的消息通信方式也遵循了发布-订阅模式,使用Topic主题进行广播,在RabbitMQ就是Exchange,在Kafka中就是Topic

4.3、Stream常用的概念与注解

1、Binder:很方便的连接中间件,屏蔽差异,是应用于消息中间件的封装,目前只实现了KafkaRabbitMQ的Binder。

2、Channel:通道,是队列Queue的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel队列进行配置

3、SourceSink:简单的可以理解为参照对象是Spring Cloud Stream自身,从Stream发布消息就是输出,接受消息就是输入。

注解 说明
@Input 注解标识输入通道,通过该输入通道接收到的消息进入应用程序
@Output 注解标识输入通道,发布的消息将通过该通道离开应用程序
@StreamListener 监听队列,用于消费者的队列的消息接收
@EnableBinding 指信道channel和exchange绑定在一起

4.4、案例实现

关于实验环境首先需要确保RabbitMQ环境OK!其次除了注册中心,还需要有如下模块:

  1. cloud_rabbitmq_provider_8801作为生产者进行发消息模块
  2. cloud_rabbitmq_consumer_8802作为消息接收模块
  3. cloud_rabbitmq_provider_8803作为消息接收模块

SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第24张图片
搭建工程模块略!建议就是使用SpringBoot的初始化向导进行搭建,版本适配问题经常发生,导致项目启动各种报错。 SpringBoot的初始化向导自动集成了spring-boot-starter-parent,也就是版本仲裁机制,让我们无需关注依赖的版本号。因此为了避免报错建议还是使用初始化向导进行搭建。

例如核心的依赖(这里使用consul)做为服务的注册中心。

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-consul-discoveryartifactId>
dependency>

<dependency>
    <groupId>org.springframework.cloudgroupId>
    <artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>

提供者配置文件application.yml示例(注意bindings配置项提供者是output):

server:
  port: 8801
spring:
  application:
    name: cloud-provider-service
  rabbitmq:
    host: IP地址
    port: 5672
    username: admin
    password: admin
  cloud:
    # consul 的配置
    consul:
      host: IP地址
      port: 8500
      discovery:
        service-name: ${spring.application.name}
        heartbeat:
          enabled: true
    stream:
      binders: # 配置需要绑定的rabbitmq消息
        defaultRabbit:
          type: rabbit # 消息组件类型
      bindings:
        output: # 这个名字是一个通道的名称
          destination: testExchange # 表示要使用的Exchange名称
          content-type: application/json # 设置消息类型,本次为json
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置

消费者者配置文件application.yml示例(注意bindings配置项消费者是input):

server:
  port: 8802
spring:
  application:
    name: cloud-consumer-service
  rabbitmq:
    host: IP地址
    port: 5672
    username: admin
    password: admin
  cloud:
    # consul 的配置
    consul:
      host: IP地址
      port: 8500
      discovery:
        service-name: ${spring.application.name}
        heartbeat:
          enabled: true
    stream:
      binders: # 配置需要绑定的rabbitmq消息
        defaultRabbit:
          type: rabbit # 消息组件类型
      bindings:
        input: # 这个名字是一个通道的名称
          destination: testExchange # 表示要使用的Exchange名称
          content-type: application/json # 设置消息类型,本次为json
          binder: defaultRabbit # 设置要绑定的消息服务的具体设置

搭建好所有工程后,我们启动我们的服务。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第25张图片
启动成功后,查看RabbitMQ的exchanges
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第26张图片
我们发现正是我们配置文件配置的通道。OK至此所有环境已经准备完毕!接下来就是编写业务测试方法进行实验。

1、provider_8801编写消息发送方法

controller

/**
 * @description: MessageController
 * @date: 2022/2/23 20:02
 */
@Slf4j
@RestController
@RequestMapping(path = "/provider")
public class MessageController {

    @Autowired
    private MessageService messageService;

    @RequestMapping(path = "/send", method = RequestMethod.GET)
    public R sendMessage() {
        return R.ok().put("data", messageService.send());
    }
}

service

/**
 * @description: MessageService
 * @date: 2022/2/22 22:27
 */
@Slf4j
@EnableBinding(value = {Source.class}) // 定义消息的推送管道
public class MessageServiceImpl implements MessageService {

    @Autowired
    private MessageChannel output; // 消息发送管道

    @Override
    public String send() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String date = sdf.format(new Date());
        output.send(MessageBuilder.withPayload(date).build());
        log.info("message -> " + date);
        return date;
    }
}

2、8801服务执行发送方法
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第27张图片
我们发现消息提供者8801服务已经发送了两条消息,做为消息消费者的88028803如何通过Stream消费消息呢?
在这里插入图片描述
3、消息消费者8802/8803编写监听(消费)方法

/**
 * @description: ConsumerController
 * @date: 2022/2/23 21:02
 */
@Slf4j
@RestController
@EnableBinding(Sink.class)
public class ConsumerController {

    @Value(value = "${server.port}")
    private String serverPort;

    @StreamListener(Sink.INPUT)
    public void receiveMessage(Message<String> message) {
        log.info("service [" + serverPort + "] message received : " + message.getPayload());
    }
}

4、重新启动消费者服务,进行测试(生产者调用方法即可看到效果)
在这里插入图片描述
在这里插入图片描述

4.5、分组消费

前面的案例,我们已经完成了使用SpringCloudStream进行消息的生产与消费,并且没有写一行操作RabbitMQJava代码,完全屏蔽了RabbitMQ的内部实现细节。

到这里看似已经万事大吉了,其实并没有,通过Stream消费消息还会产生两种问题:

  1. 有重复消费问题
  2. 消息持久化问题

这两种问题使用过程中也是一定要避免的,特别是重复消费问题,如果处理不好Sream会带来不必要的麻烦!

也就是说目前消费者是集群环境,会同时消费8801发送过来的消息,存在重复消费的问题。就会造成数据错误,我们得避免这种情况,此时我们就可以使用Stream中的消息分组来解决。

消息分组:在Stream中处于同一个Group中的多个消费者是竞争关系,就能保证消息只会被其中的一个应用消费一次,不同组是可以全面消费的(重复消费)。

总结:同一组内会发生竞争关系,只有其中一个可以消费。

可以看下RabbitMQ的交换机的Bandings,默认是有几个服务就分几个组(组流水号不一样)!导致不同组全面消费。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第28张图片
那么如何让88028803变为一组呢?修改配置(增加一个group配置项)!例如8802配testA,例如8803配testB

bindings:
  input: # 这个名字是一个通道的名称
    destination: testExchange # 表示要使用的Exchange名称
    content-type: application/json # 设置消息类型,本次为json
    binder: defaultRabbit # 设置要绑定的消息服务的具体设置
    group: testB

效果如下,配置同一个,就可以让他们变为同一组,进而解决重复消费问题,并且分为同一组后自动采取轮询策略,交替消费服务。
SpringCloud - 基础入门(服务网关、服务配置、服务总线篇、消息驱动)_第29张图片

END

THANK YOU

END

你可能感兴趣的:(JavaEE,spring,cloud,微服务)