若依框架AOP注解实战:自动填充实体类创建人与时间信息
1. 为什么我们需要告别手动“填坑”如果你用过若依框架做项目肯定对下面这段代码不陌生每次新增一条数据都得吭哧吭哧地手动去设置createBy、createTime、updateBy、updateTime这几个字段。修改数据时也一样得记得去更新updateBy和updateTime。刚开始一两个实体类还好等项目大了几十上百个insert和update方法每个里面都写一遍那感觉就像在工地上重复搬砖不仅枯燥还特别容易出错。万一哪天忘了写或者复制粘贴时漏了一个字段数据的一致性就出问题了排查起来也头疼。更麻烦的是这些字段的赋值逻辑往往还分散在各个业务 Service 层里。今天产品经理说创建人要从用户名改成用户ID明天又说时间要统一用某个特定时区。这时候你就得把所有相关方法翻出来一个一个改简直就是一场灾难。这种重复且与核心业务逻辑无关的“脏活累活”正是我们程序员要极力避免的。好的架构应该让代码更专注业务本身把这些通用的、横切关注点Cross-Cutting Concerns抽离出去。AOP面向切面编程就是干这个的“大救星”。它允许我们把像日志、事务、权限校验还有我们今天要说的自动填充这类通用逻辑从业务代码中剥离出来集中到一个地方切面去管理。这样一来业务方法变得干净清爽只需要关心“做什么”比如保存一个设备而“怎么做”的通用细节比如谁在什么时候创建的由切面统一搞定。这不仅仅是少写几行代码更是提升了代码的可维护性和健壮性。接下来我就手把手带你用自定义注解和AOP在若依框架里实现这个“自动填坑”的神器。2. 核心武器打造你的专属注解要想让AOP知道该在哪些方法上干活我们得先给它一个“暗号”这个暗号就是自定义注解。你可以把它理解成一个标签贴到哪个方法上AOP就知道要去处理哪个方法。2.1 注解类代码详解我们把这个注解命名为DataAutoFill名字一看就知道是干嘛的。把它放在com.yourproject.common.annotation包下遵循若依的包结构习惯。package com.yourproject.common.annotation; import java.lang.annotation.*; /** * 数据自动填充注解 * 用于标记需要进行创建人/时间、更新人/时间自动填充的方法 * author YourName */ Target(ElementType.METHOD) // 指明这个注解可以用在方法上 Retention(RetentionPolicy.RUNTIME) // 指明注解在运行时保留这样AOP才能获取到 Documented // 表明这个注解应该被包含在Javadoc中 public interface DataAutoFill { /** * 操作类型 */ OperationType value() default OperationType.INSERT; /** * 操作类型枚举清晰定义是新增还是修改 */ public enum OperationType { /** * 插入操作会填充创建和更新信息 */ INSERT, /** * 更新操作只填充更新信息 */ UPDATE } }看看代码非常简洁。Target(ElementType.METHOD)是关键它限定了这个注解只能标注在方法上。Retention(RetentionPolicy.RUNTIME)更重要它意味着这个注解的信息在程序运行期间也会保留而不是编译完就扔了这样我们的AOP切面才能在运行时通过反射读取到它。我在这里定义了一个枚举OperationType用来明确指定本次操作是新增还是修改这比用两个布尔参数add()和edit()更清晰不容易用错。2.2 设计思路与最佳实践为什么不用AddOrUpdateFilter这种名字从团队协作和代码可读性角度DataAutoFill的意图更直接。Filter这个词容易让人联想到数据过滤比如权限过滤而我们做的是数据填充。命名是给未来维护代码的人包括三个月后的你自己看的一定要表意清晰。使用枚举而不是布尔值是一个很好的实践。想象一下你在Service方法上写DataAutoFill(OperationType.INSERT)一目了然。如果用的是XXX(add true, edit false)不仅写起来啰嗦阅读时还要在脑子里做个逻辑转换。枚举增强了类型安全编译器能帮你检查拼写错误IDE的自动补全也更友好。这个小细节能让代码质量提升一个档次。3. 魔法生效编写AOP切面逻辑注解定义好了它只是个安静的标签。真正让魔法生效的是背后的“魔法师”——AOP切面。切面会拦截所有贴了DataAutoFill标签的方法并在方法执行前偷偷地把该填的数据填好。3.1 切面类完整实现我们在com.yourproject.framework.aspectj包下创建切面类DataAutoFillAspect。若依框架默认集成了Spring AOP和AspectJ我们可以直接使用。package com.yourproject.framework.aspectj; import com.yourproject.common.annotation.DataAutoFill; import com.yourproject.common.core.domain.BaseEntity; import com.yourproject.common.core.domain.entity.SysUser; import com.yourproject.common.core.domain.model.LoginUser; import com.yourproject.common.utils.DateUtils; import com.yourproject.common.utils.SecurityUtils; import com.yourproject.common.utils.StringUtils; import lombok.extern.slf4j.Slf4j; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.stereotype.Component; import java.lang.reflect.Method; import java.time.LocalDateTime; import java.util.Date; /** * 数据自动填充切面 * author YourName */ Aspect // 声明这是一个切面类 Component // 纳入Spring容器管理 Slf4j // 使用Lombok简化日志声明 public class DataAutoFillAspect { /** * 定义切点拦截所有被 DataAutoFill 注解标记的方法 */ Pointcut(annotation(com.yourproject.common.annotation.DataAutoFill)) public void dataAutoFillPointCut() {} /** * 前置通知在目标方法执行前执行填充逻辑 * param joinPoint 连接点可以获取方法签名、参数等信息 */ Before(dataAutoFillPointCut()) public void autoFill(JoinPoint joinPoint) { log.debug(开始执行数据自动填充...); // 1. 获取方法签名和注解 MethodSignature signature (MethodSignature) joinPoint.getSignature(); Method method signature.getMethod(); DataAutoFill dataAutoFill method.getAnnotation(DataAutoFill.class); if (dataAutoFill null) { return; // 理论上不会走到这里因为切点已经定义了 } // 2. 获取当前操作类型和用户信息 DataAutoFill.OperationType operationType dataAutoFill.value(); LoginUser loginUser SecurityUtils.getLoginUser(); if (StringUtils.isNull(loginUser)) { log.warn(自动填充数据时未获取到登录用户信息填充操作将跳过。); return; // 对于无需登录的接口如定时任务可根据业务决定是否填充默认值 } SysUser currentUser loginUser.getUser(); String username currentUser.getUserName(); // 获取用户名 // 如果实体类中存储的是用户ID这里可以改为 currentUser.getUserId().toString() Date now DateUtils.getNowDate(); // 获取当前时间 // 如果你使用的是Java 8的LocalDateTime可以这样LocalDateTime now LocalDateTime.now(); // 3. 获取方法参数通常第一个参数就是我们要操作的实体对象 Object[] args joinPoint.getArgs(); if (args null || args.length 0) { log.debug(方法参数为空跳过自动填充。); return; } // 遍历参数找到继承自BaseEntity的对象进行填充 for (Object arg : args) { if (arg instanceof BaseEntity) { BaseEntity entity (BaseEntity) arg; fillData(entity, operationType, username, now); // 通常一个方法只有一个主要实体参数找到后可以break break; } } log.debug(数据自动填充完成。); } /** * 具体的填充逻辑 * param entity 待填充的实体对象 * param operationType 操作类型 * param username 当前用户名 * param now 当前时间 */ private void fillData(BaseEntity entity, DataAutoFill.OperationType operationType, String username, Date now) { switch (operationType) { case INSERT: // 新增操作设置创建人和创建时间同时更新人和时间通常也一并设置 if (StringUtils.isBlank(entity.getCreateBy())) { entity.setCreateBy(username); } if (entity.getCreateTime() null) { entity.setCreateTime(now); } // 即使不是INSERTUPDATE分支也会设置更新信息所以这里可以不加break直接穿透 // 不我们明确分开INSERT设置全部UPDATE只设置更新部分。 entity.setUpdateBy(username); entity.setUpdateTime(now); log.debug(已为实体 [{}] 填充INSERT数据。, entity.getClass().getSimpleName()); break; case UPDATE: // 更新操作只设置更新人和更新时间 entity.setUpdateBy(username); entity.setUpdateTime(now); log.debug(已为实体 [{}] 填充UPDATE数据。, entity.getClass().getSimpleName()); break; default: log.warn(未知的操作类型: {}, operationType); break; } } }3.2 关键点解析与避坑指南这段代码有几个地方值得细细品味。首先我用了Pointcut定义了一个独立的切点表达式annotation(com...DataAutoFill)这样Before通知里引用这个切点名字就行结构更清晰。直接在Before里写表达式也可以但拆分开来以后万一切点逻辑变复杂了比如要组合多个条件修改起来更方便。第二获取用户信息这里用的是若依框架提供的SecurityUtils.getLoginUser()。这个方法会从Spring Security的上下文中拿到当前登录用户。这里有个大坑你一定要注意你的填充方法必须是在用户已登录的Web请求上下文中被调用。如果你在异步线程、定时任务、或者MQ消费者里调用这些方法SecurityUtils很可能拿不到用户信息会导致填充失败。对于这种场景你需要设计降级方案比如从任务参数中传递操作人或者填充一个系统默认账号如system。第三参数处理。我写了一个循环for (Object arg : args)来遍历方法的所有参数并判断它是否是BaseEntity的子类。为什么这么做因为有些复杂业务的方法参数可能不止一个实体或者实体被包装在另一个对象里。这样写更健壮。当然如果你能保证目标方法的第一个参数就是实体直接Object param args[0]也行但循环判断的容错性更好。第四填充逻辑里的判断if (StringUtils.isBlank(entity.getCreateBy()))。这个判断是为了防止覆盖。想象一个场景你先创建了一个草稿设置了部分信息然后再次提交保存。第二次调用insert方法时如果实体对象里已经带了createBy比如从前端传回切面里的这个判断就能避免用当前登录用户覆盖掉原有的创建人。这个细节体现了切面逻辑的严谨性它不是无脑覆盖而是“智能补充”。4. 改造代码生成器一劳永逸手动在每个Service方法上加注解还是有点麻烦。特别是用若依框架的代码生成器生成了大量基础CRUD代码后难道要一个一个去加吗当然不最高效的方式是直接改造代码生成器的模板让生成的代码自带注解。4.1 定位并修改模板文件若依的代码生成器模板文件通常放在ruoyi-generator/src/main/resources/vm目录下。我们需要修改的是Service实现层的模板一般是serviceImpl.java.vm这个文件。找到模板中生成insert和update方法的部分。原始模板可能长这样/** * 新增${functionName} * * param ${className} ${functionName} * return 结果 */ #if($table.sub) Transactional #end Override public int insert${ClassName}(${ClassName} ${className}) { #if($table.tree) ... // 树形结构处理 #end ${className}.setCreateTime(DateUtils.getNowDate()); return ${className}Mapper.insert${ClassName}(${className}); } /** * 修改${functionName} * * param ${className} ${functionName} * return 结果 */ #if($table.sub) Transactional #end Override public int update${ClassName}(${ClassName} ${className}) { ${className}.setUpdateTime(DateUtils.getNowDate()); return ${className}Mapper.update${ClassName}(${className}); }看到没模板里还在手动调用setCreateTime和setUpdateTime。我们的目标就是把这些手动设置的行删掉然后加上我们的自定义注解。4.2 修改后的模板代码/** * 新增${functionName} * * param ${className} ${functionName} * return 结果 */ #if($table.sub) Transactional #end Override DataAutoFill(OperationType.INSERT) // 新增自动填充注解 public int insert${ClassName}(${ClassName} ${className}) { #if($table.tree) ... // 树形结构处理 #end // ${className}.setCreateTime(DateUtils.getNowDate()); // 删除手动设置时间 return ${className}Mapper.insert${ClassName}(${className}); } /** * 修改${functionName} * * param ${className} ${functionName} * return 结果 */ #if($table.sub) Transactional #end Override DataAutoFill(OperationType.UPDATE) // 新增自动填充注解 public int update${ClassName}(${ClassName} ${className}) { // ${className}.setUpdateTime(DateUtils.getNowDate()); // 删除手动设置时间 return ${className}Mapper.update${ClassName}(${className}); }重要提醒修改模板前一定要先备份原文件改完之后用代码生成器重新生成一下你的业务模块代码你会发现所有的insertXxx和updateXxx方法都自动戴上了DataAutoFill的“勋章”并且原来那些setCreateTime的代码也消失了。这样一来所有新生成的代码都直接享受自动填充的便利老代码可以逐步按需改造。5. 实战演练看看改造前后的对比光说不练假把式我们拿一个具体的业务方法开刀看看用了AOP注解之后代码能清爽多少。5.1 改造前的手动填充代码假设我们有一个设备管理模块新增设备的Service方法原来是这样的/** * 新增设备信息改造前 * param fuelDevice 设备信息 * return 结果 */ Override Transactional(rollbackFor Exception.class) public int insertFuelDevice(FuelDevice fuelDevice) { // 1. 生成设备编号等业务逻辑 String deviceNo createDeviceNo(); fuelDevice.setDeviceNo(deviceNo); // 2. 手动获取并设置用户、时间信息重复劳动开始 String username SecurityUtils.getUsername(); Date now new Date(); fuelDevice.setCreateBy(username); fuelDevice.setCreateTime(now); fuelDevice.setUpdateBy(username); fuelDevice.setUpdateTime(now); // 重复劳动结束 // 3. 设置一些业务状态初始值 fuelDevice.setActiveStatus(0); fuelDevice.setTankNum(0); fuelDevice.setFuelGunNum(0); // 4. 复杂的业务逻辑判断 String isMonitor fuelDevice.getIsMonitor(); if (N.equals(isMonitor)) { fuelDevice.setAlarmStatus(9); fuelDevice.setOnlineStatus(9); } else if (Y.equals(isMonitor)) { fuelDevice.setAlarmStatus(0); fuelDevice.setOnlineStatus(2); } // 5. 执行插入 return fuelDeviceMapper.insertFuelDevice(fuelDevice); }这段代码功能没问题但你看第2步那里关于“谁在什么时候创建/更新”的通用逻辑硬生生地插在了核心业务逻辑中间。它把生成设备编号、设置状态、业务判断这些本该是主角的代码挤到了一边阅读起来思路被打断。而且SecurityUtils.getUsername()和new Date()这种调用散落在各处如果以后要换用户信息来源或者时间格式改动点就太多了。5.2 改造后的优雅代码现在我们祭出DataAutoFill注解/** * 新增设备信息改造后 * param fuelDevice 设备信息 * return 结果 */ Override Transactional(rollbackFor Exception.class) DataAutoFill(OperationType.INSERT) // 仅仅添加这一行注解 public int insertFuelDevice(FuelDevice fuelDevice) { // 1. 生成设备编号等业务逻辑 String deviceNo createDeviceNo(); fuelDevice.setDeviceNo(deviceNo); // 2. 设置业务状态初始值直接衔接核心业务没有“杂音” fuelDevice.setActiveStatus(0); fuelDevice.setTankNum(0); fuelDevice.setFuelGunNum(0); // 3. 复杂的业务逻辑判断 String isMonitor fuelDevice.getIsMonitor(); if (N.equals(isMonitor)) { fuelDevice.setAlarmStatus(9); fuelDevice.setOnlineStatus(9); } else if (Y.equals(isMonitor)) { fuelDevice.setAlarmStatus(0); fuelDevice.setOnlineStatus(2); } // 4. 执行插入 return fuelDeviceMapper.insertFuelDevice(fuelDevice); }对比一下是不是感觉神清气爽所有关于创建人、时间的代码都消失了方法体内只剩下最纯粹的业务逻辑生成编号、设置状态、业务判断、执行插入。代码的职责变得非常单一可读性大大提升。更重要的是这个方法是安全的因为填充逻辑被集中到了AOP切面里统一管理绝不会出现某个方法忘了设置的情况。当需要修改填充逻辑时比如从填充用户名改为填充用户ID你只需要修改DataAutoFillAspect这一个地方所有相关方法立即生效这才是高维护性的代码该有的样子。6. 进阶思考与扩展可能基本的自动填充实现了但实际项目总会遇到更复杂的情况。这里分享几个我踩过坑后总结的进阶思路。场景一填充字段不统一怎么办不是所有实体类都继承自BaseEntity。有些老表对应的实体或者来自外部系统的DTO字段名可能是creator、create_date。这时候我们的切面就不能写死只处理BaseEntity了。一个更通用的方案是再定义一个注解比如AutoFillField标注在实体类的字段上然后在切面里通过反射查找所有带有AutoFillField的字段并进行赋值。这样任何实体类只要标记了需要填充的字段都能被切面处理。场景二除了创建人/时间还想自动填充其他字段怎么办比如每次更新都想自动记录一个更新原因updateReason或者根据某些规则自动计算一个状态值。我们的DataAutoFill注解和切面可以很容易地扩展。在注解里增加属性比如String fillUpdateReason() default 然后在切面里根据这个属性值去进行更复杂的填充逻辑。切面的威力就在于你可以把任何横跨多个模块的通用行为“织入”进去。场景三AOP失效了这是新手常遇到的问题。最常见的原因是方法的调用发生在类内部。比如你在DeviceServiceImpl的insertA方法里直接调用了同一个类的insertB方法因为insertB上的DataAutoFill注解是通过Spring AOP代理实现的而内部调用不走代理所以切面不会生效。解决方案有两种一是将insertB方法抽到另一个Service里二是在insertA中通过AopContext.currentProxy()获取当前代理对象再来调用insertB需要开启exposeProxy。性能考量AOP会带来微小的性能开销因为涉及到代理和反射。但对于数据填充这种I/O操作数据库访问之前的准备工作这点开销几乎可以忽略不计。如果你的切面逻辑非常复杂或者被切入的方法调用频率极高比如每秒数万次才需要考虑优化。对于绝大多数业务系统AOP带来的代码结构清晰度和可维护性的收益远远大于那点性能损耗。最后记住一点技术是为人服务的。我们引入AOP、自定义注解这些看似“高级”的技术终极目的不是为了炫技而是为了让代码更干净、更健壮、更好维护。当你看到经过改造后的Service层业务逻辑一目了然再也没有那些烦人的重复代码时那种整洁和舒爽感就是对我们作为开发者最好的回报。下次在若依项目里再也不用为那几个字段头疼了一个注解全部搞定。

