springcloud-alibaba

介绍:

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案。此项目包含开发分布式应用微服务的必需组件,方便开发者通过 Spring Cloud 编程模型轻松使用这些组件来开发分布式应用服务。

依托 Spring Cloud Alibaba,您只需要添加一些注解和少量配置,就可以将 Spring Cloud 应用接入阿里微服务解决方案,通过阿里中间件来迅速搭建分布式应用系统。

此外,阿里云同时还提供了 Spring Cloud Alibaba 企业版 微服务解决方案,包括无侵入服务治理(全链路灰度,无损上下线,离群实例摘除等),企业级 Nacos 注册配置中心和企业级云原生网关等众多产品。

参考文档 请查看 WIKI

主要功能

  • 服务限流降级:默认支持 WebServlet、WebFlux、OpenFeign、RestTemplate、Spring Cloud Gateway、Dubbo 和 RocketMQ 限流降级功能的接入,可以在运行时通过控制台实时修改限流降级规则,还支持查看限流降级 Metrics 监控。
  • 服务注册与发现:适配 Spring Cloud 服务注册与发现标准,默认集成对应 Spring Cloud 版本所支持的负载均衡组件的适配。
  • 分布式配置管理:支持分布式系统中的外部化配置,配置更改时自动刷新。
  • 消息驱动能力:基于 Spring Cloud Stream 为微服务应用构建消息驱动能力。
  • 分布式事务:使用 @GlobalTransactional 注解, 高效并且对业务零侵入地解决分布式事务问题。
  • 阿里云对象存储:阿里云提供的海量、安全、低成本、高可靠的云存储服务。支持在任何应用、任何时间、任何地点存储和访问任意类型的数据。
  • 分布式任务调度:提供秒级、精准、高可靠、高可用的定时(基于 Cron 表达式)任务调度服务。同时提供分布式的任务执行模型,如网格任务。网格任务支持海量子任务均匀分配到所有 Worker(schedulerx-client)上执行。
  • 阿里云短信服务:覆盖全球的短信服务,友好、高效、智能的互联化通讯能力,帮助企业迅速搭建客户触达通道。

注册中心 nacos

官网:

https://nacos.io/zh-cn/

下载地址:https://github.com/alibaba/nacos/releases/download/2.2.0/nacos-server-2.2.0.zip

启动:

win 环境:

1
2
3
4
# 需要 java jdk 1.8 +	, maven 3.2 +
.\startup.cmd
# 或 (非集群模式)
.\startup.cmd -m standalone

image-20230210115547834

登录地址:

1
2
3
http://192.168.205.1:8848/nacos/index.html#/login
# 用户:nacos
# 密码:nacos

image-20230210115914951

springCloudAlibaba 版本对照表

springCloudAlibaba代码分支 spring Cloud spring Boot jdk
2022.x Spring Cloud 2022 Spring Boot 3.0.x 最低支持 JDK 17
2021.x Spring Cloud 2021 Spring Boot 2.6.x 最低支持 JDK 1.8
2020.0 Spring Cloud 2020 Spring Boot 2.4.x 最低支持 JDK 1.8
2.2.x Spring Cloud Hoxton Spring Boot 2.2.x 最低支持 JDK 1.8
greenwich Spring Cloud Greenwich Spring Boot 2.1.x 最低支持 JDK 1.8
finchley Spring Cloud Finchley Spring Boot 2.0.x 最低支持 JDK 1.8
1.x Spring Cloud Edgware Spring Boot 1.x 最低支持 JDK 1.7

来源:https://gitee.com/mirrors/Spring-Cloud-Alibaba?_from=gitee_search#%E5%A6%82%E4%BD%95%E6%9E%84%E5%BB%BA

项目准备

  • 创建项目

父工程 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
<?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.wu-zy</groupId>
<artifactId>study-alibaba</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
</parent>

<modules>
<module>serviceA</module>
<module>common</module>
</modules>
<properties>
<spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
<spring-cloud-alibaba.version>2.1.4.RELEASE</spring-cloud-alibaba.version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<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>
</dependencies>
</dependencyManagement>
</project>

子工程 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
<?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">
<parent>
<artifactId>study-alibaba</artifactId>
<groupId>com.wu-zy</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>serviceA</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>

给子项目(需要注册的项目)添加 nacos 服务注册依赖

1
2
3
4
5
<!--nacos 服务注册与发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

给子项目(需要注册的项目)开启服务注册/发现

1
2
3
4
5
6
7
@SpringBootApplication
@EnableDiscoveryClient //开启服务注册/发现
public class ServiceA {
public static void main(String[] args) {
SpringApplication.run(ServiceA.class,args);
}
}

给子项目(需要注册的项目)配置服务端口、服务名称和数据库连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 8081
spring:
application:
name: serviceA
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: 1qaz!QAZ

cloud:
nacos:
server-addr: 127.0.0.1:8848 # nacos 服务地址

子项目编写一个接口:

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
@RestController
@RequestMapping("/test")
public class TestController {

@Resource
BizService bizService;

@GetMapping("/order/{id}")
public Order getOrderById(@PathVariable String id){
return bizService.getOrderById(id);
}

}

// 接口实现:(省略实体类 以及 mapper 等)
@Service
public class BizService {

@Resource
OrderService orderService;

public Order getOrderById(String id){
ServletRequestAttributes aaa = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
System.out.println("serverPort:"+aaa.getRequest().getServerPort());
QueryWrapper<Order> wrapper = new QueryWrapper<Order>();
wrapper.eq("oid",id);
return orderService.getOne(wrapper);
}
}

启动项目后可发现服务

image-20230217151549669


注册中心的实现还有 eureka 关于eureka 请看我另外一篇博客“使用Eureka编写注册中心服务”

负载均衡

ribbon

