SpringCloud

yin_bo_ Lv2

1. 认识微服务

  • 随着互联网行业的发展,对服务的要求也越来越高,服务架构也从单体架构逐渐演变为现在流行的微服务架构。那这些架构之间有怎样的区别呢?

1.2 单体架构

将业务的所有功能集中在一个项目中开发,打成一个包部署

alt 单体架构
alt 单体架构

  • 单体架构优缺点如下
    • 优点
      • 架构简单
      • 部署成本低
    • 缺点
      • 耦合度高(维护困难,升级困难)

1.3 分布式架构

根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务

alt 分布式架构
alt 分布式架构

  • 分布式架构优缺点如下

    • 优点
      • 降低服务耦合
      • 有利于服务升级和拓展
    • 缺点
      • 服务调用关系错综复杂
  • 分布式结构虽然降低了服务耦合,但是服务拆分时也有许多问题需要思考

    • 服务拆分的细粒度如果界定?
    • 服务之间如何调用?
    • 服务的调用关系如果管理?
  • 人们需要指定一套行之有效的标准来约束分布式框架微服务架构应运而生

1.4 微服务

  • 微服务的架构特征

    • 单一职责:微服务拆分粒度更小,每个服务都对应唯一的业务能力,做到单一职责
    • 自治:团队独立,技术独立,数据独立,独立部署和交付
    • 面向服务:服务提供统一标准的接口,与语言和技术无关
    • 隔离性强:服务调用做好隔离,容错,降级,避免出现级联问题(例如积分服务挂了,不印象用户服务等其他服务)。
      alt 微服务架构
      alt 微服务架构
  • 微服务的上述特征其实是给分布式架构制定一个标准,进一步降低服务之间的耦合度,提供服务的独立性和灵活性,做到高内聚,低耦合。

  • 因此,可以认为微服务是一种经过良好架构设计的分布式架构

1.5 SpringCloud

  • SpringCloud目前国内使用最广泛的微服务架构 官网地址:https://spring.io/projects/spring-cloud

  • SpringCloud 集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供同了良好的开箱即用体验。

  • 其中常见的组件包括

    • 微服务注册与发现
      • Eureka
      • Nacos
      • Consul
    • 服务远程调用
      • OpenFeign
      • Dubbo
    • 服务链路监控
      • Zipkin
      • Sleuth
    • 统一配置管理
      • SpringCloudConfig
      • Nacos
    • 统一网关路由
      • SpringCloudGateway
      • Zuul
    • 流控、降级、保护
      • Hystix
      • Sentinel
  • 另外,SpringCloud底层依赖于SpringBoot,并且有版本的兼容关系,如下

Release Train Boot Version
2020.0.x aka llford 2.4.x
Hoxton 2.2.x,2.3.x (Starting with SR5)
Greenwich 2.1.x
Finchley 2.0.x
Edgware 1.5.x
Dalston 1.5.X
  • 我们学习的版本是Hoxton.SR10,因此对应的是SpringBoot版本是2.3.x

1.6 总结

  • 单体架构:简单方便,高度耦合,扩展性差,适合小型项目。例如:学生管理系统
  • 分布式架构:松耦合,扩展性好,但架构复杂,难度大。适合大型互联网项目。例如:京东、淘宝
  • 微服务:一种更好的分布式架构方案
    • 优点:拆分力度更小、服务更独立、耦合度更低
    • 缺点:架构非常复杂,运维、监控、部署难度提高
  • SpringCloud 是微服务架构的一站式解决方案,集成了各种优秀的微服务功能组件

2. 服务拆分和远程调用

  • 任何分布式架构都离不开服务的拆分,微服务也是一样

2.1 服务拆分原则

  • 微服务拆分有几个原则:
    1. 不同微服务,不要重复开发相同业务
    2. 微服务数据独立,不要访问其他微服务的数据库
    3. 微服务可以将自己的业务暴露为接口,供其他微服务调用
      alt 微服务拆分图
      alt 微服务拆分图

2.2 服务拆分示例

  • cloud-demo:父工程,管理依赖

    • order-service:订单微服务,负责订单相关业务
    • user-service:用户微服务,负责用户相关业务
  • 需求
    订单微服务和用户微服务必须有各自的数据库,相互独立
    订单服务和用户服务都对外暴露Restful的接口
    订单服务如果需要查询用户信息,只能调用用户服务的Restful接口,不能查询用户数据库

  1. 导入SQL语句
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
CREATE DATABASE cloud_order;
USE cloud_order;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名称',
`price` bigint(20) NOT NULL COMMENT '商品价格',
`num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);

SET FOREIGN_KEY_CHECKS = 1;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CREATE DATABASE cloud_user;
USE cloud_user;
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
`address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');

SET FOREIGN_KEY_CHECKS = 1;
  1. 导入demo
  • 导入黑马提供的demo,里面包含了order-serviceuser-service,将配置文件中的数据库修改为自己的配置,随后将这两个服务启动,开始我们的调用案例

2.3 实现远程调用案例

  • order-service中的web包下,有一个OrderController,是根据id查询订单的接口

    1
    2
    3
    4
    5
        @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
    // 根据id查询订单并返回
    return orderService.queryOrderById(orderId);
    }
  • 我们启动服务(注意jdk版本必须是8并且改一下连接数据库的配置),访问http://localhost:8080/order/101,是可以查到数据的,但此时的user是null

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "id": 101,
    "price": 699900,
    "name": "Apple 苹果 iPhone 12 ",
    "num": 1,
    "userId": 1,
    "user": null
    }
  • 在user-servie的web包下,也有一个UserController,其中包括一个根据id查询用户的接口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
        @Slf4j
    @RestController
    @RequestMapping("/user")
    public class UserController {

    @Autowired
    private UserService userService;

    /**
    * 路径: /user/110
    *
    * @param id 用户id
    * @return 用户
    */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
    return userService.queryById(id);
    }
    }
  • 我们打开浏览器,访问http://localhost:8081/user/1 ,查询到的数据如下

    1
    2
    3
    4
    5
    {
    "id": 1,
    "username": "柳岩",
    "address": "湖南省衡阳市"
    }
  • 案例需求:

    • 修改order-service中的根据id查询订单业务,要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一并返回
      alt 远程调用案例流程图
      alt 远程调用案例流程图
  • 因此,我们需要在order-service中向user-service 发起一个http请求,调用http://localhost:8081/user/{userId}这个接口。

  • 大概步骤

    1. 注册一个RestTemplate的实例到Spring容器
    2. 修改order-service 服务中的OrderService类中的queryOrderById方法,根据Order对象中的userID查询User。
    3. 将查询到的User 填充到Order 对象,一并返回。

