在 Spring Boot 工程中,若选用的持久化层框架是 JPA,那么要想捕捉所有实体的增删改操作,该怎么实现呢?
下面给一个具体点的需求,然后我们来探讨如何实现:「假设我们要实现一个实体(表)操作监控模块,即捕获 Spring Boot 应用程序中所有实体的变更(包括增、删、改)操作,然后将这些操作记录到一张表中。」
具体需要记录的字段如下:
本文即是探索实现该需求的几种方式。
写作本文时,用到的 Java、Spring Boot、JPA 和 Hibernate 的版本如下:
Java:17
Spring Boot:3.5.4
Spring Data JPA:3.5.2
Hibernate: 6.6.22.Final
实现表操作监控模块前,我们有一些准备工作需要做,即:植入测试数据、新建 Model 类,以及为 Model 类编写对应的 JPA Repository。
假设我们使用的数据库是 MySQL,开始前我们先将需要的表建出来(假设 user
表为需要捕获的实体表,operation_log
表为将捕获的操作信息写入的目的表):
DROP TABLE IF EXISTS user;
CREATE TABLE user (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
age INT NOT NULL,
email VARCHAR(20) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT '2025-01-01 00:00:00',
updated_at TIMESTAMP NOT NULL DEFAULT '2025-01-01 00:00:00'
);
DROP TABLE IF EXISTS operation_log;
CREATE TABLE operation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
entity VARCHAR(50) NOT NULL,
entity_id BIGINT NOT NULL,
operation VARCHAR(20) NOT NULL,
operated_at TIMESTAMP NOT NULL DEFAULT '2025-01-01 00:00:00'
);
对应 user
表和 operation_log
表的 Model 类 User
和 OperationLog
的代码如下:
package com.example.demo.model;
@Data
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Integer age;
private String email;
private Date createdAt;
private Date updatedAt;
}
package com.example.demo.model;
@Data
@Entity
public class OperationLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String entity;
private Long entityId;
private String operation;
private Date operatedAt;
}
操作 User
和 OperationLog
实体的 JPA Repository 代码如下:
package com.example.demo.repository;
public interface UserRepository extends CrudRepository<User, Long> {
}
package com.example.demo.repository;
public interface OperationLogRepository extends CrudRepository<OperationLog, Long> {
}
测试数据和基础 Java 代码准备好后,下面即探索在 Spring Data JPA 中如何监听实体的操作。经过调研,目前比较主流的解决方案有三种,下面一一道来。
第一种方案是 JPA 推荐的方式,即通过编写 Entity Listener 实现,使用的均为 jakarta.persistence
包下的类。
首先,我们需要在需要监听的实体类 User
上增加一个 @EntityListeners
注解,然后指定我们自己编写的监听类 OperationListener.class
。
package com.example.demo.model;
@Data
@Entity
@EntityListeners(OperationListener.class)
public class User {
// ...
}
接下来即是 OperationListener
类的实现。可以看到,我们在该类中分别新增了三个方法 postSave()
、postUpdate()
、postDelete()
,并在方法上分别加了 JPA 的 @PostPersist
、@PostUpdate
、@PostRemove
注解,以用来监听实体的新增后、更新后和删除后操作。最后,调用 saveOperationLog()
方法来将捕获的字段写入 operation_log
表。
package com.example.demo.listener;
public class OperationListener {
@PostPersist
public void postSave(Object entity) {
saveOperationLog(entity, "INSERT");
}
@PostUpdate
public void postUpdate(Object entity) {
saveOperationLog(entity, "UPDATE");
}
@PostRemove
public void postDelete(Object entity) {
saveOperationLog(entity, "DELETE");
}
private void saveOperationLog(Object entity, String operation) {
// get repository
OperationLogRepository repository = SpringContextHolder.getBean(OperationLogRepository.class);
// save
OperationLog log = new OperationLog();
// log.Xxx();
repository.save(log);
}
}
需要注意的是:OperationListener
类没有使用 @Autowired
方式将 OperationLogRepository
注入为一个属性然后在 saveOperationLog()
方法中调用。这是因为,OperationListener
类是通过反射实例化的,其并未交给 Spring 容器所管理,所以 @Autowired
方式是无法将依赖进行注入的。
所以,这里要获取 OperationLogRepository
实例,必须要通过一种特殊的机制,即通过一个保管 Spring 应用上下文的 SpringContextHolder
工具类来实现。
SpringContextHolder
工具类的代码如下:
package com.example.demo.util;
@Component
public class SpringContextHolder implements ApplicationContextAware {
private static ApplicationContext context = null;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
context = applicationContext;
}
public static <T> T getBean(Class<T> clazz) {
return context.getBean(clazz);
}
}
可以看到,该类拥有一个静态属性 context
,并且该类实现了 ApplicationContextAware
接口。所以在 Spring 初始化完成后会调用 setApplicationContext()
方法给 context
赋值,而在使用时直接调用静态方法 getBean()
,然后通过持有的 context
属性即可拿到所需要的 Bean。
针对该方案的完整 Spring Boot 示例工程代码已提交至 GitHub,欢迎查阅。
另一种解决方案是基于 Hibernate Integrator 来实现的。因为 JPA 是基于 Hibernate 实现的,所以在 Hibernate 层做拦截也能达到想要的效果。
首先,我们需要在 Spring Boot 工程的 application.yaml
配置文件中指定一个自定义的 Hibernate Integrator Provider。
spring:
jpa:
properties:
hibernate:
integrator_provider: com.example.demo.integrator.HibernateIntegratorProvider
然后在 HibernateIntegratorProvider
中编写一个 HibernateIntegrator
,并实现 Integrator
接口的 integrate()
方法。可以看到,我们在 integrate()
方法中添加了自定义的监听器 HibernateListener
,以监听实体的插入后、更新后、删除后动作。
package com.example.demo.integrator;
public class HibernateIntegratorProvider implements IntegratorProvider {
@Override
public List<Integrator> getIntegrators() {
return List.of(new HibernateIntegrator());
}
static class HibernateIntegrator implements Integrator {
@Override
public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) {
EventListenerRegistry registry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
if (null != registry) {
registry.appendListeners(EventType.POST_INSERT, new HibernateListener());
registry.appendListeners(EventType.POST_UPDATE, new HibernateListener());
registry.appendListeners(EventType.POST_DELETE, new HibernateListener());
}
}
@Override
public void disintegrate(SessionFactoryImplementor implementor, SessionFactoryServiceRegistry registry) {
}
}
}
最后,在 HibernateListener
类中即可以编写我们的捕获和记录的逻辑了:
package com.example.demo.listener;
public class HibernateListener implements PostInsertEventListener, PostUpdateEventListener, PostDeleteEventListener {
@Override
public void onPostInsert(PostInsertEvent event) {
Object entity = event.getEntity();
Object entityId = event.getId();
saveOperationLog(entity, entityId, "INSERT");
}
@Override
public void onPostUpdate(PostUpdateEvent event) {
Object entity = event.getEntity();
Object entityId = event.getId();
saveOperationLog(entity, entityId, "UPDATE");
}
@Override
public void onPostDelete(PostDeleteEvent event) {
Object entity = event.getEntity();
Object entityId = event.getId();
saveOperationLog(entity, entityId, "DELETE");
}
@Override
public boolean requiresPostCommitHandling(EntityPersister entityPersister) {
return false;
}
private void saveOperationLog(Object entity, Object entityId, String operation) {
// get repository
OperationLogRepository repository = SpringContextHolder.getBean(OperationLogRepository.class);
// save
OperationLog log = new OperationLog();
// log.setXxx();
repository.save(log);
}
}
可以看到,上述 HibernateListener
类实现了插入后、更新后、删除后三个 Event Listener 接口,然后在实体插入后、更新后、删除后调用 saveOperationLog()
方法将需要记录的字段保存到 operation_log
表。
需要注意的是:这里获取 OperationLogRepository
时,依然需要使用 SpringContextHolder
工具类的 getBean()
静态方法。这是因为,该方案与上一种方案类似,HibernateIntegratorProvider
是通过反射实例化的,HibernateListener
是直接 new
出来的,无法在其中直接使用 @Autowired
注入所需要的 Spring Bean。
针对该解决方案的完整 Spring Boot 示例工程代码也已提交至 GitHub,欢迎查看。
上述两种方案都是在应用层实现的,好处是不用修改数据库,坏处是有代码侵入。
最后一种方案即是以直接在数据库创建 Trigger 的方式实现表操作的捕获。这种方案简单直接,好处是可以捕获到越过应用层而直接操作数据库的表操作,坏处是给数据库带来一定的性能开销。
如下即是对应 MySQL 数据库的示例 TRIGGER
语句:
DELIMITER //
-- INSERT
CREATE TRIGGER USER_INSERT_TRIGGER
AFTER INSERT ON user
FOR EACH ROW
BEGIN
INSERT INTO operation_log (
entity,
entity_id,
operation,
operated_at
) VALUES (
'User',
NEW.ID,
'INSERT',
NOW()
);
END//
-- UPDATE ...
-- DELETE ...
DELIMITER ;
可以看到,我们使用上述 SQL 语句为 user
表创建了一个插入后 TRIGGER
。这样,user
表在数据插入后,即会触发该 TRIGGER
的执行,进而将需要捕获的字段写入 operation_log
表。因 MySQL 不支持使用一个 TRIGGER
监听多个操作,所以再补充两条更新后、删除后 TRIGGER
语句,即可以实现与上述两种方案同样的功能。
针对该方案的完整 SQL 语句也已提交至 GitHub,欢迎参阅。
综上,本文针对使用 JPA 的 Spring Boot 工程,提出如何捕获实体的增删改操作的问题,进而列出了三种可用的解决方案。在实际项目开发中,若有遇到相似问题却不知如何解决的朋友,可以参考本文提供的思路来实现自己的具体需求。