Spring Cloud 全体系学习笔记(REST、Nacos、Feign、Gateway)

Java 微服务

一、REST

REST,指的是 Re presentational S tate Transfer,表现层资源状态转移,其中涉及下面三个概念

  • 表现层 ,资源的表现形式,比如文本可以通过 txt、HTML、JSON 等形式甚至二进制的方式表现

  • 资源 ,网络上的一切都可以抽象为资源,对应唯一资源标识符(URI)

  • 状态转移 ,访问 URI,使用 HTTP 进行资源操作,对资源的增删查改都涉及到资源的状态转移

RESTful API 是指借鉴 REST 风格的接口设计,主要涉及两个概念

  • 资源 ,可以是音频、图片、视频、文本等。除二进制数据外,其他更多通过 JSON 的方式传递,通常从数据库中取到

  • 统一接口 ,RESTful 的接口,对于资源的具体操作只能从 HTTP 请求方法中看出,请求中只含有资源的种类,简单来说:接口只有名词,动词(即资源操作)体现在请求方法

REST 方法对于请求方法的要求较高,但是有的浏览器对于除 GET、POST 之外的请求方法支持不好;请求方法不在请求中直接体现,需要通过抓包的方式查看。所以依据情况选择设计风格

RestTemplate 是 Spring 提供的同步 HTTP 客户端,一般通过它调用 RESTful 的接口

二、服务注册与发现

2.1 CAP 理论

一个分布式系统,存在下面的关系图

  • P ,Partition Tolerance,分区容错性。这里的分区指的是网络分区,指的是分布式系统中各个节点沟通不畅的情况(是节点间的网络发生问题,而不是节点本身挂掉)

  • C ,Consistency,一致性,各个节点之间持有的数据是相同的

  • A ,Availability,可用性,不管何时对集群请求,总是能得到结果(正确性不保证)

根据 CAP 理论,C、A、P 是不可全部实现的

在实际操作中,网络波动是不可避免地,所以会选择实现分区容错性,那么接下来有两种架构方向:AP、CP

AP 即保证可用性,在出现网络分区时,为了保证数据可用性,仍然返回节点当前持有的数据(这意味着数据可能是错误的,或者说不是最新的),后续通过异步同步数据,最后实现数据的统一性(即 AP 架构放弃了强一致性,最终还是会实现数据一致的)

CP 即保证一致性,在发生网络分区时,选择不返回数据,等到网络分区结束并且数据同步后,再进行请求处理、数据返回

2.2 常见的注册中心

  • Eureka,AP

  • Zookeeper,CP

  • Nacos,AP/CP,默认 AP

2.3 负载均衡

Spring Cloud LoadBalancer 是 Spring Cloud 内置的负载均衡器,默认通过轮询的方式实现负载均衡(也可以改为随机的方式)

使用

给 RestTemplate 的配置加上 @LoadBalanced 注解,使用 RestTemplate 发送请求时,通过 service-name 代替 ip:port 的方式配置请求即可自动完成负载均衡的服务调用

Java 复制代码
@Component
public class BeanConfig {
    @Bean
    @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
    RestTemplate getRestTemplate(){
        return new RestTemplate();
    }
}

请求构造

Java 复制代码
String url = "http://product-service/product/" + orderInfo.getProductId();

Spring Cloud LoadBalancer 是通过 LoadBalanceInterceptor 拦截实现的,向服务注册中心获取服务并将服务名替换为 ip:port 的形式

需要注意的是,负载均衡的引入不能重复,不可以既导入 Enreka,又导入 Nacos

2.4 Eureka