2.4 实现远程调用需求

  1. 首先我们在order-service服务中的OrderApplication启动类中,注册RestTemplate实例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @MapperScan("cn.itcast.order.mapper")
    @SpringBootApplication
    public class OrderApplication {

    public static void main(String[] args) {
    SpringApplication.run(OrderApplication.class, args);
    }

    @Bean
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
    }
  2. 实现远程调用,修改order-service服务中的queryOrderById

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    //2. 远程查询User
    //2.1 URL地址,这里URL写死了,后面会改进
    String url = "http://localhost:8081/user/" + order.getUserId();
    //2.2 发起调用
    User user = restTemplate.getForObject(url, User.class);
    //3. 存入user
    order.setUser(user);
    // 4.返回
    return order;
    }
  3. 再次访问http://localhost:8080/order/101 这次就能看到User数据了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
      {
    "id": 101,
    "price": 699900,
    "name": "Apple 苹果 iPhone 12 ",
    "num": 1,
    "userId": 1,
    "user": {
    "id": 1,
    "username": "柳岩",
    "address": "湖南省衡阳市"
    }
    }

2.5 提供者与消费者

  • 在服务调用关系中,会有两个不同的角色

    • 服务提供者:一次业务中,被其他微服务调用的服务(提供接口给其他微服务
    • 服务消费者:一次业务中,调用其他微服务的服务(调用其他微服务提供的接口
  • 但是,服务提供者与服务消费者的角色并不是绝对的,而是相对于业务而言。

3. Eureka注册中心

  • 加入我们的服务提供者user-service提供了三个实例,分别占用8081,8082,8083端口
  • 我们来思考三个问题:
    1. order-service在发起远程调用的时候,该如何得知user-service实例的ip地址和端口
    2. 有多个user-service实例地址,order-service应该调用哪个?
    3. order-service如何得知某个user-service实例是否宕机?

3.1 Eureka的结构和作用

  • 这些问题都需要由SpringCloud中的注册中心来解决,其中最广为人知的注册中心就是Eureka,其结构如下:

    alt Eureka结构
    alt Eureka结构

  • 那现在来回答之前的各个问题

    • 问题一:order-service如何得知user-service的实例地址?
      1. user-service服务实例启动后,将自己的信息注册到eureka-server(Eureka服务器),这个叫服务注册
      2. eureka-server保存服务名称到服务实例地址列表的映射关系。
      3. order-server根据服务名称,拉去实例地址列表,这个叫做服务发现(服务拉取)
    • 问题二:order-service如何从多个user-service实例中获取具体的实例?
      • order-service从实例列表中利用负载均衡算法选中一个实例地址。
      • 向该实例地址发起远程调用
    • 问题三:order-service如何得知某个user-service是否宕机?
      • user-service每隔一段时间(默认30s)向eureka-server发送请求,报告自己的状态,称为心跳
      • 当超过一定时间没有发送心跳,eureka-server会认为微服务实例故障,将该实例从服务列表中剔除。
      • order-service拉取服务时,就能将该故障排除了
  • 注册中心的步骤

    1. 搭建注册中心EurekaServer
    2. 服务注册,将user-service和order-service都注册到eureka
    3. 服务发现,在order-service中完成服务拉取,然后通过负载均衡挑选一个服务实现远程调用

3.2 搭建eureka-server

  1. 创建eureka-server服务

    • 在cloud-demo父工程下创建一个子模块,这里使用maven项目,然后填写服务信息
  2. 引入eureka依赖

    • 在eureka-server模块的配置文件pom.xml里注入starter依赖
      • 这里不需要注入版本,因为在父工程中已经把版本管理好了
    1
    2
    3
    4
    5
    6
            <dependencies>
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    </dependencies>
  3. 编写启动类

    • 给eureka-server编写一个启动类EurekaApplication,必须添加@EnableEurekaSercer注解,开启eureka注册中心功能
    1
    2
    3
    4
    5
    6
    7
      @EnableEurekaServer
    @SpringBootApplication
    public class EurekaApplication {
    public static void main(String[] args) {
    SpringApplication.run(EurekaApplication.class, args);
    }
    }
  4. 配置文件

    • 编写application.yml,内容如下
      • 为什么eureka自己需要配置eureka-client? –> 因为eureka自己也是个微服务项目,也需要被自己管理
    1
    2
    3
    4
    5
    6
    7
    8
    9
      server:
    port: 10086 # 服务端口
    spring:
    application:
    name: eureka-server ## eureka的服务名称
    eureka:
    client:
    service-url: # eureka的地址信息
    defaultZone: http://127.0.0.1:10086/eureka
  5. 启动服务

    • 启动微服务,在服务器访问http://localhost:10086 看到如下页面就是启动成功
      alt Eureka注册中心界面
      alt Eureka注册中心界面
    • 从图中看eruka的确将自己注册成了一个服务

      UP (1) - localhost:eureka-server:10086

3.2 服务注册

  • 下面,我们将user-service和order-service注册到eureka-server中
  1. 引入依赖

    • 在user-service和order-service的pom.xml文件中,引入下面的eureka-client依赖
    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
  2. 配置文件

    • 在user-service和order-service中,修改application.yml文件,添加服务名称、eureka地址
    1
    2
    3
    4
    5
    6
    7
        spring:
    application:
    name: order-service/user-service
    eureka:
    client:
    service-url:
    defaultZone: http://127.0.0.1:10086/eureka
  3. 启动多个user-service实例

    • 为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service

    • 在服务里复制一份user-service的配置,name为UserApplication2,点击修改选项按钮,然后将虚拟器选项点出来,修改端口号-Dserver.port=8082,点击确定之后,在IDEA的服务选项卡中,就会出现两个user-service启动配置,一个端口是8081,一个端口是8082

    • 之后我们按照相同的方法配置order-service,并将两个user-service和一个order-service都启动,然后查看eureka-server管理页面,发现服务确实都启动了,而且user-service有两个

踩过的坑

我在把所有Application启动之后,发现全都报错了,然后看我的Eureka终端里面也没有其他的服务,这是为什么?
因为把EurekaApplication和其他的Application同时启动了,Eureka还没反应过来,所以其他的Application就会报错。
解决方法就是配置完Eureka之后不再重启

3.3 服务发现

  1. 引入依赖和配置文件

    • 我们已经在之前完成了对user-service的引入依赖和配置文件
  2. 服务拉取和负载均衡

    • 最后,我们要去eureka-server中拉取user-service服务的实例列表,并实现负载均衡
    • 不过这些操作不需要我们去做,只需要添加一些注解即可
    • 在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解
    1
    2
    3
    4
    5
        @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }
    • 修改order-service服务中的OrderService类的queryOrderById方法,修改访问路径,用服务名称来代替IP和端口(这里服务名称是配置文件中的application:name)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    //2. 远程查询User
    //2.1 URL地址,用user-service代替地址
    String url = "http://user-service/user/" + order.getUserId();
    //2.2 发起调用
    User user = restTemplate.getForObject(url, User.class);
    //3. 存入user
    order.setUser(user);
    // 4.返回
    return order;
    }
  • Spring会自动帮我们从eureka-server端,根据user-service这个服务名称,获取实例列表,然后完成负载均衡。
  • 这时我们访问localhost:8080/order/101就会发现远程调用了user-service

