I have been working on several projects now and I have observed one common requirement in all the web application projects I have worked so far. The requirement is that the application need to track the changes especially when the update happens to a record in the database. The application also need to track who updates and when the update happens on the record. Tracking changes is not a big deal as this can be easily implemented using database triggers. When the record gets updated in the database, the old record can be moved into a history table using database triggers. Let us see the big picture.
The Big Picture
I have often come across developers writing umpteen number of lines of code in the presentation layer of the web application just to compare the changes made to the domain data against the data retrieved from the database. One of the things that compelled them to implement the track changes logic in the presentation layer is to avoid invoking the business service layer unless there is a genuine change in the data.
Things can get unwieldy to implement the above mentioned track changes functionality in the presentation layer if the application demands tracking changes to a large number of objects. We also do not want to implement such functionality in the domain object either. So let us take a different approach in implementing such functionality that will scale to a very large number of objects in a less invasive way.
Use Case Requirement
Let us take Online User Management web application as an example where we will implement the track changes functionality with the help of AspectJ, Spring framework and Hibernate. The use case mandates whenever a user updates the profile, the system tracks the changes to the properties of the domain object and records the user who updates and the date on which the update happens in the database table.
Technologies Used
- AOP : Aspect Oriented Programming a.k.a. AOP is a way of modularizing crosscutting concerns much like object-oriented programming is a way of modularizing common concerns.
- AspectJ: AspectJ is an implementation of aspect-oriented programming for Java. AspectJ adds to Java just one new concept, a join point -- and that's really just a name for an existing Java concept. It adds to Java only a few new constructs: pointcuts, advice, inter-type declarations and aspects. Pointcuts and advice dynamically affect program flow, inter-type declarations statically affects a program's class hierarchy, and aspects encapsulate these new constructs. For more details, please refer the AspectJ Programming Guide.
- Spring Framework - It is the leading open source J2EE application framework .
- Hibernate - It is one of the leading Object Relational Mapping tools. Hibernate is a powerful, high performance object/relational persistence and query service.
Technical Design
We will first design our domain objects we are going to use as an example to track the changes to their properties. A typical online user management application contains a minimum of Users, Contacts & Address domain objects. Let us now verify the structure of these objects.
Figure 1. UML diagram of Domain objects
From the diagram, it is evident that Users may have one or more Contacts and each Contact may have one or more address either physical or mailing address.
Example: Users.java
package com.vbose.domain;
import java.io.Serializable;
import java.util.HashSet;
/**
* Users.java - A class that represents a row in the 'USERS' table.
* This class may be customized as it is never re-generated
* after being created.
* @author Vigil Bose
*/
public class Users implements Serializable
{private static final long serialVersionUID = 1L;
/** The cached hash code value for this instance.
Settting to 0 triggers re-calculation. */
private int hashValue = 0;
/** The composite primary key value. */
private java.lang.String username;
/** The value of the luStatusReasonCodes association. */
private LuStatusReasonCodes luStatusReasonCodes;
/** The value of the luUserStatusCodes association. */
private LuUserStatusCodes luUserStatusCodes;
/** The value of the contactsSet one-to-many association. */
private java.util.Set contactsSet = new HashSet();
/** The value of the simple password property. */
private java.lang.String password;
/** The value of the simple lockedTime property. */
private java.util.Date lockedTime;
/** The value of the simple secretQuestion property. */
private java.lang.String secretQuestion;
/** The value of the simple secretAnswer property. */
private java.lang.String secretAnswer;
/** The value of the simple createdDate property. */
private java.util.Date createdDate;
/** The value of the simple createdName property. */
private java.lang.String createdName;
/** The value of the simple updatedDate property. */
private java.util.Date updatedDate;
/** The value of the simple updatedName property. */
private java.lang.String updatedName;
/** The value of the simple passwordExpiration property. */
private java.util.Date passwordExpiration;
/** The value of the simple lastLogin property. */
private java.util.Date lastLogin;
/** The value of the non-persistent property used only in Updates **/
private Boolean trackChanges = new Boolean(false);
/** The value of the non-persistent property used only in Updates **/
private Boolean propertyChanged = new Boolean(false);
/**
* Simple constructor of Users instances.
*/
public Users(){
}
/**
* Constructor of Users instances given a simple primary key.
* @param username
*/
public Users(java.lang.String username){
this.setUsername(username);
}
/**
* Return the simple primary key value that identifies this object.
* @return java.lang.String
*/
public java.lang.String getUsername(){
return username;
}
/**
* Set the simple primary key value that identifies this object.
* @param username
*/
public void setUsername(java.lang.String username){
this.hashValue = 0;
this.username = username;
}
/**
* Return the value of the PASSWORD column.
* @return java.lang.String
*/
public java.lang.String getPassword(){
return this.password;
}
/**
* Set the value of the PASSWORD column.
* @param password
*/
public void setPassword(java.lang.String password){
this.password = password;
}
/**
* Return the value of the LOCKED_TIME column.
* @return java.util.Date
*/
public java.util.Date getLockedTime(){
return this.lockedTime;
}
/**
* Set the value of the LOCKED_TIME column.
* @param lockedTime
*/
public void setLockedTime(java.util.Date lockedTime){
this.lockedTime = lockedTime;
}
/**
* Return the value of the SECRET_QUESTION column.
* @return java.lang.String
*/
public java.lang.String getSecretQuestion(){
return this.secretQuestion;
}
/**
* Set the value of the SECRET_QUESTION column.
* @param secretQuestion
*/
public void setSecretQuestion(java.lang.String secretQuestion){
this.secretQuestion = secretQuestion;
}
/**
* Return the value of the SECRET_ANSWER column.
* @return java.lang.String
*/
public java.lang.String getSecretAnswer(){
return this.secretAnswer;
}
/**
* Set the value of the SECRET_ANSWER column.
* @param secretAnswer
*/
public void setSecretAnswer(java.lang.String secretAnswer){
this.secretAnswer = secretAnswer;
}
/**
* Return the value of the CREATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getCreatedDate(){
return this.createdDate;
}
/**
* Set the value of the CREATED_DATE column.
* @param createdDate
*/
public void setCreatedDate(java.util.Date createdDate){
this.createdDate = createdDate;
}
/**
* Return the value of the CREATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getCreatedName(){
return this.createdName;
}
/**
* Set the value of the CREATED_NAME column.
* @param createdName
*/
public void setCreatedName(java.lang.String createdName){
this.createdName = createdName;
}
/**
* Return the value of the UPDATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getUpdatedDate(){
return this.updatedDate;
}
/**
* Set the value of the UPDATED_DATE column.
* @param updatedDate
*/
public void setUpdatedDate(java.util.Date updatedDate){
this.updatedDate = updatedDate;
}
/**
* Return the value of the UPDATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getUpdatedName(){
return this.updatedName;
}
/**
* Set the value of the UPDATED_NAME column.
* @param updatedName
*/
public void setUpdatedName(java.lang.String updatedName){
this.updatedName = updatedName;
}
/**
* Return the value of the PASSWORD_EXPIRATION column.
* @return java.util.Date
*/
public java.util.Date getPasswordExpiration(){
return this.passwordExpiration;
}
/**
* Set the value of the PASSWORD_EXPIRATION column.
* @param passwordExpiration
*/
public void setPasswordExpiration(java.util.Date passwordExpiration){
this.passwordExpiration = passwordExpiration;
}
/**
* Return the value of the LAST_LOGIN column.
* @return java.util.Date
*/
public java.util.Date getLastLogin(){
return this.lastLogin;
}
/**
* Set the value of the LAST_LOGIN column.
* @param lastLogin
*/
public void setLastLogin(java.util.Date lastLogin){
this.lastLogin = lastLogin;
}
/**
* Return the value of the STATUS_REASON_CODE column.
* @return LuStatusReasonCodes
*/
public LuStatusReasonCodes getLuStatusReasonCodes(){
return this.luStatusReasonCodes;
}
/**
* Set the value of the STATUS_REASON_CODE column.
* @param luStatusReasonCodes
*/
public void setLuStatusReasonCodes(LuStatusReasonCodes luStatusReasonCodes){
this.luStatusReasonCodes = luStatusReasonCodes;
}
/**
* Return the value of the USER_STATUS_CODE column.
* @return LuUserStatusCodes
*/
public LuUserStatusCodes getLuUserStatusCodes(){
return this.luUserStatusCodes;
}
/**
* Set the value of the USER_STATUS_CODE column.
* @param luUserStatusCodes
*/
public void setLuUserStatusCodes(LuUserStatusCodes luUserStatusCodes){
this.luUserStatusCodes = luUserStatusCodes;
}
/**
* Return the value of the USERNAME collection.
* @return Contacts
*/
public java.util.Set getContactsSet(){
return this.contactsSet;
}
/**
* Set the value of the USERNAME collection.
* @param contactsSet
*/
public void setContactsSet(java.util.Set contactsSet){
this.contactsSet = contactsSet;
}
/**
* The API addContacts() not only reduces the lines of code when
* dealing with "Users" objects, but also enforces the cardinality
* of the association.Errors that arise from leaving out one of
* the two required actions are avoided.
* Whenever an association is created between a parent Users and
* a child Contacts, two actions are required.
* 1) The parent Users of the child must be set, effectively breaking
* the association between the child and its old parent (there can
* be only one parent for any child)
* 2) The child must be added to the "contactsSet" collection of the
* new parent "Users"
*
* This is a convenience method to the Users class that groups these
* operations, allowing reuse and helping ensure correctness
* @param contacts
*/
public void addContacts(Contacts contacts){
if (contacts == null)
throw new IllegalArgumentException("Null contacts Object!");
if (contacts.getUsers() != null){
contacts.getUsers().getContactsSet().remove(contacts);
}
contacts.setUsers(this);
this.contactsSet.add(contacts);
}
/**
* @return the trackChanges
*/
public Boolean getTrackChanges() {
return trackChanges;
}
/**
* @param trackChanges the trackChanges to set
*/
public void setTrackChanges(Boolean trackChanges) {
this.trackChanges = trackChanges;
}
/**
* Return the non-persistent property
* @return the propertyChanged
*/
public Boolean getPropertyChanged() {
return propertyChanged;
}
/**
* Sets the non-persistent property
* @param propertyChanged the propertyChanged to set
*/
public void setPropertyChanged(Boolean propertyChanged) {
this.propertyChanged = propertyChanged;
}
/**
* Implementation of the equals comparison on the basis of equality of the
* primary key values.
* @param rhs
* @return boolean
*/
public boolean equals(Object rhs){
if (rhs == null)
return false;
if (! (rhs instanceof Users))
return false;
Users that = (Users) rhs;
if (this.getUsername() == null || that.getUsername() == null)
return false;
return (this.getUsername().equals(that.getUsername()));
}
/**
* Implementation of the hashCode method conforming to the Bloch pattern with
* the exception of array properties (these are very unlikely primary key
* types).
* @return int
*/
public int hashCode(){
if (this.hashValue == 0){
int result = 17;
int usernameValue = this.getUsername() == null ? 0
: this.getUsername().hashCode();
result = result * 37 + usernameValue;
this.hashValue = result;
}
return this.hashValue;
}
}
Example: Contacts.java
package com.vbose.domain;
import java.io.Serializable;
import java.util.HashSet;
/**
* Contacts.java - A class that represents a row in the 'CONTACTS' table.
* This class may be customized as it is never re-generated
* after being created.
* @author Vigil Bose
*/
public class Contacts implements Serializable
{private static final long serialVersionUID = 1L;
/** The cached hash code value for this instance.
Settting to 0 triggers re-calculation. */
private int hashValue = 0;
/** The composite primary key value. */
private java.lang.Integer contactId;
/** The value of the luContactTypes association. */
private LuContactTypes luContactTypes;
/** The value of the users association. */
private Users users;
/** The value of the simple firstName property. */
private java.lang.String firstName;
/** The value of the simple middleInitial property. */
private java.lang.String middleInitial;
/** The value of the simple lastName property. */
private java.lang.String lastName;
/** The value of the simple firmName property. */
private java.lang.String firmName;
/** The value of the simple phoneNumber property. */
private java.lang.String phoneNumber;
/** The value of the simple faxNumber property. */
private java.lang.String faxNumber;
/** The value of the simple emailAddress property. */
private java.lang.String emailAddress;
/** The value of the simple createdDate property. */
private java.util.Date createdDate;
/** The value of the simple createdName property. */
private java.lang.String createdName;
/** The value of the simple updatedDate property. */
private java.util.Date updatedDate;
/** The value of the simple updatedName property. */
private java.lang.String updatedName;
/** The value of the simple title property. */
private java.lang.String title;
/** The value of the simple status property. */
private java.lang.String status;
/** The value of the geographicAddressesSet one-to-many association. */
private java.util.Set geographicAddressesSet = new HashSet();
/** The value of the non-persistent property used only in Updates **/
private Boolean trackChanges = new Boolean(false);
/** The value of the non-persistent property used only in Updates **/
private Boolean propertyChanged = new Boolean(false);
/**
* Simple constructor of Contacts instances.
*/
public Contacts(){
}
/**
* Constructor of Contacts instances given a simple primary key.
* @param contactId
*/
public Contacts(java.lang.Integer contactId){
this.setContactId(contactId);
}
/**
* Return the simple primary key value that identifies this object.
* @return java.lang.Integer
*/
public java.lang.Integer getContactId(){
return contactId;
}
/**
* Set the simple primary key value that identifies this object.
* @param contactId
*/
public void setContactId(java.lang.Integer contactId){
this.hashValue = 0;
this.contactId = contactId;
}
/**
* Return the value of the USERNAME column.
* @return Users
*/
public Users getUsers(){
return this.users;
}
/**
* Set the value of the USERNAME column.
* @param users
*/
public void setUsers(Users users){
this.users = users;
}
/**
* Return the value of the CONTACT_TYPE_CODE column.
* @return LuContactTypes
*/
public LuContactTypes getLuContactTypes(){
return this.luContactTypes;
}
/**
* Set the value of the CONTACT_TYPE_CODE column.
* @param luContactTypes
*/
public void setLuContactTypes(LuContactTypes luContactTypes){
this.luContactTypes = luContactTypes;
}
/**
* Return the value of the FIRST_NAME column.
* @return java.lang.String
*/
public java.lang.String getFirstName(){
return this.firstName;
}
/**
* Set the value of the FIRST_NAME column.
* @param firstName
*/
public void setFirstName(java.lang.String firstName){
this.firstName = firstName;
}
/**
* Return the value of the MIDDLE_INITIAL column.
* @return java.lang.String
*/
public java.lang.String getMiddleInitial(){
return this.middleInitial;
}
/**
* Set the value of the MIDDLE_INITIAL column.
* @param middleInitial
*/
public void setMiddleInitial(java.lang.String middleInitial){
this.middleInitial = middleInitial;
}
/**
* Return the value of the LAST_NAME column.
* @return java.lang.String
*/
public java.lang.String getLastName(){
return this.lastName;
}
/**
* Set the value of the LAST_NAME column.
* @param lastName
*/
public void setLastName(java.lang.String lastName){
this.lastName = lastName;
}
/**
* Return the value of the FIRM_NAME column.
* @return java.lang.String
*/
public java.lang.String getFirmName(){
return this.firmName;
}
/**
* Set the value of the FIRM_NAME column.
* @param firmName
*/
public void setFirmName(java.lang.String firmName){
this.firmName = firmName;
}
/**
* Return the value of the PHONE_NUMBER column.
* @return java.lang.String
*/
public java.lang.String getPhoneNumber(){
return this.phoneNumber;
}
/**
* Set the value of the PHONE_NUMBER column.
* @param phoneNumber
*/
public void setPhoneNumber(java.lang.String phoneNumber){
this.phoneNumber = phoneNumber;
}
/**
* Return the value of the FAX_NUMBER column.
* @return java.lang.String
*/
public java.lang.String getFaxNumber(){
return this.faxNumber;
}
/**
* Set the value of the FAX_NUMBER column.
* @param faxNumber
*/
public void setFaxNumber(java.lang.String faxNumber){
this.faxNumber = faxNumber;
}
/**
* Return the value of the EMAIL_ADDRESS column.
* @return java.lang.String
*/
public java.lang.String getEmailAddress(){
return this.emailAddress;
}
/**
* Set the value of the EMAIL_ADDRESS column.
* @param emailAddress
*/
public void setEmailAddress(java.lang.String emailAddress){
this.emailAddress = emailAddress;
}
/**
* Return the value of the CREATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getCreatedDate(){
return this.createdDate;
}
/**
* Set the value of the CREATED_DATE column.
* @param createdDate
*/
public void setCreatedDate(java.util.Date createdDate){
this.createdDate = createdDate;
}
/**
* Return the value of the CREATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getCreatedName(){
return this.createdName;
}
/**
* Set the value of the CREATED_NAME column.
* @param createdName
*/
public void setCreatedName(java.lang.String createdName){
this.createdName = createdName;
}
/**
* Return the value of the UPDATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getUpdatedDate(){
return this.updatedDate;
}
/**
* Set the value of the UPDATED_DATE column.
* @param updatedDate
*/
public void setUpdatedDate(java.util.Date updatedDate){
this.updatedDate = updatedDate;
}
/**
* Return the value of the UPDATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getUpdatedName(){
return this.updatedName;
}
/**
* Set the value of the UPDATED_NAME column.
* @param updatedName
*/
public void setUpdatedName(java.lang.String updatedName){
this.updatedName = updatedName;
}
/**
* Return the value of the TITLE column.
* @return java.lang.String
*/
public java.lang.String getTitle(){
return this.title;
}
/**
* Set the value of the TITLE column.
* @param title
*/
public void setTitle(java.lang.String title){
this.title = title;
}
/**
* Return the value of the STATUS column.
* @return java.lang.String
*/
public java.lang.String getStatus(){
return this.status;
}
/**
* Set the value of the STATUS column.
* @param status
*/
public void setStatus(java.lang.String status){
this.status = status;
}
/**
* Return the value of the CONTACT_ID Collection.
* @return the geographicAddressesSet
*/
public java.util.Set getGeographicAddressesSet(){
return geographicAddressesSet;
}
/**
* Set the value of the CONTACT_ID collection.
* @param geographicAddressesSet the geographicAddressesSet to set
*/
private void setGeographicAddressesSet(java.util.Set geographicAddressesSet) {
this.geographicAddressesSet = geographicAddressesSet;
}
/**
* The API addGeographicAddresses() not only reduces the lines of code
* when dealing with "Contacts" objects, but also enforces the cardinality
* of the association.Errors that arise from leaving out one of the two
* required actions are avoided. Whenever an association is created between
* a parent Contacts and a child GeographicAddresses, two actions are
* required.
* 1) The parent Users of the child must be set, effectively breaking the
* association between the child and its old parent (there can be only
* one parent for any child)
* 2) The child must be added to the "geographicAddressesSet" collection
* of the new parent "Contacts"
*
* This is a convenience method to the Contacts class that groups these
* operations, allowing reuse and helping ensure correctness
* @param geographicAddresses
*/
public void addGeographicAddresses(GeographicAddresses geographicAddresses){
if (geographicAddresses == null)
throw new IllegalArgumentException("Null geographicAddresses Object!");
if (geographicAddresses.getContacts() != null){
geographicAddresses.getContacts().getGeographicAddressesSet().
remove(geographicAddresses);
}
geographicAddresses.setContacts(this);
this.geographicAddressesSet.add(geographicAddresses);
}
/**
* @return the trackChanges
*/
public Boolean getTrackChanges(){
return trackChanges;
}
/**
* @param trackChanges the trackChanges to set
*/
public void setTrackChanges(Boolean trackChanges) {
this.trackChanges = trackChanges;
}
/**
* Return the non-persistent property
* @return the propertyChanged
*/
public Boolean getPropertyChanged() {
return propertyChanged;
}
/**
* Sets the non-persistent property
* @param propertyChanged the propertyChanged to set
*/
public void setPropertyChanged(Boolean propertyChanged) {
this.propertyChanged = propertyChanged;
}
/**
* Implementation of the equals comparison on the basis of
* equality of the primary key values.
* @param rhs
* @return boolean
*/
public boolean equals(Object rhs){
if (rhs == null)
return false;
if (! (rhs instanceof Contacts))
return false;
Contacts that = (Contacts) rhs;
if (this.getUsers() != null && that.getUsers() != null){
if (this.getUsers().getUsername() != null &&
that.getUsers().getUsername() != null){
if (!this.getUsers().getUsername().
equals(that.getUsers().getUsername())){
return false;
}
}
}
if (this.getLuContactTypes() != null && that.getLuContactTypes() != null){
if (this.getLuContactTypes().getContactTypeCode() != null &&
that.getLuContactTypes().getContactTypeCode() != null){
if (!this.getLuContactTypes().getContactTypeCode().
equals(that.getLuContactTypes().getContactTypeCode())){
return false;
}
}
}
return true;
}
/**
* Implementation of the hashCode method conforming to the Bloch pattern with
* the exception of array properties (these are very unlikely primary key
* types).
* @return int
*/
public int hashCode(){
if (this.hashValue == 0){
int result = 17;
int usernameValue = this.getUsers() == null ? 0 :
this.getUsers().getUsername().hashCode();
result = result * 37 + usernameValue;
int contactTypeCodeValue = this.getLuContactTypes() == null ? 0 :
this.getLuContactTypes().getContactTypeCode().hashCode();
result = result * 37 + contactTypeCodeValue;
this.hashValue = result;
}
return this.hashValue;
}
}
Example: GeographicAddresses.java
package com.vbose.domain;
import java.io.Serializable;
/**
* GeographicAddresses.java - A class that represents a
* row in the 'GEOGRAPHIC_ADDRESSES' table.
* This class may be customized as it is never re-generated
* after being created.
* @author Vigil Bose
*/
public class GeographicAddresses implements Serializable
{private static final long serialVersionUID = 1L;
/** The cached hash code value for this instance.
Settting to 0 triggers re-calculation. */
private int hashValue = 0;
/** The composite primary key value. */
private java.lang.Integer addressId;
/** The value of the luCountyCodes association. */
private LuCountyCodes luCountyCodes;
/** The value of the contacts association. */
private Contacts contacts;
/** The value of the simple addressLine1 property. */
private java.lang.String addressLine1;
/** The value of the simple addressLine2 property. */
private java.lang.String addressLine2;
/** The value of the simple city property. */
private java.lang.String city;
/** The value of the simple stateCode property. */
private java.lang.String stateCode;
/** The value of the simple postalCode property. */
private java.lang.String postalCode;
/** The value of the simple stateProvince property. */
private java.lang.String stateProvince;
/** The value of the simple isoCountryCode property. */
private java.lang.String isoCountryCode;
/** The value of the simple createdDate property. */
private java.util.Date createdDate;
/** The value of the simple createdName property. */
private java.lang.String createdName;
/** The value of the simple updatedDate property. */
private java.util.Date updatedDate;
/** The value of the simple updatedName property. */
private java.lang.String updatedName;
/** The value of the simple routingNumber property. */
private java.lang.String routingNumber;
/** The value of the simple addressType property. */
private java.lang.String addressType;
/** The value of the non-persistent property used only in Updates **/
private Boolean trackChanges = new Boolean(false);
/** The value of the non-persistent property used only in Updates **/
private Boolean propertyChanged = new Boolean(false);
/**
* Simple constructor of GeographicAddresses instances.
*/
public GeographicAddresses(){
}
/**
* Constructor of GeographicAddresses instances given a
* simple primary key.
* @param addressId
*/
public GeographicAddresses(java.lang.Integer addressId){
this.setAddressId(addressId);
}
/**
* Return the simple primary key value that identifies this
* object.
* @return java.lang.Integer
*/
public java.lang.Integer getAddressId(){
return addressId;
}
/**
* Set the simple primary key value that identifies this
* object.
* @param addressId
*/
public void setAddressId(java.lang.Integer addressId){
this.hashValue = 0;
this.addressId = addressId;
}
/**
* Return the value of the CONTACT_ID association column.
* @return contacts
*/
public Contacts getContacts(){
return this.contacts;
}
/**
* Set the value of the CONTACT_ID association column.
* @param contacts
*/
public void setContacts(Contacts contacts){
this.contacts = contacts;
}
/**
* Return the value of the ADDRESS_LINE1 column.
* @return java.lang.String
*/
public java.lang.String getAddressLine1(){
return this.addressLine1;
}
/**
* Set the value of the ADDRESS_LINE1 column.
* @param addressLine1
*/
public void setAddressLine1(java.lang.String addressLine1){
this.addressLine1 = addressLine1;
}
/**
* Return the value of the ADDRESS_LINE2 column.
* @return java.lang.String
*/
public java.lang.String getAddressLine2(){
return this.addressLine2;
}
/**
* Set the value of the ADDRESS_LINE2 column.
* @param addressLine2
*/
public void setAddressLine2(java.lang.String addressLine2){
this.addressLine2 = addressLine2;
}
/**
* Return the value of the CITY column.
* @return java.lang.String
*/
public java.lang.String getCity(){
return this.city;
}
/**
* Set the value of the CITY column.
* @param city
*/
public void setCity(java.lang.String city){
this.city = city;
}
/**
* Return the value of the COUNTY_CODE column.
* @return LuCountyCodes
*/
public LuCountyCodes getLuCountyCodes(){
return this.luCountyCodes;
}
/**
* Set the value of the COUNTY_CODE column.
* @param luCountyCodes
*/
public void setLuCountyCodes(LuCountyCodes luCountyCodes){
this.luCountyCodes = luCountyCodes;
}
/**
* Return the value of the STATE_CODE column.
* @return java.lang.String
*/
public java.lang.String getStateCode(){
return this.stateCode;
}
/**
* Set the value of the STATE_CODE column.
* @param stateCode
*/
public void setStateCode(java.lang.String stateCode){
this.stateCode = stateCode;
}
/**
* Return the value of the POSTAL_CODE column.
* @return java.lang.String
*/
public java.lang.String getPostalCode(){
return this.postalCode;
}
/**
* Set the value of the POSTAL_CODE column.
* @param postalCode
*/
public void setPostalCode(java.lang.String postalCode){
this.postalCode = postalCode;
}
/**
* Return the value of the STATE_PROVINCE column.
* @return java.lang.String
*/
public java.lang.String getStateProvince(){
return this.stateProvince;
}
/**
* Set the value of the STATE_PROVINCE column.
* @param stateProvince
*/
public void setStateProvince(java.lang.String stateProvince){
this.stateProvince = stateProvince;
}
/**
* Return the value of the ISO_COUNTRY_CODE column.
* @return java.lang.String
*/
public java.lang.String getIsoCountryCode(){
return this.isoCountryCode;
}
/**
* Set the value of the ISO_COUNTRY_CODE column.
* @param isoCountryCode
*/
public void setIsoCountryCode(java.lang.String isoCountryCode){
this.isoCountryCode = isoCountryCode;
}
/**
* Return the value of the CREATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getCreatedDate(){
return this.createdDate;
}
/**
* Set the value of the CREATED_DATE column.
* @param createdDate
*/
public void setCreatedDate(java.util.Date createdDate){
this.createdDate = createdDate;
}
/**
* Return the value of the CREATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getCreatedName(){
return this.createdName;
}
/**
* Set the value of the CREATED_NAME column.
* @param createdName
*/
public void setCreatedName(java.lang.String createdName){
this.createdName = createdName;
}
/**
* Return the value of the UPDATED_DATE column.
* @return java.util.Date
*/
public java.util.Date getUpdatedDate(){
return this.updatedDate;
}
/**
* Set the value of the UPDATED_DATE column.
* @param updatedDate
*/
public void setUpdatedDate(java.util.Date updatedDate){
this.updatedDate = updatedDate;
}
/**
* Return the value of the UPDATED_NAME column.
* @return java.lang.String
*/
public java.lang.String getUpdatedName(){
return this.updatedName;
}
/**
* Set the value of the UPDATED_NAME column.
* @param updatedName
*/
public void setUpdatedName(java.lang.String updatedName){
this.updatedName = updatedName;
}
/**
* Return the value of the ROUTING_NUMBER column.
* @return java.lang.String
*/
public java.lang.String getRoutingNumber(){
return this.routingNumber;
}
/**
* Set the value of the ROUTING_NUMBER column.
* @param routingNumber
*/
public void setRoutingNumber(java.lang.String routingNumber){
this.routingNumber = routingNumber;
}
/**
* Return the value of the ADDRESS_TYPE column.
* @return java.lang.String
*/
public java.lang.String getAddressType(){
return this.addressType;
}
/**
* Set the value of the ADDRESS_TYPE column.
* @param addressType
*/
public void setAddressType(java.lang.String addressType){
this.addressType = addressType;
}
/**
* @return the trackChanges
*/
public Boolean getTrackChanges(){
return trackChanges;
}
/**
* @param trackChanges the trackChanges to set
*/
public void setTrackChanges(Boolean trackChanges){
this.trackChanges = trackChanges;
}
/**
* Return the non-persistent property
* @return the propertyChanged
*/
public Boolean getPropertyChanged() {
return propertyChanged;
}
/**
* Sets the non-persistent property
* @param propertyChanged the propertyChanged to set
*/
public void setPropertyChanged(Boolean propertyChanged) {
this.propertyChanged = propertyChanged;
}
/**
* Implementation of the equals comparison on the basis of
* equality of the primary key values.
* @param rhs
* @return boolean
*/
public boolean equals(Object rhs){
if (rhs == null)
return false;
if (! (rhs instanceof GeographicAddresses))
return false;
GeographicAddresses that = (GeographicAddresses) rhs;
if (this.getAddressType() != null && that.getAddressType() != null){
if (!this.getAddressType().equals(that.getAddressType())){
return false;
}
}
return true;
}
/**
* Implementation of the hashCode method conforming to the Bloch
* pattern with the exception of array properties (these are
* very unlikely primary key types).
* @return int
*/
public int hashCode(){
if (this.hashValue == 0){
int result = 17;
int addressTypeValue = this.getAddressType() == null ? 0 :
this.getAddressType().hashCode();
result = result * 37 + addressTypeValue;
this.hashValue = result;
}
return this.hashValue;
}
}
Now we have identified the domain objects whose properties need to be tracked whenever they get updated by the user.
Our crosscutting concern is the notification of changes to Java bean properties. JavaBeans can have bound properties. This means we can register listeners to get notification when the value of the property changes. The java.beans package contains the classes and interfaces required to implement this functionality. The registered listener can implement the methods to record who updated the record and when the record is updated before saving the updated bean information in the database. We are going to use AspectJ, the leading AOP (Aspect Oriented Programming) implementation to address the above mentioned crosscutting concern.
Aspect-oriented programming complements object-oriented programming in many ways.
One interesting complementary feature is behavior composability. This means it should be possible to compose a class by adding behavior from different classes. OO uses inheritance and many patterns to add behavior to existing classes. AOP allows us to use mixins without changing the class inheritance hierarchy or otherwise changing the code.
Example: BeanTrackChangesAspect.aj
package com.vbose.aop;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.reflect.Method;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.vbose.domain.Contacts;
import com.vbose.domain.GeographicAddresses;
import com.vbose.domain.Users;
/**
* BeanTrackChangesAspect.aj - This aspect is used to addresses the cross-cutting
* concern of the notification of changes to the Java bean properties.
* @author Vigil Bose
*/
public aspect BeanTrackChangesAspect {private final Log logger = LogFactory.getLog(getClass());
/**Introduces a new data member variable called support **/
PropertyChangeSupport PropertySupport.support = new
PropertyChangeSupport(this);
/**
* In this mixin implementation BeanTrackChangesAspect,
* the behavior that is to be composed with the JavaBean is
* declared as an interface in our aspect.
*/
public interface PropertySupport{
/**
* Interface API addPropertyChangeListener() is used to
* add a PropertyChangeListener to the listener list.
* @param listener - The PropertyChangeListener
*/
public void addPropertyChangeListener(PropertyChangeListener listener);
/**
* Interface API addPropertyChangeListener() is used to
* add a PropertyChangeListener for a specific property.
* @param propertyName - The property of the bean
* @param listener - The PropertyChangeListener
*/
public void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener);
/**
* Interface API removePropertyChangeListener() is used to
* remove a PropertyChangeListener for a specific property.
* @param propertyName - The property of the bean
* @param listener - The PropertyChangeListener
*/
public void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener);
/**
* Interface API removePropertyChangeListener() is used to
* remove a PropertyChangeListener from the listener list.
* @param listener - The PropertyChangeListener
*/
public void removePropertyChangeListener(PropertyChangeListener listener);
/**
* Interface API hasListeners() is used to check if there
* are any listeners for a specific property.
* @param propertyName - The property of the bean
*/
public void hasListeners(String propertyName);
/**
* Interface API firePropertyChange() is used to report
* a bound property update to any registered listeners.
* @param source - The source bean whose property has been changed
* @param property - The property of the bean
* @param oldval - The old value of the property
* @param newval - The new value of the property
*/
public void firePropertyChange(Object source,
String property,
Object oldval,
Object newval );
}
/**
* In the following code, we are introducing a super-interface
* that our beans Users domain object implements
*/
declare parents: Users implements PropertySupport;
/**
* In the following code, we are introducing a super-interface
* that our beans Contacts domain object implements
*/
declare parents: Contacts implements PropertySupport;
/**
* In the following code, we are introducing a super-interface
* that our beans GeographicAddresses domain object implements
*/
declare parents: GeographicAddresses implements PropertySupport;
/**
* Add a PropertyChangeListener to the listener list.
* @param listener - The PropertyChangeListener
*/
public void PropertySupport.addPropertyChangeListener
(PropertyChangeListener listener){
support.addPropertyChangeListener(listener);
}
/**
* Add a PropertyChangeListener for a specific property.
* @param propertyName - The property of the bean
* @param listener - The PropertyChangeListener
*/
public void PropertySupport.addPropertyChangeListener
( String propertyName, PropertyChangeListener listener){
support.addPropertyChangeListener( propertyName,listener);
}
/**
* Remove a PropertyChangeListener for a specific property.
* @param propertyName - The property of the bean
* @param listener - The PropertyChangeListener
*/
public void PropertySupport.removePropertyChangeListener
( String propertyName,PropertyChangeListener listener){
support.removePropertyChangeListener( propertyName,listener);
}
/**
* Remove a PropertyChangeListener from the listener list.
* @param listener - The PropertyChangeListener
*/
public void PropertySupport.removePropertyChangeListener
(PropertyChangeListener listener) {
support.removePropertyChangeListener(listener);
}
/**
* Check if there are any listeners for a specific property.
* @param propertyName - The property of the bean
*/
public void PropertySupport.hasListeners(String propertyName){
support.hasListeners(propertyName);
}
/**
* Report a bound property update to any registered listeners.
* @param source - The source bean whose property has been changed
* @param property - The property of the bean
* @param oldval - The old value of the property
* @param newval - The new value of the property
*/
public void PropertySupport.firePropertyChange(Object source,
String property,
Object oldval,
Object newval) {
PropertyChangeEvent propertyChangeEvent =
new PropertyChangeEvent(users, property,
( oldval == null ? oldval : oldval.toString()),
(newval == null ? newval : newval.toString()));
support.firePropertyChange(propertyChangeEvent);
}
/**
* Named Point cut usersSetter. A named pointcut declaration means
* that in the around advice that follows, we can use the name of
* the pointcut and we need not repeat the pointcut definition.
* The pointcut definition call( public * *..Users.set*(*) )&&
* target( users ) picks out any call to all setter methods
* regardless of the arguments passed to the setter.
* We see that this named pointcut also takes a parameter of the
* type Users, which means that every join point picked out by this
* makes available an object of this type. This is also called the
* exposed context. This parameter is available inside of any advice
* that uses this pointcut, as we can see in the code.
* The target( users ) means that the pointcut definition applies if
* the target of the method call joinpoint is a Users. It should be
* noted that users on the right side matches the parameter type
* Users on the left side. This point cut also restricts the advice
* from acting on the join point by a conditional expression and a
* bunch of setters that are omitted.
* The assigned value can be exposed with an args pointcut
*/
pointcut usersSetter(Users users):
if (users.getTrackChanges().booleanValue()) &&
call(public * *..Users.set*(*)) &&
!call(public * *..Users.setPropertyChanged(*)) &&
!call(public * *..Users.setUsername(*)) &&
!call(public * *..Users.setCreatedDate(*)) &&
!call(public * *..Users.setCreatedName(*)) &&
!call(public * *..Users.setUpdatedName(*)) &&
!call(public * *..Users.setUpdatedDate(*)) &&
!call(public * *..Users.setContactsSet(*)) &&
target(users);
/**
* Named Point cut contactsSetter. A named pointcut declaration
* means that in the around advice that follows, we can use the
* name of the pointcut and we need not repeat the pointcut
* definition. The pointcut definition
* call( public * *..Contacts.set*(*) )&& target( contacts )
* picks out any call to all setter methods regardless of the
* arguments passed to the setter.
*/
pointcut contactsSetter(Contacts contacts):
if (contacts.getTrackChanges().booleanValue()) &&
call(public * *..Contacts.set*(*)) &&
!call(public * *..Contacts.setPropertyChanged(*)) &&
!call(public * *..Contacts.setCreatedDate(*)) &&
!call(public * *..Contacts.setCreatedName(*)) &&
!call(public * *..Contacts.setUpdatedName(*)) &&
!call(public * *..Contacts.setUpdatedDate(*)) &&
!call(public * *..Contacts.setGeographicAddressesSet(*)) &&
target(contacts);
/**
* Named Point cut geogrpahicAddressesSetter. A named pointcut
* declaration means that in the around advice that follows,
* we can use the name of the pointcut and we need not repeat
* the pointcut definition. The pointcut definition
* call( public * *..GeographicAddresses.set*(*) )&&
* target( geographicAddresses ) picks out any call to
* all setter methods regardless of the arguments passed
* to the setter.
*/
pointcut geogrpahicAddressesSetter(GeographicAddresses geographicAddresses):
if (geographicAddresses.getTrackChanges().booleanValue()) &&
call(public * *..GeographicAddresses.set*(*)) &&
!call(public * *..GeographicAddresses.setPropertyChanged(*)) &&
!call(public * *..GeographicAddresses.setCreatedDate(*)) &&
!call(public * *..GeographicAddresses.setCreatedName(*)) &&
!call(public * *..GeographicAddresses.setUpdatedName(*)) &&
!call(public * *..GeographicAddresses.setUpdatedDate(*)) &&
target(geographicAddresses);
/**
* Around advice runs in place of the join point it operates
* over, rather than before or after it. Because around is allowed
* to return a value, it must be declared with a return type,
* like a method. Even though our around advice substitutes our
* join point, we can still call the original functionality by
* using the proceed method Around advice.
* The main point here is that every time the setter method is
* called, our advice takes a copy of the value of the property,
* lets the original method proceed, and, after the new value is
* set, calls the firePropertyChange method. Now we see that the
* code to be introduced to our beans has been separated cleanly
* and is now reusable.
* @param users - The Users domain
*/
void around(Users users): usersSetter(users) {
String propertyName = thisJoinPointStaticPart.getSignature().
getName().substring("set".length());
String getMethodName = "get"+propertyName;
Method method = null;
try{
method = Users.class.getMethod(getMethodName, null);
Object oldValue = method.invoke(users, null);
proceed(users);
Object newValue = method.invoke(users, null);
users.firePropertyChange(users,
propertyName,
oldValue,
newValue);
}catch (Exception e){
logger.error("An exception occured while getting the method" +
" name of the property :"+propertyName, e);
e.printStackTrace();
}
}
/**
* Around advice runs in place of the join point it operates over,
* rather than before or after it. Because around is allowed
* to return a value, it must be declared with a return type, like
* a method. Even though our around advice substitutes our
* join point, we can still call the original functionality by
* using the proceed method Around advice.
* The main point here is that every time the setter method is
* called, our advice takes a copy of the value of the property,
* lets the original method proceed, and, after the new value is
* set, calls the firePropertyChange method. Now we see that the
* code to be introduced to our beans has been separated cleanly
* and is now reusable.
* @param contacts - The Contacts domain
*/
void around(Contacts contacts): contactsSetter(contacts) {
String propertyName = thisJoinPointStaticPart.getSignature().
getName().substring("set".length());
String ge tMethodName = "get"+propertyName;
Method method = null;
try{
method = Contacts.class.getMethod(getMethodName, null);
Object oldValue = method.invoke(contacts, null);
proceed(contacts);
Object newValue = method.invoke(contacts, null);
contacts.firePropertyChange(contacts,
propertyName,
oldValue,
newValue);
}catch (Exception e){
logger.error("An exception occured while getting the method " +
"name of the property :"+propertyName, e);
e.printStackTrace();
}
}
/**
* Around advice runs in place of the join point it operates over,
* rather than before or after it. Because around is allowed
* to return a value, it must be declared with a return type, like
* a method. Even though our around advice substitutes our
* join point, we can still call the original functionality by
* using the proceed method Around advice.
* The main point here is that every time the setter method is
* called, our advice takes a copy of the value of the property,
* lets the original method proceed, and, after the new value is
* set, calls the firePropertyChange method. Now we see that the
* code to be introduced to our beans has been separated cleanly
* and is now reusable.
* @param geographicAddresses - The GeographicAddresses domain
*/
void around(GeographicAddresses geographicAddresses):
geogrpahicAddressesSetter(geographicAddresses) {
String propertyName = thisJoinPointStaticPart.getSignature().
getName().substring("set".length());
String getMethodName = "get"+propertyName;
Method method = null;
try{
method = GeographicAddresses.class.getMethod(getMethodName, null);
Object oldValue = method.invoke(geographicAddresses, null);
proceed(geographicAddresses);
Object newValue = method.invoke(geographicAddresses, null);
geographicAddresses.firePropertyChange(geographicAddresses,
propertyName,
oldValue,
newValue);
}catch (Exception e){
logger.error("An exception occured while getting the method " +
"name of the property :"+propertyName, e);
e.printStackTrace();
}
}
}
The BeanTrackChangesAspect.aj file is self explanatory as it contains a lot of comments. This one AOP aspect adds behavior and additional methods to our Java Beans (domain objects). AOP language is invaluable in solving many problems. Let us analyze the internals of BeanTrackChangesAspect.aj file. It has references to the following properties.
- PropertyChangeSupport - A utility class that can be used by beans that support bound properties. You can use an instance of this class as a member field of your bean and delegate various work to it. This class is serializable.
- PropertySupport - This is the inter-type declaration. The behavior that needs to be composed in each of the domain object is declared as an interface. Declaring additional methods or fields on behalf of a type is called "Introductions". It is also known as an inter-type declaration in the AspectJ community.
- declare parents: Users implements PropertySupport - This "declare parents" applies the PropertySupport interface to Users domain object. In other words, it makes the Users domain object implement the PropertySupport interface. The other declare statements works the same way for other respective domain objects. The PropertySupport methods are implemented in the BeanTrackChangesAspect.aj files for each domain object.
- Join Point - a point during the execution of a program, such as the execution of a method or the handling of an exception.
- Pointcut - a predicate that matches join points. Advice is associated with a pointcut expression and runs at any join point matched by the pointcut (for example, the execution of a method with a certain name). The concept of join points as matched by pointcut expressions is central to AOP
- Around Advice - Advice that surrounds a join point such as a method invocation. This is the most powerful kind of advice. Around advice can perform custom behavior before and after the method invocation. It is also responsible for choosing whether to proceed to the join point or to shortcut the advised method execution by returning its own return value or throwing an exception.
The AOP pointcut implementation shown above uses a conditional expression to check whether "trackChanges" property is set prior to let the advice execute the joinpoint. The "trackChanges" property is set by our unit test prior to call the update on the respective domain object. This way we can control the behavior of the advice.
Now, let us look at the listener implementation that gets notified of the property changes.
Example: BeanTrackChangesListener.java
package com.vbose.listener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import com.vbose.domain.Contacts;
import com.vbose.domain.GeographicAddresses;
import com.vbose.domain.Users;
/**
* BeanTrackChangesListener.java - An implementation of PropertyChangeListener
* interface. A "PropertyChange" event gets fired whenever a bean changes a
* "bound" property. You can register a PropertyChangeListener with a source
* bean so as to be notified of any bound property updates.
* @author Vigil Bose
*/
public class BeanTrackChangesListener implements PropertyChangeListener {private final Log logger = LogFactory.getLog(getClass());
/**
* The API propertyChange() is triggered when the listener
* detects changes to the properties of the bean.
*/
public void propertyChange(PropertyChangeEvent evt) {
Object object = evt.getSource();
if (evt.getOldValue() != null || evt.getNewValue() != null){
if (object instanceof Users){
Users users = (Users)object;
users.setPropertyChanged(new Boolean(true));
users.setUpdatedName("vbose");
users.setUpdatedDate(new java.util.Date());
printPropertyChanges(evt);
}
if (object instanceof Contacts){
Contacts contacts = (Contacts)object;
contacts.setPropertyChanged(new Boolean(true));
contacts.setUpdatedName("vbose");
contacts.setUpdatedDate(new java.util.Date());
printPropertyChanges(evt);
}
if (object instanceof GeographicAddresses){
GeographicAddresses geographicAddresses = (GeographicAddresses)object;
geographicAddresses.setPropertyChanged(new Boolean(true));
geographicAddresses.setUpdatedName("vbose");
geographicAddresses.setUpdatedDate(new java.util.Date());
printPropertyChanges(evt);
}
}
}
/**
* The printPropertyChanges() is used to print the property
* changes in the log file configured.
* @param evt - The PropertyChangeEvent.
*/
private void printPropertyChanges(PropertyChangeEvent evt) {
logger.info("Property [" +
evt.getPropertyName() +
"] changed from {" +
evt.getOldValue() + "} to {" +
evt.getNewValue() +"}");
}
}
In the listener implementation, the updated by and updated date properties are set. In the example shown above, the updated by property is hard coded. In reality, this value should come from the currently logged in user who modifies the record. Such information can be obtained from the SecurityContext once the user is authenticated. The listener sets the "propertyChanged" property on the respective domain object in addition to the updated by and updated date properties. This provides a hook to the service layer to further control the subsystem processing when the properties of the domain object are changed. This may even include notifying other systems about the change in the properties of the respective domain object.
Now, let us look at what Spring framework and Hibernate does in completing the use case and help us execute the unit tests.
Spring Framework - Spring is a lightweight application framework. Spring helps structure the whole application in a consistent and productive manner. The inversion of control container enables sophisticated configuration management of POJOs (Plain Old Java Objects). Some of the benefits of using Spring framework are :
- Spring focuses around providing a way to manage the business objects.
- Spring is an ideal framework for test driven projects.
- Spring provides a consistent framework for data access, whether using JDBC or an O/R mapping product such as TopLink, Hibernate or a JDO implementation
- Spring 2.0 provides tighter integration with AspectJ AOP language.
- .....
Hibernate - Spring framework provides first class integration with Hibernate like any other ORM tools. Hibernate is used in this use case example for data persistence. Spring provides abstract templates that deals with the low level plumbing of ORM tools. However, in this use case example, I will show the usage of template less data access strategy using Hibernate.
Example: IUsersService.java
package com.vbose.bus;
import com.vbose.domain.Users;
/**
* A simple java business service interface that exposes
* the service APIs to create and update the user information.
* @author Vigil Bose
*/
public interface IUsersService {
/**
* The API createUser() is used to create the user
* information in the database
* @param users - The users domain object
*/
public void createUser(Users users);
/**
* The API updateUser() is used to update the user profile
* information in the database
* @param users - The users domain object
*/
public void updateUser(Users users);
}
The unit test will invoke the createUser in IUsersService interface API to create the user. The test also invokes the updateUser API to update the user information.
Example: UsersServiceImpl.java
package com.vbose.bus.impl;
import com.vbose.bus.IUsersService;
import com.vbose.bus.dao.IUsersServiceDao;
import com.vbose.domain.Users;
/**
* UsersServiceImpl.java - The java service implementation that
* implements the APIs to create and update the user information
* @author Vigil Bose
*/
public class UsersServiceImpl implements IUsersService {private IUsersServiceDao usersServiceDao;
/**
* A setter method of dependency injection
* @param usersServiceDao the usersServiceDao to set
*/
public void setUsersServiceDao(IUsersServiceDao usersServiceDao) {
this.usersServiceDao = usersServiceDao;
}
/**
* The API createUser() is used to create the user
* information in the database
* @param users - The users domain object
* @see com.vbose.bus.IUsersService#createUser(com.vbose.domain.Users)
*/
public void createUser(Users users) {
this.usersServiceDao.createUser(users);
}
/**
* The API updateUser() is used to update the user profile
* information in the database
* @param users - The users domain object
* @see com.vbose.bus.IUsersService#updateUser(com.vbose.domain.Users)
*/
public void updateUser(Users users) {
this.usersServiceDao.updateProfile(users);
}
}
The usersServiceImpl delegates the call to the collaborating data access object whose dependency is injected by Spring.
Example: IUsersServiceDao.java
package com.vbose.bus.dao;
import org.springframework.dao.DataAccessException;
import com.vbose.domain.Users;
/**
* IUsersServiceDao.java - The data access strategy interface that exposes API's
* to create and update user information in the database
* @author Vigil Bose
*/
public interface IUsersServiceDao {
/**
* The data access API createUser() is used to create user information
* in the database
* @param users - The users domain instance
* @throws DataAccessException
*/
public void createUser(Users users) throws DataAccessException;
/**
* The data access API updateProfile() is used to update the user profile
* in the database
* @param users - The users domain instance
* @throws DataAccessException
*/
public void updateProfile(Users users) throws DataAccessException;
/**
* The API findUsersByUserName() is used to retrieve the Users
* information for the given username
* @param username - The unique identifier of the Users domain instance
* @return - An instance of Users domain instance.
* @throws DataAccessException
*/
public Users findUsersByUserName(String username) throws DataAccessException;
}
The finder API defined in the data access interface is used to fetch the user information from the database. Let us now see the implementation of IUsersServiceDao interface below.
Example: UsersServiceDaoImpl.java
package com.vbose.bus.dao.impl;
import java.util.List;
import org.hibernate.Query;
import org.hibernate.SessionFactory;
import org.springframework.dao.DataAccessException;
import com.vbose.bus.dao.IUsersServiceDao;
import com.vbose.domain.Users;
/**
* UsersServiceDaoImpl.java - The Hibernate data access strategy
* implementation that implements the APIs to create and update
* the user information in the database.
* @author Vigil Bose
*/
public class UsersServiceDaoImpl implements IUsersServiceDao {private SessionFactory fatcory;
/**
* A constructor dependency injection
*/
public UsersServiceDaoImpl(SessionFactory factory){
this.fatcory = factory;
}
/**
* The data access API createUser() is used to create
* user information in the database
* @param users - The users domain instance
* @throws DataAccessException
*/
public void createUser(Users users) throws DataAccessException {
this.fatcory.getCurrentSession().save(users);
}
/**
* The data access API updateProfile() is used to
* update the user profile in the database
* @param users - The users domain instance
* @throws DataAccessException
*/
public void updateProfile(Users users) throws DataAccessException {
this.fatcory.getCurrentSession().saveOrUpdate(users);
}
/**
* The API findUsersByUserName() is used to retrieve the
* Users information for the given username
* @param username - The unique identifier of the Users domain instance
* @return - An instance of Users domain instance.
* @throws DataAccessException
*/
public Users findUsersByUserName(String username) throws DataAccessException {
Query query = this.fatcory.getCurrentSession().
getNamedQuery("com.vbose.domain.Users.findUsersByUserName")
.setParameter("username", username);
List list = query.list();
return (list != null && list.size()> 0 ? (Users)list.get(0) : null);
}
}
Resource management
With Hibernate 3.0.1, Spring is able to manage Hibernate Session without having to go through the Hibernate Session. Spring is able to manage the underlying resource without using templates that are available for those technologies. This is true for JPA (Java Persistence API) as well. What this means is that even if we use the Hibernate API directly (SessionFactory.getCurrentSession()), we will still be using a Spring managed Hibernate Session. This is true for EntityManager obtained through a JPA EntityManagerFactory as well.
The one thing that was missing was the exception translation. However we can still enjoy automatic exception translation using AOP, if using Hibernate in an environment where annotations are not available (example: J2sdk 1.4.x) by first declare an exception translator and then declare a piece of AOP configuration, like so in the following way:
<bean id="persistenceExceptionInterceptor"
class="org.springframework.dao.support.PersistenceExceptionTranslationInterceptor"/>
<aop:config>
<aop:advisor pointcut="execution(public * *..IUsersServiceDao.*(..))"
advice-ref="persistenceExceptionInterceptor"/>
</aop:config>
In Java 5 where we can use annotations, use the following bean instead...
<bean class="org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor"/>
Putting Things Together
Let us now wire everything in the Spring application context file such that we can run the tests to track the Bean changes.
Example: bean-track-context.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:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-2.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.1.xsd
http://www.springframework.org/schema/util
http://www.springframework.org/schema/util/spring-util-2.0.xsd">
<bean id="propertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<list>
<value>jdbc.properties</value>
</list>
</property>
</bean>
<bean id="usersService" class="com.vbose.bus.impl.UsersServiceImpl">
<property name="usersServiceDao" ref="usersServiceDao"/>
</bean>
<bean id="usersServiceDao" class="com.vbose.bus.dao.impl.UsersServiceDaoImpl">
<constructor-arg ref="sessionFactory"/>
</bean>
<bean id="dataSource"
class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
Transaction advice definition, based on method name patterns.
Defaults to PROPAGATION_REQUIRED for all methods whose name
starts with "create" ,"update" etc. By default, the transaction
is rolled back for runtime exceptions including DataAccessException.
It will not roll back during checked exceptions unless specified
in the declaration.
-->
<tx:advice id="usersServiceTransactionAdvice">
<tx:attributes>
<tx:method name="*" />
</tx:attributes>
</tx:advice>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.MySQLDialect</prop>
<prop key="hibernate.connection.pool_size">3</prop>
<prop key="hibernate.show_sql">false</prop>
<prop key="hibernate.max_fetch_depth">3</prop>
</props>
</property>
<property name="mappingResources">
<list>
<value>com/vbose/domain/Users.hbm.xml</value>
<value>com/vbose/domain/LuUserStatusCodes.hbm.xml</value>
<value>com/vbose/domain/LuStatusReasonCodes.hbm.xml</value>
<value>com/vbose/domain/Contacts.hbm.xml</value7gt;
<value>com/vbose/domain/LuContactTypes.hbm.xml</value>
<value>com/vbose/domain/GeographicAddresses.hbm.xml</value>
<value>com/vbose/domain/LuCountyCodes.hbm.xml</value>
<value>com/vbose/domain/LuCountryCodes.hbm.xml</value>
</list>
</property>
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="persistenceExceptionInterceptor"
class="org.springframework.dao.support.PersistenceExceptionTranslationInterceptor"/>
<!--
Because of a small change in Hibernate 3.0.1, Spring is able to
manage the Hibernate session for you,without you having to go through
the Hibernate session. The one thing that was missing was the exception
translation. To also get that going, you can still enjoy automatic
exception translation; using AOP, if using Hibernate in an
environment where annotations are not available (pre Java5)
by first declare an exception translator and then declare a
piece of AOP configuration, like so in the following way.
However, the following configuration can be replaced by
<bean class="org.springframework.dao.annotation.
PersistenceExceptionTranslationPostProcessor"/>
if using annotations. -->
<aop:config>
<aop:pointcut id="usersServicePointcut"
expression="execution(public * *..IUsersService.*(..))"/>
<aop:advisor pointcut-ref="usersServicePointcut"
advice-ref="usersServiceTransactionAdvice" order="1"/>
<aop:advisor pointcut="execution(public * *..IUsersServiceDao.*(..))"
advice-ref="persistenceExceptionInterceptor"/>
</aop:config>
<bean id="beanTrackChangesAspect"
class="com.vbose.aop.BeanTrackChangesAspect" factory-method="aspectOf"/>
<bean id="beanTrackChangeListener"
class="com.vbose.listener.BeanTrackChangesListener"/>
</beans>
The following figure is the Spring configuration of the managed beans declared in bean-track-context.xml file.
Figure 2. Spring Application Context
Testing
Let us now run the unit test and analyze the results. Spring Mock provides first class abstract templates that facilitates the dependency injection features and can run the tests in transaction.
Some of the Spring Mock Tests available from spring-mock.jar are given below:
This test class is used to test the code that runs inside a transaction. It creates and rolls back a transaction for each test case. We write code with the assumption that there is a transaction present. It provides the fields such as a JdbcTemplate that can be used to verify database state after test operations, or verify the results of queries performed by application code. An ApplicationContext is also inheited, and can be used for explicit lookup if necessary.
AbstractJpaTests:
This test class is used for testing JPA functionality. It provides an EntityManager instance that we can use to call JPA methods.
AbstractAspectjJpaTests:
This class extends from AbstractJpaTests and it is used for load-time weaving (LTW) purposes using AspectJ. We override the method getActualAopXmlLocation() to specify the location of AspectJ's configuration xml file.
AbstractModelAndViewTests:
This is a convenient base class for testing the presentation and controller layers using Sping MVC in the application.
We use AbstractTransactionalDataSourceSpringContextTests from the above list such that our tests can run within a transaction and provides dependency injection features. The following is the result of executing two JUnit Tests. Since the tests are run within a transaction, the data that is used to populate the domain object prior to the insert and after the update do not affect the state of the database since the transaction is rolled back after the test execution is completed.
1. testPropertyChange()
Let us now run the unit test and analyze the results. Spring Mock provides first class abstract templates that facilitates the dependency injection features and can run the tests in transaction.
Some of the Spring Mock Tests available from spring-mock.jar are given below:
Spring Mock Test Classes
AbstractDependencyInjectionSpringContextTests:
This test class injects test dependencies so we don't need to specifically perform the Spring application context lookups. It also automatically locates the corresponding object in the set of configuration files specified in the getConfigLocations() method.
AbstractTransactionalDataSourceSpringContextTests:This test class is used to test the code that runs inside a transaction. It creates and rolls back a transaction for each test case. We write code with the assumption that there is a transaction present. It provides the fields such as a JdbcTemplate that can be used to verify database state after test operations, or verify the results of queries performed by application code. An ApplicationContext is also inheited, and can be used for explicit lookup if necessary.
AbstractJpaTests:
This test class is used for testing JPA functionality. It provides an EntityManager instance that we can use to call JPA methods.
AbstractAspectjJpaTests:
This class extends from AbstractJpaTests and it is used for load-time weaving (LTW) purposes using AspectJ. We override the method getActualAopXmlLocation() to specify the location of AspectJ's configuration xml file.
AbstractModelAndViewTests:
This is a convenient base class for testing the presentation and controller layers using Sping MVC in the application.
We use AbstractTransactionalDataSourceSpringContextTests from the above list such that our tests can run within a transaction and provides dependency injection features. The following is the result of executing two JUnit Tests. Since the tests are run within a transaction, the data that is used to populate the domain object prior to the insert and after the update do not affect the state of the database since the transaction is rolled back after the test execution is completed.
1. testPropertyChange()
Figure 3. Unit Test 1
The red rectangular box in the Figure 3 above indicate the log of the bean properties that are changed and picked up properly by the listener. The blue rectangular box in the figure above indicate the log of the updated by and updated date properties of the beans.
A snippet of the Unit Test Log of the Listener
[BeanTrackChangesListener] - <Property [SecretQuestion] changed from {What is your favorite color?} to
{What is your favorite dish?}>
[BeanTrackChangesListener] - <Property [SecretAnswer] changed from {White} to {Seafood}>
[BeanTrackChangesListener] - <Property [EmailAddress] changed from {vbose@vbose.com} to {vbose@test.com}>
[BeanTrackChangesListener] - <Property [Title] changed from {Systems Architect} to {Architect}>
[BeanTrackChangesListener] - <Property [AddressLine1] changed from {Address Line 1} to {1400 GNSI Plaza}>
[BeanTrackChangesListener] - <Property [City] changed from {City} to {GnsiCity}>
A snippet of the Unit Test Log After the Updates:
[BeanTrackChangesListenerTest] - <Updated by in Users : vbose>
[BeanTrackChangesListenerTest] - <Updated Date in Users : Sun Dec 09 01:35:19 EST 2007>
[BeanTrackChangesListenerTest] - <Updated by in Contacts : vbose>
[BeanTrackChangesListenerTest] - <Updated Date in Contacts : Sun Dec 09 01:35:19 EST 2007>
[BeanTrackChangesListenerTest] - <Updated by in GeographicAddresses : vbose>
[BeanTrackChangesListenerTest] - <Updated Date in GeographicAddresses : Sun Dec 09 01:35:19 EST 2007>
2. testPropertyChangeWithoutChanges()
Figure 4. Unit Test 2
When the second test is run, the listener did not pick any changes as there are no changes in the bean properties. See the log snippet of the updated by and updated date bean properties.
A snippet of the Unit Test Log without updating the data:
[BeanTrackChangesListenerTest] - <Updated by in Users : Null>
[BeanTrackChangesListenerTest] - <Updated Date in Users : Null>
[BeanTrackChangesListenerTest] - <Updated by in Contacts : Null>
[BeanTrackChangesListenerTest] - <Updated Date in Contacts : Null>
[BeanTrackChangesListenerTest] - <Updated by in GeographicAddresses : Null>
[BeanTrackChangesListenerTest] - <Updated Date in GeographicAddresses : Null>
AspectJ Compiler
AspectJ programs can be compiled using ajc (command line compiler) that comes with the AspectJ distribution. The AspectJ Development Tools (AJDT) project provides Eclipse platform based tool support for AOSD with AspectJ. I have used AJDT 1.5 for Eclipse 3.3 released as part of Europa!
Conclusion
Concerns that crosscut a class which is the fundamental manipulation in Java could be isolated so that such code is not spread out across the codebase. Aspect Oriented Programming makes this possible to separate such crosscutting concerns. This makes the modification easier. It also helps to add a concern without changing the code in a less invasive way.
References
1. The AspectJ Development Kit
2. Spring framework Reference Document
3. The AspectJ Programming Guide
4. Bound Properties
5. AOP mixin Implementation
About The Author
Vigil Bose is a Technical and Application Integration Architect at GlobalNet Services Inc, an application and infrastructure solutions provider to US Federal Government and other commercial clients. He primarily focuses on light-weight architectures and open source frameworks to build J2EE solutions for his clients. He has designed and built successful enterprise software solutions using Spring, and other open source tools and implemented them in production for his clients. He enjoys creating business value by applying simple techniques and principles to application architecture.