Capture the benefits of asynchronous logging

Develop an asynchronous log service using JMS and Hibernate

Developing a log service that suits common needs often provokes debate. Numerous homegrown log frameworks are available in addition to many professional and open source products. Products such as log4j and J2SE 1.4 logging are worth mentioning. Many frameworks were developed before the advent of JMS (Java Message Service), and thus, without the benefits of asynchronous messaging. Frameworks designed after JMS's initiation benefited from asynchronous messaging.

In a traditional synchronous log model, the caller cannot execute further unless the log service returns successfully. All calls are blocked until the record is persisted or acknowledged by the log service. That clearly results in an overhead, especially if an application is designed to code numerous log messages. Imagine a source file consisting of a large number (sometimes hundreds) of log statements. Each statement should be logged before the following statement is processed—clearly a time-consuming process. In a distributed-computing environment, obviously, clients operate concurrently. The alternative is to build a concurrent client architecture using the traditional log service, although performance may remain an issue. Distributed computing architectures may also have deployments spread across many servers (geographically similar or dissimilar locations), in which case, logging to a central database synchronously is cumbersome, if not impossible.

This article will help you develop a simple log service. The service creates some log messages, sends them across the network to a JMS provider, and persists them in the database. For this purpose, we use JMS for its asynchronous benefits and Hibernate for its simple yet powerful persistence features. You can persist data in many ways, such as through standalone JDBC (Java Database Connectivity) connections, EJB (Enterprise JavaBeans), and stored procedures. I vote for tools that create domain entities from plain-old Java objects. In this article, I move away from constructing the domain model using EJB entities by using Hibernate.

Before developing the log service, let's look at some important concepts vital for developing this application.

Enterprise messaging

J2EE continues to mature with respect to distributed computing environments. We can leverage many of its features, such as JMS, to develop asynchronous services. JMS is a specification from Sun Microsystems and its partners that covers inter-application messaging standards. See Resources to download the specification. Third-party vendors implement the specification defined by Sun. JMS is designed to handle different message types without the messages having knowledge of each other.

Remote procedure call (RPC) models such as RMI (remote method invocation), SOAP (Simple Object Access Protocol), and ORB (object request broker) are interface-centric, meaning the publisher expects a return value. In the case of JMS, the sender has no knowledge of the receiver. The applications that send and receive messages do not communicate via a standard communication channel. In fact they delegate the responsibility of delivering the messages to the JMS provider. The applications' jobs are completed when they post a message to the destination.

Messaging flavors

JMS provides two messaging flavors: point-to-point and publish/subscribe. Point-to-point is a messaging model based on the queuing concept. Each client sends messages to a destined queue, wherein the messages are placed on a stack. The messages are persisted and can survive a server crash (guaranteed messaging benefit). The JMS provider will deliver the messages pushed onto this queue to consumers. In the case of a point-to-point model, a single message is delivered to a single consumer.

The publish/subscribe model is a broadcast model in which all clients interested in a particular subject register themselves as listeners to that subject. A publisher publishes its message to the destination (topic). The JMS provider takes responsibility for distributing the messages to the interested parties (listeners). In this model, all interested parties consume each message.

Message-driven beans

EJB 2.0 introduced a new type of bean component called the message-driven bean (MDB). This bean is nonresponsive to client invocations. In fact, no interface is provided for the client to call on the MDB. Only the container can activate MDBs upon receiving the appropriate JMS message. They are nothing but message consumers. MDB implementation is neat. All the JMS administration information is described in the deployment descriptors, while the activity of receiving and processing the messages occurs literally in the onMessage() method. Please see Resources for further information on MDBs.

Hibernate

Hibernate is an open source product that eliminates JDBC coding. It is a tool for object-relational (OR) mapping within Java environments. It creates a mapping between Java objects and database entities. Object-relational mapping is a key requirement in enterprise projects. The data produced or changed as a result of processes must be persisted. Persisting data can turn into a nightmare for developers, especially if the data model is always changing the user requirements. Hibernate can alleviate complications resulting from data persistence. Refer to Resources for more information on Hibernate. The log service framework we develop in this article uses Hibernate as a medium for OR persistence.

The asynchronous log service

In the above sections, I briefly discussed the major ingredients vital for developing this log service. Now, let's develop a simple framework that utilizes the capabilities of JMS and Hibernate persistence.