4. Ribbon负载均衡

  • 在这个小结,我们来说明@LocalBalanced注解如何实现的负载均衡功能

4.1 负载均衡原理

5. Nacos注册中心

  • 国内一般使用SpringCloud AlibabaNacos注册中心

5.1 认识和安装Nacos

  • Nacos是阿里巴巴的产品,现在是SpringCloud的一个组件,相比于Eureka,功能更加丰富
  • 在Nacos的Github页面,提供了下载连接,可以下载编译好的Nacos服务端或者源代码
  • 下载好之后,将文件安装到非中文路径下任意目录
    • bin:启动脚本
    • conf:配置脚本
  • Nacos的默认端口是8848。
  • Nacos的启动很简单,进入bin目录,打开cmd窗口执行下面命令
    1
    startup.cmd -m standalone
    之后在浏览器访问 http://localhost:8848/nacos 即可,默认的登录账号和密码都是nacos

5.2 服务注册到Nacos

  • Nacos是SpringCloudAlibaba的组件,而SpringCloudAlibaba也遵循SpringCloud中定义的服务注册,服务发现规范,因此Nacos与Eureka在微服务方面没太大区别

  • 主要差异在于:

    1. 依赖不同
    2. 服务地址不同
  1. 引入依赖

    • 在cloud-demo父工程的pom.xml文件中引入SpringCloudAlibaba的依赖
      1
      2
      3
      4
      5
      6
      7
      <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-alibaba-dependencies</artifactId>
      <version>2.2.6.RELEASE</version>
      <type>pom</type>
      <scope>import</scope>
      </dependency>
    • 然后在user-service和order-service的pom文件中引入nacos-discovery依赖
      • 同时将eureka的依赖注释掉
        1
        2
        3
        4
        <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
  2. 配置Nacos地址

    • 在user-service和order-service的application.yml文件中添加Nacos地址
      • 同时将eureka地址注释掉
    1
    2
    3
    4
    spring:
    cloud:
    nacos:
    server-addr: localhost:8848
  3. 重启服务

    • 重启微服务后,登录nacos的管理页面,可以看到微服务信息
      alt Nacos终端
      alt Nacos终端

5.3 服务分级存储模型

  • 一个服务可以有多个实例,例如我们的user-service,可以有

    • 127.0.0.1:8081
    • 127.0.0.1:8082
    • 127.0.0.1:8083
  • 假如这些实例分布于全国各地的不同机房,Nacos就会将同一个机房的所有实例划分成一个**集群**

  • 也就是说.user-service是服务,一个服务可以包含多个集群,每个集群可以有多个实例,形成分级模型。

  • 微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快,本集群内不可用再去访问其他集群。

5.3.1 集群配置

  • 修改user-service的application.yml文件,添加集群配置
1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ ## 集群名称,杭州
  • 启动两个user-service实例

  • 之后再复制一个user-service的启动配置,端口号设置8083,之后修改application.yml,将集群名称改为上海SH,之后启动该服务

  • 然后刷新Nacos终端,可以发现有两个实例的集群HZ和只有一个实例的集群SH

    alt 不同集群Nacos
    alt 不同集群Nacos

  • Nacos服务分级

    • 一级:服务 如user-service
      • 二级:集群 如上海或者杭州
        • 三级:实例 如上海某台部署了user-service的服务器

5.3.2 同集群优先的负载均衡

  • 默认的ZoneAvoidanceRule并不能根据同集群优先实现负载均衡

  • 因此Nacos中提供了一个NacosRule的实现,可以优先从集群中挑选实例

  • 给order-service配置集群信息,修改application.yml,将集群改为HZ,

    • 并修改负载均衡规则,规则是com.alibaba.cloud.nacos.ribbon.NacosRule
      1
      2
      3
      4
      5
      6
      7
      8
      9
      spring:
      cloud:
      nacos:
      server-addr: localhost:8848
      discovery:
      cluster-name: HZ # 集群名称,杭州
      user-service:
      ribbon:
      NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
  • 将三个实例的user-service的日志清空,重启order-service,然后多次调用向order-service发送请求,发现只有HZ集群的实例才会执行,而其他集群不会

    • 并且是在相同集群的实例里随机挑选
  • 将HZ集群的实例停用,然后再次发送请求,这次SH集群的实例运行了。

  • NacosRule负载均衡的策略

    1. 优先选择本地集群的服务实例列表
    2. 本地集群找不到实例,才回去其他集群勋章,并且会报警告
    3. 确定了可用实例列表之后,采用随机负载均衡挑选实例

5.4 权重配置

  • 实际部署中肯定会有这样的场景

    • 服务器设备性能有差异,部分实例所在的机器性能较好,而另一些较差,所以我们希望性能好的机器承担更多的用户请求。
    • 但默认下NacosRule是集群随机挑选,不会考虑机器性能问题。
  • 因此Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高(权重范围0~1之间)

  • 在Nacos控制台,找到user-service的实例列表,点击标记,即可以修改权重

    注意:若权重为0则该实例永远不会被访问
    我们可以将某个服务的权重修改为0,然后进行更新,然后也不会影响到用户正常访问别的服务集群,之后我们可以给更新后的该服务设置一个很小的权重,这样就会有一小部分用户访问该服务,测试服务是否稳定(类似于灰度测试)。