相关新闻

translategemma-27b-it实测:一键翻译图片中的多国语言

translategemma-27b-it实测:一键翻译图片中的多国语言

translategemma-27b-it实测:一键翻译图片中的多国语言 1. 告别繁琐流程:为什么你需要一个“看图说话”的翻译工具 想象一下这个场景:你正在处理一份海外供应商发来的产品规格书PDF,里面全是带文字的图表和截图。你需要把里面的日…

2026/7/3 22:23:31 阅读更多 →
从QML报错到完美运行:Qt5/6跨版本发布避坑全指南(含platforms插件配置)

从QML报错到完美运行:Qt5/6跨版本发布避坑全指南(含platforms插件配置)

从QML报错到完美运行:Qt5/6跨版本发布避坑全指南(含platforms插件配置) 你是否也经历过这样的场景:在开发环境中,你的Qt Quick应用运行得丝滑流畅,界面炫酷,交互完美。然而,当你满怀…

2026/7/3 3:48:08 阅读更多 →
STEP3-VL-10B多模态模型5分钟快速部署:WebUI一键启动,小白也能玩转AI识图

STEP3-VL-10B多模态模型5分钟快速部署:WebUI一键启动,小白也能玩转AI识图

STEP3-VL-10B多模态模型5分钟快速部署:WebUI一键启动,小白也能玩转AI识图 你是不是经常看到别人用AI模型分析图片、识别表格、甚至解答复杂的图表问题,觉得特别神奇,但又担心自己不会编程、环境配置太复杂,只能望而却…

