记录数据修改前后的变化

背景

追踪数据变化在各种审查场景中是一个常见的需求. 在这里我记录一个常见的错误实现, 并提供一个从基础到完整可用的框架实现. 在这里我选用 Hibernate 6.4.Final 作为框架依赖.

常见的错误实现

以下摘自 Java实现记录对象修改前后的变化 , 可见该方法明显存在以下问题:

  • 依赖暴力反射
  • 字段存在性能问题
  • 在现代 ORM 框架下会遇到大量常见的运行时错误
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

// 自定义注解
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ForUpdate {
String fieldName() default "";

/**
* 获取变更内容.
* @param newBean 更改前的Bean
* @param oldBean 更改后的Bean
*/
static <T> String getChangedFields(T newBean, T oldBean){
Field[] fields = newBean.getClass().getDeclaredFields();
StringBuilder builder = new StringBuilder();
for(Field field : fields) {
field.setAccessible(true);
if (field.isAnnotationPresent(ForUpdate.class)) {
try {
Object newValue = field.get(newBean);
Object oldValue = field.get(oldBean);
if(!Objects.equals(newValue, oldValue)) {
builder.append(field.getAnnotation(ForUpdate.class).fieldName()); //获取字段名称
builder.append(": 【更改前:");
builder.append(newValue);
builder.append(", 更改后:");
builder.append(oldValue);
builder.append("】\n");
}
} catch (Exception e) {
System.out.println(e);
}
}
}
return builder.toString();
}
}

雏形 - JPA

根据 JPA规范, ORM 必须支持实体生命回调及动态值刷新.

An update to the state of an entity includes both the assignment of a new value to a persistent property or field of the entity as well as the modification of a mutable value of a persistent property or field[26].

以下是根据规则1作的一个简易的模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.pancc.up.simple.entity;

import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;


/**
* @author Siweipancc on 2023/12/8
*/

@Entity
@Setter
@Getter
@Log4j2
public class Room {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

String name;

Integer size = 1;

Integer cups = 0;

@Version
Long version;

@PreUpdate
public void preUpdate() {
log.info("update, what properties?");
}
}

改进1 - 规则 2 的遵守

以上代码虽 HOOK 了更新生命回调, 但无法确定变动的字段以及旧值. 因此需要依赖 Hibernate 等现代 ORM 的脏值检测与更加深入的生命回调. 为 Hibernate 编写并声明如下 Service:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
package com.pancc.up.simple.spi;

import lombok.extern.slf4j.Slf4j;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.metamodel.mapping.AttributeMapping;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;


/**
* @author Siweipancc on 2023/12/8
*/
@Slf4j
public class MonitorIntegrator implements Integrator, PostUpdateEventListener {

@Override
public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor serviceRegistry) {
EventListenerRegistry eventListenerRegistry = serviceRegistry.getServiceRegistry().getService(EventListenerRegistry.class);
assert eventListenerRegistry != null;
eventListenerRegistry.appendListeners(EventType.POST_COMMIT_UPDATE, this);

}

@Override
public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {

}

@Override
public void onPostUpdate(PostUpdateEvent event) {
EntityPersister persister = event.getPersister();
Class<?> mappedClass = persister.getMappedClass();
log.info("entity update: {} ", mappedClass);

for (int idx : event.getDirtyProperties()) {
AttributeMapping attributeMapping = persister.getAttributeMapping(idx);
String attributeName = attributeMapping.getAttributeName();
Object oldValue = event.getOldState()[idx];
Object newValue = event.getState()[idx];
log.info("{} changed from: [{}] to [{}]", attributeName, oldValue, newValue);

}
}

@Override
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
}

以上代码完成了以下任务:

  • 实现了 Hibernate 的插件接口 Integrator 与 实体事件更新后接口 PostUpdateEventListener
  • 根据脏值获取到字段名与前后值

改进2 - 自定义信息

自定义注解

参考错误实现提供自定义信息, 根据 JPA 的字段访问规则 Access Type, 我们的注解需要支持两个位置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.pancc.up.simple.annotation;

import java.lang.annotation.*;