ribbon 是spring cloud 默认集成的组件

  • 复制 子项目 serviceA 为 serviceB 修改端口 为 8082

  • 新建子项目 client 作为客户端调用方

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @SpringBootApplication
    public class Client {
    public static void main(String[] args) {
    SpringApplication.run(Client.class,args);
    }

    @Bean
    // 配置 @LoadBalanced 注解
    @LoadBalanced
    public RestTemplate restTemplate() {
    return new RestTemplate();
    }

    }

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @RestController
    @RequestMapping("/client")
    public class TestController {

    @Resource
    RestTemplate template;

    @GetMapping("/order/{oid}")
    Object getOrderByOid(@PathVariable String oid){
    // 这里通过服务名调用
    return template.getForObject("http://serviceA/test/order/"+oid, Order.class);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    server:
    port: 8080
    spring:
    application:
    name: client
    datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
    username: root
    password: 1qaz!QAZ
    profiles:
    active: dev

    cloud:
    nacos:
    server-addr: 127.0.0.1:8848 # nacos 服务地址

    多次调用会发现轮询请求服务名为‘serviceA’的不同的服务实例


    详细内容请参考我的另外一篇笔记:客户端负载均衡Ribbon

feign

添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

feign 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.wu.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(value = "serviceA")// value 指定服务名
public interface OrderClient {

@GetMapping("/test/order/{oid}")
String getOrderByOid(@PathVariable String oid);

}

开启 feign

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.wu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @ClassName Client
* @Author wuzhiyong
* @Date 2023/2/21 12:10
* @Version 1.0
**/
@SpringBootApplication
@EnableFeignClients //启用 feign 客户端
public class Client {

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

}

控制器调用

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
package com.wu.controller;

import com.wu.feign.OrderClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
/**
* @ClassName TestController
* @Author wuzhiyong
* @Date 2023/2/21 12:12
* @Version 1.0
**/
@RestController
@RequestMapping("/client")
public class TestController {

@Resource
OrderClient orderClient;

@GetMapping("/order/{oid}")
Object getOrderByOid(@PathVariable String oid){
return orderClient.getOrderByOid(oid);
}
}


关于 feign 的更多内容请看我另外一篇笔记:声明式REST客户端Feign

服务容错-Sentine

官网:https://sentinelguard.io/zh-cn/docs/introduction.html

文档:https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel

https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-nacos

下载控制台

启动控制台

1
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.8.6.jar

登录:http://localhost:8090/

image-20230222100551599

1
2
# 用户名密码都是
sentinel

项目添加依赖:

pom

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

application.yml

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8090
#如果与 feign 一起使用,需要配置
feign:
sentinel:
enabled: true

这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server,该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中。

image-20230222113826291

如果这里没有数据那么手动访问下接口看看

image-20230222113853628

可直接在控制台手动设置规则:

image-20230223101206402

image-20230223101223877

image-20230223101239243

规则种类

  • 流量控制规则 (FlowRule)

流量规则的定义

重要属性:

Field 说明 默认值
resource 资源名,资源名是限流规则的作用对象
count 限流阈值
grade 限流阈值类型,QPS 或线程数模式 QPS 模式
limitApp 流控针对的调用来源 default,代表不区分调用来源
strategy 调用关系限流策略:直接、链路、关联 根据资源本身(直接)
controlBehavior 流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流 直接拒绝

同一个资源可以同时有多个限流规则。更多详细内容可以参考 流量控制

  • 熔断降级规则 (DegradeRule)

熔断降级规则包含下面几个重要的属性:

Field 说明 默认值
resource 资源名,即规则的作用对象
grade 熔断策略,支持慢调用比例/异常比例/异常数策略 慢调用比例
count 慢调用比例模式下为慢调用临界 RT(超出该值计为慢调用);异常比例/异常数模式下为对应的阈值
timeWindow 熔断时长,单位为 s
minRequestAmount 熔断触发的最小请求数,请求数小于该值时即使异常比率超出阈值也不会熔断(1.7.0 引入) 5
statIntervalMs 统计时长(单位为 ms),如 60*1000 代表分钟级(1.8.0 引入) 1000 ms
slowRatioThreshold 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)

同一个资源可以同时有多个降级规则。更多详情可以参考 熔断降级

  • 系统保护规则 (SystemRule)

Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

系统规则包含下面几个重要的属性:

Field 说明 默认值
highestSystemLoad load1 触发值,用于触发自适应控制阶段 -1 (不生效)
avgRt 所有入口流量的平均响应时间 -1 (不生效)
maxThread 入口流量的最大并发数 -1 (不生效)
qps 所有入口资源的 QPS -1 (不生效)
highestCpuUsage 当前系统的 CPU 使用率(0.0-1.0) -1 (不生效)

更多详情可以参考 系统自适应保护

  • 访问控制规则 (AuthorityRule)

    很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的访问控制(黑白名单)的功能。黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

    授权规则,即黑白名单规则(AuthorityRule)非常简单,主要有以下配置项:

    resource:资源名,即限流规则的作用对象

    limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB

strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式

更多详情可以参考 来源访问控制

  • 热点规则 (ParamFlowRule)

    热点参数规则(ParamFlowRule)类似于流量控制规则(FlowRule):

属性 说明 默认值
resource 资源名,必填
count 限流阈值,必填
grade 限流模式 QPS 模式
durationInSec 统计窗口时间长度(单位为秒),1.6.0 版本开始支持 1s
controlBehavior 流控效果(支持快速失败和匀速排队模式),1.6.0 版本开始支持 快速失败
maxQueueingTimeMs 最大排队等待时长(仅在匀速排队模式生效),1.6.0 版本开始支持 0ms
paramIdx 热点参数的索引,必填,对应 SphU.entry(xxx, args) 中的参数索引位置
paramFlowItemList 参数例外项,可以针对指定的参数值单独设置限流阈值,不受前面 count 阈值的限制。仅支持基本类型和字符串类型
clusterMode 是否是集群参数流控规则 false
clusterConfig 集群流控相关配置

详情可以参考 热点参数限流

动态规则扩展

简单的说法就是把规则集中管理(存入数据库或配置中心)

扩展常见的实现方式有:

  • 拉模式:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更;
  • 推模式:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 Nacos、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。

Sentinel 目前支持以下数据源扩展:

通过 nacos 实现动态规则

image-20230223112804732

image-20230223113005533

内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
[
{
"resource": "/client/order/{oid}",
"limitApp": "default",
"grade": 1,
"count": 2,
"strategy": 0,
"controlBehavior": 0
}
]

//grade=1 代表按QPS限流 2代表线程数
// 其他参数参考源码 RuleConstant.java

RuleConstant.java

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
public final class RuleConstant {
public static final int FLOW_GRADE_THREAD = 0;//流量控制:线程数
public static final int FLOW_GRADE_QPS = 1;//流量控制:QPS
public static final int DEGRADE_GRADE_RT = 0;
public static final int DEGRADE_GRADE_EXCEPTION_RATIO = 1;
public static final int DEGRADE_GRADE_EXCEPTION_COUNT = 2;
public static final int DEGRADE_DEFAULT_SLOW_REQUEST_AMOUNT = 5;
public static final int DEGRADE_DEFAULT_MIN_REQUEST_AMOUNT = 5;
public static final int AUTHORITY_WHITE = 0;
public static final int AUTHORITY_BLACK = 1;
public static final int STRATEGY_DIRECT = 0;//调用关系限流策略:直接
public static final int STRATEGY_RELATE = 1;//调用关系限流策略:关联
public static final int STRATEGY_CHAIN = 2;//调用关系限流策略:链路
//流控效果(直接拒绝 / 排队等待 / 慢启动模式),不支持按调用关系限流
public static final int CONTROL_BEHAVIOR_DEFAULT = 0;//(默认)直接拒绝
public static final int CONTROL_BEHAVIOR_WARM_UP = 1;
public static final int CONTROL_BEHAVIOR_RATE_LIMITER = 2;
public static final int CONTROL_BEHAVIOR_WARM_UP_RATE_LIMITER = 3;

public static final String LIMIT_APP_DEFAULT = "default";
public static final String LIMIT_APP_OTHER = "other";
public static final int DEFAULT_SAMPLE_COUNT = 2;
public static final int DEFAULT_WINDOW_INTERVAL_MS = 1000;

private RuleConstant() {
}
}

springcloud 中配置 datasource

application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
cloud:
sentinel:
transport:
port: 8719
dashboard: localhost:8090
datasource:
ds1:
nacos:
server-addr: 127.0.0.1:8848
data-id: sentinel
group-id: DEFAULT_GROUP
data-type: json
rule-type: flow
# ds2:
# nacos:
# server-addr: 127.0.0.1:8848
# data-id: sentinel
# group-id: DEFAULT_GROUP
# data-type: json
# rule-type: degrade

ds1, ds2, 是 ReadableDataSource 的名字,可随意编写。后面的 filezknacos , apollo 就是对应具体的数据源,它们后面的配置就是这些具体数据源各自的配置。注意数据源的依赖要单独引入(比如 sentinel-datasource-nacos)。

每种数据源都有两个共同的配置项: data-typeconverter-class 以及 rule-type

data-type 配置项表示 Converter 类型,Spring Cloud Alibaba Sentinel 默认提供两种内置的值,分别是 jsonxml (不填默认是json)。 如果不想使用内置的 jsonxml 这两种 Converter,可以填写 custom 表示自定义 Converter,然后再配置 converter-class 配置项,该配置项需要写类的全路径名(比如 spring.cloud.sentinel.datasource.ds1.file.converter-class=com.alibaba.cloud.examples.JsonFlowRuleListConverter)。

rule-type 配置表示该数据源中的规则属于哪种类型的规则(flow, degrade, authority, system, param-flow, gw-flow, gw-api-group)。注意网关流控规则 (GatewayFlowRule) 对应 gw-flow

启动项目报错:

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
2023-02-23 15:41:14.564 ERROR 26896 --- [           main] c.a.c.s.c.SentinelDataSourceHandler      : [Sentinel Starter] DataSource ds build error: Error creating bean with name 'ds-sentinel-nacos-datasource': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'ds-sentinel-nacos-datasource': Lookup method resolution failed; nested exception is java.lang.IllegalStateException: Failed to introspect Class [com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:265) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineConstructorsFromBeanPostProcessors(AbstractAutowireCapableBeanFactory.java:1253) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1168) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:555) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:515) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:320) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:222) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:318) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler.registerBean(SentinelDataSourceHandler.java:203) ~[spring-cloud-starter-alibaba-sentinel-2.1.4.RELEASE.jar:2.1.4.RELEASE]
at com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler.lambda$afterSingletonsInstantiated$0(SentinelDataSourceHandler.java:93) ~[spring-cloud-starter-alibaba-sentinel-2.1.4.RELEASE.jar:2.1.4.RELEASE]
at java.util.TreeMap.forEach(TreeMap.java:1005) ~[na:1.8.0_144]
at com.alibaba.cloud.sentinel.custom.SentinelDataSourceHandler.afterSingletonsInstantiated(SentinelDataSourceHandler.java:80) ~[spring-cloud-starter-alibaba-sentinel-2.1.4.RELEASE.jar:2.1.4.RELEASE]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:866) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:877) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:549) ~[spring-context-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:142) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:775) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:316) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1260) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1248) ~[spring-boot-2.1.3.RELEASE.jar:2.1.3.RELEASE]
at com.wu.Client.main(Client.java:19) ~[classes/:na]
Caused by: java.lang.IllegalStateException: Failed to introspect Class [com.alibaba.cloud.sentinel.datasource.factorybean.NacosDataSourceFactoryBean] from ClassLoader [sun.misc.Launcher$AppClassLoader@18b4aac2]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:686) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:583) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:568) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.determineCandidateConstructors(AutowiredAnnotationBeanPostProcessor.java:248) ~[spring-beans-5.1.5.RELEASE.jar:5.1.5.RELEASE]
... 22 common frames omitted
Caused by: java.lang.NoClassDefFoundError: com/alibaba/csp/sentinel/datasource/nacos/NacosDataSource
at java.lang.Class.getDeclaredMethods0(Native Method) ~[na:1.8.0_144]
at java.lang.Class.privateGetDeclaredMethods(Class.java:2701) ~[na:1.8.0_144]
at java.lang.Class.getDeclaredMethods(Class.java:1975) ~[na:1.8.0_144]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:668) ~[spring-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]
... 25 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.alibaba.csp.sentinel.datasource.nacos.NacosDataSource
at java.net.URLClassLoader.findClass(URLClassLoader.java:381) ~[na:1.8.0_144]
at java.lang.ClassLoader.loadClass(ClassLoader.java:424) ~[na:1.8.0_144]
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335) ~[na:1.8.0_144]
at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ~[na:1.8.0_144]
... 29 common frames omitted

添加依赖:

pom

1
2
3
4
5
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
<version>1.8.6</version>
</dependency>

启动成功后访问 sentinel 控制台(若没有数据 就访问一次接口)

image-20230223160024830

登录 nacos 控制台 编辑规则,添加另外一个接口的规则

image-20230223160304071

发布后在 sentinel 刷新下可看到新的规则

image-20230223160526981

快速访问接口几次即可看到被限制

image-20230223160659275

服务网关 gateway

Spring Cloud 网关 | 中文文档 (gitcode.net)

牛刀小试

创建一个 module

pom

1
2
3
4
5
6
7
8
9
10
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
</dependencies>
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: 1000

spring:
application:
name: gateway
cloud:
gateway:
routes: #路由数组 其内一个id 代表一个元素
- id: product_route #路由规则的标识 要求唯一
uri: http://127.0.0.1:8081 #请求转发的地址
order: 3 #路由的优先级,数字越小优先级越高
predicates: #断言 (就是路由转发时满足的条件)
- Path=/test-gateway/** #当请求路径满足 path= 的规则时才进行路由转发
filters: #过滤器 请求在传递过程中可以对其进行修改
- StripPrefix=1 # 转发之前去掉1层路径
- id: request_route #路由规则的标识 要求唯一
uri: http://127.0.0.1:8082 #请求转发的地址
order: 3 #路由的优先级,数字越小优先级越高
predicates: #断言 (就是路由转发时满足的条件)
- Path=/gateway/** #当请求路径满足 path= 的规则时才进行路由转发
filters: #过滤器 请求在传递过程中可以对其进行修改
- StripPrefix=1 # 转发之前去掉1层路径

浏览器请求测试:localhost:1000/test-gateway/test/order/101

image-20230225172939990

测试转发2:localhost:1000/gateway/test/system-name

image-20230225173518755


结合 nacos 一起使用

添加依赖:

pom

1
2
3
4
5
<!--nacos客户端-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

启动类添加开启注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.wu;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
/**
* @ClassName com.wu.Gateway
* @Author wuzhiyong
* @Date 2023/2/25 9:23
* @Version 1.0
**/
@SpringBootApplication
@EnableDiscoveryClient //启用客户端注册
public class Gateway {
public static void main(String[] args) {
SpringApplication.run(Gateway.class,args);
}
}

application.yml

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