2.4.1 Eureka Server
  • 导入依赖

    XML 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
  • 在微服务 SpringBoot 入口启动服务注册

    Java 复制代码
    @EnableEurekaServer //开启 Eureka 服务注册
    @SpringBootApplication
    public class EurekaServiceApplication {
    public static void main(String[] args){
        SpringApplication.*run*(EurekaServiceApplication.class, args);
    }
  • 配置 Eureka 服务注册地址

    YAML 复制代码
    eureka:
      instance:
        hostname: localhost
      client:
        fetch-registry: false 
    *    *register-with-eureka: false 
    *    *service-url:
    *      *defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

其中,eureka 几项配置为:

  • fetch-registry,是否从其他 Eureka 节点获取注册信息,默认是 true,如果是单点 Eureka 的话,设置为 false

  • register-with-eureka,是否将当前服务注册到 Eureka,默认是 true。对于一个 Eureka Server 本身来说,不需要进行注册,设置为 false

  • defaultZone,设置服务注册地址,后续注册服务、获取服务列表都需要使用这个 URL

2.4.2 Eureka Client
  • 导入依赖

    XML 复制代码
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  • 配置服务注册地址,注册自身服务

    YAML 复制代码
    eureka:
      client:
        service-url:
          defaultZone: http://127.0.0.1:10010/eureka
  • RestTemplate 开启负载均衡能力

    Java 复制代码
    @Configuration
    public class BeanConfig {
        @Bean
        @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
        public RestTemplate getRestTemplate(){
            return new RestTemplate();
        }
    }
  • 从服务中心获取服务

    Java 复制代码
    List<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表
    EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个
    String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用

2.5 Nacos

2.5.1 Docker 部署
Dockerfile 复制代码
sudo docker run -d   --name nacos   -p 8848:8848   -p 9848:9848   -e MODE=standalone   nacos/nacos-server:v2.3.2

使用 docker 部署后,进入容器可以看到 Nacos 的运行目录,如果出现问题时往往需要在这里进行处理

Shell 复制代码
diinkikot@DESKTOP-EI61K7J:~/nacos/bin$ sudo docker exec -it nacos /bin/bash
[root@79168aedf03c nacos]# ls
LICENSE  NOTICE  bin  conf  data  derby.log  logs  start.out  target  work

或者也可以在 Linux 通过 zip 压缩包解压后运行,目录如下

Shell 复制代码
diinkikot@DESKTOP-EI61K7J:~/nacos$ ls
LICENSE  LICENSE:Zone.Identifier  NOTICE  NOTICE:Zone.Identifier  bin  conf  data  logs  target

其中,bin 中包含了 nacos 启动、停止脚本,conf 中包含配置文件(如配置端口,默认为 8848,对应需同时保证 9848、9848 未被占用,data 中为服务数据

2.5.2 快速上手
  • 导入 Nacos 依赖

    XML 复制代码
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-loadbalancer</artifactId>
    </dependency>
  • 配置 Nacos 地址

    YAML 复制代码
    # application.yaml
    spring:
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
  • 进行远程调用:与 Eureka 不同,Nacos 不用通过微服务的方式运行,而是和 MySQL、Redis 一样单独运行;并且负载均衡的调用和 Eureka 的相同,因为都是通过 Spring Cloud LoadBalancer 实现负载均衡器 - 请求的转接

    • RestTemplate 开启负载均衡能力

      Java 复制代码
      @Configuration
      public class BeanConfig {
          @Bean
          @LoadBalanced // 通过 Spring Cloud Load Balancer 实现负载均衡
          public RestTemplate getRestTemplate(){
              return new RestTemplate();
          }
      }
    • 从服务中心获取服务

      Java 复制代码
      List<ServiceInstance> servers = discoveryClient.getInstances("product-service"); //获取服务实例列表
      EurekaServiceInstance server = (EurekaServiceInstance)servers.get(0); //去列表中取第一个
      String url = server.getUri() + "/product/" + orderInfo.getProductId(); //根据实例信息拼接请求,后续可以通过 RestTemplate 发起调用
2.5.3 Nacos 负载均衡

Spring Cloud LoadBalancer 有自己的负载均衡实现方式,所以需要额外的配置让 Nacos 的权重配置、同集群优先访问生效

YAML 复制代码
spring:
    cloud:
        loadbalancer:
            nacos:
                enabled: true
  • Nacos 网页管理页面中可以下线服务、配置权重。

  • Nacos 会把同一个机房中的机器上的服务认为在同一个集群上,这样这些节点之间的访问是要快于跨局域网的,即某种程度上可以是认为是同一机房优先访问

    配置集群名称

    YAML 复制代码
    spring:
        cloud:
            discovery:
                cluster-name: SH
2.5.4 Nacos 健康检查
2.5.4.1 健康检查机制
  • 客户端主动上报,即客户端主动发送信息表明尚未下线,默认间隔 5s 发送,30s 内无响应咋认为节点下线删除节点

  • 服务端反向探测,服务端发送信息主动确认客户端的情况,20s 没有反应会将节点标记为 unhealthy,而不会下线节点

2.5.4.2 临时实例和非临时实例

Nacos 对于临时实例使用客户端主动上报,而非临时实例使用的是服务端反向探测。节点默认是临时实例,如果需要设置为非临时实例,需要如下配置

YAML 复制代码
spring:
    cloud:
        discovery:
            ephemeral: false
  1. 报错:当前节点为临时实例不可转换为非临时实例,或者当前节点为非临时实例不可转换为临时实例

    原因是 Nacos 会记录每个实例的 ip、端口,在两者都没有更改时是不允许转换的。

    解决方式:

    1. 更改端口

    2. 以 docker 为例,进入容器后,删除 /data/protocol 中的内容,之后重启容器,即可以完成转换

  2. 实例状态正常,但是显示 unhealthy

    对于非临时节点来说,Nacos 的健康检查方式是主动发送信息确认。但是对于通过阿里云内部 MSE 托管的 Nacos 来说,为了安全规范禁止服务端主动向客户端发送请求,所以误认为非临时实例下线

    解决方式:将实例设置为非临时实例

2.5.5 Nacos 服务环境隔离

通过 namespace 进行,不同命名空间的服务不能互相访问(获取实例列表)

YAML 复制代码
spring:
    cloud:
        discovery:
            namespace: namespace-id # 在 nacos 中建立 namespace,并将命名空间 id 放在这里
2.5.6 Nacos 配置中心
2.5.6.1 Nacos 服务器建立配置文件
  • 目前支持的方式有 yaml、properties(其他选项仅限于有)

  • Data-Id,对应到项目名称,比如 product-service、order-service

2.5.6.2 客户端服务器获取配置
  • 客户端相应配置都要放在 bootstrap.* 中,文件类型需要和 Nacos 服务端配置文件的类型相同,默认需要指定的有项目名称和 Nacos 服务器地址。Nacos 的服务注册中心地址和配置中心地址是在两个不同的项中

    YAML 复制代码
    spring:
      application:
        name: product-service
      profiles:
        active: dev
      cloud:
        nacos:
          config:
            server-addr: localhost:8848
  • 导入 Nacos Config 依赖

    XML 复制代码
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    <!-- SpringCloud 2020.*之后版本需要引入bootstrap-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-bootstrap</artifactId>
    </dependency>
  • 在启动微服务时,会首先通过 bootstrap.yaml 尝试从 Nacos 获取配置,之后和 application.yaml/.properties 的配置进行合并

2.5.6.3 配置中心命名空间

配置中心的命名空间和服务发现的命名空间是两个不同的命令空间,但默认都是 public,同时也都可以起到隔离的作用

YAML 复制代码
spring:
  cloud:
    nacos:
      config:
        namespace: 6e61929d-a9bd-4504-9031-6ca57bc6c0b0
2.5.7 Data Id

前面提到了 Nacos 服务端的配置文件的 Data Id。Nacos 读取配置文件的完整结构为

Plain 复制代码
${spring.application.name}[-${spring.profiles.active}].[${spring.cloud.nacos.config.file-extension}]

Nacos 按照优先级读取下面三个文件

  • KaTeX parse error: Undefined control sequence: \- at position 30: ...ication\.name\}\̲-̲{spring.profiles.active}.${spring.cloud.nacos.config.file_extension}

  • KaTeX parse error: Expected group as argument to '\.' at end of input: ...ation\.name\}\.{spring.cloud.nacos.config.file_extension}

  • ${spring.application.name}