2026/5/17 9:04:26 阅读更多 →

最新新闻

Windows任务栏透明化神器:5种模式彻底改变你的桌面体验

Windows任务栏透明化神器:5种模式彻底改变你的桌面体验

Windows任务栏透明化神器:5种模式彻底改变你的桌面体验 【免费下载链接】TranslucentTB A lightweight utility that makes the Windows taskbar translucent/transparent. 项目地址: https://gitcode.com/gh_mirrors/tr/TranslucentTB 你是否厌倦了Windows任…

2026/7/4 12:00:48 阅读更多 →
量子傅里叶变换在多光子干涉测量中的高效应用

量子傅里叶变换在多光子干涉测量中的高效应用

1. 量子傅里叶变换在多光子干涉基准测试中的突破性进展在量子光学实验中,多光子干涉现象是量子计算和量子通信的核心基础。想象一下,当多个完全相同的光子同时进入一个光学系统时,它们会像训练有素的芭蕾舞者一样完美同步地舞动,产…

2026/7/4 12:00:48 阅读更多 →
MiniMax-M2.7 + DMXAPI:轻量级大模型调用新范式

MiniMax-M2.7 + DMXAPI:轻量级大模型调用新范式

1. 项目概述:这不是“又一个API接口”,而是大模型调用链路的轻量化重构 最近在多个技术群和开发者论坛里, MiniMax-M2.7 这个名字出现频率陡增——不是作为论文里的新架构,也不是某家大厂发布会上的PPT配图,而是真实…

