微服务架构与基础设施
1. 模块概述
本章解析 RuoYi-Cloud 的 微服务整体架构,包括项目模块划分、服务注册与发现、配置中心、服务调用、熔断降级和监控体系。
模块全景
1 | ruoyi-cloud (父工程) |
模块依赖关系
graph LR
subgraph 公共层
CORE[ruoyi-common-core]
REDIS[ruoyi-common-redis]
SEC[ruoyi-common-security]
DS[ruoyi-common-datascope]
LOG[ruoyi-common-log]
end
subgraph API层
API[ruoyi-api-system]
end
subgraph 服务层
AUTH[ruoyi-auth]
SYS[ruoyi-system]
GEN[ruoyi-gen]
JOB[ruoyi-job]
FILE[ruoyi-file]
end
subgraph 网关层
GW[ruoyi-gateway]
end
CORE --> REDIS
CORE --> SEC
SEC --> REDIS
SEC --> API
DS --> SEC
LOG --> SEC
API --> CORE
AUTH --> API
AUTH --> REDIS
AUTH --> SEC
SYS --> API
SYS --> SEC
SYS --> DS
SYS --> REDIS
GEN --> CORE
JOB --> CORE
FILE --> CORE
GW --> REDIS
GW --> CORE
2. 命名体系与易混淆概念对比
2.1 命名规律拆解
RuoYi-Cloud 的类命名遵循 Spring 分层惯例:
| 前缀 | 层级 | 后缀 | 含义 | 示例 |
|---|---|---|---|---|
Sys |
实体 | — | 系统表对应的实体类 | SysUser, SysRole, SysMenu |
ISys |
服务接口 | Service |
业务逻辑接口 | ISysUserService |
Sys |
服务实现 | ServiceImpl |
业务逻辑实现 | SysUserServiceImpl |
Sys |
Mapper | Mapper |
MyBatis 数据访问 | SysUserMapper |
Sys |
Controller | Controller |
REST 控制器 | SysUserController |
| — | 工具类 | Utils |
静态工具方法 | JwtUtils, SecurityUtils |
| — | 切面 | Aspect |
AOP 切面 | DataScopeAspect, LogAspect |
| — | 过滤器 | Filter |
Gateway 过滤器 | AuthFilter, XssFilter |
命名规律总结:实体/接口/实现/Mapper/Controller 五层均以 Sys + 业务对象名作为前缀,通过后缀区分层级角色。公共模块的工具类和切面则不加 Sys 前缀,以功能描述命名。
2.2 易混淆概念对比表
ruoyi-api-system vs ruoyi-modules/ruoyi-system
| 对比维度 | ruoyi-api-system | ruoyi-modules/ruoyi-system |
|---|---|---|
| 角色 | Feign 客户端契约 | 服务提供者 |
| 包含内容 | 远程接口定义 + DTO | Controller + Service + Mapper |
| 被谁依赖 | auth、gateway 等服务 | 无外部依赖 |
| 目的 | 让其他服务通过 Feign 调用 system | 实际执行业务逻辑 |
| 一句话区分 | api 模块是”合同”,modules 模块是”履约方” |
SecurityUtils vs AuthUtil vs AuthLogic
| 对比维度 | SecurityUtils | AuthUtil | AuthLogic |
|---|---|---|---|
| 职责 | 从请求上下文获取用户信息 | 权限校验的门面(静态方法) | 权限校验的核心实现 |
| 调用关系 | 被所有层调用 | Controller 层调用 | 被 AuthUtil 委托 |
| 是否抛异常 | 返回 null | 抛 NotPermissionException 等 | 抛 NotPermissionException 等 |
| 一句话区分 | SecurityUtils 是”信息获取器”,AuthUtil 是”权限守门人”,AuthLogic 是”守门逻辑” |
3. API Signatures
📍 源码:pom.xml:1-38
本节梳理项目基础设施层的关键配置与启动类。
服务注册与发现(Nacos)
1 | // 各服务通过 spring.cloud.nacos.discovery.server-addr 配置 Nacos 地址 |
远程调用(OpenFeign)
1 | // ruoyi-api-system 中定义 Feign 接口 |
调用关系图
graph TD
AUTH[TokenController.login] --> SLS[SysLoginService.login]
SLS --> RUS[RemoteUserService.getUserInfo
Feign远程调用]
RUS --> SYS[SysUserController.getInfo
System服务]
SYS --> SYSI[SysUserServiceImpl.selectUserByUserName]
SYSI --> MAPPER[SysUserMapper.selectUserByUserName]
AUTH2[TokenController.login] --> TS[TokenService.createToken]
TS --> JWT[JwtUtils.createToken]
TS --> REDIS[RedisService.setCacheObject]
4. 数据结构深度解析
4.a 服务发现模型 — 为什么需要 Nacos
在微服务架构中,服务实例的 IP 和端口是动态变化的。如果硬编码服务地址,每次扩缩容或故障转移都需要修改配置并重启所有依赖方。Nacos 作为注册中心,让服务消费者通过 服务名 而非 IP:Port 来调用,实现了调用方与被调用方的解耦。
4.b 配置结构
Nacos 配置模型(逻辑结构):
1 | spring: |
4.c 字段三层分析表
| 字段 | 设计动机(为什么需要) | 反事实(如果去掉会怎样) | 替代方案(还能怎么做) |
|---|---|---|---|
discovery.server-addr |
告诉服务去哪里注册自己 | 服务无法注册,消费者无法发现 | 也可以用 Eureka/Consul,但 Nacos 同时支持配置中心 |
discovery.namespace |
隔离不同环境(dev/test/prod) | 所有环境服务混在一起,可能路由到错误实例 | 也可以用 group 区分,但粒度更粗 |
config.shared-configs |
多个服务共享同一份配置 | 每个服务重复配置,修改时容易遗漏 | 也可以用 ConfigMap(K8s 环境),但失去了动态刷新能力 |
4.d 服务生命周期状态图
stateDiagram-v2
[*] --> 启动中: 应用启动
启动中 --> 注册: 连接Nacos成功
启动中 --> 启动失败: 连接Nacos失败
注册 --> 健康: 注册成功
健康 --> 不健康: 健康检查失败
不健康 --> 健康: 健康检查恢复
健康 --> 下线: 服务主动下线
健康 --> 注销: 应用关闭
下线 --> 注销: 应用关闭
注销 --> [*]
启动失败 --> [*]
5. 函数逐行精讲
5.a 服务调用请求头传递
函数:FeignRequestInterceptor.apply()
- 调用时机:每次 Feign 远程调用发起前
- 典型调用者:Feign 框架自动调用(RequestInterceptor 接口)
- 前置条件:当前线程绑定了一个 HttpServletRequest(即当前请求来自客户端 HTTP 调用)
- 目的:将当前请求中的用户身份信息自动传递到下游微服务,避免身份信息丢失
1 |
|
5.b 下游服务身份恢复
函数:HeaderInterceptor.preHandle()
- 调用时机:下游服务收到 HTTP 请求后,Controller 方法执行前
- 典型调用者:Spring MVC 拦截器链
- 前置条件:请求 Header 中可能包含 user_id、username、user_key
- 目的:从请求 Header 恢复用户上下文到 ThreadLocal,并自动刷新 Token 有效期
1 |
|
6. 关键架构决策剖析
6.1 服务隔离策略
RuoYi-Cloud 采用 物理隔离 + 逻辑隔离 双层策略:
- 物理隔离:
ruoyi-api-system独立模块,只暴露 Feign 接口定义和 DTO,不暴露 Service 实现 - 逻辑隔离:通过 @InnerAuth 注解 + InnerAuthAspect 切面,要求内部调用必须携带
from-source: inner请求头
为什么不直接用 Spring Security? RuoYi 选择了更轻量的自研方案:基于 AOP 注解 + 拦截器的权限体系,配置更简单,代码更可控。
6.2 Feign 请求头自动传递
微服务调用链中最大的痛点是 身份信息的传递和恢复。RuoYi 通过以下机制解决:
- FeignRequestInterceptor 在 Feign 调用前从当前请求提取 Header 并注入到 Feign 请求
- HeaderInterceptor 在下游服务收到请求后从 Header 恢复到 ThreadLocal
- 网关 AuthFilter 在入口处完成 JWT 验证并将用户信息注入 Header
6.3 Nacos 配置共享
RuoYi 利用 Nacos 的 shared-configs 机制,将 application-dev.yml 作为共享配置,所有服务自动继承数据库连接、Redis 连接等公共配置,避免了在每个服务的 application.yml 中重复定义。
7. 设计决策分析
为什么选择 Nacos 而非 Eureka?
| 维度 | Nacos | Eureka |
|---|---|---|
| 配置中心 | 内置支持 | 需额外集成 Spring Cloud Config |
| CAP 模型 | CP + AP 可切换 | AP |
| 健康检查 | TCP/HTTP/MySQL 多种方式 | 仅心跳 |
| 控制台 | 功能完善的 Web UI | 简单的 Dashboard |
| 阿里云集成 | 原生支持 | 需额外适配 |
RuoYi 选择 Nacos 的核心原因是 注册中心 + 配置中心二合一,减少了运维组件数量。
为什么 api 模块独立?
将 Feign 接口单独放在 ruoyi-api-system 模块而非直接放在 ruoyi-system 中:
- 依赖方向:调用方(如 auth)只需要依赖轻量的 api 模块,不需要引入 system 的全部依赖(MyBatis、Druid 等)
- 版本管理:api 模块作为”接口契约”独立发布,服务提供方和消费方通过同一份契约对齐
- 编译速度:修改 system 内部实现不会导致 auth 重新编译
8. 学习检查点
📝 本章小结
- RuoYi-Cloud 采用标准的 Spring Cloud 微服务架构,通过 Nacos 实现服务发现与配置管理
- 模块划分遵循”api 契约 + modules 实现 + common 基础设施”三层结构
- 服务间通过 Feign + FeignRequestInterceptor 实现用户身份的无感传递
- 内部服务调用通过 @InnerAuth 注解保护,要求携带
from-source: inner请求头 - Nacos 的 shared-configs 机制让所有服务共享公共配置,减少重复
🤔 思考题
为什么
ruoyi-api-system需要独立为一个模块,而不是直接放在ruoyi-system中?如果合并会有什么问题?参考答案
如果合并,调用方(如 ruoyi-auth)依赖
ruoyi-system时会引入其全部传递依赖(MyBatis、Druid、业务 Service 等),导致:1) 编译和打包变慢;2) 可能引入不需要的 Bean 导致自动配置冲突;3) 破坏了微服务的隔离性——调用方不应该接触到提供方的内部实现。独立 api 模块只包含 Feign 接口和 DTO,是轻量级的”契约包”。参见 FeignRequestInterceptor.java:19-53 了解 Feign 调用时如何传递上下文。FeignRequestInterceptor 中对每个 Header 都做了
isNotEmpty判断后才传递,如果不做这个判断直接putAll(headers)会有什么风险?参考答案
直接
putAll的风险:1) 可能将值为 null 的 Header 传递到下游,导致下游ServletUtils.getHeader()返回 null 后触发 NPE;2) 会传递大量无关的 HTTP Header(如Host、Connection、Accept-Encoding等),这些 Header 在服务间调用中不仅无用,还可能干扰下游的 HTTP 处理逻辑(如Content-Length与实际 Body 不匹配)。逐字段白名单式传递是一种防御性设计。HeaderInterceptor.preHandle() 中调用了
verifyLoginUserExpire()自动刷新 Token。为什么把这个逻辑放在拦截器而不是 AOP 切面中?参考答案
拦截器在请求处理的最前端执行,比 AOP 切面更早。如果放在 AOP 切面(如 PreAuthorizeAspect)中,只有被注解标记的方法才会触发刷新;未加注解的方法(如公开接口、静态资源)不会刷新 Token。放在拦截器中可以保证 所有需要用户上下文的请求 都能自动续期,覆盖面更广。此外,拦截器的执行顺序可通过
order精确控制,比 AOP 更可预测。