当配置内容重复时,会优先读取优先级更高的 Data Id 对应的配置文件的内容

2.5.8 AP 与 CP

Nacos 既可以 AP 架构,也可以 CP 架构,对于临时节点采取 AP 的策略,对于非临时节点采取 CP 策略

三、远程调用

3.1 快速上手

RPC ,Remote Process Calling,是指像调用本机方法一样调用远程的方法,在 Spring 中常使用 OpenFeign

  • 导入 OpenFeign 依赖

    XML 复制代码
    <dependency>    
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  • 给启动类增加 @EnableFeignClients 注解,开启 OpenFeign 功能

    Java 复制代码
    @EnableFeignClients
    @SpringBootApplication
    public class OrderServiceApplication {
        public static void main(String[] args){
            SpringApplication.*run*(OrderServiceApplication.class, args);
        }
    }
  • 编写 OpenFeign 客户端,通过 SpringMVC 的方式指定请求路径、请求参数、服务名称(Spring 自行通过 LoadBalancer 进行负载均衡),接口需要使用 @FeignClient 修饰

    Java 复制代码
    @FeignClient(path="/product", value="product-service")
    public interface ProductApi {
        @RequestMapping("/get_num")
        public Integer getNum(Integer num);
    
        @RequestMapping("/test1")
        public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2);
    
        @RequestMapping("/test2")
        public String test2(ProductInfo productInfo);
    
        @RequestMapping("/{id}")
        public ProductInfo selectProductById(@PathVariable("id") Integer id);
    
        @RequestMapping("/test3")
        public String test3(@RequestBody ProductInfo productInfo);
    }

