I have a class Client. I want to be able to audit the changes of property of this class(not entire class - just it's properties).
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
private Branch companyBranch;
actually this is very easy to audit the whole entity with @Audited annotation.
But what I want is to audit this changes using my class structure.
here is my desired result class:
public class Action {
private String fieldName;
private String oldValue;
private String newValue;
private String action;
private Long modifiedBy;
private Date changeDate;
private Long clientID;
the result should look like this:
fieldName + "was changed from " + oldValue + "to" + newValue + "for" clientID +"by" modifiedBy;
The reason I'm doing this is that I need to store this changes into DB under Action table - because I will be Auditing properties from different Entities and I want to store that together and then have a ability to get them when I need.
How can I do this?
Thanks
Aop is right way to go. You can use AspectJ with field set()
pointcut for your needs. With before
aspect you can extract necessary info to populate Action object.
Also you can use custom class Annotation @AopAudit
to detect classes you want to audit. You must define such annotation in your classpath and place in under target classes which you want to audit.
This approach can look like this:
AopAudit.java
@Retention(RUNTIME)
@Target(TYPE)
public @interface AopAudit {
}
Client.java
@AopAudit
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
}
AuditAnnotationAspect.aj
import org.aspectj.lang.reflect.FieldSignature;
import java.lang.reflect.Field;
public aspect FieldAuditAspect {
pointcut auditField(Object t, Object value): set(@(*.AopAudit) * *.*) && args(value) && target(t);
pointcut auditType(Object t, Object value): set(* @(*.AopAudit) *.*) && args(value) && target(t);
before(Object target, Object newValue): auditField(target, newValue) || auditType(target, newValue) {
FieldSignature sig = (FieldSignature) thisJoinPoint.getSignature();
Field field = sig.getField();
field.setAccessible(true);
Object oldValue;
try
{
oldValue = field.get(target);
}
catch (IllegalAccessException e)
{
throw new RuntimeException("Failed to create audit Action", e);
}
Action a = new Action();
a.setFieldName(sig.getName());
a.setOldValue(oldValue == null ? null : oldValue.toString());
a.setNewValue(newValue == null ? null : newValue.toString());
}
}
This is AspectJ aspect that define auditField
pointcut to capture field set operations and before
logic to create Audit
object.
To enable AspectJ Compile Time Weaving
you must do the following in case of Maven
:
pom.xml
...
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
</dependency>
</dependencies>
...
<plugins>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.6</version>
<configuration>
<showWeaveInfo>true</showWeaveInfo>
<source>${java.source}</source>
<target>${java.target}</target>
<complianceLevel>${java.target}</complianceLevel>
<encoding>UTF-8</encoding>
<verbose>false</verbose>
<XnoInline>false</XnoInline>
</configuration>
<executions>
<execution>
<id>aspectj-compile</id>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>aspectj-compile-test</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<dependencies>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${aspectj.version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>${aspectj.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
This Maven
configuration enables AspectJ compiler that makes bytecode post processing of your classes.
applicationContext.xml
<bean class="AuditAnnotationAspect" factory-method="aspectOf"/>
Also you may need to add aspect instance to Spring Application Context for dependency injection.
UPD: Here is an example of such AspectJ project configuration
If you are using Hibernate, you can use Hibernate Envers, and define your own RevisionEntity
(If you want to work with java.time
you will need Hibernate 5.x. In earlier versions even custom JSR-310 utilities won't work for auditing purposes)
If you are not using Hibernate or want to have pure JPA solution, then you will need to write your custom solution using JPA EntityListeners
mechanism.
I don't know exactly what "modifiedBy" attribute is(a user of the application or another Client?),but ignoring this one ,you can catch the modification of all the attributes in the setter
(Note: changing the setter implementation or adding others parameters to the setter are bad practice this work should be done using a LOGGER or AOP ):
public class Client {
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
private Branch companyBranch;
@OneToMany(cascade = CascadeType.ALL)
@JoinColumn("client_ID");
List<Action> actions = new ArrayList<String>();
public void setFirstName(String firstName,Long modifiedBy){
// constructor Action(fieldName, oldValue, newValue ,modifiedBy)
this.actions.add(new Action("firstName",this.firstName,firstName,modifiedBy));
this.firstName=firstName;
}
//the same work for lastName,email,mobileNumber,companyBranch
}
Note : The best and the correct solution is to use a LOGGER or AOP
AOP absulately is a solution for your case, I implemented the similar case with Spring AOP to keep the entity revisions. A point for this solution is need use around pointcut.
Another solution is use the org.hibernate.Interceptor
, the org.hibernate.EmptyInterceptor
should be the appropriate extension, I write some simple codes to simulate it(take your Client codes):
@Entity
public class Client {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstName;
private String lastName;
private String email;
private String mobileNumber;
// getter and setter
}
The Interceptor
implementation
public class StateChangeInterceptor extends EmptyInterceptor {
@Override
public boolean onFlushDirty(Object entity, Serializable id, Object[] currentState, Object[] previousState, String[] propertyNames, Type[] types) {
if (entity instanceof Client) {
for (int i = 0; i < propertyNames.length; i++) {
if (currentState[i] == null && previousState[i] == null) {
return false;
} else {
if (!currentState[i].equals(previousState[i])) {
System.out.println(propertyNames[i] + " was changed from " + previousState[i] + " to " + currentState[i] + " for " + id);
}
}
}
}
return true;
}
@Override
public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) {
return super.onSave(entity, id, state, propertyNames, types);
}
}
Register the inceptor, I'm using spring boot, so just add it into application.properties
spring.jpa.properties.hibernate.ejb.interceptor=io.cloudhuang.jpa.StateChangeInterceptor
Here is the test
@Test
public void testStateChange() {
Client client = new Client();
client.setFirstName("Liping");
client.setLastName("Huang");
entityManager.persist(client);
entityManager.flush();
client.setEmail("[email protected]");
entityManager.persist(client);
entityManager.flush();
}
Will get the output like:
email was changed from null to [email protected] for 1
So suppose it can be replaced with Action
objects.
And here is a opensource project JaVers - object auditing and diff framework for Java
JaVers is the lightweight Java library for auditing changes in your data.
You can take a look at this project.
I would prefer that you should override the equals method of your entity with the Audit property. And in the DAO you just compare the old instanceof entity with the new instance using the equals method which you have created inside entity.
You will be able to recognise whether this is auditable or not.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With