认识Springcloud

Springcloud

定义

Spring Cloud是一系列框架的有序集合
它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务发现注册、配置中心、消息总线、负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署
Spring Cloud并没有重复制造轮子,它只是将各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、易部署和易维护的分布式系统开发工具包

核心组件(本文将要学习的)
  • 服务注册与发现:AlibabaNacos
  • 服务调用和负载均衡:OpenFeign
  • 分布式事务:Alibaba Seata
  • 服务熔断和降级:Alibaba Sentinel
  • 服务网关:GateWay
  • 分布式配置管理:AlibabaNacos

Spring Cloud Alibaba

定义

Spring Cloud Alibaba是阿里巴巴结合自身的微服务实践开源的微服务全家桶
它是对Spring Cloud组件的扩展和兼容,提供了更加丰富的微服务组件和功能
2018.10.31,Spring Cloud Alibaba 正式入驻了 Spring Cloud 官方孵化器,并在 Maven 中央库发布了第一个版本

官网:https://spring.io/projects/spring-cloud-alibaba
Github:https://github.com/alibaba/spring-cloud-alibaba
中文参考文档:https://spring-cloud-alibaba-group.github.io/github-pages/2022/zh-cn/
英文参考文档:https://spring-cloud-alibaba-group.github.io/github-pages/2022/en-cn/

组件:

  • Sentinel:把流量作为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性
  • Nacos:一个更易于构建云原生应用的动态服务发现、配置管理和服务平台
  • seata:阿里巴巴开源产品,一个易于使用的高性能微服务分布式解决方案

快速开始

准备工作:
步骤一:创建一个初始java工程
步骤二:注解生效激活 setting-> Build… -> Compiler -> Annotation Processors
步骤三:字符编码 setting-> Editor -> file Encoding -> 全部选择utf-8
步骤四:setting-> Build… -> Compiler -> Java Compiler -> 选择Java17版本
步骤五:引入相关依赖

注意:

先把dependencyManagement注释掉,下载完成依赖后再写上去,不然会爆红

dependencyManagement在Maven项目中的作用主要是集中管理项目的依赖版本,确保项目中各个模块使用相同的依赖版本,减少潜在的冲突和兼容性问题,简化依赖声明,提高项目的可维护性。通过这个元素,所有依赖的版本可以由父模块统一管理,但是需要注意的是,>里只是声明依赖,并不自动实现引入(可能这就是直接下载依赖爆红的原因),需要子项目声明需要用的依赖,如果不在子项目中声明依赖,是不会从父项目中继承下来的。如果在子项目中写了该依赖项,但是没有指定具体版本,这时候才会从父项目中继承相应依赖以及相应的version和scope。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>

<groupId>com.atguigu.cloud</groupId>
<!-- 这里改成你自己的工程的名字 -->
<artifactId>xxxxxxxx</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<hutool.version>5.8.22</hutool.version>
<lombok.version>1.18.26</lombok.version>
<druid.version>1.1.20</druid.version>
<mybatis.springboot.version>3.0.2</mybatis.springboot.version>
<mysql.version>8.0.11</mysql.version>
<swagger3.version>2.2.0</swagger3.version>
<mapper.version>4.2.3</mapper.version>
<fastjson2.version>2.0.40</fastjson2.version>
<persistence-api.version>1.0.2</persistence-api.version>
<spring.boot.test.version>3.1.5</spring.boot.test.version>
<spring.boot.version>3.2.0</spring.boot.version>
<spring.cloud.version>2023.0.0</spring.cloud.version>
<spring.cloud.alibaba.version>2022.0.0.0-RC2</spring.cloud.alibaba.version>
</properties>

