模块深度解析 · 通用模块(ruoyi-common)
📍 核心源码在
ruoyi-common/src/main/java/com/ruoyi/common/。这是被framework、system、quartz、generator共同依赖的基础层,不含业务,只含通用能力。
1. 模块概述
ruoyi-common 是全项目的地基,提供:
| 分类 | 关键内容 |
|---|---|
| 响应体/域对象 | AjaxResult、R、BaseEntity、TreeEntity、LoginUser、各 SysXxx 实体 |
| 注解体系 | @Log、@DataScope、@DataSource、@Excel、@RateLimiter、@RepeatSubmit、@Sensitive、@Anonymous |
| 常量/枚举 | Constants、CacheConstants、HttpStatus、BusinessType、OperatorType、UserStatus、DataSourceType、LimitType |
| 异常体系 | BaseException、ServiceException、GlobalException 及各子包 |
| 分页模型 | PageDomain、TableDataInfo、TableSupport |
| 状态中心 | RedisCache、SpringUtils、SecurityUtils、DictUtils、StringUtils、Convert 等 |
| 能力工具 | ExcelUtil(POI)、FileUploadUtils、SqlUtil、IpUtils、XSS 过滤 等 |
graph TB
subgraph ruoyi-common 基础层
C1[常量/枚举]
C2[注解]
C3[响应体/实体]
C4[异常]
C5[分页模型]
C6[工具类]
C7[RedisCache]
end
FRAMEWORK[ruoyi-framework] --> C1 & C2 & C3 & C4 & C6 & C7
SYSTEM[ruoyi-system] --> C3 & C6 & C7
QUARTZ[ruoyi-quartz] --> C1 & C4 & C6
GENERATOR[ruoyi-generator] --> C6
ADMIN[ruoyi-admin] --> C3 & C6
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 对象 | 操作 | 含义 |
|---|---|---|---|
AjaxResult. |
success/error/warn |
重载 | 静态工厂造响应体 |
AjaxResult. |
put |
覆写 | 链式追加字段 |
BaseEntity. |
setCreateBy/getParams |
— | 审计/查询通用字段 |
SecurityUtils. |
getXxx |
— | 从安全上下文取当前用户 |
RedisCache. |
set/get/delete + CacheObject/List/Map/Set |
— | Redis 各数据结构 |
命名规律:工具类(XxxUtil/XxxUtils)以动词前缀 get/set/convert/parse 开头,语义直白;缓存方法统一 setCacheXxx/getCacheXxx/deleteCacheXxx,明确”对象/List/Map/Set”的数据结构。
2.2 易混淆函数对比
对比一:AjaxResult(HashMap 继承)vs R<T>(泛型强类型)
| 对比维度 | AjaxResult |
R<T> |
|---|---|---|
| 底层 | extends HashMap<String,Object> |
独立泛型类 |
| 序列化 | 扁平 {code,msg,data} Map |
强类型字段 |
| 用途 | 后端 → 前端统一响应 | RPC / 分页 TableDataInfo 可配合 |
| 一句话区分 | 灵活但弱类型,给页面用 | 强类型约束,给程序/RPC 用 |
对比二:SecurityUtils.hasPermi(工具)vs PermissionService.hasPermi(@ss 标签)
| 对比维度 | SecurityUtils.hasPermi |
PermissionService.hasPermi |
|---|---|---|
| 匹配实现 | PatternMatchUtils.simpleMatch 通配 + *:*:* |
Set.contains 精确/通配混合 |
| 是否设上下文 | 否 | 是(PermissionContextHolder.setContext) |
| 调用方 | Java 代码判权 | 模板/标签 ${@ss.hasPermi} |
| 一句话区分 | 纯逻辑判断工具 | 附带”记录当前权限”的标签服务 |
3. API Signatures(重点类)
1 | public class AjaxResult extends HashMap<String, Object> |
1 | public class BaseEntity implements Serializable |
1 | public class RedisCache |
⚠️ 本版本的
RedisCache没有setIfAbsent(分布式锁 CAS)方法,分布式加锁在切面/拦截器里直接用RedisTemplate(见 03 章限流)。
1 | public class DictUtils |
1 | public class StringUtils extends org.apache.commons.lang3.StringUtils |
4. 数据结构深度解析
AjaxResult
4.a 存在的理由
后端需要一种”自带语义”的 JSON 返回结构。继承
HashMap<String,Object>让它可以链式put追加任意字段(如total、rows),无需为每种响应定义类,且能被 Jackson 直接序列化为 JSON 对象。
4.c 字段三层分析表
| 字段/常量 | 设计动机 | 反事实(去掉) | 替代方案 |
|---|---|---|---|
CODE_TAG="code" |
统一状态码字段名 | 前端无法判成功失败 | 用 HTTP Status 但不够语义化 |
MSG_TAG="msg" |
提示文本 | 无法给用户反馈 | 无 |
DATA_TAG="data" |
数据载体;null 时不写 | 占位冗余 | 无 |
4.d 生命周期:由 success/error/warn 工厂创建 → 控制器 put 增强 → Jackson 序列化 → 前端消费。无复杂状态机。
BaseEntity
4.a 存在的理由
几乎每张业务表都含创建人、创建时间、更新人、更新时间、备注等审计列,且查询时需要携带”非实体字段的请求参数”(如区间
beginTime/endTime)。BaseEntity 统一承载,让实体免于重复声明。
params 字段尤为关键:因为前端参数是键值对、实体是强类型,用 Map 兜住”不在实体字段里的任意请求参数”(如数据权限注入的 dataScope、MyBatis 用于查询的 beginTime),避免为每个查询参数建字段。
4.c 字段三层分析表
| 字段 | 设计动机 | 反事实(去掉) | 替代方案 |
|---|---|---|---|
createBy/createTime/updateBy/updateTime |
审计追踪 | 无法追溯谁改了什么 | 数据库触发器,但不够灵活 |
remark |
通用备注 | 需每表重复加 | 无 |
params |
兜住非实体请求参数 | 查询区间/数据权限无法传递 | 为每种参数建字段(繁琐) |
searchValue(@JsonIgnore) |
供 MyBatis 模糊搜索 | 搜索需单独传递 | 无 |
其它实体
TreeEntity extends BaseEntity:parentName/parentId/orderNum/ancestors/children,通用树形专用字段。SysUser/SysRole/SysMenu/SysDept/SysDictData/SysDictType:六个实体均extends BaseEntity;其中 SysUser 含dept/roles/roleIds/postIds关联,isAdmin()判userId==1L。LoginUser:实现UserDetails(见 02 章),是认证核心域对象,@JSONField(serialize=false)隐藏密码。
5. 注解体系(清单)
| 注解 | 位置 | 关键属性 | 配合处理器 | 用途 |
|---|---|---|---|---|
@Log |
方法 | title/businessType/operatorType/isSaveRequestData/isSaveResponseData/excludeParamNames |
LogAspect |
操作日志 |
@DataScope |
方法 | userAlias/deptAlias/userField/deptField/permission |
DataScopeAspect |
数据权限 SQL |
@DataSource |
方法/类 | value=MASTER/SLAVE |
DataSourceAspect |
多数据源切换 |
@Excel/@Excels |
字段 | name/dateFormat/dictType/readConverterExp/cellType/... |
ExcelUtil(POI) |
导入导出列定义 |
@RateLimiter |
方法 | key/time/count/limitType(默认去)IP)` |
RateLimiterAspect |
接口限流 |
@RepeatSubmit |
方法 | interval(默认5000ms)/message |
RepeatSubmitInterceptor |
防重复提交 |
@Sensitive |
字段 | desensitizedType |
SensitiveJsonSerializer |
敏感字段脱敏 |
@Anonymous |
方法/类 | 无 | PermitAllUrlProperties |
匿名访问 |
📍 注解定义在
annotation/包,处理器分散在 common(ExcelUtil/SensitiveJsonSerializer) 与 framework(LogAspect/DataScopeAspect/...)。
@Excel 关键属性(Excel 导出/导入的核心元数据)
sort/name/dateFormat/dictType/readConverterExp(0=男)/separator/scale/roundingMode/width/suffix/prompt/combo/needMerge/isExport/targetAttr/isStatistics/cellType(数值/字符串/图片)/handler/type(ALL/EXPORT/IMPORT)。
SysUser 上可见用法范例:
1 | // 防手机号变成科学计数 |
6. 异常体系
1 | RuntimeException |
核心机制:BaseException.getMessage() 若 code 非空则用 MessageUtils.message(code, args) 查 i18n,否则回退 defaultMessage——实现”异常消息国际化”。ServiceException 是业务层主力,被 GlobalExceptionHandler 捕获转 AjaxResult。
7. 分页 5 件套(理解分页原理)
若依分页靠 PageHelper 拦截器自动在第一条 SQL 后拼接 LIMIT,并返回 Page 对象携带 total。链路:
sequenceDiagram
participant C as Controller
BaseController.startPage
participant P as PageUtils.startPage
participant T as TableSupport.buildPageRequest
participant PD as PageDomain
participant PH as PageHelper.startPage
C->>P: startPage()
P->>T: buildPageRequest()
T->>T: 从request读 pageNum/pageSize/orderByColumn/isAsc/reasonable
T-->>P: PageDomain
P->>P: SqlUtil.escapeOrderBySql(排序) 防注入
P->>PH: startPage(pageNum, pageSize, orderBy).setReasonable()
PH-->>PH: 下一次SQL自动加 LIMIT 并计数
关键点:
PageDomain.getOrderBy()把驼峰orderByColumn转下划线数据库列名 + 方向。SqlUtil.escapeOrderBySql白名单过滤排序字段,防 SQL 注入(见 SqlUtil)。BaseController.getDataTable(list)用new PageInfo(list).getTotal()取 total 封装TableDataInfo{total, rows, code, msg}。- 排序方向兼容前端
ascending/descending→asc/desc。
8. 学习检查点
📝 本章小结
AjaxResult继承HashMap用链式put支持任意扩展字段;R<T>是泛型强类型替代。BaseEntity统一审计列与paramsMap,后者是”前端任意参数 + 数据权限注入”的载体。RedisCache封装常用数据结构读写,是本项目缓存底座;DictUtils负责字典缓存读写(key=sys_dict:)。- 注解是若依的”声明式横切”入口,配 AOP/拦截器/序列化器实现日志、数据权限、限流、脱敏等。
- 分页由 PageHelper 拦截 + 请求参数模型(PageDomain/TableSupport)驱动,排序字段经防注入过滤。
🤔 思考题
BaseEntity.getParams()为什么要@JsonInclude(NON_EMPTY)且懒加载空 Map?(BaseEntity:42-112)参考答案
params常用来携带查询区间参数与数据权限注入串(如上一条回答),多数查询不需要它。@JsonInclude(NON_EMPTY)让空 Map 不序列化,减少响应体积;getParams()内if(params==null) params=new HashMap<>()(BaseEntity.java:105-112)是懒加载,避免实体刚创建就白白分配 Map。RedisCache.keys(pattern)用redisTemplate.keys通配扫描,生产量大时会有什么风险?更好的做法是什么?参考答案
KEYS命令在大 key 数量下会阻塞 Redis 主线程导致瞬时卡顿,且结果集可能巨大(RedisCache.java:264-267)。生产上建议改用SCAN游标分批扫描,或拆分 Redis 实例/异步删除。PageDomain.getOrderBy()返回”驼峰转下划线 + 方向”,为何 Controller 还要再过一层SqlUtil.escapeOrderBySql?参考答案
getOrderBy()(PageDomain.java:27-34)只做格式转换,但orderByColumn来自前端,若直接把原始值拼进order by可能被注入(如;drop table)。SqlUtil.escapeOrderBySql用白名单正则(SQL_PATTERN)过滤掉危险字符后才允许拼接,是分页安全的关键关口(PageUtils.startPage)。