Kotlin Exposed如何实现类似JPA AuditAware的审计字段自动填充
Kotlin Exposed 审计字段自动赋值实现方案
Exposed 没有提供开箱即用的类JPA AuditAware 的组件,但可以用极低侵入成本实现全场景自动填充,完全不需要业务代码逐层传参,实现效果和JPA审计能力一致。
推荐实现方案(类型安全、零业务侵入)
核心思路是抽离公共审计表基类,结合线程上下文存储操作人信息,在插入语句生成阶段自动补全字段值,不需要拦截事务提交。
- 定义线程级审计上下文
用来和当前请求/事务线程绑定,存储当前操作人信息,避免参数逐层传递:object AuditContext { private val currentOperator = ThreadLocal<String>() fun setOperator(userId: String) = currentOperator.set(userId) fun getOperator(): String = currentOperator.get() ?: "system" fun clear() = currentOperator.remove() } - 绑定Spring请求上下文
实现Spring MVC拦截器,在请求进入时从安全上下文(如Spring Security的SecurityContextHolder)提取当前登录用户ID存入上下文,请求结束后清空,避免线程池复用导致的用户串号问题:
将拦截器注册到Spring MVC拦截器链即可生效。@Component class AuditInterceptor : HandlerInterceptor { override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { val userId = SecurityContextHolder.getContext().authentication?.name ?: "anonymous" AuditContext.setOperator(userId) return true } override fun afterCompletion(request: HttpServletRequest, response: HttpServletResponse, handler: Any, ex: Exception?) { AuditContext.clear() } } - 定义公共审计表基类
所有需要审计字段的业务表统一继承该基类,内置插入时的自动填充逻辑,支持业务代码手动覆盖字段值:
业务表定义时直接继承:open class AuditableTable(tableName: String) : Table(tableName) { val createdBy = varchar("created_by", 64) val createdDt = datetime("created_dt") override fun insert(statement: InsertStatement<Number>): InsertStatement<Number> { // 仅当业务层未手动设值时自动填充 if (createdBy !in statement.columns) { statement[createdBy] = AuditContext.getOperator() } if (createdDt !in statement.columns) { statement[createdDt] = LocalDateTime.now() } return super.insert(statement) } // 如果需要更新人、更新时间字段,重写update方法实现逻辑即可 }object UserTable : AuditableTable("t_user") { val id = long("id").autoIncrement() val username = varchar("username", 32) override val primaryKey = PrimaryKey(id) }
该方案是Exposed生态下生产环境的主流实现方式,编译期可校验字段类型、名称正确性,无额外性能开销,不会出现批量操作、关联操作漏赋值的问题。
关于EntityHooks和事务拦截方案的说明
EntityHooks仅针对Exposed DAO层的实体生命周期生效,如果你使用DSL写法操作数据库,回调完全不会触发;且回调中修改实体值需要直接操作实体属性,无法覆盖DSL场景,不适合做全局统一填充。- 如果确实需要在事务提交前统一拦截处理,可以注册全局
TransactionHook,在beforeCommit阶段遍历当前事务待执行的插入语句补全字段,但该方案需要运行时匹配字段名,无编译期校验,实现复杂度高,仅建议在无法修改表继承结构的特殊场景下使用。
注意事项
- 如果是WebFlux响应式栈,将
ThreadLocal替换为Reactor上下文存储操作人信息即可,核心填充逻辑不变。 - 不建议用AOP切Service层的方式实现填充,批量插入、关联表插入等场景很容易出现漏赋值问题。
内容的提问来源于stack exchange,提问作者AFrieze
相关产品推荐
相关产品推荐

