Spring Cloud Alibaba 基础案例 - Nacos、OpenFeign、Dubbo 以及各自的负载均衡和拦截器使用

Spring Cloud Alibaba 简介

spring cloud 和 spring cloud alibaba 的关系

简单来说,Spring Cloud 和 Spring Cloud Alibaba 之间是 “标准规范”与 “具体实现” 的关系。如果把 Spring Cloud 比作一套分布式微服务架构的 “官方设计图纸”,那么 Spring Cloud Alibaba 就是由阿里巴巴团队基于这套图纸,结合自身双十一超大规模高并发的实战经验,设计出的一套高性能 “具体施工方案” 和 “全家桶组件”。

Spring Cloud 并不是一个单一的框架,而是一个微服务治理的标准规范和生态总称。它定义了一套微服务架构所必需的标准组件规范,比如:

  • 注册中心(服务发现)
  • 配置中心
  • 分布式路由(网关)
  • 断路器(熔断降级)
  • 负载均衡
  • 远程调用

随着早年 Spring Cloud 官方主推的 Netflix 套件(如 Eureka、Hystrix、Zuul)全面进入停更维护状态,市场上急需新一代高性能组件。阿里巴巴在 2018 年将其内部经过考验的微服务组件开源,并加入了 Spring Cloud 官方孵化器,成为了 Spring Cloud 的一个官方子项目。它实现了 Spring Cloud 定义的所有标准微服务规范,并用阿里自研的全新高性能组件进行了替换。

经典组件与阿里组件的演进表:

传统 Spring Cloud(基于 Netflix 套件等)运维成本高。比如要搭建注册中心和配置中心,你需要自己 new 两个 Spring Boot 项目,分别引入 Eureka Server 和 Config Server 依赖,配置繁琐,且极其消耗内存。而 Spring Cloud Alibaba 诞生于电商高并发、双十一流量大潮的真实工业级场景,有着开箱即用和高性能的特点:

  • 一个 Nacos 直接把注册中心和配置中心全干了,而且 Nacos 是开箱即用的独立独立程序(类似于中间件),不需要你自己在 Java 里去写 Server 端。
  • 强大的可视化:Sentinel 和 Nacos 都自带极度舒适的 Web 管理控制台,流控规则、降级策略直接在浏览器里点点鼠标就能 “秒级动态生效”,而不需要像 Hystrix 那样去硬编码写一堆注解参数。

在目前的国内企业选型中,Spring Cloud 提供的是微服务治理的灵魂和接口规范(比如 @EnableDiscoveryClient 注解),Spring Cloud Alibaba 则是目前国内最普及、生态最活跃、性能最强悍的肉身实现。


版本依赖与兼容性

因为 Spring Cloud Alibaba 和 Spring Cloud 是子项目与主项目的关系,在开发时绝对不能随便乱配版本,否则会遭遇极其痛苦的类冲突和启动崩溃。

  • 版本代号的跟随:Spring Cloud 的版本通常以 202x.x.x(如 2022.0.0)这样的年份形式命名。
  • 依赖引入规范:在 Maven 中,正确的姿势是先锁定 Spring Boot 的版本,再锁定兼容的 Spring Cloud 版本,最后引入 Spring Cloud Alibaba 的 BOM(版本管理)。
  • 为了防止版本打架,可以直接去 Github spring-cloud-alibaba 去查看当前最稳健的 “三合一” 版本对齐表。


Spring Cloud 和 Dubbo 的说明

为什么还要 Spring Cloud