spring:
application:
name: gateway
cloud:
gateway:
routes: #路由数组 其内一个id 代表一个元素
- id: product_route #路由规则的标识 要求唯一
uri: lb://serviceA:8081 #请求转发的地址 lb
order: 3 #路由的优先级,数字越小优先级越高
predicates: #断言 (就是路由转发时满足的条件)
- Path=/test-gateway/** #当请求路径满足 path= 的规则时才进行路由转发
filters: #过滤器 请求在传递过程中可以对其进行修改
- StripPrefix=1 # 转发之前去掉1层路径
- id: request_route #路由规则的标识 要求唯一
uri: lb://serviceA:8082 #请求转发的地址
order: 3 #路由的优先级,数字越小优先级越高
predicates: #断言 (就是路由转发时满足的条件)
- Path=/gateway/** #当请求路径满足 path= 的规则时才进行路由转发
filters: #过滤器 请求在传递过程中可以对其进行修改
- StripPrefix=1 # 转发之前去掉1层路径
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos 服务地址

注意 uri 这里 不是用 http 了。

lb 指的是从nacos中按照名称获取微服务,并遵循负载均 衡策略


精简配置版:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
server:
port: 1000

spring:
application:
name: gateway
cloud:
gateway:
discovery:
locator:
enabled: true
nacos:
discovery:
server-addr: 127.0.0.1:8848 # nacos 服务地址

请求测试:localhost:1000/serviceA/test/order/101

image-20230225180702400

解释一下:

注意这里的请求地址。http://localhost:1000/serviceA/test/order/101。其中 http://localhost:1000 是 gateway 服务的地址;/serviceA 是服务名;/test/order/101 是接口地址

gateway 架构

  • 基本概念

    路由(Route) 是 gateway 中最基本的组件之一,表示一个具体的路由信息载体。主要定义了下面的几个 信息:

    • id,路由标识符,区别于其他 Route。

    • uri,路由指向的目的地 uri,即客户端请求最终被转发到的微服务。

    • order,用于多个 Route 之间的排序,数值越小排序越靠前,匹配优先级越高。

    • predicate,断言的作用是进行条件判断,只有断言都返回真,才会真正的执行路由。

    • fifilter,过滤器用于修改请求和响应信息。

  • 执行流程

image-20230225181725854

客户端向 Spring Cloud网关提出请求。如果网关处理程序映射确定请求与路由匹配,则将请求发送到网关 Web 处理程序。此处理程序通过特定于该请求的筛选链来运行该请求。用虚线划分过滤器的原因是,过滤器可以在发送代理请求之前和之后运行逻辑。所有的“pre”过滤逻辑都会被执行。然后提出代理请求。在提出代理请求之后,将运行“POST”过滤逻辑。

注意:在没有端口的路由中定义的 URI 分别获得 HTTP 和 HTTPS URI 的默认端口号 80 和 443.

断言配置

配置断言(谓词)和过滤器有两种方法:快捷方式和完全展开的参数。下面的大多数示例都使用了快捷方式。
名称和参数名称将以code的形式在每个部分的第一个或两个表示中列出。参数通常按快捷方式配置所需的顺序列出。

下面示例用两个参数定义了Cookie路由谓词工厂,cookie 名mycookie和要匹配mycookievalue的值。

  • 快捷方式

快捷方式配置由筛选器名称识别,后面跟着一个等号(=),后面是用逗号分隔的参数值(,)。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- Cookie=mycookie,mycookievalue
  • 展开配置

完全展开的参数看起来更像是带有名称/值对的标准 YAML 配置。通常,会有name键和args键。args键是用于配置谓词或筛选器的键值对的映射。

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
- name: Cookie
args:
name: mycookie
regexp: mycookievalue

内置断言工厂

  • After路由谓词工厂

After路由谓词工厂接受一个参数,adatetime(这是一个 JavaZonedDateTime)。此谓词匹配在指定的 DateTime 之后发生的请求。下面的示例配置一个 after 路由谓词:

1
2
predicates:
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
  • Before路由谓词工厂

Before路由谓词工厂接受一个参数,adatetime(这是一个 JavaZonedDateTime)。此谓词匹配在指定的datetime之前发生的请求。下面的示例配置一个 before 路由谓词:

1
2
predicates:
- Before=2017-01-20T17:42:47.789-07:00[America/Denver]
  • Between路由谓词工厂

Between路由谓词工厂接受两个参数,datetime1datetime2,它们是 JavaZonedDateTime对象。此谓词匹配发生在datetime1之后和datetime2之前的请求。datetime2参数必须位于datetime1之后。下面的示例配置了一个 between 路由谓词:

1
2
predicates:
- Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
  • Cookie路由谓词工厂

Cookie路由谓词工厂接受两个参数,cookienameregexp(这是一个 Java 正则表达式)。此谓词匹配具有给定名称且其值与正则表达式匹配的 cookie。下面的示例配置了一个 Cookie 路由谓词工厂:

1
2
predicates:
- Cookie=chocolate, ch.p
  • Header路由谓词工厂

Header路由谓词工厂接受两个参数,headerregexp(这是一个 Java 正则表达式)。此谓词与具有给定名称的头匹配,其值与正则表达式匹配。下面的示例配置头路由谓词:

1
2
predicates:
- Header=X-Request-Id, \d+
  • Host路由谓词工厂

Host路由谓词工厂接受一个参数:主机名列表patterns。该模式是一种 Ant 样式的模式,以.作为分隔符。此谓词匹配与模式匹配的Host头。下面的示例配置了一个主机路由谓词:

1
2
predicates:
- Host=**.somehost.org,**.anotherhost.org

官网说明:

URI 模板变量(如{sub}.myhost.org)也受到支持。

如果请求具有Host头,其值为www.somehost.orgbeta.somehost.orgwww.anotherhost.org,则此路由匹配。

这个谓词提取 URI 模板变量(例如sub,在前面的示例中定义)作为名称和值的映射,并将其放在ServerWebExchange.getAttributes()中,并在ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE中定义一个键。然后这些值可供[GatewayFilter工厂使用](#gateway-route-filters)

  • Method路由谓词工厂

Method路由谓词工厂接受一个methods参数,该参数是一个或多个参数:要匹配的 HTTP 方法。下面的示例配置了一个方法路由谓词:

1
2
predicates:
- Method=GET,POST
  • Path路由谓词工厂

Path路由谓词工厂接受两个参数: Spring PathMatcher``patterns的列表和一个名为matchTrailingSlash的可选标志(默认为true)。下面的示例配置了一个路径路由谓词:

1
2
predicates:
- Path=/red/{segment},/blue/{segment}

如果请求路径是:例如:/red/1/red/1//red/blue/blue/green,则此路由匹配。如果matchTrailingSlash(尾随斜杠)被设置为false,那么请求路径/red/1/将不会被匹配。这个谓词提取 URI 模板变量(例如segment,在前面的示例中定义)作为名称和值的映射,并将其放在ServerWebExchange.getAttributes()中,并在ServerWebExchangeUtils.URI_TEMPLATE_VARIABLES_ATTRIBUTE中定义一个键。然后这些值可供[GatewayFilter工厂]使用(#gateway-route-filters)可以使用一种实用方法(称为get)来使对这些变量的访问更容易。下面的示例展示了如何使用get方法:

1
2
Map<String, String> uriVariables = ServerWebExchangeUtils.getPathPredicateVariables(exchange);
String segment = uriVariables.get("segment");
  • Query路由谓词工厂

Query路由谓词工厂接受两个参数:一个必需的param和一个可选的regexp(这是一个 Java 正则表达式)。下面的示例配置一个查询路由谓词:

1
2
3
predicates:
- Query=green #如果请求包含green查询参数,则前面的路由匹配。
- Query=red, gree. #如果请求包含一个red查询参数,其值与gree.regexp 匹配,则前面的路由匹配,因此green和greet将匹配。
  • RemoteAddr路由谓词工厂

RemoteAddr路由谓词工厂接受一个sources的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如192.168.0.1/16(其中192.168.0.1是一个 IP 地址,16是一个子网掩码)。以下示例配置了一个 RemoteAddr 路由谓词:

1
2
predicates:
- RemoteAddr=192.168.1.1/24 #如果请求的远程地址是192.168.1.10,则此路由匹配。

默认情况下,RemoteAddr 路由谓词工厂使用来自传入请求的远程地址。如果 Spring Cloud网关位于代理层的后面,这可能与实际的客户端 IP 地址不匹配。

你可以通过设置自定义RemoteAddressResolver来定制远程地址的解析方式。 Spring Cloud网关带有一个基于X-forward-for 标头 (opens new window)XForwardedRemoteAddressResolver的非默认远程地址解析器。

XForwardedRemoteAddressResolver有两种静态构造函数方法,它们采用不同的方法实现安全性:

  • XForwardedRemoteAddressResolver::trustAll返回一个RemoteAddressResolver,它总是使用在X-Forwarded-For头中找到的第一个 IP 地址。这种方法容易受到欺骗,因为恶意客户端可能会为X-Forwarded-For设置初始值,该初始值将被解析器接受。
  • XForwardedRemoteAddressResolver::maxTrustedIndex获取一个索引,该索引与在 Spring Cloud网关前运行的受信任基础设施的数量相关。 Spring 如果云网关例如只能通过 HAProxy 访问,那么应该使用 1 的值。如果在 Spring Cloud网关可访问之前需要两个值得信赖的基础设施跳,那么应该使用 2 的值。

考虑以下标头值:

1
X-Forwarded-For: 0.0.0.1, 0.0.0.2, 0.0.0.3

以下maxTrustedIndex值产生以下远程地址:

maxTrustedIndex 结果
[Integer.MIN_VALUE,0] (无效,IllegalArgumentException在初始化期间)
1 0.0.0.3
2 0.0.0.2
3 0.0.0.1
[4, Integer.MAX_VALUE] 0.0.0.1

下面的示例展示了如何使用 Java 实现相同的配置:

gatewayconfig.java

1
2
3
4
5
6
7
8
9
10
11
12
RemoteAddressResolver resolver = XForwardedRemoteAddressResolver
.maxTrustedIndex(1);

...

.route("direct-route",
r -> r.remoteAddr("10.1.1.1", "10.10.1.1/24")
.uri("https://downstream1")
.route("proxied-route",
r -> r.remoteAddr(resolver, "10.10.1.1", "10.10.1.1/24")
.uri("https://downstream2")
)
  • Weight路由谓词工厂

Weight路由谓词工厂接受两个参数:groupweight(一个 INT)。权重是按组计算的。下面的示例配置了权重路由谓词:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: weight_high
uri: https://weighthigh.org
predicates:
- Weight=group1, 8
- id: weight_low
uri: https://weightlow.org
predicates:
- Weight=group1, 2

此路线将把 80% 的流量转发给weighthigh.org (opens new window),并将 20% 的流量转发给weighlow.org (opens new window)

  • XForwarded Remote Addr路由谓词工厂

XForwarded Remote Addr路由谓词工厂接受一个sources的列表(最小大小为 1),这些字符串是 CIDR 表示法(IPv4 或 IPv6)字符串,例如192.168.0.1/16(其中192.168.0.1是一个 IP 地址,16是一个子网掩码)。

此路由谓词允许基于X-Forwarded-ForHTTP 报头对请求进行过滤。

这可以用于反向代理,例如负载均衡器或 Web 应用程序防火墙,在这些代理中,只有当请求来自由这些反向代理使用的受信任的 IP 地址列表时,才允许请求。

1
2
predicates:
- XForwardedRemoteAddr=192.168.1.1/24

如果X-Forwarded-For标头包含192.168.1.10,则此路由匹配。

内置局部过滤器工厂

  • AddRequestHeader 过滤器
1
2
filters:
- AddRequestHeader=X-Request-red, blue

对于所有匹配的请求,此清单将X-Request-red:blue头添加到下游请求的头。

  • AddRequestParameter 过滤器
1
2
filters:
- AddRequestParameter=red, blue

将为所有匹配的请求将red=blue添加到下游请求的查询字符串中。

  • AddResponseHeader 过滤器
1
2
filters:
- AddResponseHeader=X-Response-Red, Blue

将为所有匹配的请求向下游响应的头添加X-Response-Red:Blue头。

  • DedupeResponseHeader 过滤器
1
2
filters:
- DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin

接受一个name参数和一个可选的strategy参数。name可以包含一个以空格分隔的标题名称列表。

在网关 CORS 逻辑和下游逻辑都添加响应头的情况下,这将删除重复的Access-Control-Allow-CredentialsAccess-Control-Allow-Origin响应头的值。

DedupeResponseHeader过滤器还接受一个可选的strategy参数。可接受的值是RETAIN_FIRST(默认)、RETAIN_LASTRETAIN_UNIQUE

  • CircuitBreaker 断路器 过滤器

要启用 Spring Cloud电路断路器过滤器,你需要在 Classpath 上放置spring-cloud-starter-circuitbreaker-reactor-resilience4j。以下示例配置了 Spring Cloud电路断路器GatewayFilter:

1
2
filters:
- CircuitBreaker=myCircuitBreaker

Spring Cloud电路断路器过滤器还可以接受可选的fallbackUri参数。目前,只支持forward:模式 URI。如果回退被调用,请求将被转发到与 URI 匹配的控制器。下面的示例配置了这种回退:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: circuitbreaker_route
uri: lb://backing-service:8088
predicates:
- Path=/consumingServiceEndpoint
filters:
- name: CircuitBreaker
args:
name: myCircuitBreaker
fallbackUri: forward:/inCaseOfFailureUseThis
- RewritePath=/consumingServiceEndpoint, /backingServiceEndpoint

主要的场景是使用fallbackUri在网关应用程序中定义内部控制器或处理程序。但是,你也可以将请求重新路由到外部应用程序中的控制器或处理程序,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback

在此示例中,网关应用程序中没有fallback端点或处理程序。然而,在另一个应用程序中有一个,注册在[localhost:9994](http://localhost:9994)下。

在请求被转发到 Fallback 的情况下, Spring Cloud Circuitbreaker 网关过滤器还提供了导致它的Throwable。它被添加到ServerWebExchange中,作为ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR属性,该属性可以在处理网关应用程序内的回退时使用。

  • FallbackHeaders 过滤器

FallbackHeaders工厂允许你在转发到外部应用程序中的fallbackUri的请求的标题中添加 Spring Cloud Circuitbreaker 执行异常详细信息,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
spring:
cloud:
gateway:
routes:
- id: ingredients
uri: lb://ingredients
predicates:
- Path=//ingredients/**
filters:
- name: CircuitBreaker
args:
name: fetchIngredients
fallbackUri: forward:/fallback
- id: ingredients-fallback
uri: http://localhost:9994
predicates:
- Path=/fallback
filters:
- name: FallbackHeaders
args:
executionExceptionTypeHeaderName: Test-Header

在此示例中,在运行断路器时发生执行异常之后,请求被转发到在localhost:9994上运行的应用程序中的fallback端点或处理程序。带有异常类型、消息和(如果可用的话)根原因异常类型和消息的头将由FallbackHeaders过滤器添加到该请求中。

有关断路器和网关的更多信息,请参见Spring Cloud CircuitBreaker Factory section

  • MapRequestHeader 过滤器

接受fromHeadertoHeader参数。它创建一个新的命名头(toHeader),并从传入的 HTTP 请求中从现有的命名头(fromHeader)中提取该值。如果输入标头不存在,则过滤器不会产生任何影响。如果新的命名标头已经存在,那么它的值将被新的值扩充。以下示例配置MapRequestHeader:

1
2
3
4
5
routes:
- id: map_request_header_route
uri: http://example.org
filters:
- MapRequestHeader=X-Request-Id,X-Correlation-Id # 复制 X-Request-Id 的值到 X-Correlation-Id

这样,当您发送一个请求时,例如:

1
curl -H "X-Request-Id: 123456" http://localhost:8080/get

那么转发到 http://example.org 的请求头中会包含:

1
2
X-Request-Id: 123456
X-Correlation-Id: 123456
  • PrefixPath 过滤器

接受一个prefix参数。

1
2
filters:
- PrefixPath=/mypath

这将在所有匹配请求的路径前缀/mypath。因此,对/hello的请求将被发送到/mypath/hello

  • PreserveHostHeader 过滤器

没有参数。此筛选器设置一个请求属性,路由筛选器将检查该属性以确定是否应发送原始的主机头,而不是由 HTTP 客户机确定的主机头。以下示例配置

1
2
3
4
5
routes:
- id: preserve_host_route
uri: https://example.org
filters:
- PreserveHostHeader

当请求被转发到 https://example.org 时,会保留原始的 Host 消息头。

  • RequestRateLimiter 过滤器

使用RateLimiter实现来确定是否允许当前请求继续进行。如果不是,则返回HTTP 429 - Too Many Requests(默认情况下)的状态。

  • RedirectTo 过滤器

接受两个参数,statusurlstatus参数应该是 300 系列重定向 HTTP 代码,例如 301.url参数应该是一个有效的 URL。这是Location标头的值。对于相对重定向,应该使用uri: no://op作为路由定义的 URI。

1
2
3
4
5
routes:
- id: prefixpath_route
uri: https://example.org
filters:
- RedirectTo=302, https://acme.org

这将发送带有Location:https://acme.org头的状态 302 来执行重定向。

  • RemoveRequestHeader 过滤器

接受一个name参数。它是要删除的标头的名称。

1
2
filters:
- RemoveRequestHeader=X-Request-Foo

这将在向下游发送X-Request-Foo头之前删除它。

  • RemoveResponseHeader 过滤器

接受一个name参数。它是要删除的标头的名称。

1
2
filters:
- RemoveResponseHeader=X-Response-Foo

这将在响应返回到网关客户机之前从响应中删除X-Response-Foo头。

  • RemoveRequestParameter 过滤器

接受一个name参数。它是要删除的查询参数的名称。

1
2
filters:
- RemoveRequestParameter=red

这将在向下游发送red参数之前删除该参数。

  • RewritePath 过滤器

接受一个路径regexp参数和一个replacement参数。这使用 Java 正则表达式以灵活的方式重写请求路径。

1
2
3
4
5
6
7
routes:
- id: rewritepath_route
uri: https://example.org
predicates:
- Path=/red/**
filters:
- RewritePath=/red/?(?<segment>.*), /$\{segment}

对于/red/blue的请求路径,在发出下游请求之前,将路径设置为/blue。注意,由于 YAML 规范,$应该替换为$\

  • RewriteLocationResponseHeader 过滤器

修改Location响应头的值,通常是为了去掉后台特定的细节。它需要stripVersionModelocationHeaderNamehostValueprotocolsRegex参数。

  • RewriteResponseHeader 过滤器

接受nameregexpreplacement参数。它使用 Java 正则表达式以一种灵活的方式重写响应头值。

1
2
filters:
- RewriteResponseHeader=X-Response-Red, , password=[^&]+, password=***

对于标头值/42?user=ford&password=omg!what&flag=true,在发出下游请求后将其设置为/42?user=ford&password=***&flag=true

  • SaveSession 过滤器

在向下游转发之前强制执行WebSession::save操作

1
2
filters:
- SaveSession
  • SecureHeaders 过滤器

作用是给响应的包头中添加一些安全保护的字段信息,例如 X-XSS-Protection、X-Frame-Options 等。这些字段可以防止一些常见的 Web 攻击,如跨站脚本、点击劫持等。

1
2
3
4
5
6
7
8
spring:
cloud:
gateway:
routes:
- id: secure_route
uri: https://example.com
filters:
- name: SecureHeaders
  • SetPath 过滤器

接受一个路径template参数。它提供了一种简单的方法,通过允许模板化的路径段来操作请求路径。这使用了 Spring Framework 中的 URI 模板。允许多个匹配段。

1
2
3
4
filters:
- name: SetPath
args:
template: /foo/{segment}

经过这个路由的请求都会将原始路径替换为 /foo/{segment} ,其中 {segment} 是原始路径中的第一个段。例如,如果原始路径是 /bar/baz ,那么修改后的路径就是 /foo/bar 。

  • SetRequestHeader 过滤器

接受namevalue参数。

1
2
3
4
5
filters:
- name: SetRequestHeader
args:
name: X-Request-Foo
value: Bar

所有经过这个具有 X-Request-Foo 请求头路由的请求都会修改 X-Request-Foo 的字段值为 Bar 。

  • SetResponseHeader 过滤器

接受namevalue参数。

1
2
filters:
- SetResponseHeader=X-Response-Red, Blue

所有经过这个具有 X-Request-Foo 响应头路由的请求都会修改 X-Request-Foo 的字段值为 Bar 。

  • SetStatus 过滤器

只接受一个参数,status。它必须是有效的 Spring HttpStatus。它可以是整数值404,也可以是枚举的字符串表示:NOT_FOUND

1
2
3
4
5
6
7
8
9
routes:
- id: setstatusstring_route
uri: https://example.org
filters:
- SetStatus=UNAUTHORIZED
- id: setstatusint_route
uri: https://example.org
filters:
- SetStatus=401

这两种情况下,响应的 HTTP 状态都设置为 401.

  • StripPrefix 过滤器

接受一个参数,partsparts参数指示在向下游发送请求之前要从请求中剥离的路径中的部件数量。

1
2
filters:
- StripPrefix=1

所有经过这个路由的请求都会去掉第一个前缀。例如,如果原始路径是 /api/goods ,那么修改后的路径就是 /goods 。如果要去掉多个前缀,可以修改 parts 的值为相应的数字

  • Retry 过滤器

工厂支持以下参数:
retries:应该尝试的重试次数。
statuses:应该重试的 HTTP 状态代码,用org.springframework.http.HttpStatus表示。
methods:应该重试的 HTTP 方法,用org.springframework.http.HttpMethod表示。
series:要重试的一系列状态代码,用org.springframework.http.HttpStatus.Series表示。
exceptions:应该重试的抛出的异常列表。
backoff:为重试配置的指数退避。在回退间隔firstBackoff * (factor ^ n)之后执行重试,其中n是迭代。如果配置了maxBackoff,则应用的最大退避限制为maxBackoff。如果basedOnPreviousValue为真,则按prevBackoff * factor计算退避。
如果启用,以下默认值将配置为Retry过滤器:
retries:三次
series:5XX 系列
methods:get 方法
exceptions:IOException和TimeoutException
backoff:禁用
下面的清单配置了重试GatewayFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
routes:
- id: retry_test
uri: http://localhost:8080/flakey
predicates:
- Host=*.retry.com
filters:
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY
methods: GET,POST
backoff:
firstBackoff: 10ms
maxBackoff: 50ms
factor: 2
basedOnPreviousValue: false
  • RequestSize 过滤器

可以限制请求到达下游服务。过滤器接受maxSize参数。maxSizeDataSize类型,因此值可以定义为一个数字,后面跟着一个可选的DataUnit后缀,例如“KB”或“MB”。对于字节,默认值为“B”。它是以字节为单位定义的请求的允许大小限制。

1
2
3
4
5
6
7
8
9
routes:
- id: request_size_route
uri: http://localhost:8080/upload
predicates:
- Path=/upload
filters:
- name: RequestSize
args:
maxSize: 5000000

当请求由于大小而被拒绝时,RequestSize工厂将响应状态设置为413 Payload Too Large,并带有一个附加的头errorMessage。下面的示例显示了这样的errorMessage:

1
errorMessage : Request size is larger than permissible limit. Request size is 6.0 MB where permissible limit is 5.0 MB
  • SetRequestHostHeader 过滤器

可以用指定的 vaue 替换现有的主机报头。过滤器接受host参数。

1
2
3
4
5
6
7
8
9
routes:
- id: set_request_host_header_route
uri: http://localhost:8080/headers
predicates:
- Path=/headers
filters:
- name: SetRequestHostHeader
args:
host: example.org

将主机报头的值替换为example.org

  • ModifyRequestBody 过滤器

过滤器来修改请求主体,然后再由网关向下游发送。(只能通过java代码来实现)

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
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_request_obj", r -> r.host("*.rewriterequestobj.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyRequestBody(String.class, Hello.class, MediaType.APPLICATION_JSON_VALUE,
(exchange, s) -> return Mono.just(new Hello(s.toUpperCase())))).uri(uri))
.build();
}

static class Hello {
String message;

public Hello() { }

public Hello(String message) {
this.message = message;
}

public String getMessage() {
return message;
}

public void setMessage(String message) {
this.message = message;
}
}
//如果请求没有正文,RewriteFilter将被传递null。应该返回Mono.empty()以分配请求中缺少的主体。
  • ModifyResponseBody

修改响应主体,然后再将其发送回客户机。

1
2
3
4
5
6
7
8
9
10
@Bean
public RouteLocator routes(RouteLocatorBuilder builder) {
return builder.routes()
.route("rewrite_response_upper", r -> r.host("*.rewriteresponseupper.org")
.filters(f -> f.prefixPath("/httpbin")
.modifyResponseBody(String.class, String.class,
(exchange, s) -> Mono.just(s.toUpperCase()))).uri(uri))
.build();
}
//如果响应没有主体,则将传递RewriteFilter。应该返回Mono.empty(),以便在响应中分配一个缺少的主体。
  • TokenRelay 过滤器

令牌中继是 OAuth2 使用者充当客户端并将传入的令牌转发给传出的资源请求的一种方式。使用者可以是纯客户机(如 SSO 应用程序)或资源服务器。

用法:Spring Cloud 网关 | 中文文档 (gitcode.net)

  • CacheRequestBody 过滤器

由于请求体流只能被读取一次,所以需要对请求体流进行缓存。你可以使用CacheRequestBody过滤器来缓存请求主体,然后再将其发送到下游,并从 exchagne 属性获取主体。

  • default-filters 默认过滤器

要添加筛选器并将其应用到所有路由,你可以使用spring.cloud.gateway.default-filters。此属性接受一个过滤器列表。以下清单定义了一组默认筛选器:

1
2
3
4
5
6
spring:
cloud:
gateway:
default-filters:
- AddResponseHeader=X-Response-Default-Red, Default-Blue
- PrefixPath=/httpbin

链路追踪 Sleuth

概念

SpringCloud Sleuth主要功能就是在分布式系统中提供追踪解决方案。它大量借用了Google Dapper的 设计, 先来了解一下Sleuth中的术语和相关概念。

  • Trace:由一组Trace Id相同的Span串联形成一个树状结构。为了实现请求跟踪,当请求到达分布式系统 的入口端点时,只需要服务跟踪框架为该请求创建一个唯一的标识(即TraceId),同时在分布式系统 内部流转的时候,框架始终保持传递该唯一值,直到整个请求的返回。那么我们就可以使用该唯一标识 将所有的请求串联起来,形成一条完整的请求链路。
  • Span 代表了一组基本的工作单元。为了统计各处理单元的延迟,当请求到达各个服务组件的时候,也 通过一个唯一标识(SpanId)来标记它的开始、具体过程和结束。通过SpanId的开始和结束时间戳, 就能统计该span的调用时间,除此之外,我们还可以获取如事件的名称。请求信息等元数据。
  • Annotation 用它记录一段时间内的事件,内部使用的重要注释:
  • cs(Client Send)客户端发出请求,开始一个请求的生命
  • sr(Server Received)服务端接受到请求开始进行处理,
  • sr-cs = 网络延迟(服务调用的时间)
  • ss(Server Send)服务端处理完毕准备发送到客户端,
  • ss - sr = 服务器上的请求处理时间 cr(Client Reveived)客户端接受到服务端的响应,请求结束。
  • cr - sr = 请求的总时间

image-20230301110353458

入门

引入依赖:

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
</dependencies>

请求接口:

image-20230301111740138

日志中的:

1
[client,6e0f2cee8a3d0e72,6e0f2cee8a3d0e72,false]

分别代表:微服务名称, traceId, spanid,是否将链路的追踪结果输出到第三方平台

集成ZipKin

介绍:

Zipkin 是 Twitter 的一个开源项目,它基于Google Dapper实现,它致力于收集服务的定时数据,以解 决微服务架构中的延迟问题,包括数据的收集、存储、查找和展现。 我们可以使用它来收集各个服务器上请求链路的跟踪数据,并通过它提供的REST API接口来辅助我们查 询跟踪数据以实现对分布式系统的监控程序,从而及时地发现系统中出现的延迟升高问题并找出系统性 能瓶颈的根源。 除了面向开发的 API 接口之外,它也提供了方便的UI组件来帮助我们直观的搜索跟踪信息和分析请求链 路明细,比如:可以查询某段时间内各用户请求的处理时间等。 Zipkin 提供了可插拔数据存储方式:In-Memory、MySql、Cassandra 以及 Elasticsearch。

image-20230301112240318

上图展示了 Zipkin 的基础架构,它主要由 4 个核心组件构成:

  • Collector:收集器组件,它主要用于处理从外部系统发送过来的跟踪信息,将这些信息转换为 Zipkin内部处理的 Span 格式,以支持后续的存储、分析、展示等功能。

  • Storage:存储组件,它主要对处理收集器接收到的跟踪信息,默认会将这些信息存储在内存中, 我们也可以修改此存储策略,通过使用其他存储组件将跟踪信息存储到数据库中。

  • RESTful API:API 组件,它主要用来提供外部访问接口。比如给客户端展示跟踪信息,或是外接 系统访问以实现监控等。

  • Web UI:UI 组件, 基于API组件实现的上层应用。通过UI组件用户可以方便而有直观地查询和分 析跟踪信息。

Zipkin分为两端,一个是 Zipkin服务端,一个是 Zipkin客户端,客户端也就是微服务的应用。 客户端会 配置服务端的 URL 地址,一旦发生服务间的调用的时候,会被配置在微服务里面的 Sleuth 的监听器监 听,并生成相应的 Trace 和 Span 信息发送给服务端。

下载 jar 包。gitHub 地址:https://github.com/openzipkin/zipkin

启动:

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
PS D:\Zipkin> java -jar .\zipkin-server-2.21.1-exec.jar

oo
oooo
oooooo
oooooooo
oooooooooo
oooooooooooo
ooooooo ooooooo
oooooo ooooooo
oooooo ooooooo
oooooo o o oooooo
oooooo oo oo oooooo
ooooooo oooo oooo ooooooo
oooooo ooooo ooooo ooooooo
oooooo oooooo oooooo ooooooo
oooooooo oo oo oooooooo
ooooooooooooo oo oo ooooooooooooo
oooooooooooo oooooooooooo
oooooooo oooooooo
oooo oooo

________ ____ _ _____ _ _
|__ /_ _| _ \| |/ /_ _| \ | |
/ / | || |_) | ' / | || \| |
/ /_ | || __/| . \ | || |\ |
|____|___|_| |_|\_\___|_| \_|

:: version 2.21.1 :: commit c30ffc5 ::

2023-03-01 11:29:39.896 INFO 15172 --- [ main] z.s.ZipkinServer : Starting ZipkinServer on DESKTOP-SAUA6M1 with PID 15172 (D:\Zipkin\zipkin-server-2.21.1-exec.jar started by wuzhiyong in D:\Zipkin)
2023-03-01 11:29:39.899 INFO 15172 --- [ main] z.s.ZipkinServer : The following profiles are active: shared
2023-03-01 11:29:40.756 INFO 15172 --- [ main] c.l.a.c.u.SystemInfo : Hostname: desktop-saua6m1 (from 'hostname' command)
2023-03-01 11:29:41.933 INFO 15172 --- [oss-http-*:9411] c.l.a.s.Server : Serving HTTP at /0:0:0:0:0:0:0:0:9411 - http://127.0.0.1:9411/
2023-03-01 11:29:41.934 INFO 15172 --- [ main] c.l.a.s.ArmeriaAutoConfiguration : Armeria server started at ports: {/0:0:0:0:0:0:0:0:9411=ServerPort(/0:0:0:0:0:0:0:0:9411, [http])}
2023-03-01 11:29:41.951 INFO 15172 --- [ main] z.s.ZipkinServer : Started ZipkinServer in 2.751 seconds (JVM running for 4.316)

访问Zipkin服务端:http://localhost:9411/zipkin/

image-20230301113250044

项目作为客户端添加以下配置:

pom:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>

application.ym

1
2
3
4
5
6
7
spring:
zipkin:
base-url: http://127.0.0.1:9411 #zipkin server的请求地址
discovery-client-enabled: false #让nacos把它当成一个URL,而不要当做服务名
sleuth:
sampler:
probability: 1.0 #采样的百分比

访问接口:http://localhost:8080/client/order/101

后点击查询

image-20230301114407394

image-20230301114535633

image-20230301114549143

ZipKin 数据持久化

存储在 mysql :

创建表: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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
-- 来源:https://github.com/openzipkin/zipkin/blob/master/zipkin-storage/mysql-v1/src/main/resources/mysql.sql
-- ------------------------------------------------------------------------------------------------------------------

CREATE TABLE IF NOT EXISTS zipkin_spans (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL,
`id` BIGINT NOT NULL,
`name` VARCHAR(255) NOT NULL,
`remote_service_name` VARCHAR(255),
`parent_id` BIGINT,
`debug` BIT(1),
`start_ts` BIGINT COMMENT 'Span.timestamp(): epoch micros used for endTs query and to implement TTL',
`duration` BIGINT COMMENT 'Span.duration(): micros used for minDuration and maxDuration query',
PRIMARY KEY (`trace_id_high`, `trace_id`, `id`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_spans ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTracesByIds';
ALTER TABLE zipkin_spans ADD INDEX(`name`) COMMENT 'for getTraces and getSpanNames';
ALTER TABLE zipkin_spans ADD INDEX(`remote_service_name`) COMMENT 'for getTraces and getRemoteServiceNames';
ALTER TABLE zipkin_spans ADD INDEX(`start_ts`) COMMENT 'for getTraces ordering and range';

CREATE TABLE IF NOT EXISTS zipkin_annotations (
`trace_id_high` BIGINT NOT NULL DEFAULT 0 COMMENT 'If non zero, this means the trace uses 128 bit traceIds instead of 64 bit',
`trace_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.trace_id',
`span_id` BIGINT NOT NULL COMMENT 'coincides with zipkin_spans.id',
`a_key` VARCHAR(255) NOT NULL COMMENT 'BinaryAnnotation.key or Annotation.value if type == -1',
`a_value` BLOB COMMENT 'BinaryAnnotation.value(), which must be smaller than 64KB',
`a_type` INT NOT NULL COMMENT 'BinaryAnnotation.type() or -1 if Annotation',
`a_timestamp` BIGINT COMMENT 'Used to implement TTL; Annotation.timestamp or zipkin_spans.timestamp',
`endpoint_ipv4` INT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_ipv6` BINARY(16) COMMENT 'Null when Binary/Annotation.endpoint is null, or no IPv6 address',
`endpoint_port` SMALLINT COMMENT 'Null when Binary/Annotation.endpoint is null',
`endpoint_service_name` VARCHAR(255) COMMENT 'Null when Binary/Annotation.endpoint is null'
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

ALTER TABLE zipkin_annotations ADD UNIQUE KEY(`trace_id_high`, `trace_id`, `span_id`, `a_key`, `a_timestamp`) COMMENT 'Ignore insert on duplicate';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`, `span_id`) COMMENT 'for joining with zipkin_spans';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id_high`, `trace_id`) COMMENT 'for getTraces/ByIds';
ALTER TABLE zipkin_annotations ADD INDEX(`endpoint_service_name`) COMMENT 'for getTraces and getServiceNames';
ALTER TABLE zipkin_annotations ADD INDEX(`a_type`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`a_key`) COMMENT 'for getTraces and autocomplete values';
ALTER TABLE zipkin_annotations ADD INDEX(`trace_id`, `span_id`, `a_key`) COMMENT 'for dependencies job';

CREATE TABLE IF NOT EXISTS zipkin_dependencies (
`day` DATE NOT NULL,
`parent` VARCHAR(255) NOT NULL,
`child` VARCHAR(255) NOT NULL,
`call_count` BIGINT,
`error_count` BIGINT,
PRIMARY KEY (`day`, `parent`, `child`)
) ENGINE=InnoDB ROW_FORMAT=COMPRESSED CHARACTER SET=utf8 COLLATE utf8_general_ci;

启动 ZipKin 指定 mysql 数据库参数:

1
2
3
4
5
6
7
8
java -jar zipkin-server-2.21.1-exec.jar 
--STORAGE_TYPE=mysql
--MYSQL_HOST=127.0.0.1
--MYSQL_TCP_PORT=3306
# 携带自己的数据库信息
--MYSQL_DB=test
--MYSQL_USER=root
--MYSQL_PASS=1qaz!QAZ

测试:

发送接口请求,再查看数据库。发现数据已存入数据库了

image-20230301121936132

存储在 ElarsticSearch :

es官网:https://www.elastic.co/cn/start

下载解压后 执行/bin/elasticsearch.bat 后浏览器访问:http://localhost:9200/

显示类似如下信息标识启动成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
"name": "DESKTOP-SAUA6M1",
"cluster_name": "elasticsearch",
"cluster_uuid": "M-ZSPWU8SZyFqB2DWvqX2g",
"version": {
"number": "7.6.2",
"build_flavor": "default",
"build_type": "zip",
"build_hash": "ef48eb35cf30adf4db14086e8aabd07ef6fb113f",
"build_date": "2020-03-26T06:34:37.794943Z",
"build_snapshot": false,
"lucene_version": "8.4.0",
"minimum_wire_compatibility_version": "6.8.0",
"minimum_index_compatibility_version": "6.0.0-beta1"
},
"tagline": "You Know, for Search"
}

启动 ZipKin 加上 es 的存储参数

1
java -jar zipkin-server-2.21.1-exec.jar --STORAGE_TYPE=elasticsearch --ESHOST=localhost:9200

如果想要通使用 ELK 来检索日志,那么可以看我另外一篇文章:spring cloud 集成 Sleuth

消息驱动 RocketMQ

RocketMQ官网:https://rocketmq.apache.org/zh/docs/quickStart/01quickstart

快速开始

1
2
3
4
5
6
7
8
9
10
11
12
#下载包
wget https://dist.apache.org/repos/dist/release/rocketmq/5.1.0/rocketmq-all-5.1.0-bin-release.zip
#解压包
unzip rocketmq-all-5.1.0-bin-release.zip
## 没有 wget,unzip 命令就 yum install packagename 安装下

#启动 namesrv
nohup sh bin/mqnamesrv &
#验证是否启动成功
tail -f ~/logs/rocketmqlogs/namesrv.log
#启动Broker+Proxy
nohup sh bin/mqbroker -n localhost:9876 --enable-proxy &

sdk 测试消息收发

项目 pom 文件添加依赖:

1
2
3
4
5
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client-java</artifactId>
<version>5.0.4</version>
</dependency>

通过mqadmin创建 Topic。

1
sh bin/mqadmin updatetopic -n localhost:9876 -t TestTopic -c DefaultCluster

生产者测试代码

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
package com.wu.example;

import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientConfigurationBuilder;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.message.Message;
import org.apache.rocketmq.client.apis.producer.Producer;
import org.apache.rocketmq.client.apis.producer.SendReceipt;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class ProducerExample {
private static final Logger logger = LoggerFactory.getLogger(ProducerExample.class);

public static void main(String[] args) throws ClientException {
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoint = "192.168.205.137:8081";
// 消息发送的目标Topic名称,需要提前创建。
String topic = "TestTopic";
ClientServiceProvider provider = ClientServiceProvider.loadService();
ClientConfigurationBuilder builder = ClientConfiguration.newBuilder().setEndpoints(endpoint);
ClientConfiguration configuration = builder.build();
// 初始化Producer时需要设置通信配置以及预绑定的Topic。
Producer producer = provider.newProducerBuilder()
.setTopics(topic)
.setClientConfiguration(configuration)
.build();
// 普通消息发送。
Message message = provider.newMessageBuilder()
.setTopic(topic)
// 设置消息索引键,可根据关键字精确查找某条消息。
.setKeys("messageKey")
// 设置消息Tag,用于消费端根据指定Tag过滤消息。
.setTag("messageTag")
// 消息体。
.setBody("messageBody".getBytes())
.build();
try {
// 发送消息,需要关注发送结果,并捕获失败等异常。
SendReceipt sendReceipt = producer.send(message);
logger.info("Send message successfully, messageId={}", sendReceipt.getMessageId());
} catch (ClientException e) {
logger.error("Failed to send message", e);
}
// producer.close();
}
}

执行 main 方法,控制台提示发送成功!

1
18:47:18.933 [main] INFO com.wu.example.ProducerExample - Send message successfully, messageId=01005056C0000863800410C73600000000

消费者测试代码:

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
package com.wu.example;

import java.io.IOException;
import java.util.Collections;
import org.apache.rocketmq.client.apis.ClientConfiguration;
import org.apache.rocketmq.client.apis.ClientException;
import org.apache.rocketmq.client.apis.ClientServiceProvider;
import org.apache.rocketmq.client.apis.consumer.ConsumeResult;
import org.apache.rocketmq.client.apis.consumer.FilterExpression;
import org.apache.rocketmq.client.apis.consumer.FilterExpressionType;
import org.apache.rocketmq.client.apis.consumer.PushConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class PushConsumerExample {
private static final Logger logger = LoggerFactory.getLogger(PushConsumerExample.class);

private PushConsumerExample() {
}

public static void main(String[] args) throws ClientException, IOException, InterruptedException {
final ClientServiceProvider provider = ClientServiceProvider.loadService();
// 接入点地址,需要设置成Proxy的地址和端口列表,一般是xxx:8081;xxx:8081。
String endpoints = "192.168.205.137:8081";
ClientConfiguration clientConfiguration = ClientConfiguration.newBuilder()
.setEndpoints(endpoints)
.build();
// 订阅消息的过滤规则,表示订阅所有Tag的消息。
String tag = "*";
FilterExpression filterExpression = new FilterExpression(tag, FilterExpressionType.TAG);
// 为消费者指定所属的消费者分组,Group需要提前创建。
String consumerGroup = "YourConsumerGroup";
// 指定需要订阅哪个目标Topic,Topic需要提前创建。
String topic = "TestTopic";
// 初始化PushConsumer,需要绑定消费者分组ConsumerGroup、通信参数以及订阅关系。
PushConsumer pushConsumer = provider.newPushConsumerBuilder()
.setClientConfiguration(clientConfiguration)
// 设置消费者分组。
.setConsumerGroup(consumerGroup)
// 设置预绑定的订阅关系。
.setSubscriptionExpressions(Collections.singletonMap(topic, filterExpression))
// 设置消费监听器。
.setMessageListener(messageView -> {
// 处理消息并返回消费结果。
logger.info("Consume message successfully, messageId={}", messageView.getMessageId());
return ConsumeResult.SUCCESS;
})
.build();
Thread.sleep(Long.MAX_VALUE);
// 如果不需要再使用 PushConsumer,可关闭该实例。
// pushConsumer.close();
}
}

执行 main 方法,控制台日志提示 消费成功!

1
18:47:52.089 [RocketmqMessageConsumption-0-24] INFO com.wu.example.PushConsumerExample - Consume message successfully, messageId=01005056C0000863800410C73600000000

note:

1
2
3
#关闭服务
sh bin/mqshutdown broker
sh bin/mqshutdown namesrv

结合spring boot

pom 依赖:

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.apache.rocketmq/rocketmq-spring-boot-starter -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.3</version>
</dependency>

配置 application.yml 改成自己的(不配置 会导致 RocketMQTemplate 注入失败

1
2
3
4
5
rocketmq:
name-server: 192.168.205.137:9876
producer:
group: my-group1
send-message-timeout: 300000

发送消息

1
2
3
4
// Send string
SendResult sendResult = rocketMQTemplate.syncSend(springTopic, "Hello, World!");
System.out.printf("syncSend1 to topic %s sendResult=%s %n", springTopic, sendResult);
return sendResult;

idea 控制台:

1
syncSend1 to topic string-topic sendResult=SendResult [sendStatus=SEND_OK, msgId=7F00000107C818B4AAC20957F3640006, offsetMsgId=C0A8CD8900002A9F0000000000001482, messageQueue=MessageQueue [topic=string-topic, brokerName=broker-a, queueId=1], queueOffset=0] 

rocketMQ-dashboard

1
2
/** https://rocketmq.apache.org/zh/docs/deploymentOperations/04Dashboard */
下载后 进入 target 目录 执行 java -jar (本为 springboot 项目,可自行修改源代码配置后 执行 mvn 打包)