<dependencyManagement>
<dependencies>
<!--springboot 3.2.0-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud 2023.0.0-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--springcloud alibaba 2022.0.0.0-RC2-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--SpringBoot集成mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>${mybatis.springboot.version}</version>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<!--通用Mapper4之tk.mybatis-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>${mapper.version}</version>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
<version>${persistence-api.version}</version>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger3.version}</version>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<optional>true</optional>
</dependency>
<!-- spring-boot-starter-test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>${spring.boot.test.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>

步骤六:新建子工程mybatis_generator2024,并引入相关依赖

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>com.atguigu.cloud</groupId>
<!-- 改成自己的项目名称 -->
<artifactId>xxxxxxxx</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<!--我自己独一份,只是一个普通Maven工程,与boot和cloud无关-->
<artifactId>mybatis_generator2024</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!--Mybatis 通用mapper tk单独使用,自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.13</version>
</dependency>
<!-- Mybatis Generator 自己独有+自带版本号-->
<dependency>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-core</artifactId>
<version>1.4.2</version>
</dependency>
<!--通用Mapper-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--mysql8.0-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>

<build>
<resources>
<resource>
<directory>${basedir}/src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.4.2</version>
<configuration>
<configurationFile>${basedir}/src/main/resources/generatorConfig.xml</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.2.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
</project>

步骤八:创建数据库,使用navicat,先创建一个名为db2024的数据库,运行以下sql文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
DROP TABLE IF EXISTS `t_pay`;

CREATE TABLE `t_pay` (

`id` INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
`pay_no` VARCHAR(50) NOT NULL COMMENT '支付流水号',
`order_no` VARCHAR(50) NOT NULL COMMENT '订单流水号',
`user_id` INT(10) DEFAULT '1' COMMENT '用户账号ID',
`amount` DECIMAL(8,2) NOT NULL DEFAULT '9.9' COMMENT '交易金额',
`deleted` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0' COMMENT '删除标志,默认0不删除,1删除',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',

PRIMARY KEY (`id`)

) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='支付交易表';

INSERT INTO t_pay(pay_no,order_no) VALUES('pay17203699','6544bafb424a');

SELECT * FROM t_pay;

步骤七:在子工程mybatis_generator2024下的src\main\resource路径下新建以下包

此版本对应Mysql5

1
2
3
4
5
6
7
#User表包名
package.name=com.atguigu.cloud
# mysql5.7
jdbc.driverClass = com.mysql.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?useUnicode=true&characterEncoding=UTF-8&useSSL=false
jdbc.user = root
jdbc.password =123456

此版本对应Mysql8

1
2
3
4
5
6
7
8
#t_pay表包名
package.name=com.atguigu.cloud

# mysql8.0
jdbc.driverClass = com.mysql.cj.jdbc.Driver
jdbc.url= jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
jdbc.user = root
jdbc.password =123456
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
33
34
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"
"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd">

<generatorConfiguration>
<properties resource="config.properties"/>

<context id="Mysql" targetRuntime="MyBatis3Simple" defaultModelType="flat">
<property name="beginningDelimiter" value="`"/>
<property name="endingDelimiter" value="`"/>

<plugin type="tk.mybatis.mapper.generator.MapperPlugin">
<property name="mappers" value="tk.mybatis.mapper.common.Mapper"/>
<property name="caseSensitive" value="true"/>
</plugin>

<jdbcConnection driverClass="${jdbc.driverClass}"
connectionURL="${jdbc.url}"
userId="${jdbc.user}"
password="${jdbc.password}">
</jdbcConnection>

<javaModelGenerator targetPackage="${package.name}.entities" targetProject="src/main/java"/>

<sqlMapGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java"/>

<javaClientGenerator targetPackage="${package.name}.mapper" targetProject="src/main/java" type="XMLMAPPER"/>

<table tableName="t_pay" domainObjectName="Pay">
<generatedKey column="id" sqlStatement="JDBC"/>
</table>
</context>
</generatorConfiguration>

步骤八:打开侧栏maven界面,mybatis_generator2024 -> Plugins -> mybatis-generator -> 左键双击mybatis-generator:gererate一键生成entity+mapper接口+xml实现SQL
步骤九:新建子工程cloud-provider-payment8001,引入pom

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
<?xml version="1.0" encoding="UTF-8"?>
<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">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.atguigu.cloud</groupId>
<artifactId>mscloudV5</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>

<artifactId>cloud-provider-payment8001</artifactId>

<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>

<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--SpringBoot集成druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- Swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
<!--mybatis和springboot整合-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!--Mysql数据库驱动8 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--persistence-->
<dependency>
<groupId>javax.persistence</groupId>
<artifactId>persistence-api</artifactId>
</dependency>
<!--通用Mapper4-->
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!-- fastjson2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

步骤十:新建子工程cloud-provider-payment8001的resource下新建application.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
server:
port: 8001

# ==========applicationName + druid-mysql8 driver===================
spring:
application:
name: cloud-payment-service

datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db2024?characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&rewriteBatchedStatements=true&allowPublicKeyRetrieval=true
username: root
password: root #记得改成自己的密码

# ========================mybatis===================
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.atguigu.cloud.entities
configuration:
map-underscore-to-camel-case: true

步骤十一:新建子工程cloud-provider-payment8001的主启动 Main 改为Main8001

1
2
3
4
5
6
7
8
9
10
11
12
package com.atguigu.cloud;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import tk.mybatis.spring.annotation.MapperScan;

@SpringBootApplication
@MapperScan("com.atguigu.cloud.mapper")
public class Main8001 {
public static void main(String[] args) {
SpringApplication.run(Main8001.class, args);
}
}

步骤十二:将之前自动生成的实体类和mapper都迁移到cloud-provider-payment8001中,并删除原本的(个人觉得不如直接用MybatisX生成)
并且新建PayDTO,PayDTO就像Pay的儿子一样,他拥有Pay的部分可以直接暴露给前端的属性(比如密码,身份证号之类的隐私属性就不适合暴露给前端)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Data
@AllArgsConstructor
@NoArgsConstructor
// PayDTP像Pay的儿子一样,里面存放的都是可以暴露给前端的数据
public class PayDTO implements Serializable {
private Integer id;
//支付流水号
private String payNo;
//订单流水号
private String orderNo;
//用户账号ID
private Integer userId;
//交易金额
private BigDecimal amount;
}

步骤十三:完成增删改查相关代码

1
2
3
4
5
6
7
8
9
public interface PayService
{
public int add(Pay pay);
public int delete(Integer id);
public int update(Pay pay);

public Pay getById(Integer id);
public List<Pay> getAll();
}
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
@Service
public class PayServiceImpl implements PayService{
@Resource
PayMapper payMapper;
@Override
public int add(Pay pay){
return payMapper.insertSelective(pay);
}
@Override
public int delete(Integer id){
return payMapper.deleteByPrimaryKey(id);
}
@Override
public int update(Pay pay){
return payMapper.updateByPrimaryKeySelective(pay);
}
@Override
public Pay getById(Integer id){
return payMapper.selectByPrimaryKey(id);
}
@Override
public List<Pay> getAll(){
return payMapper.selectAll();
}
}
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
33
34
35
36
37
38
39
40
@RestController
@Slf4j
public class PayController {
@Resource
private PayService payService;

@PostMapping(value = "/pay/add")
public String addPay(@RequestParam Pay pay) {
log.info("pay:{}", pay);
int i = payService.add(pay);
return "成功插入记录,返回值:" + i;
}

@DeleteMapping(value = "/pay/del/{id}")
public Integer deletePay(@RequestParam("id") Integer id) {
log.info("删除的id为:{}", id);
return payService.delete(id);
}

@PutMapping(value = "/pay/update")
public String updatePay(@RequestParam PayDTO payDTO) {
log.info("pay:{}", payDTO);
Pay pay = new Pay();
BeanUtil.copyProperties(payDTO, pay);
int i = payService.update(pay);
return "成功更新记录,返回值:" + i;
}

@GetMapping(value = "/pay/get/{id}")
public Pay getById(@PathVariable("id") Integer id){
log.info("查询的id为:{}", id);
return payService.getById(id);
}

@GetMapping(value = "/pay/getAll")
public List<Pay> getAll() {
log.info("查询所有");
return payService.getAll();
}
}

注意

完成以上代码后运行Main8001,我遇到报错java: JDK isn’t specified for module ‘cloud-provider-payment8001’
解决方案:File -> Modules -> Dependencies 这里面的Module SDK版本,不要使用Project SDK17(这是idea自带的),使用自己安装的那个jdk17
但是:这样修改的话这个模块就独立了jdk版本,这意味着一旦出问题,就要全部都检查一下,微服务后面模块会非常多的,子模块应该要被父模块pom统一管理版本

步骤十四:使用Apifox/swagger进行相关测试
这里对swagger的使用再次进行讲解(还记得上次是在哪里讲的吗?)

Swagger

常用注解

最常用的是 @Tag @Schema @Operation

注解 标注位置 作用
@Tag controller类 标识controller作用
@Schema model层的JavaBean 描述模型作用及每个属性
@Operation 方法 描述方法作用
@Parameter 参数 标识参数作用
@Parameters 参数 参数多重说明
@ApiResponse 方法 描述响应状态码等
可以参考以下代码的用法:
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@Table(name = "t_pay")
@Schema(name = "支付交易表")
public class Pay {
@Id
@GeneratedValue(generator = "JDBC")
@Schema(description = "主键ID")
private Integer id;

/**
* 支付流水号
*/
@Column(name = "pay_no")
@Schema(description = "支付流水号")
private String payNo;

/**
* 订单流水号
*/
@Column(name = "order_no")
@Schema(description = "订单流水号")
private String orderNo;

/**
* 用户账号ID
*/
@Column(name = "user_id")
@Schema(description = "用户账号ID")
private Integer userId;

/**
* 交易金额
*/
@Schema(description = "交易金额")
private BigDecimal amount;

/**
* 删除标志,默认0不删除,1删除
*/
private Byte deleted;

/**
* 创建时间
*/
@Column(name = "create_time")
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
* 更新时间
*/
@Column(name = "update_time")
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;
1
2
3
4
5
6
7
8
9
10
11
12
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
@Resource
private PayService payService;

@PostMapping(value = "/pay/add")
@Operation(summary = "添加支付信息", description = "新增支付流水方法,json串做参数")
public String addPay(@RequestBody Pay pay){
log.info("插入的pay为:{}", pay);
int i = payService.add(pay);
return "成功插入记录,返回值:"+i;
}

导入配置类

含分组迭代的Config配置类:Swagger3Config

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
@Configuration
public class Swagger3Config
{
@Bean
public GroupedOpenApi PayApi()
{
return GroupedOpenApi.builder().group("支付微服务模块").pathsToMatch("/pay/**").build();
}
@Bean
public GroupedOpenApi OtherApi()
{
return GroupedOpenApi.builder().group("其它微服务模块").pathsToMatch("/other/**", "/others").build();
}
/*@Bean
public GroupedOpenApi CustomerApi()
{
return GroupedOpenApi.builder().group("客户微服务模块").pathsToMatch("/customer/**", "/customers").build();
}*/

@Bean
public OpenAPI docsOpenApi()
{
return new OpenAPI()
.info(new Info().title("cloud2024")
.description("通用设计rest")
.version("v1.0"))
.externalDocs(new ExternalDocumentation()
.description("www.atguigu.com")
.url("https://yiyan.baidu.com/"));
}
}

使用方式

访问以下网址:https://localhost:8001/swagger-ui/index.html

一些改进

时间格式问题

目前我们的时间格式是这样的:
“createTime”: “2024-11-29T00:53:31.000+00:00”
“updateTime”: “2024-11-29T00:53:31.000+00:00”

如果就这样传递给前端的话会挨揍的吧?
所以,遇事不决,注解解决

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 创建时间
*/
@Column(name = "create_time")
@Schema(description = "创建时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date createTime;

/**
* 更新时间
*/
@Column(name = "update_time")
@Schema(description = "更新时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updateTime;

“createTime”: “2024-11-29 08:53:31”,
“updateTime”: “2024-11-29 08:53:31”

统一返回值

定义返回标准格式,3大标配:

  • code状态码:由后端统一定义各种返回结果的状态码
  • message:本次接口调用的结果描述
  • data数据:本次返回的数据
  • 扩展:timestamp:接口调用时间

对返回值进行封装,有助于后期排查错误
具体实现步骤:

  1. 新建枚举类ReturnCodeEnum,HTTP请求返回的状态码
    新建枚举类 ReturnCodeEnum是为了减少因状态码拼写错误或值不匹配而导致的运行时错误,
    他主要用于定义和管理一组与应用程序返回状态相关的常量,每个枚举实例代表了一个特定的状态码和对应的描述信息
    这种设计在软件开发中非常常见,特别是在需要统一处理错误和响应状态的场景中
  2. 新建统一定义返回对象ResultData
  3. 修改PayController
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
@Getter
public enum ReturnCodeEnum {
//枚举编写主要有三步: 举值-构造-遍历
//1、举值
/**操作失败**/
RC999("999","操作XXX失败"),
/**操作成功**/
RC200("200","success"),
/**服务降级**/
RC201("201","服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202("202","热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203("203","系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204("204","授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403("403","无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401("401","匿名用户访问无权限资源时的异常"),
RC404("404","404页面找不到的异常"),
/**服务异常**/
RC500("500","系统异常,请稍后重试"),
RC375("375","数学运算异常,请稍后重试"),

INVALID_TOKEN("2001","访问令牌不合法"),
ACCESS_DENIED("2003","没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED("1001","客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR("1002","用户名或密码错误"),
BUSINESS_ERROR("1004","业务逻辑异常"),
UNSUPPORTED_GRANT_TYPE("1003", "不支持的认证模式");

//2、构造
private final String code; //自定义的状态码
private final String msg; //自定义的描述信息

ReturnCodeEnum(String code, String msg) {
this.code = code;
this.msg = msg;
}

//3、遍历枚举
//传统版
public static ReturnCodeEnum getReturnCodeEnum(String code)
{
for (ReturnCodeEnum element : ReturnCodeEnum.values()) {
if(element.getCode().equalsIgnoreCase(code))
{
return element;
}
}
return null;
}
//stream流式计算版
public static ReturnCodeEnum getReturnCodeEnumV2(String code)
{
return Arrays.stream(ReturnCodeEnum.values()).filter(x -> x.getCode()
.equalsIgnoreCase(code)).findFirst().orElse(null);
}

// // 测试代码
// public static void main(String[] args){
// System.out.println(getReturnCodeEnumV2("200"));
// System.out.println(getReturnCodeEnumV2("200").getCode());
// System.out.println(getReturnCodeEnumV2("200").getMsg());
// }
}
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
@Data
@Accessors(chain = true)
public class ResultData<T> {
//结果状态,具体状态码参见枚举类ReturnCodeEnum
private String code;
private String msg;
private T data;
private long timestamp;

public ResultData() {
this.timestamp = System.currentTimeMillis();
}
public static<T> ResultData<T> success(T data) {
ResultData<T> r = new ResultData<T>();
r.setCode(ReturnCodeEnum.RC200.getCode());
r.setMsg(ReturnCodeEnum.RC200.getMsg());
r.setData(data);
return r;
}

public static<T> ResultData<T> fail(String code, String msg){
ResultData<T> r = new ResultData<T>();
r.setCode(code);
r.setMsg(msg);
// 设置数据部分为 null,因为在失败响应中通常不包含有效的数据
r.setData(null);
return r;
}
}
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@RestController
@Slf4j
@Tag(name = "支付微服务模块", description = "支付CRUD")
public class PayController {
@Resource
private PayService payService;
@PostMapping(value = "/pay/add")
@Operation(summary = "添加支付信息", description = "新增支付流水方法,json串做参数")
public ResultData<Integer> addPay(@RequestBody Pay pay){
log.info("插入的pay为:{}", pay);
int i = payService.add(pay);
return ResultData.success(i);
}
@DeleteMapping(value = "/pay/del/{id}")
@Operation(summary = "删除支付信息", description = "根据id删除支付流水")
public ResultData<Integer> deletePay(@PathVariable("id") Integer id) {
log.info("删除的id为:{}", id);
int i = payService.delete(id);
return ResultData.success(i);
}
@PutMapping(value = "/pay/update")
@Operation(summary = "修改支付信息", description = "修改支付流水,json串做参数")
public ResultData<Integer> updatePay(@RequestBody PayDTO payDTO){
log.info("修改的payDTO为:{}", payDTO);
Pay pay = new Pay();
BeanUtils.copyProperties(payDTO, pay);

int i = payService.update(pay);
return ResultData.success(i);
}

@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "根据id查询支付信息", description = "根据id查询支付流水")
public ResultData<Pay> getById(@PathVariable("id") Integer id){
log.info("查询的id为:{}", id);
Pay pay = payService.getById(id);
return ResultData.success(pay);
}

@GetMapping(value = "/pay/getAll")
@Operation(summary = "查询所有支付信息", description = "查询所有支付流水")
public ResultData<List<Pay>> getAll() {
log.info("查询所有");
List<Pay> payList = payService.getAll();
return ResultData.success(payList);
}
}

全局异常接入返回的标准格式

为什么需要全局异常处理器?

不用再手写try……catch(如果你想写也可以,反正我不会)

具体实现步骤:

  1. 新建全局异常类GlobalExceptionHandler
  2. 修改Controller
1
2
3
4
5
6
7
8
9
10
11
12
@Slf4j
@RestControllerAdvice //为 REST 控制器提供全局的配置和增强功能
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class) //指定该方法将处理所有的RuntimeException及其子类的异常
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) //当RuntimeException被捕获并处理后,应该返回500 Internal Server Error状态码
//接受一个Exception类型的参数,表示被捕获的异常
public ResultData<String> exception(Exception e) {
log.error("全局异常信息:{}", e.getMessage());
// 返回一个封装了失败信息的ResultData对象
return ResultData.fail(ReturnCodeEnum.RC500.getCode(), e.getMessage());
}
}
1
2
3
4
5
6
7
8
9
10
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "根据id查询支付信息", description = "根据id查询支付流水")
public ResultData<Pay> getById(@PathVariable("id") Integer id){
log.info("查询的id为:{}", id);
Pay pay = payService.getById(id);
if(id < 0) {
throw new RuntimeException("id不能为负数");
}
return ResultData.success(pay);
}

HttpStatus通常指的是HTTP状态码,它们是由服务器在HTTP响应中返回的三位数字代码,用于指示客户端请求的结果。HTTP状态码分为五大类,每一类的第一个数字表示其类型:

  1. 1xx 信息响应:表示接收到请求,正在处理。
    100 Continue:客户端应该继续发送请求的剩余部分,如果服务器已经收到并将会处理请求的话。
    101 Switching Protocols:服务器已经理解了客户端的请求,并将通过Upgrade消息头把连接转换到另一个协议。
  2. 2xx 成功响应:表示请求已成功被服务器接收、理解、并接受。
    200 OK:请求成功。
    201 Created:请求已经被成功处理,服务器创建了新的资源。
    202 Accepted:服务器已接受请求,但尚未处理。
    203 Non-Authoritative Information:服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。
    204 No Content:服务器成功处理了请求,但不需要返回任何内容。
    205 Reset Content:服务器成功处理了请求,且没有返回任何内容。但是与204响应不同,此响应要求请求者重置文档视图(例如,清除表单内容以输入新内容)。
    206 Partial Content:服务器成功处理了部分GET请求。
  3. 3xx 重定向响应:表示需要客户端采取进一步的操作才能完成请求。
    300 Multiple Choices:被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的视图。
    301 Moved Permanently:请求的资源已永久移动到新的URL上。
    302 Found:请求的资源现在临时从不同的URI响应请求。
    303 See Other:对应当前请求的响应可以在另一个URI上被找到,而且客户端应当采用GET的方式访问那个资源。
    304 Not Modified:如果客户端发送了一个带条件的GET请求且该请求已被允许,而文档的内容(自上次访问以来或者根据请求的条件)并没有改变。
    307 Temporary Redirect:请求的资源现在临时从不同的URI响应请求。
  4. 4xx 客户端错误响应:表示请求包含语法错误或无法完成请求。
    400 Bad Request:服务器无法理解请求的格式,客户端不应当重复此请求。
    401 Unauthorized:请求要求身份验证。
    402 Payment Required:此响应码保留以便将来使用。初始目的是用于表示要求付款。
    403 Forbidden:服务器理解请求但拒绝执行它。
    404 Not Found:服务器无法根据客户端的请求找到资源(网页)。
    405 Method Not Allowed:服务器禁用了请求中指定的方法。
    406 Not Acceptable:根据客户端请求头中的Accept字段,服务器无法提供客户端想要的格式。
    407 Proxy Authentication Required:要求客户端在代理服务器上进行身份验证。
    408 Request Timeout:服务器等候请求时发生超时。
    409 Conflict:请求因为与资源的当前状态相冲突而无法完成。
    410 Gone:请求的资源已不在服务器上,且没有任何已知的转发地址。
  5. 5xx 服务器错误响应:表示服务器在处理请求的过程中发生了错误。
    500 Internal Server Error:服务器内部错误,无法完成请求。
    501 Not Implemented:服务器不支持请求的功能,无法完成请求。
    502 Bad Gateway:作为网关或代理工作的服务器从上游服务器收到无效响应。
    503 Service Unavailable:服务器目前无法使用(由于超载或停机维护)。
    504 Gateway Timeout:服务器作为网关或代理,但是没有及时从上游服务器收到请求。
    505 HTTP Version Not Supported:服务器不支持请求中所用的HTTP协议版本。

RuntimeException异常 是 Java 编程语言中的一种异常类型,它继承自Exception类,但是与Exception类中的其他异常不同,RuntimeException及其子类属于未检查异常(Unchecked Exception)的范畴
这意味着当 RuntimeException 或其子类异常发生时,Java 编译器不会强制要求开发者在编译时对其进行处理,需要在方法签名中使用 throws 关键字声明,或者在方法内部使用 try-catch 块捕获,这里我们当然推荐throws 关键字声明啦
它通常表示程序中的逻辑错误或资源访问问题,如空指针引用、数组越界、类型转换错误等

RuntimeException常见子类:
NullPointerException:当应用程序试图在需要对象的地方使用null时抛出。例如,当调用null对象的实例方法或访问null对象的字段时,会抛出此异常。
ArrayIndexOutOfBoundsException:当索引超出字符串或数组的有效索引范围时抛出。这通常发生在访问数组或字符串的非法索引时。
ArithmeticException:当发生数学错误时抛出。例如,整数除零时会抛出此异常。
ClassCastException:当试图将对象强制转换为不是实例的类时抛出。这通常发生在类型转换失败时。
NumberFormatException:当应用程序试图将字符串转换为一种数值类型,但该字符串没有有效的转换格式时抛出。
下面我们对空指针异常进行测试:

1
2
3
4
5
6
7
8
@ExceptionHandler(NullPointerException.class) //指定该方法将处理所有的RuntimeException及其子类的异常
@ResponseStatus(HttpStatus.NOT_FOUND) //当RuntimeException被捕获并处理后,应该返回404 NOT_FOUND状态码
//接受一个Exception类型的参数,表示被捕获的异常
public ResultData<String> handleNullPointerexception(Exception e) {
log.error("捕获到空指针异常:{}", e.getMessage());
// 返回一个封装了失败信息的ResultData对象
return ResultData.fail(ReturnCodeEnum.RC404.getCode(), e.getMessage());
}
1
2
3
4
5
6
7
8
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "根据id查询支付信息", description = "根据id查询支付流水")
public ResultData<Pay> getById(@PathVariable("id") Integer id){
log.info("查询的id为:{}", id);
Pay pay = payService.getById(id);
System.out.println(pay.getPayNo());
return ResultData.success(pay);
}
1
2
3
4
5
6
{
"code": "404",
"msg": "Cannot invoke \"com.atguigu.cloud.entity.Pay.getPayNo()\" because \"pay\" is null",
"data": null,
"timestamp": 1732938329396
}

微服务理念引入

回顾:构建微服务标配的五个步骤:
  • 建module(此处为cloud-consumer-order80)
  • 改pom
  • 写yml
  • 主启动(修改Main类名为Main80)
  • 业务类
    • entity、ResultData
    • RestTemplate
    • config配置类
    • controller
  • Apifox测试
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
33
34
35
36
37
38
39
40
41
<dependencies>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
server:
port: 80
1
2
3
4
5
6
@SpringBootApplication
public class Main80 {
public static void main(String[] args) {
SpringApplication.run(Main80.class, args);
}
}
1
2
3
4
5
6
7
8
9
@Configuration
public class RestTemplateConfig
{
@Bean
public RestTemplate restTemplate()
{
return new RestTemplate();
}
}

业务类具体说明

首先,将PayDTO和ResultData拷贝过来

RestTemplate是什么?

RestTemplate提供了多种便捷访问远程Http服务的方法,
是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集

官网地址:https://docs.spring.io/spring-framework/docs/6.0.11/javadoc-api/org/springframework/web/client/RestTemplate.html

RestTemplate怎么使用?

使用restTemplate访问restful接口非常的简单粗暴无脑。
url requestMap ResponseBean.class这三个参数分别代表
REST请求地址、请求参数、HTTP响应转换被转换成的对象类型

Modifier and Type Method Description
<T>ResponseEntity<T> getForEntity(String url, Class responseType, Object... uriVariables) Retrieve an entity by doing a GET on the specified URL.
通过对指定的 URL 执行 GET 来检索表示形式
<T> T getForObject(String url, Class responseType, Object... uriVariables) Retrieve a representation by doing a GET on the specified URL.
通过对指定的 URL 执行 GET 来检索表示形式

代码示例:

返回对象为响应体中数据转化成的对象,基本上可以理解为Json

1
2
3
4
5
@GetMapping("/consumer/payment/get/{id}")
public CommonResult<Payment> getPayment(@PathVariable("id") Long id)
{
return restTemplate.getForObject(PAYMENT_SRV + "/payment/get/"+id, CommonResult.class);
}

返回对象为ResponseEntity对象,包含了响应中的一些重要信息,比如响应头、响应状态码、响应体等

1
2
3
4
5
6
7
8
9
10
@GetMapping("/consumer/payment/getForEntity/{id}")
public CommonResult<Payment> getPayment2(@PathVariable("id") Long id)
{
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(PAYMENT_SRV + "/payment/get/"+id, CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return entity.getBody();
}else{
return new CommResult(444,"操作失败");
}
}
1
2
3
4
5
6
7
@GetMapping("/consumer/payment/creat")
public CommonResult<Payment> creat(Payment payment)
{
return restTemplate.postForObject(PAYMENT_SRV+"/payment/create",payment,CommonResult.class);

//return restTemplate.postForEntity(PAYMENT_SRV+"/payment/create",payment,CommonResult.class).getBody();
}
1
2
3
4
5
6
7
8
9
10
11
<T> T getForObject(String url, Class<T> responseType, Object... uriVariables);

<T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables);

<T> T getForObject(URI url, Class<T> responseType);

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Object... uriVariables);

