若依自定义注解体系:@Log、@DataScope、@DataSource、@Excel 全览
本文基于若依3.9.2、SpringBoot3版本。

若依自定义注解体系:@Log/@DataScope/@DataSource/@Excel 全览
写若依的业务代码,离不开它自己的注解:Controller 方法上标 @Log 记操作日志,Service 方法上标 @DataScope 做数据权限过滤,实体字段上标 @Excel 控制导入导出。这些注解全部定义在 com.ruoyi.common.annotation 包下,共九个。
注解本身只是挂在类、方法、字段上的元数据,不含任何执行逻辑,真正干活的是与它配套的切面、拦截器或处理器——注解负责声明意图,配套实现负责兑现。
本文梳理这九个注解的定义与属性、元注解的选择,以及每个注解由谁消费、以什么方式配合。
九个注解一览
先给出全貌:
| 注解 | 标注位置 | 作用 | 配套实现 |
|---|---|---|---|
@Log |
方法 | 记录操作日志 | LogAspect 切面,异步写入 sys_oper_log |
@DataScope |
方法 | 数据权限过滤 | DataScopeAspect 切面,拼接 SQL 条件 |
@DataSource |
类、方法 | 切换主从数据源 | DataSourceAspect 切面 + 动态数据源 |
@RateLimiter |
方法 | 接口限流 | RateLimiterAspect 切面 + Redis Lua 脚本 |
@RepeatSubmit |
方法 | 防重复提交 | RepeatSubmitInterceptor 拦截器 + Redis |
@Anonymous |
类、方法 | 标记免鉴权接口 | 启动时收集,注册进 Security 放行规则 |
@Excel |
字段 | 单列导入导出标注 | ExcelUtil 反射解析 |
@Excels |
字段 | @Excel 的容器注解 |
ExcelUtil 反射解析 |
@Sensitive |
字段 | JSON 输出脱敏 | SensitiveJsonSerializer 序列化器 |
按消费方式可以分成四类:四个由 AOP 切面消费(@Log、@DataScope、@DataSource、@RateLimiter),一个由 MVC 拦截器消费(@RepeatSubmit),一个在启动阶段收集(@Anonymous),三个由反射工具或序列化器消费(@Excel、@Excels、@Sensitive)。消费方式不同,注解能用在哪里、怎么被读取也随之不同。
元注解的选择
九个注解的元注解组合高度一致,以 @Log 的定义为例:
@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
}
@Target 限定注解能标在哪里,@Retention(RetentionPolicy.RUNTIME) 保证注解信息保留到运行期、可以通过反射读取,@Documented 让注解出现在 javadoc 中。
RUNTIME 是整套体系能工作的前提:切面拦截到方法后要读出注解属性,序列化器要在字段上找注解,这些都要求注解在运行期可见。若依所有注解统一使用 RUNTIME,没有一个使用 CLASS 或 SOURCE。
@Target 的差异表达了每个注解的用途边界:
- 只能标在方法上:
@DataScope、@RateLimiter、@RepeatSubmit,它们都作用于某个接口方法这一粒度 - 可标在方法和类上:
@Anonymous、@DataSource,标在类上时对整个类生效 - 只能标在字段上:
@Excel、@Excels、@Sensitive,它们描述的是单个数据列或单个输出值 @Log声明了方法和参数两种位置,属于特例,后文单独说明
@Inherited 只有 @DataSource 和 @RepeatSubmit 使用,两者效果不同。@Inherited 的实际能力是注解标在父类上时子类可以继承到,但它只对标注在类上的注解生效,对方法注解无效。@DataSource 同时允许类和方法两种位置,标在类上时子类能继承,这个配置有意义;@RepeatSubmit 的 @Target 只有 METHOD,类上根本标不了,它的 @Inherited 没有任何作用,属于冗余配置。
方法上的四个切面注解
@Log:三个通知接力
@Log 有六个属性:
title:模块名称,如「用户管理」businessType:操作类型,BusinessType枚举,默认OTHER,可选 INSERT、UPDATE、DELETE、EXPORT 等operatorType:操作人类别,OperatorType枚举,默认MANAGE(后台用户),另有MOBILE(手机端用户)isSaveRequestData:是否保存请求参数,默认 trueisSaveResponseData:是否保存响应结果,默认 trueexcludeParamNames:排除的请求参数数组,用于过滤密码等敏感字段
配套的 LogAspect 用三个通知接力完成一次日志记录。@Before 在方法执行前把时间戳放进 ThreadLocal;@AfterReturning 在方法正常返回后、@AfterThrowing 在方法抛出异常后分别触发,两者都调用同一个 handleLog 方法,从切点绑定的注解对象里读出 title、businessType 等属性填进 SysOperLog,收集请求参数和响应结果、算出执行耗时,最后交给 AsyncManager 异步写入数据库。
三个通知的切点表达式统一为 @annotation(controllerLog),这个表达式只匹配方法上的注解。
这里能解释 @Target 里的 PARAMETER:虽然注解声明了可以标在参数上,但 @annotation 表达式不匹配参数注解,标在参数上不会触发日志记录。系统里所有 @Log 都标在方法上,PARAMETER 只是定义时预留的扩展位,配套机制并没有支持它。
@DataScope:拼接 SQL 写入 params
@DataScope 有五个属性:
deptAlias、userAlias:部门表、用户表在 SQL 里的别名deptField、userField:部门、用户字段名,默认dept_id、user_idpermission:权限字符,多个用逗号分隔,默认从权限上下文获取(由@ss.hasPermi校验时写入)
配套的 DataScopeAspect 用 @Before 通知处理:取出当前登录用户的角色列表,按每个角色的数据权限范围(全部、自定义、本部门、部门及以下、仅本人)生成对应的 SQL 片段,拼接后放进方法第一个参数(要求是 BaseEntity 子类)的 params 集合中,Mapper 层的 XML 再把这段 SQL 追加到查询语句后面。
超级管理员直接跳过过滤;进入处理前还会先清空 params 里已有的 dataScope 值,防止调用方传入的内容被当作 SQL 拼接进去。
@DataSource:类上可继承,方法可覆盖
@DataSource 只有一个属性 value,类型是枚举 DataSourceType,只有主库 MASTER 和从库 SLAVE 两个值,默认 MASTER:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
public DataSourceType value() default DataSourceType.MASTER;
}
配套的 DataSourceAspect 切点写法与前面几个不同:
@Pointcut("@annotation(com.ruoyi.common.annotation.DataSource)"
+ "|| @within(com.ruoyi.common.annotation.DataSource)")
public void dsPointCut() {
}
@annotation 匹配方法上标注的情况,@within 匹配方法所在类上标注的情况,两种标法都能被切面拦截。拦截后先确定用哪个数据源:AnnotationUtils.findAnnotation 先找方法上的注解,找不到再找声明类上的注解,方法级配置覆盖类级配置。确定后把数据源类型写进 ThreadLocal,方法执行完在 finally 里清除,真正执行 SQL 时由动态数据源根据 ThreadLocal 里的值选择对应的数据源。这个注解用 @Around 通知,因为需要在方法执行前设置、执行后清理,一组前后动作放在同一个通知里最直接。
@Inherited 在这里发挥了作用:数据源类型标在 Service 父类上,子类自动继承同样的配置。
@RateLimiter:就绪但未启用
@RateLimiter 有四个属性:
key:限流 key 前缀,默认rate_limit:time:限流时间窗口,默认 60 秒count:窗口内允许的最大次数,默认 100limitType:限流类型,LimitType枚举,DEFAULT全局限流、IP按请求者 IP 限流
配套的 RateLimiterAspect 用 @Before 通知处理:把 key、IP(按配置)、类名、方法名拼成完整的 Redis key,执行一段 Lua 脚本对这个 key 计数,超过 count 就抛出 ServiceException。
脚本实现的算法是固定窗口计数器:先读当前计数值,已经超过 count 就直接返回、不再累加;否则自增一,并且只有计数值自增后等于 1(即窗口内第一次访问)时才给 key 设置 time 秒的过期时间。窗口从第一次访问开始计时,time 秒后计数连同 key 一起过期归零,窗口边界对齐首次访问时刻,既不对齐自然时间周期,也不是随请求滑动的滑动窗口。用 Lua 脚本是为了让读取计数、判断超限、自增、设置过期这几步在 Redis 端原子完成。
系统业务代码目前没有使用这个注解,限流机制处于就绪状态,需要时在方法上加注解即可。
拦截器消费的 @RepeatSubmit
@RepeatSubmit 有两个属性:interval 间隔时间,默认 5000 毫秒;message 提示消息,默认「不允许重复提交,请稍候再试」。
配套的 RepeatSubmitInterceptor 是一个抽象拦截器,实现了 Spring MVC 的 HandlerInterceptor,在 preHandle 里处理:
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
RepeatSubmit annotation = method.getAnnotation(RepeatSubmit.class);
if (annotation != null) {
if (this.isRepeatSubmit(request, annotation)) {
AjaxResult ajaxResult = AjaxResult.error(annotation.message());
ServletUtils.renderString(response, JSON.toJSONString(ajaxResult));
return false;
}
}
return true;
}
拦截器在 DispatcherServlet 解析出 HandlerMethod 之后执行,所以可以直接 method.getAnnotation(...) 读取注解,命中就调用抽象方法 isRepeatSubmit 判断是否重复,重复则把注解里的 message 写回响应并中断请求。具体判断规则由子类实现,默认实现 SameUrlDataInterceptor 基于 Redis 对比请求 URL、请求参数和时间间隔。这个注解当前同样没有业务代码使用,机制就绪。
这里与 @Anonymous 形成一组对照:同样是读方法上的注解,拦截器层能拿到 HandlerMethod,运行时读取一次即可;而 Security 过滤器层拿不到,只能启动时提前收集。
启动时收集的 @Anonymous
@Anonymous 是一个没有任何属性的标记注解,作用是标注哪些接口可以不登录访问:
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Anonymous {
}
它的消费发生在应用启动阶段:PermitAllUrlProperties 在 Bean 初始化完成后,通过 RequestMappingHandlerMapping 拿到全部接口映射,遍历找出标注了 @Anonymous 的方法及其所在类,把这些接口的 URL 收集到一个列表里,路径变量统一替换成 * 以便按模式匹配;Security 配置再把列表里的所有 URL 注册为 permitAll,请求到达时直接放行。
之所以必须在启动时收集,是因为 Spring Security 的过滤器执行在 DispatcherServlet 之前,那时还不知道请求会落到哪个 Controller 方法上,无法读取方法注解;把注解提前翻译成 URL 放行规则后,运行期的过滤器只需按 URL 匹配,不再碰注解。
字段上的 Excel、Excels 与 Sensitive
@Excel 与 @Excels
@Excel 只能标在字段上,是若依 Excel 导入导出的核心标注。它的属性有二十多个,按功能分组:
- 基本信息类:
name列名、sort排序、width列宽、height行高、cellType单元格类型 - 数据转换类:
dateFormat日期格式、dictType字典类型、readConverterExp值转换表达式(如0=男,1=女,2=未知)、separator分隔符、defaultValue空值默认值 - 显示控制类:
prompt提示信息、combo下拉框内容、needMerge是否合并单元格、isStatistics是否追加统计行 - 样式类:表头与单元格的背景色、字体颜色、对齐方式
- 关联扩展类:
targetAttr指定关联对象中的属性名、handler自定义数据处理器
注解内部还定义了两个枚举:
public enum Type {
ALL(0), EXPORT(1), IMPORT(2);
}
public enum ColumnType {
NUMERIC(0), STRING(1), IMAGE(2), TEXT(3);
}
Type 控制字段参与导出还是导入:ALL 两者都参与,EXPORT 仅导出,IMPORT 仅导入。ColumnType 控制导出单元格的数据类型,支持数值、字符串、图片、文本四种。配套的 ExcelUtil 在导入导出时反射读取实体字段上的 @Excel 标注,按注解属性完成列名生成、值转换、样式设置等全部动作,实体类只负责标注。
一个字段需要映射到多个 Excel 列时,用容器注解 @Excels 包装多个 @Excel。SysUser 的 dept 字段是关联对象,导出时需要取它内部的两个属性:
@Excels({
@Excel(name = "部门名称", targetAttr = "deptName", type = Type.EXPORT),
@Excel(name = "部门负责人", targetAttr = "leader", type = Type.EXPORT)
})
private SysDept dept;
targetAttr 指定关联对象里的属性名,ExcelUtil 反射读取嵌套属性值填入对应列。这里没有使用 Java 8 的 @Repeatable 可重复注解机制,@Excels 是手动定义的容器注解,效果等价——同一个字段上不能直接写两个 @Excel,必须包一层。
@Sensitive:由 Jackson 序列化机制消费
@Sensitive 也标在字段上,作用是接口返回 JSON 时对值脱敏。它的消费方式与前面所有注解都不同,不经过切面和拦截器,而是通过 Jackson 序列化机制:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@JacksonAnnotationsInside
@JsonSerialize(using = SensitiveJsonSerializer.class)
public @interface Sensitive {
DesensitizedType desensitizedType();
}
唯一属性 desensitizedType 指定脱敏规则。注解上叠了两个 Jackson 注解:@JacksonAnnotationsInside 把 @Sensitive 声明为组合注解,让叠加在它上面的 @JsonSerialize 能被 Jackson 识别并应用到目标字段;@JsonSerialize 指定该字段的序列化器为 SensitiveJsonSerializer。使用时只写一个 @Sensitive,Jackson 就会自动用它指定的序列化器处理该字段。
SensitiveJsonSerializer 继承 JsonSerializer<String> 并实现 ContextualSerializer 接口,脱敏发生在 JSON 序列化阶段。createContextual 在每个属性开始序列化前被调用,从这里读取字段上的 @Sensitive 注解、记住脱敏类型;真正写出值时,serialize 按脱敏类型对字符串做替换再写入。数据库里存的仍是完整值,只有返回给前端时被处理。管理员不脱敏:判断当前登录用户是否 admin,是则原样输出,取不到登录用户时默认脱敏。
脱敏规则由 DesensitizedType 枚举定义,每个枚举值持有一个 Function<String, String>:手机号保留前三后四(138****5678)、身份证保留前四和末四、邮箱只露首字符、密码全部星号等,共七种。
思考
注解与配套实现分离的结构:九个注解没有一个自带逻辑,全部把执行交给切面、拦截器、工具类或序列化器。这个结构的好处是职责干净——注解只描述要什么,配套实现决定怎么做,更换实现不需要动业务代码;代价是只看注解看不出效果,理解成本全部转移到机制侧,这也是若依这些注解必须对着配套实现一起学的原因。
消费时机决定读取方式:同样是读注解,不同机制在不同时机读取。AOP 切面和 MVC 拦截器在请求处理链中执行,此时已经解析出目标方法,运行时读取一次即可;Security 过滤器在 DispatcherServlet 之前执行,拿不到目标方法,只能启动时把注解翻译成 URL 规则。判断一个注解该怎么做实现,先看它要在哪个时机生效。
定义与实现不同步留下的痕迹:@Log 的 PARAMETER 声明、@RepeatSubmit 冗余的 @Inherited,都是注解定义先于配套机制定型留下的无效配置。使用这些注解时,以配套实现真正支持的位置和行为为准,不要按注解声明想当然。

浙公网安备 33010602011771号