2026/7/4 12:00:48 阅读更多 →
MLOps实战:从Notebook到生产环境的模型服务化与可观测性

MLOps实战:从Notebook到生产环境的模型服务化与可观测性

1. 项目概述:当模型走出Jupyter,真正开始养家糊口 “From Notebook to Production: Running ML in the Real World (Part 4)”——这个标题里藏着一个被无数数据科学家反复咀嚼、又悄悄咽下的现实:我们花了80%的时间调参、画图、写 print(mo…

2026/7/4 11:58:47 阅读更多 →
AI提示词四要素法:参考信息、动作、目标、要求

AI提示词四要素法:参考信息、动作、目标、要求

1. 为什么“1分钟学会”是个误导,但“1分钟上手专业指令”真能做到?你点开这篇内容,大概率是被标题里的“1分钟”勾住了——这很真实。我也试过,在刚接触文心一言那会儿,翻遍官方文档、看十几条短视频、收藏五六个“万…

2026/7/4 11:56:46 阅读更多 →
基于YOLOv5的养殖场猪只行为AI监测系统开发

基于YOLOv5的养殖场猪只行为AI监测系统开发

1. 项目背景与核心价值去年帮农学院做毕设指导时,发现养殖场每天要安排4个工人轮班盯着监控屏幕,用肉眼判断母猪是否出现异常行为。这种传统监测方式不仅效率低下,夜间漏检率更是高达30%。这正是我们开发这套系统的初衷——用AI视觉技术实现猪…

2026/7/4 11:56:46 阅读更多 →

日新闻

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 发布:关键安全修复版本,多项问题得到解决

Memcached 1.6.43 正式发布,这是一个关键的安全修复版本,修复了多个方面的问题,还对部分功能进行了优化。 安全修复亮点 此次发布在安全修复上表现突出。binprot 避免了项目引用计数溢出,mcmc 因安全问题提升了上游版本号&#xf…

2026/7/4 0:04:29 阅读更多 →
终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案

终极指南:使用HMCL启动器跨平台畅玩Minecraft的完整解决方案 【免费下载链接】HMCL A Minecraft Launcher which is multi-functional, cross-platform and popular 项目地址: https://gitcode.com/gh_mirrors/hm/HMCL HMCL(Hello Minecraft! Lau…

2026/7/4 0:06:29 阅读更多 →
KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

KMX63与PIC18F66K40在嵌入式HMI中的硬件协同与低功耗设计

1. KMX63与PIC18F66K40的硬件协同架构解析KMX63作为一款三轴加速度计和磁力计组合传感器,与PIC18F66K40微控制器的搭配堪称嵌入式HMI开发的黄金组合。这套硬件组合的核心优势在于KMX63提供的高精度运动感知能力与PIC18F66K40强大的信号处理能力形成了完美互补。KMX6…

2026/7/4 0:06:29 阅读更多 →

周新闻

月新闻