<T> ResponseEntity<T> getForEntity(String url, Class<T> responseType, Map<String, ?> uriVariables);

<T> ResponseEntity<T> getForEntity(URI var1, Class<T> responseType);
1
2
3
4
5
6
7
8
9
10
11
<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);

<T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);

<T> T postForObject(URI url, @Nullable Object request, Class<T> responseType);

<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables);

<T> ResponseEntity<T> postForEntity(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables);

<T> ResponseEntity<T> postForEntity(URI url, @Nullable Object request, Class<T> responseType);

controller代码如下(可以正常运行但是感觉写的有的有点问题):

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
@RestController
public class OrderController {
public static final String PaymentSrv_URL = "http://localhost:8001";//先写死,硬编码

@Resource
private RestTemplate restTemplate;

/**
* 创建订单
*/
@PostMapping(value = "/consumer/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO)
{
return restTemplate.postForObject(PaymentSrv_URL + "/pay/add", payDTO, ResultData.class);
}
/**
* 根据id查询订单
*/
@GetMapping(value = "/consumer/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id)
{
return restTemplate.getForObject(PaymentSrv_URL + "/pay/get/" + id, ResultData.class,id);
}

/**
* 查询所有订单
*/
@GetMapping(value = "/consumer/pay/getAll")
public ResultData getAllPayInfo()
{
return restTemplate.getForObject(PaymentSrv_URL + "/pay/getAll", ResultData.class);
}

/**
* 删除订单
*/
@DeleteMapping(value = "/consumer/pay/del/{id}")
public ResultData deletePayInfo(@PathVariable("id") Integer id) {
// 使用 delete 方法而不是 getForObject
ResponseEntity<ResultData> response = restTemplate.exchange(
PaymentSrv_URL + "/pay/del/" + id,
HttpMethod.DELETE,
null, // 因为是 DELETE 请求,通常不需要请求体
ResultData.class,
id // 这里的 id 作为 URI 变量传递,但在 DELETE 请求中通常不需要作为参数传递
);
return response.getBody();
}

/**
* 修改订单
*/
@PutMapping(value = "/consumer/pay/update")
public ResultData updatePayInfo(@RequestBody PayDTO payDTO) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<PayDTO> request = new HttpEntity<>(payDTO, headers); // 假设您已经设置了适当的headers
ResponseEntity<ResultData> response = restTemplate.exchange(
PaymentSrv_URL + "/pay/update",
HttpMethod.PUT,
request,
ResultData.class
);
return response.getBody();
}
}

工程重构

项目进行到这里,大家应该会发现,我们的项目中有一些重复的类,例如:PayDTO、ResultData等等
如果每次都要这样写的话,那不是很麻烦吗?
所以我们要新建一个module:cloud-api-commons,封装对外暴露通用的组件/api/接口/工具类等
①改pom文件
②将entity下的PayDTO、统一返回(resp)粘贴到当前目录下,全局异常类可加可不加,酌情考虑
③maven命令 clean install
④删除订单80和支付8001中,刚才粘贴走的内容
⑤各自粘贴pom内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
1
2
3
4
5
6
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

Openfeign服务接口调用

基本介绍

Feign是一个声明性web服务客户端,它使编写web服务客户端变得更容易
使用Feign创建一个接口并对其进行注释,它具有可插入的注释支持,包括Feign注释和JAX-RS注释
Feign还支持可插拔编码器和解码器
Spring Cloud添加了对Spring MVC注释的支持,以及对使用Spring Web中默认使用的HttpMessageConverter的支持
Spring Cloud集成了Eureka、Spring Cloud CircuitBreaker以及Spring Cloud LoadBalancer,以便在使用Feign时提供负载平衡的http客户端

官方网站:https://springdoc.cn/spring-cloud-openfeign/
GitHub:https://github.com/OpenFeign

总结:openfeign是一个声明式的web服务客户端,只需要创建一个Rest接口并在该接口上添加注解@FeignClient即可
OpenFeign基本上就是当前微服务之间调用的事实标准

OpenFeign能干什么

前面在使用SpringCloud LoadBalancer+RestTemplate时,利用RestTemplate对http请求的封装处理形成了一套模版化的调用方法
但是在实际开发中,由于对服务依赖的调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用
所以,OpenFeign在此基础上做了进一步封装,由他来帮助我们定义和实现依赖服务接口的定义
在OpenFeign的实现下,我们只需创建一个接口并使用注解的方式来配置它(在一个微服务接口上面标注一个@FeignClient注解即可),即可完成对服务提供方的接口绑定,统一对外暴露可以被调用的接口方法,大大简化和降低了调用客户端的开发量,也即由服务提供者给出调用接口清单,消费者直接通过OpenFeign调用即可,O(∩_∩)O
OpenFeign同时还集成SpringCloud LoadBalancer
可以在使用OpenFeign时提供Http客户端的负载均衡,也可以集成阿里巴巴Sentinel来提供熔断、降级等功能
而与SpringCloud LoadBalancer不同的是,通过OpenFeign只需要定义服务绑定接口且以声明式的方法,优雅而简单的实现了服务调用
总结:

  • 可插拔的注解支持,包括Feign注解和JAX-RS注解
  • 支持可插拔的HTTP编码器和解码器
  • 支持Sentinel和他的Fallback
  • 支持SpringCloudLoadBanancer的负载均衡
  • 支持HTTP请求和响应的压缩

使用流程

  1. 新建Module:cloud-consumer-feign-order80
  2. 改pom
  3. 写yml
  4. 主启动,修改类名为MainOpenFeign0
  5. 业务类
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<dependencies>
<!--openfeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--SpringCloud consul discovery-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--hutool-all-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--fastjson2-->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<!-- swagger3 调用方式 http://你的主机IP地址:5555/swagger-ui/index.html -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 80

spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient //该注解用于向使用consul为注册中心时注册服务
@EnableFeignClients//启用feign客户端,定义服务+绑定接口,以声明式的方法优雅而简单的实现服务调用
public class MainOpenFeign80 {
public static void main(String[] args) {
SpringApplication.run(MainOpenFeign80.class,args);
}
}
业务类详细步骤
  1. 前置准备
    订单模块要去调用支付模块,订单和支付两个微服务,需要通过Api接口解耦,一般不要在订单模块写非订单相关的业务,自己的业务自己做+其它模块走FeignApi接口调用
    服务消费者 -> 公共API模块(服务接口,与服务提供者对应)-> 服务提供者

方法签名,注解路径要和服务提供者一致

1
2
3
4
5
@FeignClient(name = "服务名")
public interface xxxService{
@GetMapping("...")
boolean getxxx(...);
}
1
2
3
4
5
@RestController
public class xxxController{
@GetMapping("...")
boolean getxxx(...);
}
  1. 修改cloud-api-commons通用模块
    • 引入openfeign依赖
    • 新建服务接口PayFeignApi,配置@FeignClient注解
    • 参考微服务8001的Controller层,新建PayFeignApi接口
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
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
@FeignClient(value = "cloud-payment-service")
public interface PayFeignApi {
/**
* 添加支付信息
* @param payDTO
* @return
*/
@PostMapping(value = "/pay/add")
public ResultData addPayInfo(@RequestBody PayDTO payDTO);

/**
* 根据id查询支付信息
* @param id
* @return
*/
@GetMapping(value = "/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id);

/**
* 获取负载均衡信息(openfeign天然支持负载均衡演示)
* @return
*/
@GetMapping(value = "/pay/get/info")
public String mylb();
}
  1. 拷贝之前80工程的controller到本模块
  2. 修改controller层
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
@RestController
public class OrderController {
@Resource
private PayFeignApi payFeignClient;

@PostMapping(value = "/feign/pay/add")
public ResultData addOrder(@RequestBody PayDTO payDTO) {
System.out.println("第一步:模拟本地addOrder新增订单成功 第二步:在开启addPay支付微服务远程调用");
ResultData resultData = payFeignClient.addPayInfo(payDTO);
return resultData;
}

@GetMapping(value = "/feign/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id) {
System.out.println("支付微服务远程调用,按照id查询订单支付流水");
ResultData resultData = payFeignClient.getPayInfo(id);
return resultData;
}

/**
* 测试负载均衡
* @return
*/
@GetMapping(value = "feign/pay/mylb")
public String mylb() {
return payFeignClient.mylb();
}
}

测试