其中,value 指定服务名称,用于服务发现、负载均衡;path 为所有请求的前缀路径,对应 @RequestMapping 作用于 Controller 类时的效果

  • 远程调用

    Java 复制代码
    ProductApi productApi;
    
    @RequestMapping("get_num")
    Integer getNum(Integer num){
        *log*.info(num + "");
        return productApi.getNum(num);
    }

3.2 参数传递

FeignClient 起到了一个代替我们手动拼接字符串发送请求的作用,所以说参数对应的关系依然是存在的,需要满足 Spring MVC 的参数传递规则,例如

  1. 单参数传递

    Java 复制代码
    @RequestMapping("/get_num")
    public Integer getNum(Integer num)
  2. 多参数,需要使用 @RequestParam 绑定参数(默认 required = true)

    Java 复制代码
    @RequestMapping("/test1")
    public String test1(@RequestParam("num1") Integer num1, @RequestParam("num2") Integer num2);
  3. 传递对象,如果目的是将对象作为请求字符串的一部分,需要加上 @SpringQueryMap 注解,自动将类序列化后加入请求参数;否则默认将对象加入请求正文

    Java 复制代码
    @RequestMapping("/test2")
    public String test2(@SpringQueryMap ProductInfo productInfo);
  4. 传递 JSON,放在请求正文中。在服务消费者接受请求,从正文中取数据,使用 @RequestBody;服务消费者调用 Feign 客户端,同样使用 @RequestBody,不过这里的意思是告诉 FeignClient 进行远程调用的时候,将数据拼接到请求正文中。一个是取数据,一个是放数据

    Java 复制代码
    //ServiceConsumerController or ServiceProvidorController
    @RequestMapping("/test3")
    public String test3(@RequestBody ProductInfo productInfo){
    
    //FeignClient
    @RequestMapping("/test3")
    public String test3(@RequestBody ProductInfo productInfo);

3.3 抽取式和继承式

按照上面的方式,我们要根据服务提供方的接口设计单独"描绘"出来一个 FeignClient 客户端,同时不同服务调用这个服务时,都需要重复一遍上述步骤,多出了很多代码。

所以我们选择把这些接口单独抽出来,放到一个 api 模块中,这个模块只放接口和 DTO,由服务提供者实现接口中的内容,这样就解决了一个问题:每个服务调用者都要"描绘"一遍服务提供者的 Controller

现在还有一个问题,每个服务调用者的 FeighClient 呢?有两种代码构建方式

继承式 是 OpenFeign 推荐的方式,将存放接口的模块单独打成 jar 包,放到本地仓库中,然后需要调用服务的模块引入,自己的 FeignClient(使用 @FeignClient 修饰的类)继承这个接口,这样就完成了 FeignClient 的编写

抽取式 使更多实际使用的方式,在 api 模块中定义接口时,将这个接口作为 FeignClient,使用 @FeignClient 注解修饰并由服务提供者实现,将这个模块打 jar 包后分发给服务调用者;服务调用者必须要再自己定义 FeignClient,只需要在自己的启动类处通过 @EnableFeignClients 的方式指定扫描路径或者客户端类即可

Java 复制代码
@EnableFeignClients(basePackages = {"com.wjl.api"}) //指定扫描路径
@EnableFeignClients(clients = {ProductApi.class}) //指定要加载的 feign client 客户端

四、网关

前面介绍了服务注册与发现、负载均衡、远程调用,还缺少一个网关,作为整个后端服务的入口。

网关可以起到如下作用

  • 对请求进行鉴权过滤

  • 一切请求经过网关,起到动态路由的作用

  • 流量限制,按照后端服务能接受的流量放行请求

网关我们介绍 Spring Cloud Gateway,和 Eureka 一样作为一个服务在后端运行

4.1 快速上手

  • 导入依赖

    XML 复制代码
    *<!--网关-->*
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway</artifactId>
    </dependency>
    *<!--基于nacos实现服务发现依赖-->*
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    *<!--负载均衡-->*
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-loadbalancer</artifactId>
    </dependency>
  • 添加路由配置

    YAML 复制代码
    server:
      port: 10030
    spring:
      application:
        name: gateway
      cloud:
        nacos:
          discovery:
            server-addr: localhost:8848
        gateway:
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
              *#filters:*
    *            #- AddRequestParameter=username, wjl*
    *        *- id: order-service
              uri: lb://order-service
              predicates:
                - Path=/order/** # 路径匹配规则
                - After=2025-01-01T00:00:00.000+08:00[Asia/Shanghai] # 时间限制规则
    • server.port,网关的ip、端口

    • spring.cloud.gateway.routes,路由配置,后续通过 yaml 数组的语法添加多条路由配置

      • id,路由id,保证唯一即可

      • uri,只能是普通 uri ;或者通过 lb://service-name 的方式,到注册中心获取实例列表,由 Spring LoadBalancer 负载均衡的调用(lb 代表负载均衡)

      • predicates,路由条件,只有符合要求要求的请求会被路由到相应服务

4.2 Gateway Filter Factories

Predicates 决定了哪些请求需要被处理,而 filter 负责对请求处理前后增添额外的逻辑,包含两种

  • GatewayFilter,作用于一个或一组路由

    YAML 复制代码
    spring:
      cloud:
        gateway:
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
              *filters:*
    *            - AddRequestParameter=username, wjl*

    GatewayFilter 提供了很多 Filter,AddRequestParameter 会在所有请求中加上参数,可以后端直接获取

    这种方式作用于部分接口,可以通过 default-filters 作用于所有路由接口

    YAML 复制代码
    spring:
      cloud:
        gateway:
          default-filters:
            - AddRequestParameter=username, wjl
          routes:
            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/product/**
  • GlobalFilter,作用于所有路由

添加依赖

XML 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

添加配置

YAML 复制代码
spring:
  cloud:
    gateway:
      metrics:
        enabled: true
management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true
相关推荐
qeen871 小时前
【数据结构】搜索二叉树的介绍与算法原理解析及实现
数据结构·c++·学习·算法·模板
重庆小透明1 小时前
agent开发学习【第一篇】
学习
玖石书1 小时前
ASP.NET Core迁移Spring对照系列:文件系统操作篇
spring·asp.net
weixin_BYSJ19871 小时前
springboot医疗信息管理系统---附源码17465
java·javascript·spring boot·python·django·flask·php
风曦Kisaki1 小时前
# Kubernetes(K8s)笔记Day13 :工作负载资源概述与 Job (CronJob)控制器
linux·笔记·云原生·容器·kubernetes
xqqxqxxq1 小时前
吃透 Spring 高频注解(包含SpringMVC Spring Boot)
java·spring boot·spring
都叫我大帅哥2 小时前
Embabel 1.0 Agent Framework 完全指南:从入门到生产级实践
java
zhongjunyao2 小时前
Spec-Driven Development (SDD): The Definitive 2026 Guide
javascript·spring boot·python·eclipse·代理模式
福大大架构师每日一题2 小时前
agno v2.8.7 发布:顾问模型、精准路线、调度能力全面升级,10项关键修复一次看懂
java·开发语言·数据库