可能有人会问:既然项目中使用了 dubbo 这套微服务架构,为什么我们还要使用 springcloud?一句话总结核心原因就是:Dubbo 是极其优秀的“专科医生”,主攻高性能 RPC 通信;而 Spring Cloud 是一个全能的 “医院生态”,提供整套微服务治理全家桶。使用 Spring Cloud 是为了用它的生态去补齐 Dubbo 在 RPC 之外的治理短板。具体来说,如果一个企业只用纯 Dubbo 架构,很快就会在开发中面临以下 “生态荒”:

  • 统一网关层(Gateway)的缺失
    • Dubbo 内部是走 RPC(如 Netty/Triple)二进制流或长连接通信的。但是前端(App/H5/小程序)或者第三方系统是无法直接发起 Dubbo RPC 请求的,它们只认标准的 HTTP Restful 接口。
    • Dubbo 官方原生并没有提供类似 Spring Cloud Gateway 这样成熟、强大、能完美与 Spring 生态鉴权(如 Spring Security/OAuth2)集成的网关。我们需要利用 Spring Cloud Gateway 挡在最前面,作为流量入口负责路由、限流和安全验签,然后再由网关或者内部的 Web 模块将请求转化为 Dubbo RPC 投递给内网的 Service。
  • 配置中心与动态刷新机制
    • 虽然 Dubbo 3.x 支持动态配置,但它更倾向于治理 Dubbo 自身的参数(如路由规则、限流比例)。
    • 业务开发中大量的业务配置(如数据库连接池、业务开关、活动优惠券参数)需要一整套成熟的统一管理和动态刷新机制。Spring Cloud Alibaba Nacos Config 对 Spring 内部 @Value 和 @RefreshScope 的原生支持无缝且丝滑,这是传统 RPC 框架不具备的业务级治理能力。
  • 分布式链路追踪与监控
    • 微服务线上一旦报错,调用链可能跨越七八个服务。
    • Spring Cloud 生态有极其成熟的 Micrometer / Spring Cloud Sleuth / SkyWalking / Zipkin 整合方案。它能自动拦截 HTTP 请求,并把 TraceId 一路隐式传递到底层 Dubbo 的 RPC 上下文中,让整条跨协议的调用链在监控大屏上一览无遗。
  • 异步事件驱动架构
    • 微服务之间除了同步 RPC 调用,还需要大量的异步削峰(如订单支付成功,发送 MQ 通知库存、积分、短信服务)。
    • Spring Cloud 提供了 Spring Cloud Stream 这种高级抽象,让你只需要写几行注解就能随意切换 RabbitMQ、Kafka 或 RocketMQ。Dubbo 作为 RPC 框架,本身是不涉及任何 MQ 消息驱动封装的。

两者结合后的企业级架构拓扑:


实际技术选型

  • 如果只是纯内部老系统改造,或者不需要复杂的网关和 MQ 业务:单挂一个 Dubbo + Nacos 就能跑得非常欢快,架构更轻量。
  • 如果并发量不大,团队全员熟悉 Spring 生态:直接用全套 Spring Cloud (用 OpenFeign 做 HTTP 通信) 是开发速度最快的。
  • 如果面临高并发场景,既要 Spring 的庞大生态,又要极端的通信性能:Spring Cloud 做皮(外围治理),Dubbo 做骨(内网 RPC),是目前最最可行的方案。


演示项目源码

项目的说明

本项目旨在说明 spring cloud alibaba 结合 spring cloud、spring boot 的具体使用,你也可以将其当做项目构建的基础代码来使用。项目中会用到三种远程调用技术:

  • 基于 RestTemplate 的原始的 HTTP 方式的调用
  • 基于 OpenFeign 的改进版 HTTP 方式的调用
  • 基于 dubbo 的高性能远程服务调用

服务的注册发现、配置中心采用 Nacos。此外我们还会演示调用中遇到的其他关键基础技术,例如 Nacos 的基本使用、服务的超时设置、负载均衡、各自的拦截器等。

项目模块使用 maven 进行统一管理,整个演示项目分为三个模块:

  • zdemo-scloud-api:定义公共类和接口
  • zdemo-scloud-order:服务的提供者,提供 dubbo 或 http 接口给下游的 user 调用
  • zdemo-scloud-user:普通的服务的调用者


zdemo-scloud-order 和 zdemo-scloud-user 服务注册和配置中心的体现:


父项目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
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
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
<?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.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>zdemo-scloud-api</module>
<module>zdemo-scloud-order</module>
<module>zdemo-scloud-user</module>
</modules>

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

<spring-boot.version>3.3.0</spring-boot.version>
<spring-cloud.version>2023.0.1</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.2</spring-cloud-alibaba.version>

<dubbo.version>3.2.19</dubbo.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<hutool.version>5.8.26</hutool.version>
<lombok.version>1.18.44</lombok.version>
<mapstruct.version>1.5.5.Final</mapstruct.version>
</properties>

<dependencyManagement>
<dependencies>
<!--版本说明:https://spring.io/projects/spring-boot#support-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--版本说明:https://spring.io/projects/spring-cloud#support-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>

<!--版本说明:https://github.com/alibaba/spring-cloud-alibaba-->
<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>

<!--版本说明:https://github.com/apache/dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-fastjson2</artifactId>
<version>${dubbo.verison}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-triple</artifactId>
<version>${dubbo.verison}</version>
</dependency>

<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>

<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>

<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${mapstruct.version}</version>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<build>
<pluginManagement>
<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>
</plugins>
</pluginManagement>

<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
<parameters>true</parameters>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
</path>
<path>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${mapstruct.version}</version>
</path>
</annotationProcessorPaths>
<parameters>true</parameters>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>dev</id>
<properties>
<profile.active>dev</profile.active>
</properties>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
</profile>
<profile>
<id>test</id>
<properties>
<profile.active>test</profile.active>
</properties>
</profile>
<profile>
<id>prod</id>
<properties>
<profile.active>prod</profile.active>
</properties>
</profile>
</profiles>

