1. 模块概述
ruoyi-common 是若依微服务平台的 基础设施层,包含 9 个子模块,为上层的网关、认证和业务服务提供统一的基础能力。
子模块全景
| 子模块 | 职责 | 核心组件 |
|---|---|---|
| ruoyi-common-core | 核心工具与基础类 | JwtUtils, StringUtils, R(统一响应), BaseEntity |
| ruoyi-common-redis | Redis 缓存服务 | RedisService, RedisConfig |
| ruoyi-common-security | 认证鉴权框架 | AuthLogic, TokenService, PreAuthorizeAspect, HeaderInterceptor, FeignRequestInterceptor |
| ruoyi-common-datascope | 数据权限 | DataScopeAspect, @DataScope |
| ruoyi-common-log | 操作日志 | LogAspect, @Log, AsyncLogService |
| ruoyi-common-swagger | API 文档 | SpringDoc 自动配置 |
| ruoyi-common-sensitive | 数据脱敏 | @Sensitive, SensitiveJsonSerializer |
| ruoyi-common-datasource | 多数据源 | @Master, @Slave |
| ruoyi-common-seata | 分布式事务 | Seata 自动配置 |
依赖关系
graph TB
CORE[ruoyi-common-core
基础工具]
REDIS[ruoyi-common-redis
缓存服务]
SEC[ruoyi-common-security
认证鉴权]
DS[ruoyi-common-datascope
数据权限]
LOG[ruoyi-common-log
操作日志]
SWAGGER[ruoyi-common-swagger
API文档]
SENSITIVE[ruoyi-common-sensitive
数据脱敏]
DATASOURCE[ruoyi-common-datasource
多数据源]
SEATA[ruoyi-common-seata
分布式事务]
CORE --> REDIS
CORE --> SEC
CORE --> LOG
CORE --> SWAGGER
CORE --> SENSITIVE
CORE --> DATASOURCE
REDIS --> SEC
SEC --> DS
SEC --> LOG
本章重点分析 core、redis、log 三个模块(security 和 datascope 已在 认证授权体系 和 系统管理模块 中详细剖析)。
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 模块 | 操作 | 含义 | 所属 |
|---|---|---|---|---|
setCache |
Object |
— | 缓存任意类型对象 | RedisService |
setCache |
List |
— | 缓存 List 集合 | RedisService |
setCache |
Set |
— | 缓存 Set 集合 | RedisService |
setCache |
Map |
— | 缓存 Map 集合 | RedisService |
setCache |
MapValue |
— | 缓存 Hash 的单个字段 | RedisService |
getCache |
Object |
— | 获取缓存对象 | RedisService |
getCache |
List |
— | 获取缓存 List | RedisService |
getCache |
Set |
— | 获取缓存 Set | RedisService |
getCache |
Map |
— | 获取缓存 Map | RedisService |
getCache |
MapValue |
— | 获取 Hash 的单个字段 | RedisService |
delete |
Object |
— | 删除单个 Key | RedisService |
delete |
CacheMapValue |
— | 删除 Hash 的单个字段 | RedisService |
命名规律总结:RedisService 采用 操作 + 缓存 + 数据类型 的命名模式,set/get/delete + Cache + Object/List/Set/Map/MapValue。操作粒度从粗(Object)到细(MapValue),覆盖了 Redis 的所有常用数据结构。
2.2 易混淆函数对比表
setCacheObject(key, value) vs setCacheObject(key, value, timeout, timeUnit)
| 对比维度 | setCacheObject(key, value) | setCacheObject(key, value, timeout, timeUnit) |
|---|---|---|
| 过期时间 | 无(永久有效) | 指定时间和单位 |
| 使用场景 | 永久缓存(如系统配置) | 临时缓存(如 Token、验证码) |
| 一句话区分 | 永久缓存 vs 带 TTL 的临时缓存 |
setCacheMap vs setCacheMapValue
| 对比维度 | setCacheMap(key, dataMap) | setCacheMapValue(key, hKey, value) |
|---|---|---|
| 操作粒度 | 整个 Hash(批量 putAll) | 单个字段(put) |
| 使用场景 | 初始化缓存 | 增量更新 |
| 一句话区分 | 整存整取 vs 字段级操作 |
3. API Signatures
调用关系图
graph TD
subgraph "Redis缓存层"
RS1["setCacheObject"] --> RT1["redisTemplate.opsForValue.set"]
RS2["getCacheObject"] --> RT2["redisTemplate.opsForValue.get"]
RS3["setCacheList"] --> RT3["redisTemplate.opsForList.rightPushAll"]
RS4["getCacheList"] --> RT4["redisTemplate.opsForList.range"]
RS5["setCacheMap"] --> RT5["redisTemplate.opsForHash.putAll"]
RS6["getCacheMap"] --> RT6["redisTemplate.opsForHash.entries"]
RS7["deleteObject"] --> RT7["redisTemplate.delete"]
RS8["hasKey"] --> RT8["redisTemplate.hasKey"]
RS9["expire"] --> RT9["redisTemplate.expire"]
end
subgraph "日志记录层"
LA1["LogAspect.doBefore"] --> LA2["TIME_THREADLOCAL.set
记录开始时间"]
LA3["LogAspect.doAfterReturning"] --> LA4["handleLog"]
LA5["LogAspect.doAfterThrowing"] --> LA4
LA4 --> LA6["构建SysOperLog"]
LA4 --> LA7["getControllerMethodDescription
提取注解信息"]
LA4 --> LA8["AsyncLogService.saveSysLog
异步入库"]
end
subgraph "调用方"
AUTH["TokenService"] --> RS1
AUTH --> RS2
AUTH --> RS8
GATEWAY["AuthFilter"] --> RS8
CTRL["业务Controller"] -.->|Log| LA1
end
完整签名(核心函数)
1 | // === RedisService === |
4. 数据结构深度解析
4.a R — 统一响应体
在微服务架构中,服务间的调用结果需要一个统一的格式。R 类是若依的 Feign 远程调用响应体,定义了
code(状态码)、msg(消息)、data(数据)三个字段。所有 Feign 接口都返回R<T>,调用方通过R.FAIL == result.getCode()判断调用是否成功。
4.b SysOperLog — 操作日志实体
1 | public class SysOperLog extends BaseEntity { |
4.c SysOperLog 字段三层分析表
| 字段 | 设计动机(为什么需要) | 反事实(如果去掉会怎样) | 替代方案(还能怎么做) |
|---|---|---|---|
operParam |
记录操作时的请求参数,用于审计和问题排查 | 出问题时无法还原用户操作的具体内容 | 可以用链路追踪(如 SkyWalking),但增加运维复杂度 |
costTime |
记录方法执行耗时,用于性能监控 | 无法识别慢接口,性能优化无数据依据 | 可以用 APM 工具(如 Pinpoint),但侵入性更强 |
errorMsg |
记录异常时的错误信息,用于故障定位 | 只知道”操作失败”,不知道”为什么失败” | 可以查应用日志,但需要关联时间戳和用户,效率低 |
status |
区分成功和失败的操作,用于统计成功率 | 无法统计系统的操作成功率 | 可以在日志系统聚合,但增加分析成本 |
4.d 操作日志生命周期
stateDiagram-v2
[*] --> 方法调用: 用户请求到达Controller
方法调用 --> 记录开始时间: @Before触发
TIME_THREADLOCAL.set(now)
记录开始时间 --> 方法执行: Controller方法体执行
方法执行 --> 正常返回: 无异常
方法执行 --> 抛出异常: 发生异常
正常返回 --> 构建日志: @AfterReturning触发
status=SUCCESS
抛出异常 --> 构建日志: @AfterThrowing触发
status=FAIL
构建日志 --> 计算耗时: costTime = now - startTime
计算耗时 --> 异步入库: AsyncLogService.saveSysLog()
异步入库 --> 清理ThreadLocal: finally { TIME_THREADLOCAL.remove() }
清理ThreadLocal --> [*]
5. 函数逐行精讲
5.a 操作日志记录
- 调用时机:被 @Log 标记的方法正常返回或抛出异常后
- 典型调用者:LogAspect.doAfterReturning() / LogAspect.doAfterThrowing()
- 前置条件:方法已执行完毕(无论成功或失败)
- 目的:构建完整的 SysOperLog 对象并异步入库
1 | protected void handleLog(final JoinPoint joinPoint, Log controllerLog, final Exception e, Object jsonResult) |
5.b 请求参数提取
函数:LogAspect.setRequestValue()
- 调用时机:handleLog() → getControllerMethodDescription() 调用链中
- 典型调用者:LogAspect.getControllerMethodDescription()
- 前置条件:@Log 注解的 isSaveRequestData() 返回 true
- 目的:从请求中安全地提取参数并序列化为 JSON
1 | private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog, String[] excludeParamNames) throws Exception |
5.c 参数过滤
- 调用时机:参数序列化时判断是否需要跳过某个参数
- 典型调用者:LogAspect.argsArrayToString()
- 前置条件:正在遍历方法参数
- 目的:过滤掉无法序列化的对象(MultipartFile、HttpServletRequest、HttpServletResponse、BindingResult)
1 |
|
6. 关键算法剖析
6.1 操作日志的参数安全序列化
LogAspect 的参数提取有两层安全保护:
- 类型过滤:isFilterObject() 跳过 MultipartFile、HttpServletRequest 等不可序列化对象
- 敏感字段排除:PropertyPreExcludeFilter 过滤 password、oldPassword、newPassword、confirmPassword 字段
1 | 参数序列化流程: |
6.2 RedisService 的数据结构适配
RedisService 封装了 Spring Data Redis 的 5 种数据结构操作:
| Redis 数据结构 | Spring Data Redis API | RedisService 方法 | 使用场景 |
|---|---|---|---|
| String | opsForValue().set/get |
setCacheObject/getCacheObject |
Token、配置、验证码 |
| List | opsForList().rightPushAll/range |
setCacheList/getCacheList |
队列、时间线 |
| Set | opsForSet().members/boundSetOps |
setCacheSet/getCacheSet |
标签、去重集合 |
| Hash | opsForHash().putAll/entries |
setCacheMap/getCacheMap |
对象缓存、字段级更新 |
| Key | hasKey/delete/expire |
hasKey/deleteObject/expire |
通用 Key 操作 |
7. 设计决策分析
为什么操作日志使用异步写入?
AsyncLogService 通过 @Async 注解实现异步日志写入:
- 不阻塞业务响应:日志写入涉及数据库 I/O,如果同步写入会增加接口响应时间
- 故障隔离:即使数据库暂时不可用,日志记录失败也不会导致业务请求失败(LogAspect 中有 try-catch 保护)
- 批量优化空间:异步线程池可以后续优化为批量写入,减少数据库连接开销
为什么 LogAspect 使用 NamedThreadLocal?
1 | private static final ThreadLocal<Long> TIME_THREADLOCAL = new NamedThreadLocal<Long>("Cost Time"); |
NamedThreadLocal 是 Spring 提供的 ThreadLocal 子类,唯一的区别是支持给 ThreadLocal 命名。这个命名在调试时非常有用——当发生 ThreadLocal 内存泄漏时,可以通过名称快速定位到是哪个 ThreadLocal 未清理。
为什么参数序列化限制 2000 字符?
LogAspect 中 PARAM_MAX_LENGTH = 2000:
- 数据库字段限制:
sys_oper_log表的oper_param字段通常是 VARCHAR(2000) - 存储成本:操作日志量大(每次操作一条),控制字段长度可减少存储开销
- 可读性:2000 字符足以记录关键参数,超出部分通常是大文本或文件内容,无记录价值
8. 学习检查点
📝 本章小结
- ruoyi-common 包含 9 个子模块,core 是基础(被所有模块依赖),其他模块按需引入
- RedisService 封装了 String/List/Set/Hash 四种数据结构的缓存操作,统一了 Redis 的使用方式
- LogAspect 通过 @Before + @AfterReturning + @AfterThrowing 三个切面完整记录操作日志
- 日志参数序列化有两层安全保护:类型过滤(排除 MultipartFile 等)和敏感字段过滤(排除 password 等)
- 操作日志使用
@Async异步入库,确保日志记录不阻塞业务响应
🤔 思考题
LogAspect.isFilterObject() 对 Collection 和 Map 的遍历中使用了
return,这意味着只检查第一个元素。这是一个 bug 还是有意的设计?参考答案
这是一个潜在的 bug。如果 Collection 的第一个元素不是 MultipartFile 但第二个元素是,
isFilterObject()会返回 false,导致尝试序列化 MultipartFile。不过在实际业务中,Controller 方法的参数类型通常是确定的——一个参数要么是 MultipartFile 要么不是,极少出现混合类型的集合。所以这个 bug 在正常使用场景下不会触发。但作为防御性编程,应该使用循环检查所有元素而非只检查第一个,或者在循环中找到 MultipartFile 时返回 true。为什么 LogAspect.handleLog() 中先用
BusinessStatus.SUCCESS.ordinal()设置 status,然后再判断e != null改为 FAIL?为什么不直接根据 e 是否为 null 来设置?参考答案
这是一种”默认成功 + 异常覆盖”的编程模式。好处是:1) 减少了一个 else 分支,代码更简洁;2) 如果未来增加了更多的状态(如 PARTIAL_SUCCESS),只需要在一个地方修改默认值。
ordinal()方法返回枚举的序号(0-based),SUCCESS 的 ordinal 通常是 0,FAIL 是 1。这种写法假设了枚举的顺序,如果枚举定义发生变化(如在 SUCCESS 前插入新值),ordinal 值会改变导致数据错乱。更安全的写法是使用字符串或显式映射。RedisService.setCacheSet() 使用迭代器逐个 add,而 setCacheList 使用 rightPushAll 批量写入。为什么 Set 不用批量方法?
参考答案
Spring Data Redis 的
BoundSetOperations没有提供批量 add 方法(只有add(V... values)可变参数方法)。理论上可以用redisTemplate.opsForSet().add(key, dataSet.toArray()),但这样需要将 Set 转为数组,对于大数据量反而增加了内存开销。迭代器逐个 add 虽然每次都是网络调用,但在 Redis 中 SADD 命令本身支持多个 member,所以实际上可以通过opsForSet().add(key, dataSet.toArray(new Object[0]))一次完成。当前实现确实有优化空间。