5.5 环境隔离

  • Nacos提供了namespace来实现环境隔离功能
    • nacos中可以有多个namespace
    • namespace下可以有group,service等
    • 不同的namespace之间互相隔离,例如不同的namespace的服务互相不可见
  1. 创建namespace

    • 默认情况下,所有service,data,group都是在同一个namespace,名为public
    • 我们点击命名空间->新建命名空间->填写表单,可以创建一个新的namespcae
    • 如图,我们新建了个命名空间dev
      alt 命名空间
      alt 命名空间
  2. 给微服务配置namespace

    • 给微服务配置namespace只能通过修改配置了实现
    • 例如,修改order-service的application.yml文件
    1
    2
    3
    4
    5
    6
    cloud:
    nacos:
    server-addr: localhost:8848
    discovery:
    cluster-name: HZ # 集群名称,杭州
    namespace: 982827c7-8362-478f-a1c5-c1a208fa5ab4 # 命名空间,填上图中的命名空间ID
    • 重启order-service,访问nacos控制台,可以看到order-service在dev下,此时访问order-service,因为namespace不同,就会导致找不到user-service,若访问则会直接报错

5.6 Nacos和Eureka的区别

  • Nacos的服务实例可以分为两种类型

    1. 临时实例:如果实例宕机超过一定时间,会从服务列表中删除,是默认的类型
    2. 非临时实例:如果实例宕机,不会从服务列表剔除,也叫永久实例
  • 配置一个服务实例为永久实例

    1
    2
    3
    4
    5
    spring:
    cloud:
    nacos:
    discovery:
    ephemeral: false ## 设置为非临时实例
  • Nacos和Eureka整体结构相似,服务注册,服务拉取,心跳等待,但是也存在一定的差异

    alt Nacos注册中心原理图
    alt Nacos注册中心原理图

  • Nacos与Eureka的共同点

    1. 都支持服务注册和服务拉取
    2. 都支持服务提供者心跳方式做健康监测
  • Nacos与Eureka的区别

  1. Nacos支持服务端主动检测提供者状态:
    • 临时实例采用心跳模式。
    • 非临时实例采用主动检测模式 (但是对服务器压力比较大,不推荐)
  2. 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
  3. Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
  4. Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式

    AP方式:强调数据可用性
    CP方式:强调数据一致性和可靠性

6. Nacos配置管理

  • Nacos除了可以做注册中心,同样还可以做配置管理来使用

6.1 统一配置管理

  • 当微服务部署的实例越来越多,达到数十数百时,修改服务器配置就会让人抓狂,而且容易出错,所以我们需要一种统一配置管理方案,可以集中管理所有实例的配置
  • Nacos以方便可以将配置集成管理,另一方面可以在配置变更时,及时通过微服务,实现配置的热更新。

6.1.1 在Nacos中配置文件

  • 如何在Nacos中管理配置?
    • 点击 配置管理 –> 配置列表 –> 创建配置
    • 选择YAML文件格式,将需要热更新的配置写进去

      注意:只有需要热更新的配置才有放到Nacos管理的必要,一些基本不会更改的配置,如数据库的连接,还是保存到微服务本地比较好