<repositories>
<repository>
<id>alibaba-maven-central</id>
<url>https://maven.pkg.github.com/alibaba/spring-cloud-alibaba</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>alibaba-maven-central-plugins</id>
<url>https://maven.pkg.github.com/alibaba/spring-cloud-alibaba</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
<releases>
<enabled>true</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
</project>


API 公共模块


依赖配置

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
<?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.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>zdemo-scloud-api</artifactId>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<!--使用 open feign 接口定义所需要的依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</dependency>
</dependencies>
</project>


公共类

OrderDTO

1
2
3
4
5
6
7
8
@Data
@AllArgsConstructor
@NoArgsConstructor
public class OrderDTO implements Serializable {
private String orderNo;
private BigDecimal amount;
private String productName;
}


接口定义

dubbo 接口的定义:

1
2
3
4
5
6
7
8
9
10
package zdemo.scloud.api.service.dubbo;

import zdemo.scloud.api.dto.OrderDTO;

/**
* Dubbo 接口定义
*/
public interface OrderService {
OrderDTO getOrderDetails(String orderNo);
}

feign 接口的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package zdemo.scloud.api.service.feign;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import zdemo.scloud.api.dto.OrderDTO;

/**
* 核心注解:@FeignClient。保持干净利落,不绑定任何硬编码的 Class(包括负载均衡策略),任何配置均在调用端配置文件完成
* value 指定目标服务在 Nacos 中的 application.name
* path 指定 Controller 类上定义的 RequestMapping 路径
*/
//@FeignClient(value = "zdemo-scloud-order", path = "/order", configuration = FeignConfig.class)
@FeignClient(value = "zdemo-scloud-order", path = "/order")
public interface OrderFeignService {

/**
* 映射路径必须与 order 服务暴露的实际 HTTP 接口一模一样
* @RequestParam@PathVariable 等参数必须显式指定 ("orderNo")
*/
@GetMapping("/details")
OrderDTO getUserOrderDetails(@RequestParam("orderNo") String orderNo);
}

FeignConfig(可选)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package zdemo.scloud.api.service.feign;

import feign.Logger;
import feign.Request;
import org.springframework.context.annotation.Bean;

/**
* 不要使用 @Configuration 修饰,否则就变成了全局配置
* 不建议使用这种方式对 feign 设置,强烈建议都写到客户端配置文件!
*/
public class FeignConfig {

@Bean
public Logger.Level feignLoggerLevel() {
// 日志级别
return Logger.Level.FULL;
}

@Bean
public Request.Options feignRequestOptions() {
// 超时时间
return new Request.Options(5000 ,10000);
}
}


服务提供者 order


依赖配置

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
<?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.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>zdemo-scloud-order</artifactId>

<dependencies>
<!--引入 API 公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--spring boot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!--dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-triple</artifactId>
<version>${dubbo.version}</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-fastjson2</artifactId>
<version>${dubbo.version}</version>
</dependency>
</dependencies>

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


日志配置

src/main/resources/logback-spring.xml(打印 traceId)

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
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<property name="LOG_PATH" value="./logs"/>
<property name="APP_NAME" value="order"/>

<property name="CONSOLE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%15.15thread] [%X{traceId}] %-40.40logger{39} : %m%n"/>

<property name="FILE_LOG_PATTERN"
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level ${PID:- } --- [%thread] [%X{traceId}] %logger{50} - [%method,%line] - %m%n"/>

<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>

<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-sys-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-sys-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>30</maxHistory>
<totalSizeCap>30GB</totalSizeCap>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>DENY</onMatch>
<onMismatch>ACCEPT</onMismatch>
</filter>
</appender>

<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${LOG_PATH}/${APP_NAME}-sys-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<fileNamePattern>${LOG_PATH}/archive/${APP_NAME}-sys-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<maxFileSize>100MB</maxFileSize>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>UTF-8</charset>
</encoder>
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>ERROR</level>
</filter>
</appender>

<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</configuration>


核心配置

src/main/resources/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
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
server:
port: 8081

