Implementing Full Data Change Auditing with Spring AOP and Custom Annotations
This article explains how to achieve non‑intrusive, efficient full‑record data change auditing in a Spring‑based backend by leveraging Aspect‑Oriented Programming, custom annotations, and reflection to automatically capture before‑and‑after values for both primary and related tables.
1. Background
In the field of administrative enforcement, the State Council requires a full‑record system for all enforcement actions, meaning every insert, update, or delete of enforcement data must be logged with timestamps, operators, and change details. The challenge is to implement this full audit in a non‑intrusive, reusable way.
Problems with a naïve approach
Each business table would need additional logic for change logging, increasing development effort.
The added logic breaks the principle of high cohesion and low coupling.
The change‑logging functionality cannot be reused across other business modules.
Therefore, a simple CRUD‑style solution lacks technical value. The article proposes an elegant, non‑intrusive method.
2. Overall Idea
Functionality: Record change time, content, and operator for every field of a business table.
Reusability: Decouple from business logic so it can be applied to any table.
Non‑intrusiveness: Do not affect existing CRUD operations.
The core idea is to capture the state before and after a data‑modifying method runs. Using Aspect‑Oriented Programming (AOP) provides the required non‑intrusive entry point.
AOP extracts cross‑cutting concerns into independent aspects that are woven into the business flow at designated join points.
3. Prerequisite Knowledge
3.1 Spring AOP
Spring AOP is a core technology that allows common behavior to be modularized into aspects, reducing repetitive code and improving reusability. It also prevents code scattering and high coupling when many non‑business requirements are added.
3.2 Custom Annotations
Custom annotations act as markers on classes, methods, or fields. By defining an annotation, placing it on target elements, and processing it at compile‑time or runtime, developers can trigger special logic without modifying the original code.
@Target specifies where the annotation can be applied (e.g., classes, methods, fields). @Retention defines the lifecycle of the annotation (SOURCE, CLASS, or RUNTIME). @Documented marks the annotation for inclusion in Javadoc. @Inherited allows the annotation to be inherited by subclasses.
4. Defining Annotations
4.1 Annotation for Changeable Fields
@Retention(RUNTIME)
public @interface RevisionComment {
String value() default "";
}4.2 Annotation for Changeable Methods
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface EgovaRevision {
RevisionEnum type();
RevisionEntity[] value();
}4.3 Annotation for Change Parameters
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({})
public @interface RevisionEntity {
Class
javaType() default void.class;
String paramKey() default "";
String relKey() default ""; // for one‑to‑many related tables
boolean primary() default false; // indicates the main table
}5. Using the Annotations
5.1 Marking Tables and Fields
@Table(name = "ci_pun_case")
@RevisionComment("Case Basic Information")
public class CiPunCase {
@Id
@Column(name = "case_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long caseId;
@Column(name = "doc_code")
@RevisionComment("Case Document Code")
private String docCode;
@Column(name = "case_type")
@RevisionComment("Case Type")
private String caseType;
}5.2 Marking Methods that Modify Data
@Override
@EgovaRevision(
type = "Administrative Penalty Change",
value = {
@RevisionEntity(javaType = CiPunCase.class, paramKey = "ciPunCase.caseId", primary = true),
@RevisionEntity(javaType = CiPunCause.class, relKey = "caseId")
}
)
public ResultInfo saveCaseBasicInfo(HumanSession humanSession, CiPunCase ciPunCase, String caseCauseJson) {
ResultInfo resultInfo = new ResultInfo(true);
// do something...
return resultInfo;
}6. Implementing the Aspect
/**
* Data change auditing aspect
*/
@Aspect
@Component
public class RevisionAspect implements ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(RevisionAspect.class);
@Pointcut("@annotation(com.egova.common.utils.revision.EgovaRevision)")
public void revisionPointCut() {}
@Around(value = "revisionPointCut() && @annotation(egovaRevision)")
public Object around(ProceedingJoinPoint joinPoint, EgovaRevision egovaRevision) throws Throwable {
RevisionLog log = new RevisionLog();
RevisionEnum revisionType = egovaRevision.type();
RevisionEntity[] entities = egovaRevision.value();
List
revs = new ArrayList<>(entities.length);
boolean success = true;
try {
for (RevisionEntity entity : entities) {
RevisionBean rev = new RevisionBean();
rev.setRevisionInfo(entity);
rev.setBeforeUpdate(getValue(rev, jdbcTemplate));
revs.add(rev);
}
} catch (Exception e) {
logger.error("Error during pre‑audit", e);
success = false;
}
Object result = joinPoint.proceed();
if (!success) return result;
try {
for (RevisionBean rev : revs) {
rev.setAfterUpdate(getValue(rev, jdbcTemplate));
}
log.setChangeDetail(changeDetail(revs));
saveChangeLog(log);
} catch (Exception e) {
logger.error("Error during post‑audit", e);
}
return result;
}
}The aspect uses three key techniques:
Define EgovaRevision as the pointcut annotation, so any method annotated with it becomes a join point.
Use reflection to obtain the target entity class and field information from the custom annotations.
Apply @Around advice to execute logic before and after the original method, allowing capture of before‑and‑after values.
7. Auditing Changes in Related Tables
To record changes across a main table and its related tables, the RevisionEntity annotation is extended with relKey (the foreign‑key field) and primary (identifies the main table). By setting primary = true for the main entity and providing relKey for child entities, the aspect can audit both in a single operation.
8. Presenting Change Information
After persisting audit records, a front‑end comparison component visualizes the differences, offering a complete back‑to‑front solution for administrative penalty data changes.
9. Conclusion
The presented technique decouples audit logic from business code, making it applicable to a wide range of scenarios such as system operation logs, personnel file updates, and business registration changes. By leveraging Spring AOP and custom annotations, developers can avoid repetitive boiler‑plate and achieve consistent, non‑intrusive data change tracking.
Zhengtong Technical Team
How do 700+ nationwide projects deliver quality service? What inspiring stories lie behind dozens of product lines? Where is the efficient solution for tens of thousands of customer needs each year? This is Zhengtong Digital's technical practice sharing—a bridge connecting engineers and customers!
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.