6.1.2 从微服务拉取配置

  • 这里为了演示,将时间格式配置放到Nacos配置文件中

    1
    2
    pattern:
    dateformat: yyyy-MM-dd HH:mm:ss
  • 微服务要拉取Nacos中管理的配置,并且与本地的application.yml配置合并,才能完成项目启动

  • 但是如果上位读取application.yml,怎么得知Nacos地址?

  • Spring引入了一种新的配置文件:bootstrap.yml,会在application.yml之前被读取,流程如下:

    1. 项目启动
    2. 加载bootstrap.yml,获取Nacos地址,配置文件id
    3. 根据配置文件id,读取Nacos中的配置文件
    4. 读取本地配置文件application.yml,与Nacos拉取的配置合并
    5. 创建Spring容器
    6. 加载bean
  • 引入 nacos-config依赖

    • 首先在user-service服务中,引入nacos-config的客户端依赖
    1
    2
    3
    4
    5
     <!--nacos配置管理依赖-->
    <dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  • 添加bootstrap.yml

    • 在user-service下添加一个bootstrap.yml文件,内容如下
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      spring:
      application:
      name: user-service # 服务名称
      profiles:
      active: dev #开发环境,这里是dev
      cloud:
      nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
      file-extension: yaml # 文件后缀名
      enabled: true
      namespace: 982827c7-8362-478f-a1c5-c1a208fa5ab4
      discovery:
      cluster-name: SH # 集群名称,上海
      namespace: 982827c7-8362-478f-a1c5-c1a208fa5ab4 # 命名空间,填上图中的命名空间ID
  • 这里根据spring.cloud.nacos:server-addr获取Nacos地址,再根据${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}作为文件ID,来读取配置

  • 在本例中,就是读取了user-service-dev.yaml

  • 为了测试是否真的读取到,我们在user-service的UserController中添加业务,读取nacos中的配置信息pattern.dateformat配置

    1
    2
    3
    4
    5
    6
    7
    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/time")
    public String time(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
  • 打开浏览器,访问http://localhost:8081/user/time 会看到时间

踩过的坑

这是我踩过最傻逼的坑,傻逼黑马,傻逼grok,我在这里搞了三个小时才搞出来🤬

  1. 我的user-service被dev命名空间管理着,所以我就像将配置管理也写在dev命名空间里,但是发现咋运行也运行不了。
  2. 我问了grok,他一会说我需要用@NacosValue注解 ,一会又要让我加bootstrap的依赖,结果都不行
  3. 然后我将这个配置也写在了public里,发现终于能运行了
  4. 为什么?我明明这个实例被dev管理啊,为啥能运行public里的配置
  5. 然后我才发现nacos的傻逼之处😅
  6. 我之前是在discovery里配置了服务注册在dev里,我需要在config(配置中心)里也加上一个namespace指向dev。
  7. 太傻逼了。

6.2 配置热更新

  • 我们最终的目的,是修改Nacos中的配置,微服务中无需重启也可以让配置生效,也就是配置热更新
  • 要实现配置热更新,有两种方式
  1. 刷新作用域

    • @Value注入的变量类上添加注解@RefreshScope(刷新作用域)
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    @Slf4j
    @RestController
    @RequestMapping("/user")
    @RefreshScope
    public class UserController {

    @Resource
    private UserService userService;

    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
    return userService.queryById(id);
    }



    @Value("${pattern.dateformat}")
    private String dateformat;

    @GetMapping("/time")
    public String time(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
    }
    • 测试是否热更新
      • 启动服务,打开浏览器,访问http://localhost:8081/user/time,由于我们之前配置的dateformatyyyy-MM-ddMM:hh:ss,所以看到的日期为2025-11-23 21:06:36
      • 将Nacos中编辑配置信息改一下,并保存
    1
    yyyy年MM月dd日 HH:mm:ss
    • 无需重启服务器,直接刷新页面,看到的是2025年11月23日 21:07:35
  2. 使用@ConfigurationProperties注解代替@Value注解

    • 在user-service服务中,添加一个类,读取pattern.dateformat属性
    1
    2
    3
    4
    5
    6
    @Component
    @Data
    @ConfigurationProperties(prefix = "pattern")
    public class PatternProperties {
    private String dateformat;
    }
    • 在UserController中使用这个类来代替@Value
    1
    2
    3
    4
    5
    6
    7
        @Resource
    private PatternProperties patternProperties;

    @GetMapping("/time")
    public String time(){
    return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }

关于第二种我的理解

SpringBoot会自动把Nacos里最新的配置值实时绑定到标注了@ConfigurationProperties的对象上

Nacos配置中心

│ 配置发生变更(比如把 pattern.dateformat 改了)

Spring Cloud Nacos 客户端监听到变更事件

│ 发出 RefreshRemoteApplicationEvent 事件(刷新事件)

Spring Boot 的 ConfigurationPropertiesBindingPostProcessor

│ 重新把最新的配置值“重新绑定”(re-bind)到所有
│ @ConfigurationProperties 标注的 Bean 的字段上

代码里直接使用这个普通 Java 对象,就能拿到最新值
所以这个配置属性类需要加四个注解
@Component // 必须让 Spring 管理
@ConfigurationProperties(prefix = “pattern”) // 前缀是 pattern
@Data // lombok 自动生成 getter/setter

  • 在实际开发场景中,我们通常就使用第二种,@RefreshScope+Value不利于维护性和健壮性,而ConfigurationProperties实现了零注入入侵,只改值,不会重建Bean

6.3 配置共享

  • 微服务启动时会取Nacos读取了多个配置文件:

    1. [spring.application.name]-[spring.profiles.active].yaml 例:user-service-dev.yaml
    2. [spring.application.name].yaml 例:userservice.yaml

      这里的意思是读取了一个dev也就是开发环境和读取了一个总环境

  • [spring.application.name].yaml是总环境,会给多个子环境比如dev/test的环境共享配置

6.3.1 添加一个环境共享配置

  • 我们在Nacos中添加一个Data IDuser-service.yaml的文件,编写配置内容如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
        pattern:
    envSharedValue: 多环境共享属性值
    ```

    * 修改`user-service-dev.yaml`
    ```yml
    pattern:
    dateformat: yyyy/MM/dd HH:mm:ss
    env: user-service开发环境配置

6.3.2 在user-service中读取共享配置

  • 修改我们的PatternProperties类,添加envSharedValueenv属性

    1
    2
    private String envSharedValue;
    private String env;
  • 同时修改UserController,添加一个方法prop

    • 这里是为了测试有没有读取到总环境配置的共享
      1
      2
      3
      4
          @GetMapping("/prop")
      public PatternProperties prop() {
      return patternProperties;
      }
  • 先启动UserApplication1,这时我们在bootstrap.yml的配置文件里写了,这个是dev开发环境

  • 将profiles:active从dev改成test,然后再启动UserApplication2,这样就是两个不同环境下的两个实例,但是他们都归属于user-service总环境,被其配置共享。

  • 然后在Nacos里新建一个user-service-test.yaml

    1
    2
    3
    pattern:
    dateformat: yyyy-MM-dd HH:mm:ss
    env: user-service测试环境配置
  • 现在

    • UserApplication实例读取了user-service-dev.yamluser-service.yaml
    • UserApplication2实例读取了user-service-test.yamluser-service.yaml
  • 打开浏览器分别访问http://localhost:8081/user/prophttp://localhost:8082/user/prop,看到的结果如下

    • 8081: {"dateformat":"yyyy/MM/dd HH:mm:ss","envSharedValue":"多环境共享属性值","env":"user-service开发环境配置"}
    • 8082: {"dateformat":"yyyy-MM-dd HH:mm:ss","envSharedValue":"多环境共享属性值","env":"user-service测试环境配置"}
  • 由此可见,实例1和实例2都读取到了总配置环境user-service.yaml的配置,然后分别读取他们自己的子环境的配置

6.3.3 配置共享的优先级

  • 当Nacos、服务笨蛋同时出现相同属性时,优先级也有高低之分
    • 服务名-profile.yaml > 服务名.yaml > 本地配置
      • user-service-dev.yaml > user-service.yaml > application.yaml

6.4 搭建Nacos集群

  • 集群结构图
    • Nacos生产环境下一定要部署为集群状态

    • 官方给的Nacos集群图:

      alt Nacos集群图
      alt Nacos集群图

    • 其中包含了三个Nacos节点,然后一个负载均衡器配置三个Nacos,这里负载均衡器可以用 Nginx。

7. Feign远程调用

  • 先看看以前如何使用RestTemplate发起远程调用的代码

    1
    2
    3
    4
    //2.1 URL地址,用user-service代替地址
    String url = "http://user-service/user/" + order.getUserId();
    //2.2 发起调用
    User user = restTemplate.getForObject(url, User.class);
  • 存在以下问题:

    • 代码可读性差,编程体验不统一
    • 参数复杂的URL难以维护(百度随便搜一个中文名词,然后看一下url有多长,有多少参数)
  • 我们可以利用Feign来解决上面提到的问题

7.1 Feign代替RestTemplate

  • Feign的使用步骤:

    1. 引入依赖

      • 给order-service的pom文件引入Feign的依赖
        1
        2
        3
        4
        <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>
    2. 添加注解

      • 在order-service的启动类上添加@EnableFeignClients注解,开启Feign功能
    3. 编写Feign客户端

      • 在order-service中新建com.itcast.order.client包,然后新建一个接口,内容如下

      • 这个客户端主要是基于SpringMVC的注解来声明远程调用的信息,比如

        • 服务名称:user-service
        • 请求方式:GET
        • 请求路径:/user/{id}
        • 请求参数:Long id
        • 返回值类型:User

    这样,Feign就可以帮助我们发送http请求,无需自己使用RestTemplate来发送了

    1. 测试
      • 修改order-service中的OrderService类中的queryOrderById方法,使用Feign客户端代替RestTemplate
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
        @Resource
    private OrderMapper orderMapper;
    @Resource
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
    // 1.查询订单
    Order order = orderMapper.findById(orderId);
    //2. 远程查询User
    User user = userClient.findById(order.getUserId());
    //3. 存入user
    order.setUser(user);
    // 4.返回
    return order;
    }
    1. 总结
    • 使用Feign的步骤
      • 引入依赖
      • 主启动类添加@EnableFeignClients注解
      • 编写FeignClient接口
      • 使用FeignClient中定义的方法替代RestTemplate
      • 注意:这里两个服务的组必须一样(别踩坑)