spring:
application:
# 注册的服务名称
name: zdemo-scloud-order
cloud:
nacos:
discovery:
# Nacos 服务注册中心地址
# DNS + SLB(vip) + Nacos 集群的安装请参考官网:https://nacos.io/docs/v2/guide/admin/cluster-mode-quick-start/
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
namespace: prod-zdemo # 命名空间默认为 public,Spring Cloud 应用服务实例隔离作用,必须事先在 nacos 创建好对应的命名空间!
cluster-name: DEFAULT # Nacos的集群名称,多集群部署会用到(尽量显式指定,不然可能会有警告)
# group: DEFAULT_GROUP # 分组和命名空间类似,它是对应用更细粒度的管理
# weight: 1 # 通常要结合负载均衡策略使用,权重越高分配的流浪就越大
# metadata: version=1 # 可以结合元数据做过滤或其他定制扩展
# ephemeral: true # 默认就是临时实例,服务挂掉或心跳超过30s就会被nacos剔除
config:
# Nacos 配置中心地址
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
file-extension: yml
namespace: prod-zdemo # 命名空间默认为 public,Spring Cloud 应用业务配置隔离作用,必须事先在 nacos 创建好对应的命名空间!
cluster-name: DEFAULT
# 新版 Config Import 语法
# 显式告诉 Spring Boot 3.x 去哪里导入远程配置
# optional: 代表即使 Nacos 挂了或者没有这个配置文件,应用也能正常启动(生产推荐,防止注册中心单点崩溃导致微服务大面积起不来)
config:
import:
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}

dubbo:
application:
# 在服务提供端指定注册行为:关闭兼容式的双注册,只注册应用级(新),不再注册接口级(老)
register-mode: INSTANCE
# 注意和 spring cloud application 暴露的应用名明确区分划清界限,否则 LoadBalancer 根本无法分辨谁是走 HTTP 的、谁是走 Dubbo 的!
name: ${spring.application.name}-rpc
qos-enable: true
qos-port: 22222
registry:
# 有些极端的互联网大厂在做架构时,喜欢让微服务管理(Spring Cloud 网关走一套 Nacos)
# 与内部底层高性能 RPC 调用(Dubbo 走另一套独立的 Nacos 集群)物理隔离。
# 如果是这种场景(不使用 spring-cloud:// 协议),Dubbo 才会需要自己单独去连 Nacos。
# 这时候在 Dubbo 内部配置密码,它的协议前缀叫 nacos://,且支持标准的 URL 认证拼写。
# 强烈建议采用这种方案,这样既能保持代码的现代和纯净,又能彻底摆脱由于版本和桥梁包带来的 SPI 缺失泥潭。
# 最后强烈不建议 dubbo 中使用分组 &group=xxx,这样很容易导致客户端找不到服务!
address: nacos://192.168.1.149:8848?namespace=prod-zdemo
username: nacos
password: nacos
# 既然使用了 spring-cloud:// 协议,Dubbo 会自动去复制并使用上面 spring.cloud.nacos 里
# 已经配置好的、带安全认证的连接上下文。所以这里绝对不需要、也不能加账号密码!
# 使用 spring-cloud:// 共享协议需要引入 spring-cloud-starter-dubbo 包
# address: spring-cloud://192.168.1.149:8848
protocol:
name: tri # 指定使用最新的 Triple 协议
port: 20881 # Dubbo RPC 端口
serialization: fastjson2
prefer-serialization: fastjson2

logging:
level:
org.apache.dubbo: INFO
org.apache.dubbo.rpc.filter: ERROR
org.apache.dubbo.config.deploy.DefaultMetricsServiceExporter: ERROR


启动类

OrderApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package zdemo.scloud.order;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient // 服务的注册与发现支持
@EnableDubbo // 激活 Dubbo 扫描
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}


feign 接口的实现

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
package zdemo.scloud.order.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import zdemo.scloud.api.dto.OrderDTO;
import java.math.BigDecimal;

/**
* 通过 http 暴露接口
*/
@Slf4j
@RestController
@RequestMapping("/order")
public class OrderController {

@Value("${server.port}")
private Integer port;

@GetMapping("/details")
public OrderDTO getUserOrderDetails(@RequestParam String orderNo) {
log.info("收到客户端订单:{}", orderNo);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return new OrderDTO(orderNo, new BigDecimal("200.00"), "Owlias教程:" + port);
}
}


dubbo 接口的实现

OrderServiceImpl

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

import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboService;
import zdemo.scloud.api.dto.OrderDTO;
import zdemo.scloud.api.service.dubbo.OrderService;
import java.math.BigDecimal;

/**
* 通过 dubbo 暴露接口
*/
@Slf4j
@DubboService // 使用 Dubbo 的 @DubboService 注解暴露出该服务
public class OrderServiceImpl implements OrderService {

@Override
public OrderDTO getOrderDetails(String orderNo) {
log.info("收到 RPC 请求,订单号: {}", orderNo);
return new OrderDTO(orderNo, new BigDecimal("199.00"), "Owlias教程");
}
}


dubbo 服务端 Filter