访问:http://192.168.205.137:6060/#/ (根据自己的启动端口访问,记得关闭防火墙或开发端口)

image-20230302193602908

接收消息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.wu.consumer;

import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Service;

/**
* StringConsumer
*/
@Service
@RocketMQMessageListener(nameServer = "192.168.205.137:9876",
topic = "string-topic",
consumerGroup = "string_consumer",
// selectorExpression = "${demo.rocketmq.tag}",
tlsEnable = "false")
public class StringConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.printf("------- StringConsumer received: %s \n", message);
}
}

控制台输出:

image-20230302193916459

发送对象并接受回调 生产者

1
2
3
4
5
6
7
8
9
10
11
rocketMQTemplate.sendAndReceive(objectRequestTopic,  new Order().setOid(1666L).setUid(434233).setUsername("name-dfg"), new RocketMQLocalRequestCallback<Order>() {
@Override
public void onSuccess(Order message) {
System.out.printf("send user object and receive %s %n", message.toString());
}

@Override
public void onException(Throwable e) {
e.printStackTrace();
}
}, 5000);

消费者:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import com.wu.entry.Order;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQReplyListener;
import org.springframework.stereotype.Service;

/**
* The consumer that replying Object
*/
@Service
@RocketMQMessageListener(nameServer = "192.168.205.137:9876",
topic = "objectRequestTopic",
consumerGroup = "object_consumer")
public class ObjectConsumerWithReplyOrder implements RocketMQReplyListener<Order, Order> {

@Override
public Order onMessage(Order order) {
System.out.printf("------- ObjectConsumerWithReplyOrder received: %s \n", order);
Order replyOrder = new Order();
replyOrder.setUid(99999);
replyOrder.setUsername("reply-name");
return replyOrder;
}
}