Conceptually, the asynchronous log service works like this: The client creates a log message (value object) and asks a helper class, JMSLogger, to publish the message to the queue. JMSLogger creates a JMS ObjectMessage with a log message and posts it to the queue. The container invokes an MDB listening to this queue as soon as the message arrives by calling the MDB's callback method, onMessage(). The MDB reads the message and persists into the database using Hibernate.

The log service uses JMS's point-to-point messaging. Once a log message is sent, the client doesn't need to worry about where the message has been sent or whether it has even been sent. It relies on the underlying JMS messaging implementation for the reliable and guaranteed message delivery.

The figure below illustrates the log service's main components.

Functional diagram: Main components of log service

Framework components

Let's look at the components in detail before we delve into implementation specifics. The log client is this application's entry point. Minor differences arise when this client is used as a non-J2EE client (see examples in Resources). The log client uses JMSLogger to post the messages to the queue. It creates a log message with appropriate severity and instantiates JMSLogger to post the messages to the LogMessageQueue destination.

JMSLogger is a JMS-aware class that associates itself with the destination once it is instantiated. Hence, when created by the client, the JMSLogger looks up the connection factory and queue from the JNDI (Java Naming and Directory Interface) naming space and opens the session with the JMS provider. The class then starts sending the messages to the destination. Clients access the log service by simply creating an instance of the log client and formally calling the methods on it. If necessary, in a J2EE environment, this object can be instantiated only once during startup using Java Management Extensions (JMX) managed beans (MBeans) when the application server starts. Clients can then look for this service from JNDI and call the methods to persist the log messages as usual.

LogMessageQueue is the JMS destination wherein the log messages are posted. An MDB listens to this queue. As soon as a message arrives to the queue, the EJB container invokes the MDB and delegates the message for further processing. The MDB extracts the log messages, and, by using Hibernate's persistence mechanism, it persists the log message into the database.

The LogMessage is a Java value object that carries log information to be persisted. It is designed to hold minimal log information, such as short description, long description, log date, and severity.

Implementation

Now I'll describe the log service's implementation details. You can download the all implementation code from Resources and unpack it to a local directory.

Create the log table

First let's create a table that can store the log information. All we need is the log message's short description, long description, log level, and log date. The script listed in Listing 1 creates a table called LOG_DATA with those five fields. Except for the MESSAGE_ID, all the fields are self-explanatory. Use the script provided in the source code to create this table.

MESSAGE_ID is the primary key attribute assigned by the application. I'll discuss this field further in the "Hibernate Mapping File" section.

Listing 1: Create log table script

create table LOG_DATA
(
  MESSAGE_ID VARCHAR2(30) not null,
  SHORT_DESC VARCHAR2(20),
  LONG_DESC  VARCHAR2(200),
  LOG_DATE   DATE,
  LOG_LEVEL  VARCHAR2(10)
)

LogMessage

LogMessage is a value object to be persisted in the log database. Log clients create this object with proper log information.

As I mentioned earlier, we use Hibernate for persisting this LogMessage object.

Create a Java class that represents the above table. I have created the class with five attributes, each representing the table's columns. Also create the attributes' setters and getters, as Hibernate requires them.

Hibernate mapping file

To persist the LogMessage, Hibernate requires a mapping file. As the name suggests, the mapping file is an XML file that defines the object-relational mapping. In our case, it defines the attributes of the LogMessage class tied with table columns. The syntax of this XML-mapping file is quite straightforward:

Listing 2: Hibernate OR mapping file

<hibernate-mapping>
<!-Provide the name of the class and a mapping table here à
   <class name="com.madhusudhan.articles.logger.LogMessage"
table="LOG_DATA">
      <!-id represents the primary key. In our case, MESSAGE_ID is the
primary key.
      The key is generated by the application  à
         <id name="messageId" unsaved-value="id_value">
         <column name="MESSAGE_ID" sql-type="VARCHAR2(30)"
not-null="true"/>
          <generator class="assigned"/>
      </id>
      <!- map the Attributes of the class with the table columns à
      <property name="shortDesc">
         <column name="SHORT_DESC" sql-type="VARCHAR2(20)"
not-null="false"/>
      </property>
      <property name="longDesc">
         <column name="LONG_DESC" sql-type="VARCHAR2(200)"
