12

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

Irakli
  • 973
  • 3
  • 19
  • 45
  • 1
    JPA does not provide a way to audit changes to specific properties. If you are using Hibernate as the JPA provider, you can write your own [Interceptor](https://docs.jboss.org/hibernate/core/3.6/javadocs/org/hibernate/Interceptor.html), implement the `onFlushDirty` method, inspect the fields to find which ones have changed and then generate an audit log. – manish Sep 16 '16 at 23:38
  • 1
    Can you give me example of Interceptor usage? I use Hibernate as JPA provider – Irakli Sep 18 '16 at 15:50
  • 1
    See [the official documentation](http://docs.jboss.org/hibernate/orm/current/userguide/html_single/Hibernate_User_Guide.html#events). – manish Sep 19 '16 at 03:05
  • I started thinking about Custom Anotations is Spring. But coudn't figured out how to properly get old and new instance there. For example I create anotation @CaptureChange that will start the process of capturing change s in my exact way. is this actually good idea at all for my solution? – Irakli Sep 22 '16 at 18:03
  • @JONIVar As seen form my answer below custom annotation can be processed with AOP (Spring AOP or AspecJ compile time). This approach little bit more complicated than Hibernate Interceptors but it is more flexible solution with no performance overhead. – Sergey Bespalov Sep 23 '16 at 02:55

5 Answers5

9

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

Sergey Bespalov
  • 1,746
  • 1
  • 12
  • 29
  • 1
    @JarrodRoberson if you downvote all the bad answers then It will be fair to upvote good answers. – Sergey Bespalov Sep 20 '16 at 02:22
  • 1
    I was commenting on the quality of the attempt, I not sure it is actually useful or correct, but it is better than the one liner comments as answers you were doing, I am trying to give you some credit for the improvement. All that said, this is a **too broad** question that is basically **send me teh codez** which is the worst kind of question. It also falls foul of **off-topic: recommendations** so I would normally down vote all the answers to discourage anyone answering stuff like this. The bounty is the only reason this is still open. –  Sep 20 '16 at 02:24
  • @Sergey Bespalov -can you give me the example how to transfer the old and new object to that anotation? – Irakli Sep 23 '16 at 06:45
  • As I understand your goal is not Old and New object versions, you want to populate `Action` object with information about property change. My example already show how you can create `Action` object with required information in it. – Sergey Bespalov Sep 23 '16 at 07:22
  • @SergeyBespalov yes you are right, but how should I pass Object target, Object newValue to before function? – Irakli Sep 23 '16 at 17:46
  • AspectJ will make it for you. I updated the answer to show how to enable AspectJ in your project. – Sergey Bespalov Sep 26 '16 at 09:51
  • @JONIVar is it helps you? Which approach you decide to use in the end? – Sergey Bespalov Sep 28 '16 at 02:49
  • It didn't worke actually. I receive such error message: Warning:(12, 0) ajc: no match for this type name: AopAudit [Xlint:invalidAbsoluteTypeName] ///// Warning:(14, 0) ajc: advice defined in ge.shemo.config.FieldAuditAspect has not been applied [Xlint:adviceDidNotMatch] – Irakli Oct 18 '16 at 20:19
  • @Purmarili this `Warning` probably means that there is no `AopAudit` annotation classes in your class path. – Sergey Bespalov Oct 19 '16 at 02:08
  • @SergeyBespalov I have it my class path but anyway receive this message. Can you please provide full example of Spring + JPA + Audit functionality? – Irakli Oct 19 '16 at 18:47
  • 2
    @Purmarili [here](https://github.com/sbespalov/aop-aspectj-examples) is an axample – Sergey Bespalov Oct 20 '16 at 05:33
3

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.

Maciej Marczuk
  • 3,593
  • 29
  • 28
1

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

SEY_91
  • 1,615
  • 15
  • 26
1

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("test@example.com");
    entityManager.persist(client);
    entityManager.flush();
}

Will get the output like:

email was changed from null to test@example.com 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.

Liping Huang
  • 4,378
  • 4
  • 29
  • 46
1

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.

Prashant Katara
  • 95
  • 1
  • 1
  • 14