给服务提供端定义一个过滤器,用于链路追踪(可选)。

首先在服务提供端定义一个过滤器 ProviderDubboFilter:

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
package zdemo.scloud.order.filter;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.MDC;
import java.util.concurrent.ThreadLocalRandom;

/**
* Dubbo 提供端过滤器
* 作用:接收 RPC 请求后,将 traceId 注入本地 MDC 供日志打印,结束后清除防止线程池污染
*/
@Activate(group = CommonConstants.PROVIDER) // 只在提供者端激活
public class ProviderDubboFilter implements Filter {
private static final String TRACE_ID_KEY = "traceId";

@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 1. 从 RPC 远程传过来的附件中抠出 traceId
String traceId = (String) RpcContext.getServiceContext().getObjectAttachment(TRACE_ID_KEY);

// 2. 兜底策略:如果是定时任务或直接内部触发无 traceId,则本地生成一个 16 位小写十六进制串
if (traceId == null || traceId.isEmpty()) {
traceId = Long.toHexString(ThreadLocalRandom.current().nextLong());
if (traceId.length() < 16) {
traceId = String.format("%16s", traceId).replace(' ', '0');
}
}

// 3. 注入日志框架 MDC
MDC.put(TRACE_ID_KEY, traceId);

try {
// 4. 真正进入你的具体的 Service 业务实现方法
return invoker.invoke(invocation);
} finally {
// 5. 在远程调用结束离开前,必须彻底清空 MDC!
MDC.remove(TRACE_ID_KEY);
}
}
}

其次,在服务端的 src/main/resources/META-INF/dubbo 目录中定义一个名为 “org.apache.dubbo.rpc.Filter” 的文件,文件的内容是拦截器列表(dubbo的拦截器是通过SPI进行发现和注册的):

1
providerDubboFilter=zdemo.scloud.order.filter.ProviderDubboFilter


服务调用者 user


依赖配置

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
<?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.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>zdemo-scloud-user</artifactId>

<dependencies>
<!--公共模块-->
<dependency>
<groupId>com.zdemo.scloud</groupId>
<artifactId>zdemo-scloud-api</artifactId>
<version>${project.version}</version>
</dependency>

<!--springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

<!--dubbo-->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>

<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

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

日志配置与 order 模块基本一致(略 )。


核心配置

src/main/resources/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
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
server:
port: 8180

spring:
application:
name: zdemo-scloud-user
cloud:
nacos:
discovery:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
namespace: prod-zdemo # Spring Cloud 服务实例隔离
cluster-name: DEFAULT
config:
server-addr: 192.168.1.149:8848
username: nacos
password: nacos
file-extension: yml
namespace: prod-zdemo # Spring Cloud 业务配置隔离
cluster-name: DEFAULT
loadbalancer:
# 标记当前服务部署在 “北京机房A区”,配合负载均衡使用
# zone: beijing-zone-a
cache:
# 显式声明开启客户端负载均衡缓存(提高轮询效率)
enabled: false
nacos:
# 全局开启 Nacos 智能接管负载均衡,自动将挑机器的权力无缝移交给 Nacos 客户端。开发人员只需要在 Nacos 控制台界面上动动鼠标即可。
# 想让某台机器流量大一点,就把它的权重从1改成2;想做同机房就近路由,就在实例上配置集群属性Cluster。该属性默认为 false。
## 需要注意的 502 问题:
## 当 nacos.enabled=true 时,阿里官方的 NacosLoadBalancer 做了一个极其智能的现代化改动——它天生完美支持 IPv6 环境。
## 当它在实例元数据里敏锐地抓到 IPv6=[2409...] 字段时,它会自作聪明地优先把这个 IPv6 地址组合成目标 URL 抛给 OpenFeign。
## 当 OpenFeign 试图去访问 http://{ipv6}:8081/order/details 时,Tomcat 容器默认只监听了ipv4地址,压根没有监听ipv6,
## 这就会导致调用端抛出 502 Bad Gateway。
## 解决方案一:在调用端启动时加入 JVM 参数 -Djava.net.preferIPv4Stack=true -Djava.net.preferIPv6Addresses=false
## 解决方案二:使用 spring.cloud.loadbalancer.nacos.ip 强行指定应用的 ipv4 地址
## 解决方案三:使用 spring.cloud.loadbalancer.nacos.preferred-networks 指定例如 “192.168.1.” 强制只匹配 IPv4 的局域网网段
enabled: true
openfeign:
client:
config:
# 全局默认配置(对所有微服务生效)
default:
logger-level: none
connect-timeout: 5000
read-timeout: 5000
# 特定微服务生效
zdemo-scloud-order:
logger-level: full
connect-timeout: 5000 # 连接超时时间(默认2s)
read-timeout: 11000 # 请求处理超时时间(默认5s)
request-interceptors:
- zdemo.scloud.user.filter.CustomOpenFeignInterceptor
# Spring Boot 3.x 统一配置导入,这个语法的确是 Spring Boot 3.x(以及 2.4+) 引入的全新配置加载机制
config:
import:
# optional:表示可选的,告诉 Spring Boot,如果在项目启动时,Nacos 配置中心挂了,或者 Nacos 里根本没有建这个配置文件,请不要报错崩溃,直接跳过并用本地配置启动。
## nacos:表示指明驱动,告诉 Spring Boot 的 ConfigDataLocationResolver 机制:我接下来要导入一个外部配置,请调用 Nacos 的客户端插件去帮我下载,而不是去本地磁盘找。
### ${x}.${x}:动态变量占位符,最终这一串占位符在运行时会被翻译成字符串:zdemo-scloud-user.yml
- optional:nacos:${spring.application.name}.${spring.cloud.nacos.config.file-extension}