not-null="false"/>
      </property>
      <property name="logDate">
         <column name="LOG_DATE" sql-type="DATE"  not-null="false"/>
      </property>
      <property name="logLevel">
         <column name="LOG_LEVEL" sql-type="VARCHAR2(10)"
not-null="false"/>
      </property>
   </class>
</hibernate-mapping>

I'll run through this file briefly:

The first line of the file (<class name="com.madhusudhan.articles.logger.LogMessage" table="LOG_DATA">) establishes mapping between our class and the database table. Provide the fully qualified name for the class attribute. The id attribute represents the primary key value. The attribute of the class messageId maps to key column MESSAGE_ID, which has a tag called generator. This class generates the primary keys for the table, according to your requests. In our example, I told Hibernate to assign the id to the application. You can also assign the id using generator classes supplied with the Hibernate distribution. Refer to Hibernate documentation for further details.

Remember to name this file LogMessage.hbm.xml

JMSLoggerBean (MDB)

The JMSLoggerBean listens to the messages posted to the queue and persists them to the database. The container invokes JMSLoggerBean when a message arrives. In the onMessage() method, the MDB asks Hibernate to persist the object. Because Hibernate simplifies the persistence process, I don't ask the MDB to pass the message to some other component, such as the session bean, for further processing. The MDB can do all the business processing itself.

The onMessage() method looks like this:

public void onMessage(javax.jms.Message message) {
   try {
   // Cast the JMS message to ObjectMessage type
   ObjectMessage objMsg = (ObjectMessage)message;
   // And extract the log message from it
   LogMessage logMsg = (LogMessage)objMsg.getObject();
   // Persist the message
   persistMessage(logMsg);
   }catch (Exception t) {
      t.printStackTrace();
   }
}

As soon as the message hits the queue, the MDB is invoked by the container and then calls the onMessage() method. In the method, retrieve the LogMessage from the JMS ObjectMessage and persist it by calling the following method:

private void persistMessage(LogMessage message) throws 
HibernateException {
   net.sf.hibernate.Session session = null;
   SessionFactory sessionFactory = null;
   Transaction tx = null;
   try {
      // Create a session factory from the configuration
(hibernate.properties
      // File should be present in the class path)
      sessionFactory = new Configuration(). addClass(LogMessage.class).
        buildSessionFactory();
      // Create a session
      session = sessionFactory.openSession();
      // Begin a transaction
      tx = session.beginTransaction ();
   }catch(..){
   .....
}
try{
   // Assign the message id
   message.setMessageId (""+System.currentTimeMillis ());
   // Save the object to the database and commit the transaction
   session.save(message);
   tx.commit ();
}catch(Exception ex){
....
}finally {
   // Close the session
   session.close();
}
}

In the first try-catch block, a SessionFactory object is created, which opens up a live session to the database. I start the transaction created from the session:

   sessionFactory = new Configuration().
   addClass(LogMessage.class).
   buildSessionFactory();
   session = sessionFactory.openSession();
   tx = session.beginTransaction ();

In the next try block, the ID is assigned to the LogMessage object and persists (saves) it to the database as shown below:

   message.setMessageId (""+System.currentTimeMillis ());
   // Save the object to the database.
   session.save(message);
   tx.commit ();

As you can see, once you fix the object-relational mapping file, persisting the log information into the database is easy. Hibernate does all that behind the scenes.

Deployment descriptor

The deployment descriptors follow. Listing 3 is the ejb-jar.xml file, while Listings 4 and 5 are the vendor specific JBoss and JRun deployment descriptors.

Listing 3: ejb-jar.xml deployment descriptor

   ....
   <message-driven>
      ....
      <ejb-name>JMSLoggerBean</ejb-name>
      
<ejb-class>com.madhusudhan.articles.logger.JMSLoggerBean</ejb-class>
      <transaction-type>Container</transaction-type>
      <acknowledge-mode>Auto-acknowledge</acknowledge-mode>
      <message-driven-destination>
         <destination-type>javax.jms.Queue</destination-type>
         
<subscription-durability>NonDurable</subscription-durability>
      </message-driven-destination>
      <resource-env-ref>
         <description>Asynchronous Log Queue</description>
         
<resource-env-ref-name>AsynchronousLogQueue</resource-env-ref-name>
         
<resource-env-ref-type>javax.jms.Queue</resource-env-ref-type>
      </resource-env-ref>
   </message-driven>
   ....

Listing 4: JBoss deployment descriptor

   <message-driven>
      <ejb-name>JMSLoggerBean</ejb-name>
      <configuration-name>Standard Message Driven 
Bean</configuration-name>
<destination-jndi-name>queue/AsynchronousLogQueue</destination-jndi-name>
    </message-driven>

Listing 5: JRun deployment descriptor

....
<message-driven>
      ....
   <resource-ref>
      <res-ref-name>ConnectionFactory</res-ref-name>
      <jndi-name>jms/ConnectionFactory</jndi-name>
   </resource-ref>
   <resource-env-ref>
      <resource-env-ref-name>AsynchronousLogQueue 
</resource-env-ref-name>
      <jndi-name>jms/queue/AsynchronousLogQueue </jndi-name>
      <mdb-destination>jms/queue/AsynchronousLogQueue 
</mdb-destination>
   </resource-env-ref>
</message-driven>

You can download full listings of the deployment descriptors from Resources.

Client code

Now the rest of the coding involves developing the client programs: The classes JMSLogger, LogService, and Logger are used for the client side interactions.

JMSLogger is a message-posting client. The LogService instantiates it by calling its constructor, which takes the connection factory and destination name:

     Logger logger = new JMSLogger("ConnectionFactory",
                              "queue/CyrusLogQueue");

Once the logger is instantiated, the LogService only needs to call the sendMessage() method as shown below:

   private void sendMessage(LogMessage logMessage){
   JMSLogger logger = null;
   ....
   logger = new JMSLogger("ConnectionFactory",
     "queue/CyrusLogQueue");
   ...
   logger.sendMessage(logMessage);
      System.out.println("[ LogService::sendMessage: The message sent ]");
   ....
}

The Logger is a wrapper class around LogService and is used by the client. The client creates Logger as Logger logger = new Logger() and calls the methods on this object appropriately.

Deploy the bean

Before accessing the log service, deploy the MDB JAR into the application server. Create the respective queue and connection factory. In JBoss, you can create these JMS-administered objects in two ways: either use the console or write your own destination-service XML (create the <some-name>-destinations-service.xml file and deploy this file under server\<servername>\deploy\jms\).

In the case of JRun, create the objects by editing jrun-resources.xml or use the JMX console.

Refer to Resources for further details of JBoss's and JRun's deployment procedures.

Using the log service

The simple way to use the log service is by instantiating the Logger from the client code. Once instantiated, log methods are called on the logger as shown below:

   try{
      Logger logger = new Logger();
      logger.info("MSG000111",
        "CONFIG SEVICE is not initialised. Please check the settings.");
      logger.severe("ERR101010",
        "APPSERVER CRASHED. Notification to the Admin sent.")
   }catch(LogException lex){
   ....
}

Some JUnit tests are available for testing this log service. Unpack the resources for the LogServiceTestCase.java and LogServiceTestSuite.java test-source files.

Wrap up

This article discussed how to develop an asynchronous log service. JMS is a powerful feature of J2EE. JMS's asynchronous behavior coupled with Hibernate's object-relational persistence provides flexible and reliable frameworks. Many Java applications that rely on protocols such as RMI, SOAP, and CORBA can benefit to a large extent by using JMS. After EJB 2.0, since JMS is now integrated into the application server architecture, EJBs can take advantage of messaging models. A messaging proxy program that invokes session beans can be successfully replaced by message-driven beans.

Asynchronous messaging principles can be applied to develop services such as property services, configuration services, and other data services. This article's asynchronous log service can be extended by making its controls manageable using JMX managed beans (MBeans). Also you could create a log viewer to view the log messages when and while they are persisted to the database.

I would like to thank my manager Russell Bryer and my colleague Adrian Bigland at First Choice Holidays in London for their support while preparing this article. I'd also like to thank my wife Jeannette D'Souza for her invaluable assistance.

Madhusudhan Konda is a senior J2EE consultant at First Choice Holidays, London. He specializes in enterprise architectures and JMS. He has designed and developed J2EE frameworks that include asynchronous messaging models, system performance, health testing suites, and EJB logging. He has worked on a variety of projects with clientele such as Halifax (Intelligent Finance), British Petroleum, British Airways, and First Choice. He holds an MBA from the University of Westminster in London and a master's of technology from IIT Kharagpur (India). Log on to http://www.madhusudhan.com to learn more about him.

Learn more about this topic

Join the discussion
Be the first to comment on this article. Our Commenting Policies