更多调用示例代码请参考官方示例:apache/rocketmq-spring: Apache RocketMQ Spring Integration (github.com)

或点这里下载示例代码:https://github.com/apache/rocketmq-spring/archive/refs/heads/master.zip


note:

1
2
3
4
5
6
7
8
9
10
11
[root@localhost rocketmq-all-5.1.0-bin-release]# sh bin/mqnamesrv
Java HotSpot(TM) 64-Bit Server VM warning: INFO: os::commit_memory(0x0000000700000000, 4294967296, 0) failed; error='Not enough space' (errno=12)
#
# There is insufficient memory for the Java Runtime Environment to continue.
# Native memory allocation (mmap) failed to map 4294967296 bytes for committing reserved memory.
# An error report file with more information is saved as:
# /usr/local/rocketMQ/rocketmq-all-5.1.0-bin-release/hs_err_pid8932.log

#编辑 启动脚本 调整 jvm 参数。 例如 4g 改成 512m 等等(相同大小的改一样的小)
vim bin/runserver.sh
vim bin/runbroker.sh

短信服务

这里以腾讯云短信为例(为什么不用阿里云,因为腾讯云可以购买100条的小套餐,便宜!)

首先登录腾讯云找到短信(服务)平台:https://console.cloud.tencent.com/smsv2/csms-sign

