Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Audit Property change - Spring MVC+ JPA

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;

  • mobileNumber was changed from 555 to 999 for Bill Gates by George.

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

like image 795
Irakli Avatar asked Aug 29 '16 14:08

Irakli


5 Answers

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

like image 98
Sergey Bespalov Avatar answered Sep 29 '22 20:09

Sergey Bespalov


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.

like image 32
Maciej Marczuk Avatar answered Oct 02 '22 20:10

Maciej Marczuk


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

like image 40
SEY_91 Avatar answered Sep 29 '22 20:09

SEY_91


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.

like image 24
Liping Huang Avatar answered Oct 01 '22 20:10

Liping Huang


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.

like image 32
Prashant Katara Avatar answered Sep 28 '22 20:09

Prashant Katara