7.2 自定义配置

  • Feign可以支持很多自定义配置,如下表
类型 作用 说明
feign.Logger.Level 修改日志级别 包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 响应结果的解析器 http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder 请求参数编码 将请求参数编码,便于通过http请求发送
feign. Contract 支持的注解格式 默认是SpringMVC的注解
feign. Retryer 失败重试机制 请求失败的重试机制,默认是没有,不过会使用Ribbon的重试
  • 一般情况下,默认值就能满足我们的使用,如果需要自定义,只需要创建自定义的@Bean覆盖默认的Bean即可,下面以日志为例演示如果自定义配置
  1. 基于配置文件修改Feign的日志级别

    • 可以针对单个服务

      1
      2
      3
      4
      5
      feign:  
      client:
      config:
      userservice: ## 针对某个微服务的配置
      loggerLevel: FULL ## 日志级别
    • 也可以针对所有服务

      1
      2
      3
      4
      5
      feign:  
      client:
      config:
      default: ## 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
      loggerLevel: FULL ## 日志级别
    • 而日志的级别分为四种

      • NONE:不记录任何日志信息,这是默认值
      • BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
      • HEADERS:在BASIC的基础上,额外记录了请求和响应头的信息
      • FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
    • 修改之后发送请求,发现日志很完整

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    11-24 18:40:17:623  INFO 32172 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
    11-24 18:40:17:623 INFO 32172 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
    11-24 18:40:17:626 INFO 32172 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 3 ms
    11-24 18:40:17:654 INFO 32172 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
    11-24 18:40:17:768 INFO 32172 --- [nio-8080-exec-1] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
    11-24 18:40:17:775 DEBUG 32172 --- [nio-8080-exec-1] c.i.order.mapper.OrderMapper.findById : ==> Preparing: select * from tb_order where id = ?
    11-24 18:40:17:785 DEBUG 32172 --- [nio-8080-exec-1] c.i.order.mapper.OrderMapper.findById : ==> Parameters: 101(Long)
    11-24 18:40:17:796 DEBUG 32172 --- [nio-8080-exec-1] c.i.order.mapper.OrderMapper.findById : <== Total: 1
    11-24 18:40:17:800 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] ---> GET http://user-service/user/1 HTTP/1.1
    11-24 18:40:17:800 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] ---> END HTTP (0-byte body)
    11-24 18:40:18:402 INFO 32172 --- [nio-8080-exec-1] c.netflix.loadbalancer.BaseLoadBalancer : Client: user-service instantiated a LoadBalancer: DynamicServerListLoadBalancer:{NFLoadBalancer:name=user-service,current list of Servers=[],Load balancer stats=Zone stats: {},Server stats: []}ServerList:null
    11-24 18:40:18:406 INFO 32172 --- [nio-8080-exec-1] c.n.l.DynamicServerListLoadBalancer : Using serverListUpdater PollingServerListUpdater
    11-24 18:40:18:455 WARN 32172 --- [nio-8080-exec-1] c.alibaba.cloud.nacos.ribbon.NacosRule : A cross-cluster call occurs,name = user-service, clusterName = HZ, instance = [Instance{instanceId='192.168.74.1#8081#SH#DEFAULT_GROUP@@user-service', ip='192.168.74.1', port=8081, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='SH', serviceName='DEFAULT_GROUP@@user-service', metadata={preserved.register.source=SPRING_CLOUD}}, Instance{instanceId='192.168.74.1#8082#SH#DEFAULT_GROUP@@user-service', ip='192.168.74.1', port=8082, weight=1.0, healthy=true, enabled=true, ephemeral=true, clusterName='SH', serviceName='DEFAULT_GROUP@@user-service', metadata={preserved.register.source=SPRING_CLOUD}}]
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] <--- HTTP/1.1 200 (671ms)
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] connection: keep-alive
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] content-type: application/json
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] date: Mon, 24 Nov 2025 10:40:18 GMT
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] keep-alive: timeout=60
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] transfer-encoding: chunked
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById]
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] {"id":1,"username":"柳岩","address":"湖南省衡阳市"}
    11-24 18:40:18:474 DEBUG 32172 --- [nio-8080-exec-1] cn.itcast.order.client.UserClient : [UserClient#findById] <--- END HTTP (59-byte body)
  2. 基于Java代码修改日志级别

    • 先声明一个类,然后声明一个Logger.Level的对象
    1
    2
    3
    4
    5
    6
    public class DefaultFeignConfiguration {
    @Bean
    public Logger.Level feignLogLevel(){
    return Logger.Level.BASIC; //日志级别设置为 BASIC
    }
    }
    • 如果要全局生效,将其放到启动类@EnableFeignClients这个注解中
      • 注意,这里修改的是启动类里的@EnableFeignClients注解里
    1
    @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class)
    • 如果局部生效,则把它放到对应的@FeignClient注解中
    1
    @FeignClient(value = "user-service", configuration = DefaultFeignConfiguration.class)

绝大部分情况下,我们都用第一种方式来实现对Feign的自定义配置

7.3 Feign使用优化

  • Feign底层发起http请求,依赖于其他的框架,其底层框架实现包括

    1. URLconnnection:默认实现,不知识连接池
    2. Apache HttpClient:支持连接池
    3. OKHttp:支持连接池
  • 因此提高Feign的性能主要手段就是使用连接池,

  • 这里使用Apache HttpClient

  1. 引入依赖

    • 在 order-service的pom文件中引入Apache的HTTPClient依赖
    1
    2
    3
    4
    5
    <!--httpClient的依赖 -->
    <dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
    </dependency>
  2. 配置连接池

    • 在order-service的application.yml文件中添加配置
    1
    2
    3
    4
    httpclient:
    enabled: true
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 每个路径的最大连接数
  • 总结
    1. 日志级别尽量用BASIC
    2. 使用httpClient或者OKHttp来代替URLConnection