# Dubbo 独立注册中心配置(与 Order 端无缝通信的关键)
dubbo:
application:
# 注意和 spring cloud application 暴露的应用名明确区分划清界限,否则 LoadBalancer 根本无法分辨谁是走 HTTP 的、谁是走 Dubbo 的!
name: ${spring.application.name}-rpc
qos-enable: true
qos-port: 22233
registry:
# 彻底告别 spring-cloud://,直接走原生的 nacos:// 协议
# 也可以将 namespace 参数拼在连接地址的后面,8848?namespace=prod-zdemo
# 这样config-center.namespace 和 consumer.provider-namespace 甚至都可以省略不写,更不容易出错
address: nacos://192.168.1.149:8848?namespace=prod-zdemo
username: nacos
password: nacos

logging:
level:
org.apache.dubbo: INFO
org.apache.dubbo.rpc.filter: ERROR
org.apache.dubbo.config.deploy.DefaultMetricsServiceExporter: ERROR
# OpenFeign 日志是 DEBUG 级别的,需配合 spring.cloud.openfeign.client.config.{service-name}.logger-level 开启
zdemo.scloud.api.service.feign: DEBUG


启动类

UserApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package zdemo.scloud.user;

import org.apache.dubbo.config.spring.context.annotation.EnableDubbo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableDubbo // 激活 Dubbo 扫描
@EnableFeignClients(basePackages = {"zdemo.scloud.api.service.feign"}) // 开启 Feign 客户端扫描器并指定接口扫描路径
public class UserApplication {
public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}


应用 controller 层

UserController

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
package zdemo.scloud.user.controller;

import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import zdemo.scloud.api.dto.OrderDTO;
import zdemo.scloud.api.service.feign.OrderFeignService;
import zdemo.scloud.api.service.dubbo.OrderService;