image-20230305120003296

申请短信签名和模板(前提是需要完成个人认证或企业认证)

image-20230305120205227

代码示例控制台:https://console.cloud.tencent.com/api/explorer?Product=sms&Version=2021-01-11&Action=SendSms

在这里可以测试发送短信,并且能自动生成代码示例。依赖信息也可以在这里找到。参数不知道怎么填的,点击参数旁边的*号可以跳转到对应的指引页面(配置)获取参数

image-20230305121208795

自己用代码在本地运行的时候需要 secretID 和 secretKey 这两个东西在这里可以去创建

image-20230305120532109

本地运行结果:

image-20230305121834482

846cd454d0c04fda638c9c8d96ed5f4

简单说两句:

短信服务功能很简单,不管是 阿里云,腾讯云,sendCloud 或者其他服务商基本都要经过这么几个步骤

1、注册账号并通过认证(个人认证或企业认证)

2、申请短信签名。即发送短信的主体(一般显示在短信的开头用中括号括起来的比如:【腾讯云】【阿里云】【xx小店】)

3、申请短信模板。(一般这里会区分 验证码类短信或通知类短信)

然后就是参考官方给出的文档了。写的好的 一般只要把代码复制下来然后替换掉自己的参数就可以了。

另外我在本地运行的时候出了小问题:

1
2
3
4
5
6
7
8
9
D:\Java\jdk1.8.0_144\bin\java.exe "-javaagent:D:\Program Files\ideaIU-2019.2win\lib\idea_rt.jar=5418:D:\Program Files\ideaIU-2019.2win\bin" -Dfile.encoding=UTF-8 -classpath "D:\Java\jdk1.8.0_144\jre\lib\charsets.jar;D:\Java\jdk1.8.0_144\jre\lib\deploy.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\localedata.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunec.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8.0_144\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8.0_144\jre\lib\javaws.jar;D:\Java\jdk1.8.0_144\jre\lib\jce.jar;D:\Java\jdk1.8.0_144\jre\lib\jfr.jar;D:\Java\jdk1.8.0_144\jre\lib\jfxswt.jar;D:\Java\jdk1.8.0_144\jre\lib\jsse.jar;D:\Java\jdk1.8.0_144\jre\lib\management-agent.jar;D:\Java\jdk1.8.0_144\jre\lib\plugin.jar;D:\Java\jdk1.8.0_144\jre\lib\resources.jar;D:\Java\jdk1.8.0_144\jre\lib\rt.jar;D:\IdeaProjects\studyalibaba\client\target\classes;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter\2.1.3.RELEASE\spring-boot-starter-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot\2.1.3.RELEASE\spring-boot-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-context\5.1.5.RELEASE\spring-context-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-autoconfigure\2.1.3.RELEASE\spring-boot-autoconfigure-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter-logging\2.1.3.RELEASE\spring-boot-starter-logging-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;D:\Program Files\apache-maven-3.5.3\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\logging\log4j\log4j-to-slf4j\2.11.2\log4j-to-slf4j-2.11.2.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\logging\log4j\log4j-api\2.11.2\log4j-api-2.11.2.jar;D:\Program Files\apache-maven-3.5.3\repository\org\slf4j\jul-to-slf4j\1.7.25\jul-to-slf4j-1.7.25.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\annotation\javax.annotation-api\1.3.2\javax.annotation-api-1.3.2.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-core\5.1.5.RELEASE\spring-core-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-jcl\5.1.5.RELEASE\spring-jcl-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\yaml\snakeyaml\1.23\snakeyaml-1.23.jar;D:\Program Files\apache-maven-3.5.3\repository\org\slf4j\slf4j-api\1.7.25\slf4j-api-1.7.25.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\xml\bind\jaxb-api\2.3.1\jaxb-api-2.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\activation\javax.activation-api\1.2.0\javax.activation-api-1.2.0.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter-web\2.1.3.RELEASE\spring-boot-starter-web-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter-json\2.1.3.RELEASE\spring-boot-starter-json-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\core\jackson-databind\2.9.8\jackson-databind-2.9.8.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\core\jackson-annotations\2.9.0\jackson-annotations-2.9.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.9.8\jackson-datatype-jdk8-2.9.8.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.9.8\jackson-datatype-jsr310-2.9.8.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\module\jackson-module-parameter-names\2.9.8\jackson-module-parameter-names-2.9.8.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter-tomcat\2.1.3.RELEASE\spring-boot-starter-tomcat-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\tomcat\embed\tomcat-embed-core\9.0.16\tomcat-embed-core-9.0.16.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\tomcat\embed\tomcat-embed-el\9.0.16\tomcat-embed-el-9.0.16.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\tomcat\embed\tomcat-embed-websocket\9.0.16\tomcat-embed-websocket-9.0.16.jar;D:\Program Files\apache-maven-3.5.3\repository\org\hibernate\validator\hibernate-validator\6.0.14.Final\hibernate-validator-6.0.14.Final.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\validation\validation-api\2.0.1.Final\validation-api-2.0.1.Final.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jboss\logging\jboss-logging\3.3.2.Final\jboss-logging-3.3.2.Final.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\classmate\1.4.0\classmate-1.4.0.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-web\5.1.5.RELEASE\spring-web-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-beans\5.1.5.RELEASE\spring-beans-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-webmvc\5.1.5.RELEASE\spring-webmvc-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-aop\5.1.5.RELEASE\spring-aop-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-expression\5.1.5.RELEASE\spring-expression-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\cloud\spring-cloud-starter-alibaba-nacos-discovery\2.1.4.RELEASE\spring-cloud-starter-alibaba-nacos-discovery-2.1.4.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\cloud\spring-cloud-alibaba-commons\2.1.4.RELEASE\spring-cloud-alibaba-commons-2.1.4.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\nacos\nacos-client\1.4.1\nacos-client-1.4.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\nacos\nacos-common\1.4.1\nacos-common-1.4.1.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-io\commons-io\2.2\commons-io-2.2.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\httpcomponents\httpasyncclient\4.1.4\httpasyncclient-4.1.4.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\httpcomponents\httpcore\4.4.11\httpcore-4.4.11.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\httpcomponents\httpcore-nio\4.4.11\httpcore-nio-4.4.11.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\nacos\nacos-api\1.4.1\nacos-api-1.4.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\google\guava\guava\24.1.1-jre\guava-24.1.1-jre.jar;D:\Program Files\apache-maven-3.5.3\repository\com\google\code\findbugs\jsr305\1.3.9\jsr305-1.3.9.jar;D:\Program Files\apache-maven-3.5.3\repository\org\checkerframework\checker-compat-qual\2.0.0\checker-compat-qual-2.0.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\google\errorprone\error_prone_annotations\2.1.3\error_prone_annotations-2.1.3.jar;D:\Program Files\apache-maven-3.5.3\repository\com\google\j2objc\j2objc-annotations\1.1\j2objc-annotations-1.1.jar;D:\Program Files\apache-maven-3.5.3\repository\org\codehaus\mojo\animal-sniffer-annotations\1.14\animal-sniffer-annotations-1.14.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-codec\commons-codec\1.11\commons-codec-1.11.jar;D:\Program Files\apache-maven-3.5.3\repository\com\fasterxml\jackson\core\jackson-core\2.9.8\jackson-core-2.9.8.jar;D:\Program Files\apache-maven-3.5.3\repository\io\prometheus\simpleclient\0.5.0\simpleclient-0.5.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\spring\spring-context-support\1.0.10\spring-context-support-1.0.10.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-commons\2.1.0.RELEASE\spring-cloud-commons-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\security\spring-security-crypto\5.1.4.RELEASE\spring-security-crypto-5.1.4.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-context\2.1.0.RELEASE\spring-cloud-context-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-starter-netflix-ribbon\2.1.0.RELEASE\spring-cloud-starter-netflix-ribbon-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-starter\2.1.0.RELEASE\spring-cloud-starter-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\security\spring-security-rsa\1.0.7.RELEASE\spring-security-rsa-1.0.7.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\bouncycastle\bcpkix-jdk15on\1.60\bcpkix-jdk15on-1.60.jar;D:\Program Files\apache-maven-3.5.3\repository\org\bouncycastle\bcprov-jdk15on\1.60\bcprov-jdk15on-1.60.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-netflix-ribbon\2.1.0.RELEASE\spring-cloud-netflix-ribbon-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-netflix-archaius\2.1.0.RELEASE\spring-cloud-netflix-archaius-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\cloud\spring-cloud-starter-netflix-archaius\2.1.0.RELEASE\spring-cloud-starter-netflix-archaius-2.1.0.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\archaius\archaius-core\0.7.6\archaius-core-0.7.6.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-configuration\commons-configuration\1.8\commons-configuration-1.8.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\ribbon\ribbon\2.3.0\ribbon-2.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\ribbon\ribbon-transport\2.3.0\ribbon-transport-2.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\io\reactivex\rxnetty-contexts\0.4.9\rxnetty-contexts-0.4.9.jar;D:\Program Files\apache-maven-3.5.3\repository\io\reactivex\rxnetty-servo\0.4.9\rxnetty-servo-0.4.9.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\hystrix\hystrix-core\1.5.18\hystrix-core-1.5.18.jar;D:\Program Files\apache-maven-3.5.3\repository\org\hdrhistogram\HdrHistogram\2.1.9\HdrHistogram-2.1.9.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\inject\javax.inject\1\javax.inject-1.jar;D:\Program Files\apache-maven-3.5.3\repository\io\reactivex\rxnetty\0.4.9\rxnetty-0.4.9.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\ribbon\ribbon-core\2.3.0\ribbon-core-2.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-lang\commons-lang\2.6\commons-lang-2.6.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\ribbon\ribbon-httpclient\2.3.0\ribbon-httpclient-2.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-collections\commons-collections\3.2.2\commons-collections-3.2.2.jar;D:\Program Files\apache-maven-3.5.3\repository\org\apache\httpcomponents\httpclient\4.5.7\httpclient-4.5.7.jar;D:\Program Files\apache-maven-3.5.3\repository\com\sun\jersey\jersey-client\1.19.1\jersey-client-1.19.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\sun\jersey\jersey-core\1.19.1\jersey-core-1.19.1.jar;D:\Program Files\apache-maven-3.5.3\repository\javax\ws\rs\jsr311-api\1.1.1\jsr311-api-1.1.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\sun\jersey\contribs\jersey-apache-client4\1.19.1\jersey-apache-client4-1.19.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\servo\servo-core\0.12.21\servo-core-0.12.21.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\netflix-commons\netflix-commons-util\0.3.0\netflix-commons-util-0.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\ribbon\ribbon-loadbalancer\2.3.0\ribbon-loadbalancer-2.3.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\netflix\netflix-commons\netflix-statistics\0.1.1\netflix-statistics-0.1.1.jar;D:\Program Files\apache-maven-3.5.3\repository\io\reactivex\rxjava\1.3.8\rxjava-1.3.8.jar;D:\IdeaProjects\studyalibaba\commen\target\classes;D:\Program Files\apache-maven-3.5.3\repository\com\baomidou\mybatis-plus-boot-starter\3.5.3.1\mybatis-plus-boot-starter-3.5.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\baomidou\mybatis-plus\3.5.3.1\mybatis-plus-3.5.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\baomidou\mybatis-plus-extension\3.5.3.1\mybatis-plus-extension-3.5.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\baomidou\mybatis-plus-core\3.5.3.1\mybatis-plus-core-3.5.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\baomidou\mybatis-plus-annotation\3.5.3.1\mybatis-plus-annotation-3.5.3.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\github\jsqlparser\jsqlparser\4.4\jsqlparser-4.4.jar;D:\Program Files\apache-maven-3.5.3\repository\org\mybatis\mybatis\3.5.10\mybatis-3.5.10.jar;D:\Program Files\apache-maven-3.5.3\repository\org\mybatis\mybatis-spring\2.0.7\mybatis-spring-2.0.7.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\boot\spring-boot-starter-jdbc\2.1.3.RELEASE\spring-boot-starter-jdbc-2.1.3.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\zaxxer\HikariCP\3.2.0\HikariCP-3.2.0.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-jdbc\5.1.5.RELEASE\spring-jdbc-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\org\springframework\spring-tx\5.1.5.RELEASE\spring-tx-5.1.5.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\mysql\mysql-connector-java\8.0.15\mysql-connector-java-8.0.15.jar;D:\Program Files\apache-maven-3.5.3\repository\com\alibaba\cloud\spring-cloud-starter-alibaba-nacos-config\2.1.4.RELEASE\spring-cloud-starter-alibaba-nacos-config-2.1.4.RELEASE.jar;D:\Program Files\apache-maven-3.5.3\repository\com\tencentcloudapi\tencentcloud-sdk-java-sms\3.1.706\tencentcloud-sdk-java-sms-3.1.706.jar;D:\Program Files\apache-maven-3.5.3\repository\com\tencentcloudapi\tencentcloud-sdk-java-common\3.1.706\tencentcloud-sdk-java-common-3.1.706.jar;D:\Program Files\apache-maven-3.5.3\repository\commons-logging\commons-logging\1.2\commons-logging-1.2.jar;D:\Program Files\apache-maven-3.5.3\repository\com\squareup\okio\okio\3.2.0\okio-3.2.0.jar;D:\Program Files\apache-maven-3.5.3\repository\com\squareup\okio\okio-jvm\3.2.0\okio-jvm-3.2.0.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jetbrains\kotlin\kotlin-stdlib-jdk8\1.2.71\kotlin-stdlib-jdk8-1.2.71.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jetbrains\kotlin\kotlin-stdlib\1.2.71\kotlin-stdlib-1.2.71.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jetbrains\annotations\13.0\annotations-13.0.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jetbrains\kotlin\kotlin-stdlib-jdk7\1.2.71\kotlin-stdlib-jdk7-1.2.71.jar;D:\Program Files\apache-maven-3.5.3\repository\org\jetbrains\kotlin\kotlin-stdlib-common\1.6.20\kotlin-stdlib-common-1.6.20.jar;D:\Program Files\apache-maven-3.5.3\repository\com\squareup\okhttp3\okhttp\3.8.1\okhttp-3.8.1.jar;D:\Program Files\apache-maven-3.5.3\repository\com\google\code\gson\gson\2.8.5\gson-2.8.5.jar;D:\Program Files\apache-maven-3.5.3\repository\com\squareup\okhttp3\logging-interceptor\3.8.1\logging-interceptor-3.8.1.jar;D:\Program Files\apache-maven-3.5.3\repository\org\ini4j\ini4j\0.5.4\ini4j-0.5.4.jar" com.wu.sendmsg.SendSms
Exception in thread "main" java.lang.NoSuchMethodError: kotlin.collections.ArraysKt.copyInto([B[BIII)[B
at okio.Buffer.write(Buffer.kt:1275)
at okio.RealBufferedSink.write(RealBufferedSink.kt:183)
at okhttp3.RequestBody$2.writeTo(RequestBody.java:98)
at okhttp3.internal.http.CallServerInterceptor.intercept(CallServerInterceptor.java:62)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)
at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.java:45)
at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.java:92)