7.4 最佳实践

  • 仔细观察,Feign的客户端于服务提供者与Controller层的代码十分相似,
1
2
3
4
5
@FeignClient("user-service")
public interface UserClient {
@GetMapping("/user/{id}")
User findById(@PathVariable("id") Long id);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@RequestMapping("order")
public class OrderController {

@Resource
private OrderService orderService;

@GetMapping("{orderId}")
public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
// 根据id查询订单并返回
return orderService.queryOrderById(orderId);
}
}

  • 他们都有同样的请求方式GET,都是一样的参数,因为他们都是去实现一个功能:返回给前端用户信息,我们有什么方法取简化这种重复代码的编写呢?

7.4.1 继承方法

  • 这两部分相同的代码,可以通过继承来共享

    1. 定义一个API接口,利用定义方法,并基于SpringMVC注解做声明
    1
    2
    3
    4
    public interface UserAPI{
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
    }
    1. Feign客户端和Controller都继承该接口
    • 这里不做演示,直接说缺点
      • 服务提供方和消费方都继承了一个接口,造成了紧耦合。
      • 参数列表中的注解映射不会被继承,所以Controller层必须再次声明方法,参数和注解

7.4.2 抽取方法

  • 将Feign的Client抽取为独立的模块,并且把接口有关的POJO,默认的Feign都配置到这个模块,提供给所有消费者使用。
  • 举例
    • 将UserClient,User,Feign的默认配置都抽取到一个feign-api包中,所有的微服务都引用该依赖包,即可直接使用
      alt Feign抽取方法实现远程调用
      alt Feign抽取方法实现远程调用

7.4.3 实现基于抽取的Feign远程调用

  1. 首先新建一个module,命名为feign-api,然后在POM文件中引入feign的starter依赖

    1
    2
    3
    4
    <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
  2. 将order-service中的UserClient,User等类和接口都转移到feign-api模块里,然后在order-service中的pom文件中引入我们自己编写的feign-api依赖

    1
    2
    3
    4
    5
    6
    <!--        引入feign的统一api-->
    <dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
    </dependency>
  3. 解决包扫描问题

    • 我们实现了上面的代码,启动但是报错了,为什么?

    • 现在UserClient在feign-api模块的feign.client包下,而order-service的@EnableFeignClients注解在order模块下,不在同一个模块同一个包,无法扫描UserClient

    • 解决方法一:指定Feign应该扫描的包

    1
    @EnableFeignClients(basePackages = "cn.itcast.feign.clients")
    • 解决方法二:指定需要加载的Client字节码
    1
    @EnableFeignClients(clients = {UserClient.class})
  4. 转移feign配置文件

    • 因为所有的feign代码都拿到了feign-api的模块,在微服务模块中只有@EnableFeignClients(clients = UserClient.class)与feign有关,所以将feign所有的配置全转移到feign-api中
    1
    2
    3
    4
    5
    6
    7
    8
    9
    feign:
    client:
    config:
    default:
    loggerLevel: BASIC
    httpclient:
    enabled: true
    max-connections: 200 # 最大连接数
    max-connections-per-route: 50 # 每个路径的最大连接数
  • 方法一有个缺陷,会扫描包里的所有客户端,第二个方法只会扫描指定的客户端,所以更推荐第二种

8. Gateway服务网关

  • SpringCloudGateway只在于为微服务框架提供一个简单有效的统一的API路由管理方式

8.1 为什么需要网关

  • Gateway网管是服务的守门神,是所有微服务的统一入口

  • 网管的核心功能特性:

    1. 请求路由
    2. 权限控制
    3. 限流
  • 架构图如下

    alt Gateway架构图
    alt Gateway架构图

  • 路由一切请求必须先经过gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由,当路由的目标服务有多个时,还需要做到负载均衡

  • 权限控制:网关作为微服务入口,需要校验用户是否有请求资格,如果没有则拦截

  • 限流:当请求量过高时,在网关中按照微服务能够接受的速度来放行请求,避免服务压力过大。

  • 在SpringCloud中网关的实现包括两种:

    1. gateway
    2. zuul
  • Zuul是基于Servlet实现的,属于阻塞式编程,而SpringCloudGateway则是基于Spring5提供的WebFlux,属于响应式编程,具有更好的性能。