@Slf4j
@RestController
public class UserController {

/**
* RPC 调用方式一:高效的 dubbo
* dubbo 支持的负载均衡策略有:
* random(随机):机器配置相同或有权重差异的常规集群。调用量越大,流量分布越均衡。
* roundrobin(轮询加权):希望请求绝对均匀地落在每台机器上。缺点是如果某台机器卡死,依然会有等量的请求送过去排队。
* leastactive(最小活跃调用数):极度推荐。谁手里的活儿没干完(当前并发处理的请求最少),新请求就发给谁。能自动绕开慢机器。
* shortestresponse(最短响应时间,推荐):3.x 强推策略。结合了最小活跃数和成功响应时间,优先挑内网RT最快、最闲的机器轰炸。
* consistenthash(一致性hash):相同的请求参数(如相同的 orderNo)永远打到同一台 Provider 机器上,非常适合 Provider 本地有二级缓存的场景。
*/
@DubboReference(loadbalance = "roundrobin")
private OrderService orderService;

/**
* RPC 调用方式二:原始的 RestTemplate,RestTemplate 被 @LoadBalanced 修饰
* 负载均衡可以被 Nacos 接管,配置 spring.cloud.loadbalancer.nacos.enabled=true 即可
*/
@Resource
private RestTemplate restTemplate;

/**
* RPC 调用方式三:比 RestTemplate 更加丝滑的 OpenFeign
* 负载均衡可以被 Nacos 接管,配置 spring.cloud.loadbalancer.nacos.enabled=true 即可
*/
@Resource
private OrderFeignService orderFeignService;


/**
* 访问 /dubbo/user/order (Dubbo 路线):
* [UserController] ──(RPC 代理)──> [Dubbo Cluster] ──(Triple协议/HTTP2)──> [Order 20881/20882 端口]
*/
@GetMapping("/dubbo/user/order")
public OrderDTO getUserOrderDetailsUsingDubbo(@RequestParam String orderNo) {
log.info("使用 dubbo 发起 RPC 请求,订单号: {}", orderNo);
return orderService.getOrderDetails(orderNo);
}

/**
* 访问 /http/user/order/http (Spring Cloud 路线):
* [UserController] ──(RestTemplate)──> [LoadBalancer拦截器] ──(标准HTTP/1.1)──> [Order 8081/8082 端口]
*/
@GetMapping("/http/user/order")
public OrderDTO getUserOrderDetailsUsingRestTemplate(@RequestParam String orderNo) {
log.info("使用 http 发起 RPC 请求,订单号: {}", orderNo);
// Nacos 中目标服务的 application.name
String serviceName = "zdemo-scloud-order";

// 拼接标准 HTTP 请求。注意:这里不写 IP:Port,写的是服务名!
// 目标 order 服务必须提供对应的 Web 映射接口(例如端口 8091 对应的 /order/details)
String url = "http://" + serviceName + "/order/details?orderNo=" + orderNo;
System.out.println("正在通过 LoadBalancer 准备投递请求至: " + url);

// 发起标准的 Rest 远程网络请求
// 这里的 RestTemplate 会被 LoadBalancer 自动拦截,将 zdemo-scloud-order 轮询替换为真实的机器 IP
return restTemplate.getForObject(url, OrderDTO.class);
}

/**
* 访问 /feign/user/order/http (Spring Cloud 路线):
* 与 RestTemplate 的方式是相同的。
*/
@GetMapping("/feign/user/order")
public OrderDTO getUserOrderDetailsUsingOpenFeign(@RequestParam String orderNo) {
log.info("使用 feign 发起 RPC 请求,订单号: {}", orderNo);
return orderFeignService.getUserOrderDetails(orderNo);
}
}


普通应用的过滤器

TraceIdMdcFilter

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
package zdemo.scloud.user.filter;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.concurrent.ThreadLocalRandom;

/**
* WEB应用拦截器
* 拦截和塞入 TraceId
*/
@Component
public class TraceIdMdcFilter implements Filter {

private static final String TRACE_ID_KEY = "traceId";

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
// 1. 从 Header 中获取网关传过来的 traceId
String traceId = request.getHeader(TRACE_ID_KEY);

// 2. 兜底处理:如果是开发环境绕过网关直接单体调 Feign,则本地自动生成一个
if (traceId == null || traceId.isEmpty()) {
// traceId = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 16);
traceId = Long.toHexString(ThreadLocalRandom.current().nextLong()); // 性能比 UUID 提升数倍,直接生成标准的 16 位小写十六进制 TraceId
if (traceId.length() < 16) {
traceId = String.format("%16s", traceId).replace(' ', '0'); // 不满16位前面补0
}
}

// 3. 塞入本地日志框架的 MDC
MDC.put(TRACE_ID_KEY, traceId);
try {
// 继续执行后续的 Controller 业务
filterChain.doFilter(servletRequest, servletResponse);
} finally {
// 4. 在请求结束离开前,必须清空 MDC!
// 原因:Tomcat/Netty 底层是线程池复用的。如果不清空,当这个线程处理下一个倒霉用户的请求时,
// 就会打印上一个用户的 traceId,导致链路日志彻底错乱!
MDC.remove(TRACE_ID_KEY);
}
}
}


dubbo 调用的支持

通过以下方式使用 dubbo 接口:

1
2
3
4
5
6
7
8
@DubboReference(loadbalance = "roundrobin") // 在此配置负载均衡策略
private OrderService orderService;

@GetMapping("/dubbo/user/order")
public OrderDTO getUserOrderDetailsUsingDubbo(@RequestParam String orderNo) {
log.info("使用 dubbo 发起 RPC 请求,订单号: {}", orderNo);
return orderService.getOrderDetails(orderNo);
}


dubbo 调用端 Filter

给调用端增加一个过滤器,用于在服务发起时透传 traceId(可选)。

ConsumerDubboFilter:

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
package zdemo.scloud.user.filter;

import org.apache.dubbo.common.constants.CommonConstants;
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.*;
import org.slf4j.MDC;

