1) Introduction
This article deals with Hibernate Interceptors. Hibernate is an open-source project that provides ORM solution. For more information about Hibernate, novice readers are encouraged to read the article An Introduction to Hibernate on javabeat before reading this article.
Situations may demand to perform some set of pre-requisite/post-requisite operations before/after the core functional logic. In such a case, an interceptor can be used to intercept the existing business functionality to provide extensible or add-on features. They provide pluggable architecture and are generally callback methods that will be called by the framework in response to a particular set of events/actions if properly registered and configured. They follow the standard Interceptor pattern and they have various advantages in an application design. They can be used to monitor the various parts of the input that are passed to an application to validate them. They even have the capability to overwrite the core functional logic of the module.
For example, consider an online shopping system that ships goods to the customer’s shipping address upon placing a request. Suppose, there is an enhancement to this application telling that the request has to be validated because of the increasing number of spams and the customer should be notified through e-mail (or mobile) upon successful delivery of the goods. These two enhancements have to be projected into the application’s core logic.
Having a general overview of the core logic will look something like the following, Validate the User Request
- Ship the Goods to the customer
- Notify the customer about its successful delivery
As we can see above, the two enhancements have to be projected within the application’s core logic which requires code changes. But, if the application has to be properly designed with the notion of Interceptors, then the code change can be eliminated to the maximum.
2) Interceptors in Hibernate
Hibernate provides an ORM solution for persisting and querying data in the database. A Hibernate application can be structured in a way such that certain methods can be make to be invoked when a particular life-cycle event occurs. Not always the API in a software/product will completely satisfy the application needs and requirements. Hibernate is no more away from this. Therefore, Hibernate API is designed in such a way to provide pluggable framework through the notion of Interceptors.
In a multi-tiered application, the situation for the inclusion of Interceptors can happen at any level. It can happen at the Client level, Server level and even at the persistence level. Imagine an application is saving employee records in a database and now the application mandates to display to the Database admin about the history of inserts and updates.
A simple general overview of the logic looks like the following,
- Insert/Update the records in the Database
- During Insert/Update, maintain the log information in a file
As we can see, the maintenance of this logging information should happen whenever when an insert/update goes to the Database. Such a logger interceptor can be easily plugged into the application with minimal code change because of the flexible design of Hibernate.
2.1) Types of Interceptors
Based on their scope, Interceptors in Hibernate can fall under two categories. They are, Application-scoped Interceptors
- Session-scoped Interceptors
2.1.1) Application-scoped Interceptor
An application can contain one or more database sessions represented by the Session
interface. If an application is configured to use Global Interceptors, then it will affect the persistent objects in all the sessions. The following code configures a global interceptor,
Configuration configuration = new Configuration(); configuration.setInterceptor(new MyInterceptor()); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session1 = sessionFactory.openSession(); Employee e1, e2 = null; // Assume e1 and e2 objects are associated with session1. Session session2 = sessionFactory.openSession(); User u1, u2 = null //Assume u1 and u2 objects are associated with session1.
A global-scoped interceptor can be set to an application by calling the Configuration.setInterceptor(Interceptor)
method. In the above code, we have two different session objects ‘session1’ and ‘session2’. Let us assume that e1 and e2 Employee
objects are associated with session ‘session1’ and u1 and u2 User
objects are associated with session ‘session2’. The applied application-scoped interceptor would have affected all the objects (e1, e2, u1 and u2), even though they are in different sessions.
2.1.2) Session-scoped Interceptor
A session-scoped interceptor will affect all the persistent objects that are associated with that particular session only. The following code shows how to configure a session-scoped interceptor,
Configuration configuration = new Configuration(); SessionFactory sessionFactory = configuration.buildSessionFactory(); MyInterceptor myInterceptor = new MyInterceptor(); Session session1 = sessionFactory.openSession(myInterceptor); Employee e1, e2 = null; // Assume e1 and e2 objects are associated with session ‘session1′. MyAnotherInterceptor myAnotherInterceptor = new MyAnotherInterceptor (); Session session2 = sessionFactory.openSession(myAnotherInterceptor); User u1, u2 = null; // Assume u1 and u2 objects are associated with session ‘session2′.
From the above code, we can infer that a session-scoped interceptor can be set by calling the method SessionFactory.openSession(Interceptor)
. In the above code, we have two different session objects ‘session1’ and ‘session2’ being configured with Interceptors MyInterceptor
and MyAnotherInterceptor
respectively. So, e1 and e2 objects will be affected by MyInterceptor
, whereas u1 and u2 objects will be affected by MyAnotherInterceptor
.
3) Interceptor API
Three interfaces related to Interceptors are available in Hibernate, out of which 2 are the classical interfaces. Lifecycle
and Validatable
are the classic interfaces and whereas Interceptor
is available in org.hibernate
package.
Following sections discusses more about the Interceptor interfaces in detail:
3.1) The ‘Validatable’ Interface
This classic interface can be implemented by Persistent Java Class to validate the state of the persistent object. This interface has a single method called Validatable.validate()
, which can be given implementation to check the validity of the state of the object. Consider the following code, import java.util.Date; import org.hibernate.classic.Validatable; import org.hibernate.classic.ValidationFailure;
public class ProjectDuration implements Validatable{ private Date startDate; private Date endDate; /// Other Code here. public void validate(){ if (startDate.after(endDate)){ throw new ValidationFailure( “Start Date cannot be greater than the End Date.”) } } }
The above persistent class ProjectDuration implements the Validatable
interface, and has a simple validation rule in the validate
method, stating that the project start date cannot come after the end date. This Validatable.validate()
method will be called by the framework during the save operation. A save opeation can happen whenever Session.save()
, Session.update()
, Session.saveOrUpdate()
or Session.flush()
methods are invoked.
3.2) The ‘Lifecycle’ Interface
A persistent object goes through the various phases in its life-cycle. It can be newly created, persisted in the database, can be loaded at a later-time, will undergo modifications if needed and finally deleted. The various phases that happen in the life of a persistent object are encapsulated in the Lifecycle
interface. Following are the available methods in the Lifecycle
Interface.
onLoad() | This method will be called by the framework before the loading of the persistent object, i.e, when the Session.load() is called. |
onSave() | This method will be called by the framework before the save operation, when the Session.save() orSession.saveorUpdate() method is called. |
onUpdate() | This method will be called by the framework, before updating any properties on the persistent object, i.e, when a call to Session.update() is made. |
onDelete() | This method is called before the delete operation, i.e. a call to Session.delete() is made. |
All the four methods are passed a Session object, which represents the session in which the persistent objects are associated with . A persistent class can implement the above interceptor for providing any customization, like the following,
import org.hibernate.Session; import org.hibernate.classic.Lifecycle; class MyCustomizedPeristentClass implements Lifecycle{ … public boolean onDelete(Session s) throws CallbackException { return false; } public void onLoad(Session s, Serializable id) { System.out.println(“Loading”); } public boolean onSave(Session s) throws CallbackException { return false; } public boolean onUpdate(Session s) throws CallbackException { return false; } }
3.3) The ‘Interceptor’ Interface
This interface allows application to provide greater customization for persisting the objects. It even allows the code to modify the state of the persistent object. It has more than 15 different methods and so the designers of Hibernate provide the concrete EmptyInterceptor
class which implements the Interceptor
interface to provide default/empty method implementations. Applications can use EmptyInterceptor
class instead of depending on the Interceptor
interface. Following are the most common operations that are available in the Interceptor
interface,
afterTransactionBegin() | Called by the framework before the start of a transaction, i.e. when the Session.startTransaction() method is called. |
beforeTransactionCompletion() | Called by the framework, before the transaction is about to end (either committed or rolled-back) , i.e. when a call is made to Transaction.commit() orTransaction.rollback() |
afterTransactionCompletion() | Called by the framework after the transaction has ended (committed or rolled-back) |
onSave() | This revised onSave() method is passed with various information like the property names of the entities, their values, their states etc., as arguments and will be called by the framework during the save operation. A save operation may happen during Session.save() ,Session.saveOrUpdate() ,Session.flush() orTransaction.commit() method calls. |
onUpdate() | This revised onUpdate() method is passed with various information like the property names of the entities, their values, their states etc., as arguments and will be called by the framework when the properties of a persistent object is about to be updated. An update operation may happen during a call to Session.update(), Session.saveOrUpdate() ,Session.flush() orTransaction.commit() is made. |
onLoad() | This revised onLoad() method is passed with various information like the property names of the entities, their values, their states etc., as arguments and will be called by the framework when a persistent object is about to be loaded, i.e. when Session.load() is called. |
onDelete() | This revised onDelete() method which is passed with various information like the property names of the entities, their values, their states etc., as arguments will be called by the framework when a persistent object is about to be deleted, i.e., when a call to Session.delete() is made. |
4) Test Application
Following section provides a sample application, which uses the Interceptors that was discussed in the preceding few sections.
4.1) Pre-requisites
The following are the pre-requisite softwares/products needed to run the sample application.
- Java Development Kit (http://java.sun.com/javase/downloads/index.jsp)
- Hibernate 3.2 (http://www.hibernate.org/6.html)
- MySQL Database Server (http://dev.mysql.com/downloads/mysql/6.0.html)
- MySQL Database Driver (http://dev.mysql.com/downloads/connector/j/3.1.html)
Let us have a simple scenario which makes use of the two Interceptors.
The first interceptor called the CustomSaveInterceptor
populates the persistent object with additional values apart from the original values that are given by the user. A database table called “PlayersName” is created with column names “fName”, “mName”, “lName”, “completeName” representing the first name, middle name, last name and the complete name of the player. Let the user give values only for fName, mName, lName and not for the completeName. The completeName which is just the concatenation of the fName, mName and lName along with white-spaces between them will be taken care by the CustomSaveInterceptor
. The second is the LoggerInterceptor
which keeps tracks of all the Insertions that are made to the Database table.
PlayerName.java:
package interceptor; import java.io.Serializable; import org.hibernate.CallbackException; import org.hibernate.Session; import org.hibernate.classic.Lifecycle; import org.hibernate.classic.Validatable; import org.hibernate.classic.ValidationFailure; public class PlayerName implements Validatable, Lifecycle { private String firstName; private String middleName; private String lastName; private String completeName; private String primaryKey; public PlayerName(){ } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getMiddleName() { return middleName; } public void setMiddleName(String middleName) { this.middleName = middleName; } public String getCompleteName() { return completeName; } public void setCompleteName(String completeName) { this.completeName = completeName; } public String getPrimaryKey() { return primaryKey; } public void setPrimaryKey(String primaryKey) { this.primaryKey = primaryKey; } public void validate() throws ValidationFailure { if ((firstName.equals(middleName)) && (middleName.equals(lastName))){ throw new ValidationFailure("First Name, Middle Name and Last Name cannot be the same"); } } public boolean onDelete(Session s) throws CallbackException { return false; } public void onLoad(Session s, Serializable id) { System.out.println("Loading"); } public boolean onSave(Session s) throws CallbackException { return false; } public boolean onUpdate(Session s) throws CallbackException { return false; } }
The above class ‘PlayerName’ represents the persistent class that we wish to save to the database. This class has various properties like firstName, middleName, lastName, completeName which represents the first name, middle name, last name and the complete name of the player. It also has a field called primaryKey for storing the primary key values which will be set manually by the application.
This class implements the validation Interceptor called Validatable
for doing a simple validate on the value of the name. It will just ensure whether the first name, middle name and the last name are unique by giving simple implementation in the validate()
method. If they are not unique, then a ValidationFailureException
is thrown by the program. PlayerName class also implements the Lifecycle
interface and the methods onLoad()
, onSave()
, onUpdate()
, onDelete()
are given default implementation.
CustomSaveInterceptor.java:
Following is the code for cutom save interceptor which extends the EmptyInterceptor
class and provides a simple logic of updating the persistent object with the complete name value.
package interceptor; import java.io.Serializable; import org.hibernate.EmptyInterceptor; import org.hibernate.type.Type; public class CustomSaveInterceptor extends EmptyInterceptor { public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { if (entity instanceof PlayerName){ PlayerName playerName = (PlayerName)entity; String completeName = playerName.getFirstName() + " " + playerName.getMiddleName() + " " + playerName.getLastName(); playerName.setCompleteName(completeName); } return super.onSave(entity, id, state, propertyNames, types); } }
The onSave()
method is the method of interest here, and we can see that this method is passed with various parameters. The entity represents the persistent entity object that is about to be saved. The id represents the Serializable primary key (which in our case will be a simple string object). The state array represents the values of the properties of the persistent object, The propertyNames array holds a list of string values, which are firstName, middleName, lastName, completeName. Since all the types in the PlayerName class are strings, the Type array points to the string type. The code initially does a pre-conditionary check to ensure whether the entity is of the right PlayerName type, and then updates the completeName property which is simply the concatenation of the firstName, middleName and the lastName values.
LoggerInterceptor.java:
package interceptor; import java.io.Serializable; import org.hibernate.EmptyInterceptor; import org.hibernate.type.Type; public class LoggerInterceptor extends EmptyInterceptor{ public boolean onSave(Object entity, Serializable id, Object[] state, String[] propertyNames, Type[] types) { System.out.println("Saving the persistent Object " + entity.getClass() + " with Id " + id); return super.onSave(entity, id, state, propertyNames, types); } }
The implementation of the LoggerInterceptor
class is relatively simple, as this class does nothing apart from overriding the onSave()
method and writing the log information to the console.
InterceptorTest.java:
package interceptor; import java.util.List; import org.hibernate.*; import org.hibernate.cfg.Configuration; import org.hibernate.classic.Validatable; public class InterceptorTest { public static void main(String[] args) { Configuration configuration = new Configuration().configure(); configuration.setInterceptor(new CustomSaveInterceptor()); SessionFactory sessionFactory = configuration.buildSessionFactory(); Session session = sessionFactory.openSession(new LoggerInterceptor()); createPlayerNames(session); listPlayerNames(session); } private static void createPlayerNames(Session session){ PlayerName rahul = createPlayerName("Rahul", "Sharad", "Dravid", "RSD"); PlayerName dhoni = createPlayerName("Mahendra", "Singh", "Dhoni", "MSD"); PlayerName karthik = createPlayerName("Krishnakumar", "Dinesh", "Karthik", "KDK"); PlayerName same = createPlayerName("Same", "Same", "Same", "SME"); Transaction transaction = session.beginTransaction(); try{ session.save(rahul); session.save(dhoni); session.save(karthik); Transaction innerTransaction = null; try{ innerTransaction = session.beginTransaction(); session.save(same); }catch(Exception exception){ System.out.println("\n" + exception.getMessage()); }finally{ if (innerTransaction.isActive()){ innerTransaction.commit(); } } }catch(Exception exception){ System.out.println(exception.getMessage()); transaction.rollback(); session.clear(); }finally{ if (transaction.isActive()){ transaction.commit(); } } session.flush(); } private static PlayerName createPlayerName(String fName, String mName,String lName, String id){ PlayerName playerName = new PlayerName(); playerName.setFirstName(fName); playerName.setMiddleName(mName); playerName.setLastName(lName); playerName.setPrimaryKey(id); return playerName; } private static void listPlayerNames(Session session){ Query query = session.createQuery("From PlayerName"); List allPlayers = query.list(); System.out.println("\n"); for(PlayerName player : allPlayers){ listPlayerName(player); } } private static void listPlayerName(PlayerName player){ StringBuilder result = new StringBuilder(); result.append("First Name = ").append(player.getFirstName()) .append(" , Middle Name = ") .append(player.getMiddleName()). append(" , Last Name = "). append(player.getLastName()). append(" , Full Name = ").append(player.getCompleteName()); System.out.println(result.toString()); } }
The above code sets the CustomSaveInterceptor
globally by calling the Configuration.setInterceptor(new CustomSaveInterceptor())
. LoggerInterceptor
is configured on the session-basis by calling the SessionFactory.openSession(new LoggerInterceptor())
. The createPlayerNames()
method creates some test player objects. Note that the player object ‘same’ is created with first-name, middle-name and last-name pointing out to ‘Same’,which is expected to be captured by the Validator Interceptor.
All the objects are saved by calling the Session.save()
method, within a transactional context, but the save operation for the ‘same’ object has been done within a separate transaction, so that even if the persistence operation of the ‘same’ object fails, the rest of the operation wont gets affected. This also tells the support for nested transaction. The code then lists out the player objects fetched from the database by executing a simple query “FROM PlayerName”. The output of the above program will look like this,
Saving the persistent Object class interceptor.PlayerName with Id RSD Saving the persistent Object class interceptor.PlayerName with Id MSD Saving the persistent Object class interceptor.PlayerName with Id KDK First Name, Middle Name and Last Name cannot be the same First Name = Rahul , Middle Name = Sharad , Last Name = Dravid , Full Name = Rahul Sharad Dravid First Name = Mahendra , Middle Name = Singh , Last Name = Dhoni , Full Name = Mahendra Singh Dhoni First Name = Krishnakumar , Middle Name = Dinesh , Last Name = Karthik , Full Name = Krishnakumar Dinesh Karthik
4.2) Hibernate Configuration and Mapping Files
Following are the Hibernate configuration and the mapping files that have to be placed in the sample application’s run-time class path.
Hibernate.cfg.xml:
The Hibernate Configuration File (hibernate.cfg.xml) provides configuration parameters to the application like the database URL, the username/password in the Database server etc. Given below is the Configuration file for the sample application.
<?xml version='1.0' encoding='utf-8'?> <!DOCTYPE hibernate-configuration PUBLIC "-//Hibernate/Hibernate Configuration DTD//EN" "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd"> <hibernate-configuration> <session-factory> <property name="hibernate.connection.driver_class">com.mysql.jdbc.Driver</property> <property name="hibernate.connection.url">jdbc:mysql://localhost/dbforhibernate</property> <property name="hibernate.connection.username">root</property> <property name="hibernate.connection.password">root</property> <property name="dialect">org.hibernate.dialect.MySQLDialect</property> <!-- Mapping files --> <mapping resource="playername.hbm.xml" /> </session-factory> </hibernate-configuration>
playername.hmb.xml:
Mapping Files provides mapping information like how a Java class is mapped to the relational database table. Any number of mapping files can be referenced from an application. Given below is the playername mapping file used in the sample application.
<?xml version="1.0"?> <!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN" "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd"> <hibernate-mapping> <class name="interceptor.PlayerName" table="PlayerNames"> <id name="primaryKey" column="Id" type = "string"> <generator class="assigned"/> </id> <property name="firstName"> <column name="fName"/> </property> <property name="middleName"> <column name="mName"/> </property> <property name="lastName"> <column name="lName"/> </property> <property name="completeName"> <column name="completeName"/> </property> </class> </hibernate-mapping>
5) Summary
This article started with the definition of Interceptors, then defined where Interceptors can fit into the Hibernate Technology. Then are explained the differences between a global interceptor and a session-scoped interceptor. The various API’s related to Hibernate interceptor are given detailed discussion. Finally, the article concluded with a simple sample application making use of the Interceptors.
Some of the popular articles published in our website related to the hibernate framework. Please read this article to learn more about the hibernate. If you have any doubts on the framework, please post it in the comments section.
If you are interested in receiving the future java articles and tips from us, please subscribe here. If you are looking for any book suggestions, please post it in the comments section.
Happy reading!!!