/**
* @author Siweipancc on 2023/12/10
*/
@Target({ElementType.FIELD, ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface LogProperty {

String value();
}

改造实体类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

package com.pancc.up.simple.entity;

import com.pancc.up.simple.annotation.LogProperty;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.log4j.Log4j2;


/**
* @author Siweipancc on 2023/12/8
*/

@Entity
@Setter
@Log4j2
@Getter
public class Room {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
@Getter
private Long id;

@LogProperty("名字")
@Getter
String name;

@Access(AccessType.PROPERTY)
Integer size = 1;

/**
* @see org.hibernate.property.access.spi.Getter#getMember()
*/
@LogProperty("大小")
public Integer getSize() {
return size;
}

@LogProperty("配套")
@Getter
Integer cups = 0;

@Version
Long version;
}

改造 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64

package com.pancc.up.simple.spi;

import com.pancc.up.simple.annotation.LogProperty;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.boot.Metadata;
import org.hibernate.boot.spi.BootstrapContext;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.integrator.spi.Integrator;
import org.hibernate.metamodel.mapping.AttributeMapping;
import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.service.spi.SessionFactoryServiceRegistry;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.Optional;


/**
* @author Siweipancc on 2023/12/8
*/
@Slf4j
public class MonitorIntegrator implements Integrator, PostUpdateEventListener {

@Override
public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor serviceRegistry) {
EventListenerRegistry eventListenerRegistry = serviceRegistry.getServiceRegistry().getService(EventListenerRegistry.class);
assert eventListenerRegistry != null;
eventListenerRegistry.appendListeners(EventType.POST_COMMIT_UPDATE, this);
}

@Override
public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) {
}

@Override
public void onPostUpdate(PostUpdateEvent event) {
EntityPersister persister = event.getPersister();
Class<?> mappedClass = persister.getMappedClass();
log.info("entity update: {} ", mappedClass);

for (int idx : event.getDirtyProperties()) {
AttributeMapping attributeMapping = persister.getAttributeMapping(idx);
String attributeName = attributeMapping.getAttributeName();
Object oldValue = event.getOldState()[idx];
Object newValue = event.getState()[idx];
Member member = attributeMapping.getPropertyAccess().getGetter().getMember();
String msg = Optional.ofNullable(member).filter(AnnotatedElement.class::isInstance).map(AnnotatedElement.class::cast)
.map(x -> x.getAnnotation(LogProperty.class)).map(LogProperty::value).orElse(attributeName);
log.info("{}({}) changed from: [{}] to [{}]", attributeName, msg, oldValue, newValue);

}
}

@Override
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
}

至此, 我们可以根据注解在字段或者方法上动态地获得自定义信息.

改进3 - 融合 IOC 框架

对于 IOC 环境, 为了更好的融合与使用自动依赖注入特性, 例如 Spring , 我们将 Service 声明文件删除, 删除 Integrator 接口声明, 因此以上的 Service 代码可变为如下:

改造 Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.pancc.up.simple.spi;

import com.pancc.up.simple.annotation.LogProperty;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.event.spi.PostUpdateEvent;
import org.hibernate.event.spi.PostUpdateEventListener;
import org.hibernate.metamodel.mapping.AttributeMapping;
import org.hibernate.persister.entity.EntityPersister;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Member;
import java.util.Optional;


/**
* @author Siweipancc on 2023/12/8
*/
@Slf4j
public class MonitorIntegrator implements PostUpdateEventListener {

@Override
public void onPostUpdate(PostUpdateEvent event) {
EntityPersister persister = event.getPersister();
Class<?> mappedClass = persister.getMappedClass();
log.info("entity update: {} ", mappedClass);

for (int idx : event.getDirtyProperties()) {
AttributeMapping attributeMapping = persister.getAttributeMapping(idx);
String attributeName = attributeMapping.getAttributeName();
Object oldValue = event.getOldState()[idx];
Object newValue = event.getState()[idx];
Member member = attributeMapping.getPropertyAccess().getGetter().getMember();
String msg = Optional.ofNullable(member).filter(AnnotatedElement.class::isInstance).map(AnnotatedElement.class::cast)
.map(x -> x.getAnnotation(LogProperty.class)).map(LogProperty::value).orElse(attributeName);
log.info("{}({}) changed from: [{}] to [{}]", attributeName, msg, oldValue, newValue);

}
}

@Override
public boolean requiresPostCommitHandling(EntityPersister persister) {
return true;
}
}

增加配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package com.pancc.up.simple.spi;

import jakarta.persistence.EntityManagerFactory;
import org.hibernate.engine.spi.SessionFactoryImplementor;
import org.hibernate.event.service.spi.EventListenerRegistry;
import org.hibernate.event.spi.EventType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* @author Siweipancc on 2023/12/10
*/
@Configuration
public class MConfig {
@Configuration
static class config {
@Bean
public MonitorIntegrator monitorIntegrator() {
return new MonitorIntegrator();
}
}

@Autowired
public void monitorIntegrator(MonitorIntegrator monitorIntegrator, EntityManagerFactory entityManagerFactory) {
SessionFactoryImplementor sessionFactory = entityManagerFactory.unwrap(SessionFactoryImplementor.class);
EventListenerRegistry eventListenerRegistry = sessionFactory.getServiceRegistry().getService(EventListenerRegistry.class);
assert eventListenerRegistry != null;
eventListenerRegistry.appendListeners(EventType.POST_COMMIT_UPDATE, monitorIntegrator);
}
}

改造效果

1
2
3
4
2023-12-10T23:48:38.719+08:00  INFO 21480 --- [nio-8080-exec-4] c.pancc.up.simple.spi.MonitorIntegrator  : entity update: class com.pancc.up.simple.entity.Room 
2023-12-10T23:48:38.719+08:00 INFO 21480 --- [nio-8080-exec-4] c.pancc.up.simple.spi.MonitorIntegrator : cups(配套) changed from: [3] to [5]
2023-12-10T23:48:38.719+08:00 INFO 21480 --- [nio-8080-exec-4] c.pancc.up.simple.spi.MonitorIntegrator : size(大小) changed from: [2] to [4]
2023-12-10T23:48:38.719+08:00 INFO 21480 --- [nio-8080-exec-4] c.pancc.up.simple.spi.MonitorIntegrator : version(version) changed from: [20] to [21]

注意

追踪值时, 应注意值的类型, 序列化值的尽量为 Basic 类型, 日志也按照单表思想记录

额外资料

Hibernate_Integration

JPA3.1