/**
* Dubbo 消费端过滤器
* 作用:在发起 RPC 调用前,将 MDC 中的 16位 traceId 偷渡到 RPC 上下文中
*/
@Activate(group = CommonConstants.CONSUMER) // 只在消费者端激活
public class ConsumerDubboFilter implements Filter {
private static final String TRACE_ID_KEY = "traceId";

@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
// 1. 从当前线程的 MDC 中获取 16 位的 traceId
String traceId = MDC.get(TRACE_ID_KEY);

if (traceId != null && !traceId.isEmpty()) {
// 2. 通过 RpcContext 的 ObjectAttachment 将其隐式传给下游
RpcContext.getServiceContext().setObjectAttachment(TRACE_ID_KEY, traceId);
}

// 3. 让 RPC 调用继续往下走
return invoker.invoke(invocation);
}
}

将 ConsumerDubboFilter 通过 SPI 机制注册进来。在调用端 src/main/resources/META-INF/dubbo 中写入文件:org.apache.dubbo.rpc.Filter

1
consumerDubboFilter=zdemo.scloud.user.filter.ConsumerDubboFilter


RestTemplate 支持

配置和注入 RestTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package zdemo.scloud.user.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced // 激活 S Cloud LoadBalancer 拦截器机制,默认的负载策略是轮询(可被nacos接管)
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

使用 RestTemplate 发起远程调用:

1
2
3
4
5
6
7
8
9
10
11
@Resource
private RestTemplate restTemplate;

@GetMapping("/http/user/order")
public OrderDTO getUserOrderDetailsUsingRestTemplate(@RequestParam String orderNo) {
log.info("使用 http 发起 RPC 请求,订单号: {}", orderNo);
String serviceName = "zdemo-scloud-order";
String url = "http://" + serviceName + "/order/details?orderNo=" + orderNo;
System.out.println("正在通过 LoadBalancer 准备投递请求至: " + url);
return restTemplate.getForObject(url, OrderDTO.class);
}


feign 调用支持

1
2
3
4
5
6
7
8
@Resource
private OrderFeignService orderFeignService;

@GetMapping("/feign/user/order")
public OrderDTO getUserOrderDetailsUsingOpenFeign(@RequestParam String orderNo) {
log.info("使用 feign 发起 RPC 请求,订单号: {}", orderNo);
return orderFeignService.getUserOrderDetails(orderNo);
}

对于 feign 的超时、日志级别、拦截器都可以在配置文件中设置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
spring:
cloud:
openfeign:
client:
config:
# 全局默认配置(对所有微服务生效)
default:
logger-level: none
connect-timeout: 5000
read-timeout: 5000
# 特定微服务生效
zdemo-scloud-order:
logger-level: full
connect-timeout: 5000 # 连接超时时间(默认2s)
read-timeout: 11000 # 请求处理超时时间(默认5s)
request-interceptors:
- zdemo.scloud.user.filter.CustomOpenFeignInterceptor


feign 拦截器

CustomOpenFeignInterceptor

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
package zdemo.scloud.user.filter;

import feign.RequestInterceptor;
import feign.RequestTemplate;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.MDC;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.util.Enumeration;

/**
* 自定义 OpenFeign 前置拦截器
* 注意不要写 @Configuration,否则会变成全局配置
*
* 典型的应用场景:
* 鉴权、日志记录、全链路TraceId追踪、灰度发布与流量隔离等
*/
public class CustomOpenFeignInterceptor implements RequestInterceptor {

@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
// 第一:全量透传除了 content-length 的 Header 给下游
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
// 排除 content-length,防止有些老版本网关计算请求体长度错乱导致请求卡死
if (!"content-length".equalsIgnoreCase(name)) {
String value = request.getHeader(name);
requestTemplate.header(name, value);
}
}
}

// 第二:精准投递,放入安全令牌
String token = request.getHeader("Authorization");
if (token != null) {
requestTemplate.header("Authorization", token);
} else {
requestTemplate.header("Authorization", "DEFAULT");
}
}

// 第三:从日志 MDC 中捞取分布式 traceId 强行塞给下游微服务
// 如果在 UserController 里,你为了提高效率,开了个 CompletableFuture.runAsync(() -> { orderFeign.create(); }) 异步去调 Feign,上面的代码会瞬间翻车。
// 因为 RequestContextHolder 底层是基于 ThreadLocal 实现的。你开了新线程,新线程里根本没有请求上下文,attributes 拿出来就是 null,导致 Token 无法透传。
// 解法是在 RequestContextHolder.getRequestAttributes() 拿到老线程的上下文,然后强行 setRequestAttributes 塞给新线程。
String traceId = MDC.get("traceId"); // 或者是你自定义的链路追踪 Key
if (traceId != null) {
requestTemplate.header("X-B3-TraceId", traceId);
}
}
}

将其注入spring容器,使其生效:

1
2
3
4
5
6
7
8
spring:
cloud:
openfeign:
client:
config:
zdemo-scloud-order:
request-interceptors:
- zdemo.scloud.user.filter.CustomOpenFeignInterceptor