  1. 先启动Consul服务器
  2. 启动微服务8001、8002、cloud-consumer-feign-order80
  3. Apifox测试
  4. 启动8002,测试负载均衡:http://localhost/feign/pay/mylb

高级特性

OpenFeign超时控制

在Spring Cloud微服务架构中,大部分公司都是利用OpenFeign进行服务间的调用,而比较简单的业务使用默认配置是不会有多大问题的,但是如果是业务比较复杂,服务要进行比较繁杂的业务计算,那后台很有可能会出现Read Timeout这个异常,因此定制化配置超时时间就有必要了
此处我们故意设置超时,演示出错情况:

  1. 服务提供方cloud-provider-payment8001故意写暂停62秒钟程序
  2. 服务调用方cloud-consumer-feign-order80写好捕捉超时异常
  3. 测试:http://localhost/feign/pay/get/1

结论:OpenFeign静默等待60秒钟,超时后报错

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@GetMapping(value = "/pay/get/{id}")
@Operation(summary = "根据id查询支付信息", description = "根据id查询支付流水")
public ResultData<Pay> getById(@PathVariable("id") Integer id){
log.info("查询的id为:{}", id);
// 暂停62秒钟线程,模拟超时,测试出feign的默认调用超时时间
try {
TimeUnit.SECONDS.sleep(62);
} catch (InterruptedException e) {
e.printStackTrace();
}
Pay pay = payService.getById(id);
System.out.println(pay.getPayNo());
// if(pay == null) {
// throw new RuntimeException("空指针异常");
// }
return ResultData.success(pay);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@GetMapping(value = "/feign/pay/get/{id}")
public ResultData getPayInfo(@PathVariable("id") Integer id) {
System.out.println("支付微服务远程调用,按照id查询订单支付流水");
ResultData resultData = null;
try {
System.out.println("feign调用开始" + DateUtil.now());
resultData = payFeignClient.getPayInfo(id);
}catch (Exception e){
e.printStackTrace();
System.out.println("feign调用结束" + DateUtil.now());
}
return resultData;
}

默认OpenFeign客户端等待60秒钟,但是服务端处理超过规定时间会导致Feign客户端返回报错
为了避免这样的情况,有时候我们需要设置Feign客户端的超时控制,默认60秒太长或者业务时间太短都不好
yml文件中开启配置:
connectTimeout 连接超时时间
readTimeout 请求处理超时时间

修改cloud-consumer-feign-order80,yml文件里需要开启OpenFeign客户端超时控制

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
server:
port: 80

spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
#全局
#default:
#connectTimeout: 4000 #连接超时时间
#readTimeout: 4000 #读取超时时间
#自定义(优先级更高)
cloud-payment-service:
connectTimeout: 8000 #连接超时时间
readTimeout: 8000 #读取超时时间

OpenFeign重试机制

在微服务架构中,服务之间的调用是不可避免的,但是由于网络延迟、服务故障或负载过高等原因,服务调用可能会失败
开启重试机制可以在初次调用失败后,自动进行若干次重试,从而提高服务的可用性,确保请求能够成功到达目标服务

1
2
3
4
5
6
7
8
9
10
@Configuration
public class FeignConfig {
@Bean
public Retryer myRetryer(){
//return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的

//最大请求次数为3(1+2)次,每次请求间隔时间为100毫秒,重试最大间隔时间为1s
return new Retryer.Default(100, 1000, 3);
}
}

OpenFeign默认HttpClient修改

如果不做特殊配置,OpenFeign默认使用JDK自带的HttpURLConnection发送HTTP请求
由于默认HttpURLConnection没有连接池、性能和效率比较低,如果采用默认,性能上不能达到最佳,所以加到最大
按照官网建议,我们将采用使用Apache HttpClient5 替换OpenFeign默认的HttpURLConnection
在微服务cloud-consumer-openfeign-order80下进行修改:

  1. 在FeignConfig类里面将Retryer属性修改为默认
  2. 修改pom
  3. Apache HttpClient5配置开启
  4. yml修改
1
2
3
4
5
6
7
8
9
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
return Retryer.NEVER_RETRY; //Feign默认配置是不走重试策略的
}
}
1
2
3
4
5
6
7
8
9
10
11
12
<!-- httpclient5-->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
<!-- feign-hc5-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-hc5</artifactId>
<version>13.1</version>
</dependency>
1
2
3
4
5
6
7
#  Apache HttpClient5 配置开启
spring:
cloud:
openfeign:
httpclient:
hc5:
enabled: true
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
server:
port: 80

spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
default:
connectTimeout: 4000 #连接超时时间
readTimeout: 4000 #读取超时时间
httpclient:
hc5:
enabled: true
#cloud-payment-service:
#connectTimeout: 4000 #连接超时时间
#readTimeout: 4000 #读取超时时间
```
<!-- endtab -->
{% endtabs %}

### OpenFeign请求/响应压缩
对请求和响应进行GZIP压缩
Spring Cloud OpenFeign支持对请求和响应进行GZIP压缩,以减少通信过程中的性能损耗
通过下面的两个参数设置,就能开启请求与相应的压缩功能:
<code>spring.cloud.openfeign.compression.request.enabled=true</code>
<code>spring.cloud.openfeign.compression.response.enabled=true</code>

细粒度化设置
对请求压缩做一些更细致的设置,比如下面的配置内容指定压缩的请求数据类型并设置了请求压缩的大小下限:
只有超过这个大小的请求才会进行压缩:
spring.cloud.openfeign.compression.request.enabled=true
spring.cloud.openfeign.compression.request.mime-types=text/xml,application/xml,application/json #触发压缩数据类型
spring.cloud.openfeign.compression.request.min-request-size=2048 #最小触发压缩的大小

对application.yml进行配置:
```yml
server:
port: 80

spring:
application:
name: cloud-consumer-openfeign-order
####Spring Cloud Consul for Service Discovery
cloud:
consul:
host: localhost
port: 8500
discovery:
prefer-ip-address: true #优先使用服务ip进行注册
service-name: ${spring.application.name}
openfeign:
client:
config:
#全局
default:
connectTimeout: 4000 #连接超时时间
readTimeout: 4000 #读取超时时间
#自定义(优先级更高)
#cloud-payment-service:
#connectTimeout: 8000 #连接超时时间
#readTimeout: 8000 #读取超时时间
httpclient:
hc5:
enabled: true
compression:
request:
enabled: true
min-request-size: 2048 #最小触发压缩的大小
mime-types: text/xml,application/xml,application/json #触发压缩数据类型
response:
enabled: true
```

### OpenFeign日志打印功能
Feign 提供了日志打印功能,我们可以通过配置来调整日志级别,从而了解 Feign 中 Http 请求的细节
说白了就是对Feign接口的调用情况进行监控和输出
>日志级别
NONE:默认的,不显示任何日志;
BASIC:仅记录请求方法、URL、响应状态码及执行时间;
HEADERS:除了 BASIC 中定义的信息之外,还有请求和响应的头信息;
FULL:除了 HEADERS 中定义的信息之外,还有请求和响应的正文及元数据。

配置日志Bean以及application.yml
{% tabs %}
<!-- tab FeignConfig-->
```java
@Configuration
public class FeignConfig
{
@Bean
public Retryer myRetryer()
{
return Retryer.NEVER_RETRY; //默认
}

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

公式(三段):logging.level + 含有@FeignClient注解的完整带包名的接口名+debug

1
2
3
4
5
6
7
8
# feign日志以什么级别监控哪个接口
logging:
level:
com:
atguigu:
cloud:
apis:
PayFeignApi: debug

Nacos服务注册与发现

Nacos简介

Nacos是什么

Nacos:Dynamic Naming and Configuration Service
前四个字母分别为Naming和Configuration的前两个字母,最后的s为Service

官网:https://nacos.io/
一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台

Nacos就是 注册中心 + 配置中心的组合
Nacos = Eureka + Config + Bus
Nacos = Spring Cloud Consul

Nacos能干嘛

①代替Eureka/Consul做服务注册中心
②代替(Config + Bus)/Consul做服务配置中心和满足动态刷新广播通知
各种注册中心的比较:

服务注册与发现框架 CAP模型 控制台管理 社区活跃度
Eureka AP 支持 低(2.x版本闭源)
Zookeeper CP 不支持
Consul CP 支持
Nacos AP 支持
据说 Nacos 在阿里巴巴内部有超过 10 万的实例运行,已经过了类似双十一等各种大型流量的考验 Nacos默认是AP模式,但也可以调整切换为CP,我们一般用默认AP即可
小Tip:CAP模型

CAP模型,又称为CAP理论或Brewer’s理论,由Eric Brewer教授在2000年提出
它描述了在分布式系统中三个重要属性之间的取舍关系,这三个属性分别是一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)
CAP理论指出,在一个分布式系统中,不可能同时满足这三个属性的强一致性。因此,系统设计者需要在它们之间进行权衡和取舍。具体来说,有以下三种组合方式:

  1. CA模型:
    保证系统的一致性和可用性,但损失了分区容错性。
    这种模型通常适用于单机系统或小规模分布式系统,但在大型分布式系统中,由于网络分区和节点故障的可能性增加,因此很难同时满足一致性和可用性。
  2. CP模型:
    保证了分布式架构中数据的一致性,但牺牲了可用性。
    这种模型适用于对一致性要求苛刻的业务场景,如金融、医疗等领域。在这些场景中,数据的一致性至关重要,即使牺牲一些可用性也是值得的。
  3. AP模型:
    保证了服务在分布式架构中的可用性,但牺牲了一致性。
    这种模型适用于对可用性要求较高的业务场景,如互联网服务、社交媒体等领域。在这些场景中,系统的可用性至关重要,因为用户希望随时能够访问和使用服务。同时,由于数据量巨大且变化频繁,因此很难保证数据的一致性。不过,可以通过最终一致性(Eventual Consistency)等机制来尽量保证数据的正确性。

快速开始

步骤一:为了和前面的版本相匹配,此处的Nacos务必下载2.2.3版本
步骤二:下载完成后解压压缩包,直接运行bin目录下的startup.cmd(或者在bin目录下运行cmd,运行命令startup.cmd -m standalone)
步骤三:命令运行成功后直接访问:http://192.168.x.x:8848/nacos/index.html 根据自己命令行输出结果访问
默认账号密码都是nacos
步骤四:关闭服务器,有两种方法,可以直接运行shutdown.cmd 也可以 Ctrl+C

Nacos Discovery服务注册中心

通过 Nacos Server 和 spring-cloud-starter-alibaba-nacos-discovery 实现服务的注册与发现
建议和官方文档搭配食用:https://spring-cloud-alibaba-group.github.io/github-pages/2022/zh-cn/

基于Nacos的服务提供者