8.2 Gateway快速入门

  • 下面,我们来演示一下网关的基本路由功能,基本步骤如下:
    1. 创建Gateway的模块,引入SpringCloudGateway和nacos的依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <!--网关-->
    <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>
  1. 编写启动类

    1
    2
    3
    4
    5
    6
    @SpringBootApplication
    public class GatewayApplication {
    public static void main(String[] args) {
    SpringApplication.run(GatewayApplication.class,args);
    }
    }
  2. 编写基础配置和路由规则

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    server:
    port: 10010 # 网关端口
    spring:
    application:
    name: gateway # 服务名称
    cloud:
    nacos:
    server-addr: localhost:8848 # nacos地址
    discovery:
    cluster-name: HZ # 集群名称,杭州
    namespace: 982827c7-8362-478f-a1c5-c1a208fa5ab4
    gateway:
    routes: # 网关路由配置
    - id: user-service # 路由id,自定义,只要唯一即可,这里配置的user-service的路由
    uri: lb://user-service # 路由地址,lb表示负载均衡,后面跟服务名
    # url: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
    predicates: # 路由断言,也就是判断是否符合路由规则
    - Path=/user/** # 这个按照路径匹配,只要是以/user开头就符合规则

    - id: order-service # 路由id,这里配置的order-service的路由
    uri: lb://order-service # 路由地址,lb表示负载均衡,后面跟服务名
    predicates: # 路由断言,也就是判断是否符合路由规则
    - Path=/order/** # 这个按照路径匹配,只要是以/user开头就符合规则
  3. 重启网关服务进行测试

  • 访问http://localhost:10010/user/1 时,符合/user/**规则 会访问到 http://user-service/user/1

  • 网关路由流程图

    alt 网关路由流程图
    alt 网关路由流程图

  • 总结

    • 网关搭建的步骤
      1. 创建项目,引入nacos和gateway依赖
      2. 配置application.yml,包括服务基本信息,nacos地址、路由
    • 路由配置
    1. 路由id:路由的唯一表示
    2. 路由目标(uri):路由的目标地址,http代表固定地址,lb代表根据服务名称负载均衡
    3. 路由断言(predicates):判断路由的规则
    4. 路由过滤器(filters):对请求或相应做处理

8.3 断言工厂

  • 我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并且处理,转变为路由判断的条件。
  • 例如Path=/user/**式按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类处理的,像这样的断言工厂吗,在SpingCloudGateway中还有十几个
名称 说明 示例
After 是某个时间点后的请求 - After=2037-01-20T17c:42:47.789-07:00[America/Denver]
Before 是某个时间点之前的请求 - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai]
Between 是某两个时间点之前的请求 - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver]
Cookie 请求必须包含某些cookie - Cookie=chocolate, ch.p
Header 请求必须包含某些header - Header=X-Request-Id, \d+
Host 请求必须是访问某个host(域名) - Host=.somehost.org,.anotherhost.org
Method 请求方式必须是指定方式 - Method=GET,POST
Path 请求路径必须符合指定规则 - Path=/red/{segment},/blue/**
Query 请求参数必须包含指定参数 - Query=name, Jack或者- Query=name
RemoteAddr 请求者的ip必须是指定范围 - RemoteAddr=192.168.1.1/24
Weight 权重处理

8.4 过滤器工厂

  • GatewayFliter是网关中提供的一种过滤器,可以对进去网关的请求和微服务返回的响应做处理
    alt Gateway过滤器工厂原理图
    alt Gateway过滤器工厂原理图

8.4.1 路由过滤器的种类

  • Spring提供了31种不同的路由过滤器,例如
名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

官方文档的使用举例

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: add_request_header_route
uri: https://example.org
filters:
- AddRequestHeader=X-Request-red, blue

This listing adds X-Request-red:blue header to the downstream request’s headers for all matching requests.

8.4.2 请求头过滤器

  • 下面我们以AddRequestHeader为例,作为讲解

    需求:给所有进去user-service实例的请求都添加一个请求头: World.search(you);

  • 只需要修改gateway服务的application.yml文件,添加路由过滤即可

    1
    2
    3
    4
    5
    6
    7
    8
    9
    gateway:
    routes: # 网关路由配置
    - id: user-service # 路由id,自定义,只要唯一即可,这里配置的user-service的路由
    uri: lb://users-service # 路由地址,lb表示负载均衡,后面跟服务名
    # url: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
    predicates: # 路由断言,也就是判断是否符合路由规则,如果不符合就拦截
    - Path=/user/** # 这个按照路径匹配,只要是以/user开头就符合规则
    filters:
    - AddRequestHeader=HelloWorld,where are you # 添加请求头,注意是KV对
  • 当前过滤器写在user-service路由下,因为仅对user-service实例的请求有效,我们在UserController中编写对应的方法来测试

    1
    2
    3
    4
    @GetMapping("/test")
    public void test(@RequestHeader("Truth") String tmp) {
    System.out.println(tmp);
    }

8.5 全局过滤器

  • 上面的过滤器的每一种作用都是固定的,如果我们希望拦截请求,做业务逻辑,则无法实现,这就需要用到我们的全局过滤器

8.5.1 全局过滤器的作用

  • 也是处理一切进入网关的请求和微服务相应,与GatewayFilter的作用一样,区别在于GatewayFilter通过配置定义,处理的逻辑是固定的,GlobalFilter的逻辑需要我们自己编写代码去实现

  • 定义的方式就是借助GlobalFilter接口

1
2
3
4
5
6
7
8
9
10
public interface GlobalFilter {
/**
* 处理当前请求,有必要的话通过{@link GatewayFilterChain}将请求交给下一个过滤器处理
*
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}
  • 在filter中编写自定义逻辑,可以实现以下功能
    1. 登录状态判断
    2. 权限校验
    3. 请求限流

8.5.2 自定义全局过滤器

  • 需求:定义全局过滤器,拦截请求,判断请求参数是否满足以下条件

    1. 参数中是否含有authorization
    2. authorization参数是否有admin
  • 如果同时满足,则放行,否则拦截

  • 具体实现如下

    • 在gateway模块下新建一个filter包,在其中编写AuthorizationFilter类,实现GlobalFilter接口,重写filter方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    //1. 获取请求参数
    MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();

    //2. 获取authorization参数 getFirst代表取出第一个匹配的
    String authorization = queryParams.getFirst("authorization");

    //3. 校验
    if("admin".equals(authorization)){
    //4. 满足条件放行 这里是传给下一个过滤器
    return chain.filter(exchange);
    }

    //5. 不满足条件,设置状态码,常量底层是401 在restful中401表示未登录
    exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
    //6. 结束处理
    return exchange.getResponse().setComplete();
    }
  • 给过滤器加上@Component,注入到Spring里,并加上@Order注解来设置过滤器优先级

  • 这时访问 http://localhost:10010/user/1 会显示401未登录。
    访问 http://localhost:10010/user/1?authorization=admin 就能查到数据

  • 过滤器执行顺序

    • 请求进入网关会碰到三类过滤器:当前路由的过滤器,DefaultFilter,GlobalFilter
    • 请求路由后,会将当前路由过滤器和DefaultFilter,GlobalFilter合并到一个过滤器链(集合)中,排序后一次执行每个过滤器
    • 排序的顺序就是看过滤器的order值,order值越小,优先级越高,执行顺序越靠前(默认值为2147483647,即int最大值)

8.6 跨域问题

  • 跨域
    域名不一样就是跨域,包括:

    1. 域名不同www.baidu.comwww.bilibili.com
    2. 域名相同,端口不同localhost:8080localhost:8081
      **
  • 跨域问题浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题

  • 解决方法CORS

    • CORS是一个W3C标准,全程是跨域资源共享(Cross-origin resource sharing)
    • 他允许浏览器向跨源服务器发送XMLHttpRequest请求。
  • 解决跨域问题:

    • 在gateway服务的application.yml文件中添加以下配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
cloud:
gateway:
globalcors: ## 全局的跨域处理
add-to-simple-url-handler-mapping: true ## 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: ## 允许哪些网站的跨域请求
- "http://localhost:9527"
allowedMethods: ## 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" ## 允许在请求中携带的头信息
allowCredentials: true ## 是否允许携带cookie
maxAge: 360000 ## 这次跨域检测的有效期

9. 结语

  • 一共学了四天,完结撒花 -2025/11/25
  • 标题: SpringCloud
  • 作者: yin_bo_
  • 创建于 : 2025-11-21 15:25:59
  • 更新于 : 2025-11-25 17:50:00
  • 链接: https://www.blog.yinbo.xyz/2025/11/21/微服务/SpringCloud/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。