之后在 pom 文件中加了个 kotlin 依赖就好了

1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-stdlib -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>1.3.72</version>
</dependency>

配置中心 nacos

配置中心的介绍就不多说了,常用的配置中心有 apollo (阿波罗) , springcloud config , consul 和 nacos 等。关于 apollo 大家可以看我另外一篇文章 “配置中心之Apollo”。这里我们来学习下 nacos 配置中心。

快速体验

配置文件优先级(由高到低): bootstrap.properties -> bootstrap.yml -> application.properties -> application.yml

项目中新建 bootstrap.yml 配置文件

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: client
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml
profiles:
active: dev

在 nacos 新建配置:

image-20230303142929686

1
2
3
4
5
6
7
8
server:
port: 8080
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
username: root
password: 1qaz!QAZ

注意 data ID 与 项目中 bootstrap.yml 内容的关系

image-20230303142827266

pom 添加配置:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

启动项目:看到类似以下日志表示成功!

1
2
3
4
5
6
7
8
2023-03-03 14:31:37.200  INFO 24892 --- [           main] com.wu.Client                            : Started Client in 6.817 seconds (JVM running for 7.813)
2023-03-03 14:31:37.202 INFO 24892 --- [ main] c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [subscribe] client+DEFAULT_GROUP
2023-03-03 14:31:37.203 INFO 24892 --- [ main] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [add-listener] ok, tenant=, dataId=client, group=DEFAULT_GROUP, cnt=1
2023-03-03 14:31:37.203 INFO 24892 --- [ main] c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [subscribe] client.yaml+DEFAULT_GROUP
2023-03-03 14:31:37.203 INFO 24892 --- [ main] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [add-listener] ok, tenant=, dataId=client.yaml, group=DEFAULT_GROUP, cnt=1
2023-03-03 14:31:37.204 INFO 24892 --- [ main] c.a.n.client.config.impl.ClientWorker : [fixed-127.0.0.1_8848] [subscribe] client-dev.yaml+DEFAULT_GROUP
2023-03-03 14:31:37.204 INFO 24892 --- [ main] c.a.nacos.client.config.impl.CacheData : [fixed-127.0.0.1_8848] [add-listener] ok, tenant=, dataId=client-dev.yaml, group=DEFAULT_GROUP, cnt=1
2023-03-03 14:31:37.690 INFO 24892 --- [g.push.receiver] com.alibaba.nacos.client.naming : received push data: {"type":"dom","data":"{\"name\":\"DEFAULT_GROUP@@client\",\"clusters\":\"DEFAULT\",\"cacheMillis\":10000,\"hosts\":[{\"instanceId\":\"192.168.205.1#8080#DEFAULT#DEFAULT_GROUP@@client\",\"ip\":\"192.168.205.1\",\"port\":8080,\"weight\":1.0,\"healthy\":true,\"enabled\":true,\"ephemeral\":true,\"clusterName\":\"DEFAULT\",\"serviceName\":\"DEFAULT_GROUP@@client\",\"metadata\":{\"preserved.register.source\":\"SPRING_CLOUD\"},\"instanceHeartBeatTimeOut\":15000,\"instanceHeartBeatInterval\":5000,\"ipDeleteTimeout\":30000}],\"lastRefTime\":1677825097685,\"checksum\":\"\",\"allIPs\":false,\"reachProtectionThreshold\":false,\"valid\":true}","lastRefTime":230424516007500} from /192.168.205.1

动态获取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @ClassName TestController
* @Author wuzhiyong
* @Date 2023/2/21 12:12
* @Version 1.0
**/
@RestController
@RequestMapping("/client")
@RefreshScope //@Value 方式取值,再加上这个注解,即可动态获取配置(nacos 上更新后 项目中会自动获取到新值)
public class TestController {

Logger log = LoggerFactory.getLogger(TestController.class);

@Value("${custom.value.testConfStr}")
private String testconfStr;
......
}

配置的继承与聚合

继承:

1
2
3
4
5
6
7
8
9
# nacos 中新建如下配置
client.yaml
client-test.yaml
client-dev.yaml
client-prod.yaml
# 其中 test,dev,prod 会继承 client.yaml 中的配置
# 如果 client.yaml 与 dev,prod,test...等 配置了相同的内容,子配置会覆盖父配置(即重写)。
# 使用时 依然是通过项目的 bootstrap.yml 文件内的 profiles.active: (dev) 来指定具体配置
# 如果 profiles.active: 的值为空,即采用的父配置

聚合:

在 bootstrap.yml 中配置

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
nacos:
server-addr: 127.0.0.1:8848
config:
file-extension: yaml
shared-configs: # 此处开始
- data-id: commons.yaml
group: DEFAULT_GROUP
refresh: true # 开启当前 分享的配置刷新
# refresh-enabled: true 开启所有刷新

聚合可以把不同 group 的配置引入到当前配置。可以配置多个。这种组合只能在 bootstrap.yml 中配置。


命名空间(Namespace)

命名空间可用于进行不同环境的配置隔离。一般一个环境划分到一个命名空间

配置分组(Group)

配置分组用于将不同的服务可以归类到同一分组。一般将一个项目的配置分到一组

配置集(Data ID)

在系统中,一个配置文件通常就是一个配置集。一般微服务的配置就是一个配置集

image-20230303230411230

分布式事务 seata

官网文档:Seata 是什么

Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。

image-20230304195251068

下载网站:Releases · seata/seata (github.com)

代码来源:seata-samples/springcloud-nacos-seata at master · seata/seata-samples (github.com)

我下的 seate 版本是 1.6.1 :Release v1.6.1 · seata/seata (github.com)

springboot 版本是:2.1.3.RELEASE

springcloud 版本是:Greenwich.RELEASE

spring cloud alibaba 版本是:2.1.4.RELEASE

image-20230304173651191

这里有三个文件说明下:

  • nacos-config.txt

这个文件是自己建立的内容如下:

1
2
service.vgroupMapping.stock-service-group=default
service.vgroupMapping.order-service-group=default
  • registry.config

这个文件也是自己建立的内容如下:(可从示例代码 resources 下复制过来改下)

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
registry {
# file 、nacos 、eureka、redis、zk、consul、etcd3、sofa
type = "nacos"

nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
# file、nacos 、apollo、zk、consul、etcd3
type = "nacos"

nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
dataId = "seataServer.properties"
}
}
  • config.txt

这个文件原本就有的,里边列出了所有配置项。当我执行初始化脚本发现初始化失败!看到这里边的配置说明我明白了我出现的一些错误是和我没关系的。有些是可以注释掉的


另外执行初始化配置脚本的位置在 /seata/script/config-center/nacos 下面 (如果你是其他的配置中心 那么就在对应的地方)

可通过 git bash 到这里执行

1
$ sh nacos-config.sh 127.0.0.1

初始化配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
wuzhiyong@DESKTOP-SAUA6M1 MINGW64 /d/IdeaProjects/studyalibaba/seata/script/config-center/nacos
$ sh nacos-config.sh 127.0.0.1
set nacosAddr=localhost:8848
set group=SEATA_GROUP
Set transport.type=TCP successfully
Set transport.server=NIO successfully
Set transport.heartbeat=true successfully
........
...........
Set metrics.registryType=compact successfully
Set metrics.exporterList=prometheus successfully
Set metrics.exporterPrometheusPort=9898 successfully
=========================================================================
Complete initialization parameters, total-count:107 , failure-count:4
=========================================================================
init nacos config fail.
# 发现错误请看 config.txt 里的内容
# 关于参数的说明 看这里:https://seata.io/zh-cn/docs/user/configurations.html

我的配置是 type = nacos 的所以初始化后可以在 nacos 看到很多参数:

image-20230304180544747

开始

启动 seata

1
2
3
4
5
6
7
8
9
# 切换到 /seata/bin 路径
wuzhiyong@DESKTOP-SAUA6M1 MINGW64 /d/IdeaProjects/studyalibaba/seata/script/config-center/nacos
$ cd /d/IdeaProjects/studyalibaba/seata/bin/

wuzhiyong@DESKTOP-SAUA6M1 MINGW64 /d/IdeaProjects/studyalibaba/seata/bin
$ sh seata-server.sh -p 8091 -m file
apm-skywalking not enabled
seata-server is starting, you can check the /d/IdeaProjects/studyalibaba/seata/logs/start.out

启动项目(stock-service,order-service)

注意数据库 url 加上时间参数。(示例代码的配置里是没有加的)

1
spring.datasource.url=jdbc:mysql://localhost:3306/seata_stock?allowMultiQueries=true&serverTimezone=UTC

测试

部分代码:

OrderController:

1
2
3
4
5
6
7
8
9
10
11
12
@RequestMapping("/placeOrder/commit")
public Boolean placeOrderCommit() {

orderService.placeOrder("1", "product-1", 1);
return true;
}
@RequestMapping("/placeOrder/rollback")
public Boolean placeOrderRollback() {
// product-2 扣库存时模拟了一个业务异常,
orderService.placeOrder("1", "product-2", 1);
return true;
}

OrderService:

1
2
3
4
5
6
7
8
9
10
@GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public void placeOrder(String userId, String commodityCode, Integer count) {
BigDecimal orderMoney = new BigDecimal(count).multiply(new BigDecimal(5));
Order order = new Order().setUserId(userId).setCommodityCode(commodityCode).setCount(count).setMoney(
orderMoney);
orderDAO.insert(order);
stockFeignClient.deduct(commodityCode, count);

}

StockController:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 减库存
*
* @param commodityCode 商品代码
* @param count 数量
* @return
*/
@RequestMapping(path = "/deduct")
public Boolean deduct(String commodityCode, Integer count) {
stockService.deduct(commodityCode, count);
return true;
}

StockService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 减库存
*
* @param commodityCode
* @param count
*/
@Transactional(rollbackFor = Exception.class)
public void deduct(String commodityCode, int count) {
if (commodityCode.equals("product-2")) {
throw new RuntimeException("异常:模拟业务异常:stock branch exception");
}

QueryWrapper<Stock> wrapper = new QueryWrapper<>();
wrapper.setEntity(new Stock().setCommodityCode(commodityCode));
Stock stock = stockDAO.selectOne(wrapper);
stock.setCount(stock.getCount() - count);

stockDAO.updateById(stock);
}

通过请求接口和查询数据库,可发现异常的都回滚了,回滚的数据可从不连续的自增id中得到验证。

image-20230304194619086