  1. 新建Module:cloudalibaba-provider-payment9001
  2. pom
  3. yml
  4. 主启动
  5. 业务类
  6. 测试
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
server:
port: 9001

spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 #配置Nacos地址 根据上一步的地址写
1
2
3
4
5
6
7
8
9
@SpringBootApplication
@EnableDiscoveryClient
public class Main9001
{
public static void main(String[] args)
{
SpringApplication.run(Main9001.class,args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.atguigu.cloud.controller;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class PayAlibabaController
{
@Value("${server.port}")
private String serverPort;

@GetMapping(value = "/pay/nacos/{id}")
public String getPayInfo(@PathVariable("id") Integer id)
{
return "nacos registry, serverPort: "+ serverPort+"\t id"+id;
}
}

测试:访问http://localhost:9001/pay/nacos/11
显示:nacos registry, serverPort: 9001 id11
访问控制台,查看服务列表,nacos-payment-provider已存在

基于Nacos的服务消费者

  1. 新建Module:cloudalibaba-consumer-nacos-order83
  2. pom
  3. yml
  4. 主启动
  5. 业务类
    • 配置config
    • OrderNacosController
  6. 测试
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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<dependencies>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- 引入自己定义的api通用包 -->
<dependency>
<groupId>com.atguigu.cloud</groupId>
<artifactId>cloud-api-commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--SpringBoot通用依赖模块-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--hutool-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.28</version>
<scope>provided</scope>
</dependency>
<!--test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 83

spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
#消费者将要去访问的微服务名称(nacos微服务提供者叫什么你写什么)
service-url:
nacos-user-service: http://nacos-payment-provider
1
2
3
4
5
6
7
8
9
@EnableDiscoveryClient
@SpringBootApplication
public class Main83
{
public static void main(String[] args)
{
SpringApplication.run(Main83.class,args);
}
}
1
2
3
4
5
6
7
8
9
10
@Configuration
public class RestTemplateConfig
{
@Bean
@LoadBalanced //赋予RestTemplate负载均衡的能力
public RestTemplate restTemplate()
{
return new RestTemplate();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RestController
public class OrderNacosController
{
@Resource
private RestTemplate restTemplate;

@Value("${service-url.nacos-user-service}")
private String serverURL;

@GetMapping("/consumer/pay/nacos/{id}")
public String paymentInfo(@PathVariable("id") Integer id)
{
String result = restTemplate.getForObject(serverURL + "/pay/nacos/" + id, String.class);
return result+"\t"+" 我是OrderNacosController83调用者。。。。。。";
}
}

测试:访问http://localhost:83/consumer/pay/nacos/15
显示:nacos registry, serverPort: 9001 id15 我是OrderNacosController83调用者。。。。。。
访问控制台,查看服务列表,nacos-order-consumer已存在

负载均衡

为了方便,我们这里采用直接拷贝虚拟端口映射的方式来新建9002端口:
①在IDEA的service中,右键Main9001,点击copy Configuration…选项
②name命名为Main9002(copy from 9001)
③点击Modify options,点击 Add VM options,填写-DServer.port=9002
④运行9001和9002.打开Nacos控制台,发现当前nacos-payment-provider的实例个数变为2

测试:访问http://localhost:83/consumer/pay/nacos/15,不断点击刷新
现象:端口号9001和9002交替出现,说明负载均衡达到

Nacos Config服务配置中心

通过Nacos Server和spring-cloud-starter-alibaba-nacos-config实现配置的动态变更

  1. 新建Module:cloudalibaba-config-nacos-client3377
  2. pom
  3. application.yml 和 bootstrap.yml
    为什么配置两个?
    Nacos同Consul一样,在项目初始化时,要保证先从配置中心进行配置拉取,拉取配置之后,才能保证项目的正常启动
    springboot中配置文件的加载是存在优先级顺序的,bootstrap优先级高于application
    这样配置不仅遵循了Spring Boot配置文件加载的优先级规则,还确保了项目在初始化阶段能够正确地从配置中心拉取配置信息,并在运行时根据动态刷新的配置进行灵活调整
  4. 主启动
  5. 业务类
  6. 在Nacos中添加配置信息(重点)
  7. 测试
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
33
34
35
36
37
38
39
40
41
<dependencies>
<!--bootstrap-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--nacos-discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--web + actuator-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!--lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# nacos配置
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置

# nacos端配置文件DataId的命名规则是:
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# 本案例的DataID是:nacos-config-client-dev.yaml
1
2
3
4
5
6
7
8
server:
port: 3377

spring:
profiles:
active: dev # 表示开发环境
#active: prod # 表示生产环境
#active: test # 表示测试环境
1
2
3
4
5
6
7
8
9
@EnableDiscoveryClient
@SpringBootApplication
public class NacosConfigClient3377
{
public static void main(String[] args)
{
SpringApplication.run(NacosConfigClient3377.class,args);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@RestController
@RefreshScope //在控制器类加入@RefreshScope注解使当前类下的配置支持Nacos的动态刷新功能。
public class NacosConfigClientController
{
@Value("${config.info}")
private String configInfo;

@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}

小Tip:Nacos命名规则
在Nacos Spring Cloud中,数据集(Data Id)的配置完整格式如下:${prefix}-${spring.profile.active}.${file-extension}
prefix: 表示配置的服务名,通常是在服务注册时注册到服务中心的服务名
spring.profile.active: 表示配置的开发环境,如开发环境(dev)、测试环境(test)、生产环境(prod)等。通过配置不同的环境标识,可以方便地切换不同环境的配置文件
file-extension: 表示配置文件的类型,如properties、yaml等
例如,对于一个名为nacos-config-client的服务,在开发环境(dev)下的配置文件命名为nacos-config-client-dev.yaml

进入Nacos控制台 -> 配置列表 -> 创建配置 -> Data ID:nacos-config-client-dev.yaml -> 配置格式为yaml

1
2
config:
info: nacos-config-client-dev.yaml, come from nacosconfig, version=1

Nacos数据模型之Namespace-Group-DataId

问题1:

实际开发中,通常一个系统会准备
dev开发环境
test测试环境
prod生产环境
如何保证指定环境启动时服务能正确读取到Nacos上相应环境的配置文件呢?

问题2:

一个大型分布式微服务系统会有很多微服务子项目,
每个微服务项目又都会有相应的开发环境、测试环境、预发环境、正式环境…
那怎么对这些微服务配置进行分组和命名空间管理呢?

Namespace+Group+dataId 三者的关系是什么?为什么这么设计?
Nacos数据模型Key由三元组唯一确定,Namespace默认是空串,公共命名空间(public),分组默认是DEFAULT_GROUP

  1. Namespace(命名空间):
    作用:用于进行粗粒度的配置隔离。不同的命名空间下,可以存在相同的Group或Data ID的配置
    场景:常用于区分不同的开发或部署环境,如开发测试环境和生产环境的资源(如配置、服务)隔离等
  2. Group(分组):
    作用:位于Namespace之下,用于区分不同的微服务或应用组件。当不同的应用或组件使用了相同的配置类型时,可以利用Group来区分
    场景:例如,一个应用可能使用了database_url配置和MQ_topic配置,可以将这些配置分别划分到不同的Group中,以便更好地管理和维护
  3. DataID(数据标识符):
    作用:位于最内层,用于唯一标识具体的配置信息。每个DataID对应一个具体的配置信息,例如一个数据库连接信息或消息队列的配置
    场景:通过DataID,可以轻松地查找、获取和更新配置信息

GateWay新一代网关

Gateway是在Spring生态系统之上构建的API网关服务,基于Spring6,Spring Boot 3和Project Reactor等技术
它旨在为微服务架构提供一种简单有效的统一 API 路由管理方式,并为它们提供跨领域的关注点,例如:安全性、监控/度量和恢复能力

Cloud全家桶中有个很重要的组件就是网关,在1.x版本中都是采用的Zuul网关
但在2.x版本中,zuul的升级一直跳票,SpringCloud最后自己研发了一个网关SpringCloud Gateway替代Zuul
那就是SpringCloud Gateway一句话:gateway是原zuul1.x版的替代

官网地址:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/
中文文档(看起来版本很旧):https://springdoc.cn/spring-cloud-gateway/

Gateway的作用:

  • 反向代理
  • 鉴权
  • 流量控制
  • 熔断
  • 日志监控
总结

Spring Cloud Gateway组件的核心是一系列的过滤器,通过这些过滤器可以将客户端发送的请求转发(路由)到对应的微服务
Spring Cloud Gateway是加在整个微服务最前沿的防火墙和代理器,隐藏微服务结点IP端口信息,从而加强安全保护
Spring Cloud Gateway本身也是一个微服务,需要注册进服务注册中心

小Tip:GateWay 和 Nginx
  1. Nginx
    类型:高性能Web服务器和反向代理服务器
    特点:基于C语言开发,具有低内存使用率和高并发性
    架构:独立的Web服务器,可以与各种Web框架集成
    配置:使用Nginx.conf配置文件来配置反向代理、负载均衡、缓存等功能
    性能:由于Nginx是基于C语言开发的,并且经过了大量的优化,因此其性能非常出色,特别是在静态内容和/或高并发请求的情况下
    扩展性:Nginx可以通过编写Nginx模块来扩展其功能,但需要一定的C语言编程能力
    应用场景:适用于需要高性能、高并发性的Web服务器场景,可以作为第一层网关,抵御第一波的并发流量。
  2. GateWay
    类型:Spring Cloud生态中的网关组件
    特点:基于Java开发,旨在为微服务架构提供一种简单而有效的统一的API路由管理方式
    架构:基于Spring Cloud和Spring Boot构建的微服务网关
    配置:通过Java代码或YAML配置文件来定义路由规则和过滤器链
    性能:相对于Nginx来说,Gateway的性能可能会有一些损失,因为它是基于Java和Spring Boot构建的
    扩展性:Gateway可以通过Spring Cloud的插件机制来扩展其功能,这对于Java开发者来说更加友好
    应用场景:适用于微服务架构中的API路由管理场景,可以作为第二层网关,根据不同的微服务来整合不同的网关系统(如移动端网关、自媒体端网关、管理员端网关等)。

Gateway的三大核心:

  1. Route(路由): 构建网关的基本模块,它由ID、目标URI、一系列断言(Predicate)和过滤器(Filter)组成。如果断言为true,则路由被匹配
  2. Predicate(断言): 参考的是 Java 8的java.util.function.Predicate。开发人员可以匹配HTTP请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
  3. Filter(过滤器): 指的是Spring框架中的GatewayFilter 的实例,使用过滤器,可以在请求被路由前或之后修改请求和响应
总结

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

GateWay工作流程:
客户端向 Spring Cloud Gateway 发出请求。然后在 Gateway Handler Mapping 中找到与请求相匹配的路由,将其发送到 Gateway Web Handler。Handler 再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回
过滤器之间用虚线分开是因为过滤器可能会在发送代理请求之前(Pre)或之后(Post)执行业务逻辑
在“pre”类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等;
在“post”类型的过滤器中可以做响应内容、响应头的修改,日志的输出,流量监控等有着非常重要的作用。

核心逻辑:路由转发 + 断言判断 + 执行过滤器链

入门配置

  1. 新建Module:cloud-gateway9527
  2. pom
  3. yml
  4. 主启动
  5. 业务类 不用写!网关和业务无关
  6. 测试
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
<dependencies>
<!--gateway-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--服务注册发现consul discovery,网关也要注册进服务注册中心统一管控-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- 指标监控健康检查的actuator,网关是响应式编程删除掉spring-boot-starter-web dependency-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
1
2
3
4
5
6
7
8
9
10
11
12
13
server:
port: 9527

spring:
application:
name: cloud-gateway #以微服务注册进consul或nacos服务列表内
cloud:
consul: #配置consul地址
host: localhost
port: 8500
discovery:
prefer-ip-address: true
service-name: ${spring.application.name}
1
2
3
4
5
6
7
8
@SpringBootApplication
@EnableDiscoveryClient
public class Main9527 {
public static void main(String[] args) {

SpringApplication.run(Main9527.class,args);
}
}