Every now and then we come across a common requirement of building an Audit framework. In simple words it means whenever a record is added, edited or removed there should be an entry made in a separate table so that different actions of users can be monitored. Today in this tutorial we will build an audit framework in Java using Spring AOP and JPA. Records will be saved in xml form. We will reuse code from JPA OneToMany Mapping Using Hibernate and MySQL tutorial.
Our database schema consists of BookShelf, Book and Audit.
First we will setup our maven dependency (Need help creating Maven Java Project in Eclipse ?)
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>AuditFrameWork</groupId> <artifactId>AuditFrameWork</artifactId> <version>0.0.1-SNAPSHOT</version> <build> <plugins> <plugin> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-clean-plugin</artifactId> <version>2.5</version> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aop</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>cglib</groupId> <artifactId>cglib</artifactId> <version>2.2.2</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.6.11</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.21</version> </dependency> <dependency> <groupId>org.hibernate.javax.persistence</groupId> <artifactId>hibernate-jpa-2.0-api</artifactId> <version>1.0.1.Final</version> </dependency> <dependency> <groupId>com.thoughtworks.xstream</groupId> <artifactId>xstream</artifactId> <version>1.4.2</version> </dependency> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.6</version> </dependency> <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2</version> </dependency> <dependency> <groupId>commons-beanutils</groupId> <artifactId>commons-beanutils</artifactId> <version>1.8.3</version> </dependency> <dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>1.0</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-core</artifactId> <version>${hibernate.version}</version> </dependency> <dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-entitymanager</artifactId> <version>${hibernate.version}</version> <exclusions> <exclusion> <groupId>cglib</groupId> <artifactId>cglib</artifactId> </exclusion> <exclusion> <groupId>dom4j</groupId> <artifactId>dom4j</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <properties> <hibernate.version>3.6.7.Final</hibernate.version> <spring.version>3.1.2.RELEASE</spring.version> </properties> </project>
Second we will start with our JPA mapping code. There are few important classes and interfaces involved.
- IEntity – A marker interface to depict an database entity
- IXMLConvertable – As name suggests this interface let’s conversion from Java to XML generic
- IAuditable – A marker interface to depict an entity which can be auditable
Let’s start building our framework.
IAuditable
package com.mumz.test.audit.interfaces; import java.io.Serializable; /** * The Interface IAuditable. * * @author prabhat.jha */ public interface IAuditable extends Serializable{ /** The Constant OPERATION_INSERT. */ public static final String OPERATION_INSERT = "Insert"; /** The Constant OPERATION_UPDATE. */ public static final String OPERATION_UPDATE = "Update"; /** The Constant OPERATION_DELETE. */ public static final String OPERATION_DELETE = "Delete"; }
IEntity
package com.mumz.test.audit.interfaces; import java.io.Serializable; /** * The Interface IEntity. * @author prabhat.jha */ public interface IEntity extends Serializable{ }
IXMLConvertable
package com.mumz.test.audit.interfaces; import java.io.Serializable; import java.util.List; import java.util.Map; /** * The Interface IXMLConvertable. * @author prabhat.jha */ public interface IXMLConvertable extends Serializable { /** * Gets the class alias. * * @return the class alias */ public String getClassAlias(); /** * Gets the field to be omitted. * * @return the field to be omitted */ public List<String> getFieldsToBeOmitted(); /** * Gets the field aliases. * * @return the field aliases */ public Map<String, String> getFieldAliases(); }
With the basic structure of our code base setup we will reuse the code from this post. Explanation on OneToMany can be obtained from the link.
Book.java
package com.mumz.test.audit.beans; import java.util.List; import java.util.Map; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.JoinColumn; import javax.persistence.ManyToOne; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.Table; import javax.persistence.Transient; import com.mumz.test.audit.interfaces.IAuditable; import com.mumz.test.audit.interfaces.IEntity; import com.mumz.test.audit.interfaces.IXMLConvertable; /** * The Class Book. * * @author prabhat.jha */ @Entity @Table(name = "BOOK") @NamedQueries({ @NamedQuery(name = "fetchAllBooks", query = "SELECT ALLBOOKS FROM Book ALLBOOKS") }) public class Book implements IXMLConvertable, IEntity, IAuditable { /** The Constant serialVersionUID. */ private static final long serialVersionUID = -4788522141255171404L; /** The id. */ private Long id = null; /** The name. */ private String name = null; /** The author. */ private String author = null; /** The book shelf. */ private BookShelf bookShelf = null; /** * Gets the id. * * @return the id */ @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name = "BOOK_ID") public Long getId() { return id; } /** * Sets the id. * * @param id * the id to set */ public void setId(Long id) { this.id = id; } /** * Gets the name. * * @return the name */ @Column(name = "BOOK_NAME") public String getName() { return name; } /** * Sets the name. * * @param name * the name to set */ public void setName(String name) { this.name = name; } /** * Gets the author. * * @return the author */ @Column(name = "BOOK_AUTHOR") public String getAuthor() { return author; } /** * Sets the author. * * @param author * the author to set */ public void setAuthor(String author) { this.author = author; } /** * Gets the book shelf. * * @return the bookShelf */ @ManyToOne @JoinColumn(name = "BOOK_SHELF_ID") public BookShelf getBookShelf() { return bookShelf; } /** * Sets the book shelf. * * @param bookShelf * the bookShelf to set */ public void setBookShelf(BookShelf bookShelf) { this.bookShelf = bookShelf; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getClassAlias() */ @Override @Transient public String getClassAlias() { return "Book"; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldsToBeOmitted() */ @Override @Transient public List<String> getFieldsToBeOmitted() { return null; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldAliases() */ @Override @Transient public Map<String, String> getFieldAliases() { return null; } /** * (non-Javadoc). * * @return the int * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((author == null) ? 0 : author.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } /** * (non-Javadoc). * * @param obj * the obj * @return true, if successful * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof Book)) { return false; } Book other = (Book) obj; if (author == null) { if (other.author != null) { return false; } } else if (!author.equals(other.author)) { return false; } if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } /** * (non-Javadoc). * * @return the string * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("Book [id=%s, name=%s, author=%s]", id, name, author); } }
BookShelf.java
package com.mumz.test.audit.beans; import java.util.List; import java.util.Map; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.NamedQueries; import javax.persistence.NamedQuery; import javax.persistence.OneToMany; import javax.persistence.Table; import javax.persistence.Transient; import com.mumz.test.audit.interfaces.IEntity; import com.mumz.test.audit.interfaces.IXMLConvertable; /** * The Class BookShelf. * @author prabhat.jha */ @Entity @Table(name = "BOOK_SHELF") @NamedQueries({ @NamedQuery(name="fetchAllBookShelves", query="SELECT ALLBOOKSHELVES FROM BookShelf ALLBOOKSHELVES") }) public class BookShelf implements IEntity, IXMLConvertable{ /** The Constant serialVersionUID. */ private static final long serialVersionUID = -7867320637075813912L; /** The id. */ private Long id = null; /** The name. */ private String name = null; /** The books. */ private List<Book> books = null; /** * Gets the id. * * @return the id */ @Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name="BOOK_SHELF_ID") public Long getId() { return id; } /** * Sets the id. * * @param id * the id to set */ public void setId(Long id) { this.id = id; } /** * Gets the name. * * @return the name */ @Column(name="BOOK_SHELF_NAME") public String getName() { return name; } /** * Sets the name. * * @param name * the name to set */ public void setName(String name) { this.name = name; } /** * Gets the books. * * @return the books */ @OneToMany(cascade=CascadeType.ALL, mappedBy="bookShelf") public List<Book> getBooks() { return books; } /** * Sets the books. * * @param books * the books to set */ public void setBooks(List<Book> books) { this.books = books; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getClassAlias() */ @Override @Transient public String getClassAlias() { return "BookShelf"; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldsToBeOmitted() */ @Override @Transient public List<String> getFieldsToBeOmitted() { return null; } /** (non-Javadoc) * @see com.mumz.test.audit.interfaces.IXMLConvertable#getFieldAliases() */ @Override @Transient public Map<String, String> getFieldAliases() { return null; } /** * (non-Javadoc). * * @return the int * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((books == null) ? 0 : books.hashCode()); result = prime * result + ((id == null) ? 0 : id.hashCode()); result = prime * result + ((name == null) ? 0 : name.hashCode()); return result; } /** * (non-Javadoc). * * @param obj * the obj * @return true, if successful * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof BookShelf)) { return false; } BookShelf other = (BookShelf) obj; if (books == null) { if (other.books != null) { return false; } } else if (!books.equals(other.books)) { return false; } if (id == null) { if (other.id != null) { return false; } } else if (!id.equals(other.id)) { return false; } if (name == null) { if (other.name != null) { return false; } } else if (!name.equals(other.name)) { return false; } return true; } /** * (non-Javadoc). * * @return the string * @see java.lang.Object#toString() */ @Override public String toString() { return String.format("BookShelf [id=%s, name=%s, books=%s]", id, name, books); } }
And our Audit bean which will hold pre and post image of record being inserted, updated.
Audit.java
package com.mumz.test.audit.beans; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.Table; import com.mumz.test.audit.interfaces.IEntity; /** * The Class Auditable. * * @author prabhat.jha */ @Entity @Table(name="AUDIT") public class Audit implements IEntity { /** The Constant serialVersionUID. */ private static final long serialVersionUID = 6161413362358931496L; /** The id. */ private Long id = null; /** The pre image. */ private String preImage = null; /** The post image. */ private String postImage = null; /** The operation. */ private String operation = null; @Id @GeneratedValue(strategy = GenerationType.AUTO) @Column(name="AUDIT_ID") public Long getId() { return id; } /** * Sets the id. * * @param id * the new id */ public void setId(Long id) { this.id = id; } /** * Sets the pre image. * * @param preImage * the new pre image */ public void setPreImage(String preImage) { this.preImage = preImage; } /** * Gets the pre image. * * @return the pre image */ @Column(name="AUDIT_PRE_IMAGE") public String getPreImage() { return this.preImage; } /** * Gets the post image. * * @return the post image */ @Column(name="AUDIT_POST_IMAGE") public String getPostImage() { return this.postImage; } /** * Sets the post image. * * @param postImage * the new post image */ public void setPostImage(String postImage) { this.postImage = postImage; } /** * Sets the operation. * * @param operation * the new operation */ public void setOperation(String operation) { this.operation = operation; } /** * Gets the operation. * * @return the operation */ @Column(name="AUDIT_OPERATION") public String getOperation() { return this.operation; } /** (non-Javadoc) * @see java.lang.Object#hashCode() */ @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((operation == null) ? 0 : operation.hashCode()); result = prime * result + ((postImage == null) ? 0 : postImage.hashCode()); result = prime * result + ((preImage == null) ? 0 : preImage.hashCode()); return result; } /** (non-Javadoc) * @see java.lang.Object#equals(java.lang.Object) */ @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (!(obj instanceof Audit)) { return false; } Audit other = (Audit) obj; if (operation == null) { if (other.operation != null) { return false; } } else if (!operation.equals(other.operation)) { return false; } if (postImage == null) { if (other.postImage != null) { return false; } } else if (!postImage.equals(other.postImage)) { return false; } if (preImage == null) { if (other.preImage != null) { return false; } } else if (!preImage.equals(other.preImage)) { return false; } return true; } /** (non-Javadoc) * @see java.lang.Object#toString() */ @Override public String toString() { return "Auditable [preImage=" + preImage + ", postImage=" + postImage + ", operation=" + operation + "]"; } }
Since we are trying to build a framework, all actions should be routed through a controller, hence we will build an interface IAuditController
and implementation class AuditControllerImpl
.
IAuditController
package com.mumz.test.audit.controller; import java.util.List; import com.mumz.test.audit.beans.Book; import com.mumz.test.audit.beans.BookShelf; /** * The Interface IAuditController. * @author prabhat.jha */ public interface IAuditController { /** * Adds the book. * * @param book * the book * @return the book */ public Book addBook(Book book); /** * Update book. * * @param book * the book * @return the book */ public Book updateBook(Book book); /** * Removes the book. * * @param book * the book * @return the book */ public Book removeBook(Book book); /** * Fetch all books. * * @return the list */ public List<Book> fetchAllBooks(); /** * Adds the book shelf. * * @param bookShelf * the book shelf * @return the book shelf */ public BookShelf addBookShelf(BookShelf bookShelf); /** * Update book shelf. * * @param bookShelf * the book shelf * @return the book shelf */ public BookShelf updateBookShelf(BookShelf bookShelf); /** * Removes the book shelf. * * @param bookShelf * the book shelf * @return the book shelf */ public BookShelf removeBookShelf(BookShelf bookShelf); /** * Fetch all book shelves. * * @return the list */ public List<BookShelf> fetchAllBookShelves(); }
AuditControllerImpl
package com.mumz.test.audit.controller; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Controller; import com.mumz.test.audit.beans.Book; import com.mumz.test.audit.beans.BookShelf; import com.mumz.test.audit.dao.IAuditPersistableService; import com.mumz.test.audit.dao.IAuditQueryService; /** * The Class AuditController. * * @author prabhat.jha */ @Controller(value="auditControllerImpl") public class AuditControllerImpl implements IAuditController { /** The book dao service. */ @Autowired private IAuditPersistableService auditPersistableService = null; /** The audit query service. */ @Autowired private IAuditQueryService auditQueryService = null; /** * Gets the audit persistable service. * * @return the audit persistable service */ public IAuditPersistableService getAuditPersistableService() { return auditPersistableService; } /** * Sets the audit persistable service. * * @param auditPersistableService * the new audit persistable service */ public void setAuditPersistableService( IAuditPersistableService auditPersistableService) { this.auditPersistableService = auditPersistableService; } /** * Gets the audit query service. * * @return the audit query service */ public IAuditQueryService getAuditQueryService() { return auditQueryService; } /** * Sets the audit query service. * * @param auditQueryService * the new audit query service */ public void setAuditQueryService(IAuditQueryService auditQueryService) { this.auditQueryService = auditQueryService; } /** * (non-Javadoc) * * @see * com.mumz.test.audit.controller.IAuditController#addBook(com.mumz.test. * audit.beans.Book) */ @Override public Book addBook(Book book) { return this.getAuditPersistableService().addEntity(book); } /** (non-Javadoc) * @see com.mumz.test.audit.controller.IAuditController#updateBook(com.mumz.test.audit.beans.Book) */ @Override public Book updateBook(Book book) { return this.getAuditPersistableService().updateEntity(book); } /** * (non-Javadoc) * * @see * com.mumz.test.audit.controller.IAuditController#removeBook(com.mumz.test * .audit.beans.Book) */ @Override public Book removeBook(Book book) { return this.getAuditPersistableService().removeEntity(book); } /** * (non-Javadoc) * * @see com.mumz.test.audit.controller.IAuditController#fetchAllBooks() */ @SuppressWarnings("unchecked") @Override public List<Book> fetchAllBooks() { return (List<Book>) this.getAuditQueryService().fetchAllBooks(); } /** * (non-Javadoc) * * @see * com.mumz.test.audit.controller.IAuditController#addBookShelf(com.mumz. * test.audit.beans.BookShelf) */ @Override public BookShelf addBookShelf(BookShelf bookShelf) { return this.getAuditPersistableService().addEntity(bookShelf); } /** (non-Javadoc) * @see com.mumz.test.audit.controller.IAuditController#updateBookShelf(com.mumz.test.audit.beans.BookShelf) */ public BookShelf updateBookShelf(BookShelf bookShelf) { return this.getAuditPersistableService().updateEntity(bookShelf); } /** * (non-Javadoc) * * @see * com.mumz.test.audit.controller.IAuditController#removeBookShelf(com.mumz * .test.audit.beans.BookShelf) */ @Override public BookShelf removeBookShelf(BookShelf bookShelf) { return this.getAuditPersistableService().removeEntity(bookShelf); } /** * (non-Javadoc) * * @see com.mumz.test.audit.controller.IAuditController#fetchAllBookShelves() */ @SuppressWarnings("unchecked") @Override public List<BookShelf> fetchAllBookShelves() { return (List<BookShelf>) this.getAuditQueryService() .fetchAllBookShelves(); } }
Next we will implement our dao layer. IAuditPersistableService
will be used when we are doing any add or update where as IAuditQueryService
will be used fetching records.
IAuditPersistableService.java
package com.mumz.test.audit.dao; import com.mumz.test.audit.interfaces.IEntity; /** * The Interface IAuditPersistableService. * * @author prabhat.jha */ public interface IAuditPersistableService { /** * Adds the book. * * @param entityToBeAdded * the book * @return the book */ public <T extends IEntity> T addEntity(T entityToBeAdded); /** * Update book. * * @param entityToBeUpdated * the book * @return the book */ public <T extends IEntity> T updateEntity(T entityToBeUpdated); /** * Removes the book. * * @param entityToBeRemoved * the book * @return the book */ public <T extends IEntity> T removeEntity(T entityToBeRemoved); }
AuditPersistableServiceImpl.java
package com.mumz.test.audit.dao; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.mumz.test.audit.interfaces.IEntity; /** * The Class AuditPersistableServiceImpl. * @author prabhat.jha */ @Transactional @Service public class AuditPersistableServiceImpl implements IAuditPersistableService { /** The entity manager. */ @PersistenceContext private EntityManager entityManager = null; /** * Gets the entity manager. * * @return the entity manager */ public EntityManager getEntityManager() { return entityManager; } /** * Sets the entity manager. * * @param entityManager the new entity manager */ public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } /** (non-Javadoc) * @see com.mumz.test.audit.dao.IAuditPersistableService#addBook(com.mumz.test.audit.beans.Book) */ @Override public <T extends IEntity> T addEntity(T entityToBeAdded) { return this.getEntityManager().merge(entityToBeAdded); } /** (non-Javadoc) * @see com.mumz.test.audit.dao.IAuditPersistableService#removeBook(com.mumz.test.audit.beans.Book) */ @Override public <T extends IEntity> T removeEntity(T entityToBeRemoved) { //Un-tested code, I think JPA doesn't allow direct remove, first a fetch and then subsequent remove is required. this.getEntityManager().remove(entityToBeRemoved); return entityToBeRemoved; } /** (non-Javadoc) * @see com.mumz.test.audit.dao.IAuditPersistableService#updateBook(com.mumz.test.audit.beans.Book) */ @Override public <T extends IEntity> T updateEntity(T entityToBeUpdated) { return this.getEntityManager().merge(entityToBeUpdated); } }
IAuditQueryService
package com.mumz.test.audit.dao; import java.util.List; import com.mumz.test.audit.interfaces.IEntity; /** * The Interface IAuditQueryService. * @author prabhat.jha */ public interface IAuditQueryService { /** * Fetch all books. * * @return the list */ public List<? extends IEntity> fetchAllBooks(); /** * Fetch all book shelves. * * @return the list */ public List<? extends IEntity> fetchAllBookShelves(); }
AuditQueryServiceImpl.java
package com.mumz.test.audit.dao; import java.util.Collections; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import javax.persistence.Query; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.mumz.test.audit.beans.Book; import com.mumz.test.audit.beans.BookShelf; /** * The Class AuditQueryServiceImpl. * @author prabhat.jha */ @Transactional(readOnly = true) @Service public class AuditQueryServiceImpl implements IAuditQueryService { /** The entity manager. */ @PersistenceContext private EntityManager entityManager = null; /** * Gets the entity manager. * * @return the entity manager */ public EntityManager getEntityManager() { return entityManager; } /** * Sets the entity manager. * * @param entityManager * the new entity manager */ public void setEntityManager(EntityManager entityManager) { this.entityManager = entityManager; } /** (non-Javadoc) * @see com.mumz.test.audit.dao.IAuditPersistableService#fetchAllBooks() */ @SuppressWarnings("unchecked") @Override public List<Book> fetchAllBooks() { try { Query namedQuery = this.getEntityManager().createNamedQuery("fetchAllBooks"); return namedQuery.getResultList(); } catch (Exception e) { e.printStackTrace(); } return Collections.emptyList(); } /** (non-Javadoc) * @see com.mumz.test.audit.dao.IAuditPersistableService#fetchAllBookShelves() */ @SuppressWarnings("unchecked") @Override public List<BookShelf> fetchAllBookShelves() { try { Query namedQuery = this.getEntityManager().createNamedQuery("fetchAllBookShelves"); return namedQuery.getResultList(); } catch (Exception e) { e.printStackTrace(); } return null; } }
Next is our Audit Advice which actually does the audit entry.
AuditAdvice.java
package com.mumz.test.audit.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import com.mumz.test.audit.beans.Audit; import com.mumz.test.audit.dao.IAuditPersistableService; import com.mumz.test.audit.interfaces.IAuditable; import com.mumz.test.audit.interfaces.IXMLConvertable; import com.mumz.test.audit.utils.XStreamUtils; /** * The Class BookAdvice. * * @author prabhat.jha */ @Aspect @Component public class AuditAdvice { /** The audit persistable service. */ @Autowired private IAuditPersistableService auditPersistableService = null; /** * Gets the audit persistable service. * * @return the audit persistable service */ public IAuditPersistableService getAuditPersistableService() { return auditPersistableService; } /** * Sets the audit persistable service. * * @param auditPersistableService * the new audit persistable service */ public void setAuditPersistableService( IAuditPersistableService auditPersistableService) { this.auditPersistableService = auditPersistableService; } /** * Around remove advice. * * @param pjp * the pjp */ @Around("execution(* com.mumz.test.audit.controller.AuditControllerImpl.*(..))") public void aroundAddBookShelfAdvice(ProceedingJoinPoint pjp) { String methodName = pjp.getSignature().getName(); String operation = null; if (methodName.toUpperCase().contains("ADD")) { operation = IAuditable.OPERATION_INSERT; } else if (methodName.toUpperCase().contains("UPDATE")) { operation = IAuditable.OPERATION_UPDATE; } else { operation = IAuditable.OPERATION_DELETE; } Object[] arguments = pjp.getArgs(); IXMLConvertable preImage = null; IXMLConvertable postImage = null; for (Object object : arguments) { if (!operation.equalsIgnoreCase(IAuditable.OPERATION_INSERT) && object instanceof IAuditable && object instanceof IXMLConvertable) { preImage = (IXMLConvertable) object; } } try { Object returnValue = pjp.proceed(); if (operation.equalsIgnoreCase(IAuditable.OPERATION_UPDATE) && returnValue instanceof IAuditable && returnValue instanceof IXMLConvertable) { postImage = (IXMLConvertable) returnValue; } } catch (Throwable e) { e.printStackTrace(); } if (preImage != null || postImage != null) { Audit auditableEntity = new Audit(); auditableEntity.setOperation(IAuditable.OPERATION_INSERT); auditableEntity.setPreImage(XStreamUtils.getXMLFromObject(preImage, preImage.getClass().getName(), preImage.getFieldAliases(), preImage.getFieldsToBeOmitted())); auditableEntity.setPostImage(XStreamUtils.getXMLFromObject( postImage, postImage.getClass().getName(), postImage.getFieldAliases(), postImage.getFieldsToBeOmitted())); this.getAuditPersistableService().addEntity(auditableEntity); } } }
Next is our XStreamUtils which is a generic code and converts given IXMLConvertable object and returns a XML representation of it.
XStreamUtils.java
package com.mumz.test.audit.utils; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import com.thoughtworks.xstream.XStream; import com.thoughtworks.xstream.io.xml.DomDriver; /** * The Class XStreamUtils. * * @author prabhat.jha */ public class XStreamUtils { /** * Gets the xML from object. * * @param toBeConverted * the to be converted * @param classNameAlias * the class name alias * @param fieldAlias * the field alias * @param fieldsToBeOmitted * the fields to be omitted * @return the xML from object */ public static String getXMLFromObject(Object toBeConverted, String classNameAlias, Map<String, String> fieldAlias, List<String> fieldsToBeOmitted) { StringBuilder objectAsXML = new StringBuilder(); if (toBeConverted != null) { XStream xStream = new XStream(new DomDriver()); if (StringUtils.isNotEmpty(classNameAlias)) { xStream.alias(classNameAlias, toBeConverted.getClass()); } if (fieldAlias != null && !fieldAlias.isEmpty()) { for (Entry<String, String> entry : fieldAlias.entrySet()) { xStream.aliasField(entry.getKey(), toBeConverted.getClass(), entry.getValue()); } } if (CollectionUtils.isNotEmpty(fieldsToBeOmitted)) { for (String fieldToBeOmitted : fieldsToBeOmitted) { xStream.omitField(toBeConverted.getClass(), fieldToBeOmitted); } } objectAsXML.append(xStream.toXML(toBeConverted)); } return objectAsXML.toString(); } }
Next is our spring configuration file.
audit-bean-definition.xml
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation= "http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd"> <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> <property name="location"> <value>jdbc.properties</value> </property> </bean> <bean class="org.springframework.orm.jpa.support.PersistenceAnnotationBeanPostProcessor" /> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>${jdbc.driverClassName}</value> </property> <property name="url"> <value>${jdbc.url}</value> </property> <property name="username"> <value>${jdbc.username}</value> </property> <property name="password"> <value>${jdbc.password}</value> </property> </bean> <bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="database"> <value>${jdbc.databaseVendor}</value> </property> <property name="showSql" value="false" /> </bean> </property> </bean> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory" /> </bean> <tx:annotation-driven transaction-manager="transactionManager" /> <context:component-scan base-package="com.mumz.test.audit"></context:component-scan> <context:annotation-config></context:annotation-config> <aop:aspectj-autoproxy/> </beans>
I have placed all the database properties inside jdbc.properties file
jdbc.properties
jdbc.driverClassName=com.mysql.jdbc.Driver jdbc.url=jdbc\:mysql\://localhost\:3306/AuditSchema jdbc.username=root jdbc.password=root hibernate.dialect=org.hibernate.dialect.MySQLDialect jdbc.databaseVendor=MYSQL
JPA needs persistence.xml inside META-INF folder in the classpath, so we will create our persistence.xml.
persistence.xml
<?xml version="1.0" encoding="UTF-8"?> <persistence xmlns="http://java.sun.com/xml/ns/persistence" version="1.0"> <persistence-unit name="auditPersitenceUnit"> <class>com.mumz.test.audit.beans.Book</class> <class>com.mumz.test.audit.beans.BookShelf</class> <class>com.mumz.test.audit.beans.Audit</class> </persistence-unit> </persistence>
Finally some test code to check our implementation.
AuditMainApp.java
package com.mumz.test.audit.app; import java.util.ArrayList; import java.util.List; import org.springframework.context.ApplicationContext; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.mumz.test.audit.beans.Book; import com.mumz.test.audit.beans.BookShelf; import com.mumz.test.audit.interfaces.IAuditController; public class AuditMainApp { public static void main(String[] args) { ApplicationContext applicationContext = new ClassPathXmlApplicationContext("audit-bean-definition.xml"); IAuditController auditController = applicationContext.getBean(IAuditController.class); BookShelf bookShelf = new BookShelf(); Book book = new Book(); book.setAuthor("Test Author 1"); book.setName("Test Book 1"); book.setBookShelf(bookShelf); List<Book> books = new ArrayList<Book>(); books.add(book); bookShelf.setBooks(books); bookShelf.setName("Test 1"); auditController.addBookShelf(bookShelf); } }
Database schema used for this post is below.
Database Schema
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0; SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0; SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='TRADITIONAL'; DROP SCHEMA IF EXISTS `auditschema` ; CREATE SCHEMA IF NOT EXISTS `auditschema` DEFAULT CHARACTER SET utf8 ; USE `auditschema` ; -- ----------------------------------------------------- -- Table `auditschema`.`audit` -- ----------------------------------------------------- DROP TABLE IF EXISTS `auditschema`.`audit` ; CREATE TABLE IF NOT EXISTS `auditschema`.`audit` ( `AUDIT_ID` INT(11) NOT NULL AUTO_INCREMENT , `AUDIT_PRE_IMAGE` VARCHAR(3000) NULL DEFAULT NULL , `AUDIT_POST_IMAGE` VARCHAR(3000) NULL DEFAULT NULL , `AUDIT_OPERATION` VARCHAR(45) NULL DEFAULT NULL , PRIMARY KEY (`AUDIT_ID`) ) ENGINE = InnoDB AUTO_INCREMENT = 2 DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `auditschema`.`book_shelf` -- ----------------------------------------------------- DROP TABLE IF EXISTS `auditschema`.`book_shelf` ; CREATE TABLE IF NOT EXISTS `auditschema`.`book_shelf` ( `BOOK_SHELF_ID` INT(11) NOT NULL AUTO_INCREMENT , `BOOK_SHELF_NAME` VARCHAR(45) NOT NULL , PRIMARY KEY (`BOOK_SHELF_ID`) ) ENGINE = InnoDB AUTO_INCREMENT = 10 DEFAULT CHARACTER SET = utf8; -- ----------------------------------------------------- -- Table `auditschema`.`book` -- ----------------------------------------------------- DROP TABLE IF EXISTS `auditschema`.`book` ; CREATE TABLE IF NOT EXISTS `auditschema`.`book` ( `BOOK_ID` INT(11) NOT NULL AUTO_INCREMENT , `BOOK_NAME` VARCHAR(45) NOT NULL , `BOOK_AUTHOR` VARCHAR(45) NOT NULL , `BOOK_SHELF_ID` INT(11) NOT NULL , PRIMARY KEY (`BOOK_ID`) , INDEX `FK_BOOK_SHELF_ID` (`BOOK_SHELF_ID` ASC) , CONSTRAINT `FK_BOOK_SHELF_ID` FOREIGN KEY (`BOOK_SHELF_ID` ) REFERENCES `auditschema`.`book_shelf` (`BOOK_SHELF_ID` ) ON DELETE NO ACTION ON UPDATE NO ACTION) ENGINE = InnoDB AUTO_INCREMENT = 9 DEFAULT CHARACTER SET = utf8; SET SQL_MODE=@OLD_SQL_MODE; SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS; SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
If you have followed every step your code structure should look similar to:
That's all, It has been a very long post, I hope you find it useful.
how do you know that the save to Book or BookeShelf is
successful as you are catching the execution of the action and not
completion of the transaction
yes, this is what i wanna
Excellent post- tried to run your source, and run into jpa issues in that your persistence.xml file seems to be invalid?
Could not load properties; nested exception is java.util.InvalidPropertiesFormatException: org.xml.sax.SAXParseException; lineNumber: 4; columnNumber: 66; Document root element “persistence”, must match DOCTYPE root “null”.