Switch from synchronous to asynchronous processing at runtime
Asynchronous processing is a must for many Java EE or Web-based applications -- but who has time to set up and configure JMS? In this article Di Wang shows you how to decouple your architecture so that it accommodates both synchronous and asynchronous processing. Then you can develop your business logic synchronously and plug in the JMS at runtime. The decoupled architecture enables a quicker development cycle, and also makes it easier to switch from asynchronous to synchronous processing in a deployed application.
It's a common problem in client-server applications that the server spends too much time processing data. Many of us use asynchronous processing, by way of JMS (the Java Message Service API), to get around this problem. JMS is a proven, mature technology that is supported by the Java EE specification. It's also time-consuming and complicated to work with.
Using JMS entails first setting up the JMS provider and message listener (a message-driven bean, or MDB, in many cases). In order to develop and test a JMS application from end to end you need to do the following, at minimum:
- Specify the JMS provider
- Define a physical queue or topic
- Define a connection factory
- Associate the physical queue or topic with a JMS JNDI name
- Define an activation specification if using the JCA (Java Connector Architecture)
No matter what IDE or application server you use, these steps are required. While JMS is a Java EE standard, each JMS provider is a vendor-specific implementation that could require various configuration tasks. Some vendors provide scripts to expedite the setup; but learning the script itself might be a challenge. Many Java developers would agree that the JMS configuration process is tedious, error prone, and even obscure, if not exasperating.
As an alternative to using JMS you could code your business logic using POJOs and then unit test those objects. But it's often the case that meaningful test data can only be gathered if the full work flow of the application is tested. In other words, you still have to go across different layers of your Java EE architecture to create a valid request.
In this article I propose a third option -- a way to reap the benefits of asynchronous processing without the burden of configuring JMS during the development process. In the discussion that follows, I explain how to build an application that runs synchronously for most of the development process, but can then be switched to run asynchronously when you're ready to add JMS to the picture. One of the benefits of this approach is that developers are free to focus on business logic during the development cycle (as you would with a POJO-based solution) without losing the benefits of JMS.
A decoupled architecture
Imagine an application scenario where a client sends a request to a business object (BO), which in turn delegates the processing to a processor. To achieve this goal, you could decouple the BO and the processor and place between them a middle layer, called an Invoker
. Instead of calling the processor directly, the BO calls the Invoker
, which references the processor. In this case, the Invoker
is just an interface with different implementations: synchronous and asynchronous. The introduction of the Invoker
changes the workflow of the application. The architecture diagram for the sample application is shown in Figure 1, and the Invoker
interface is shown in Listing 1.
Figure 1. Sample architectural diagram, showing the Invoker
Listing 1. The Invoker interface
...
/**
* The Invoker interface is used to define various invoker implementation
* strategies. An invoker can be a synchronous invoker or a JMS-based
*asynchronous invoker.
*
*/
public interface Invoker {
public void invoke(Serializable request);
}
Invoker
is referenced by the business layer. As you can see in Listing 1, the interface defines only one method, which takes a serializable object as its input argument. The serializable interface is required by the MDB when the message casts to an object.
The SynchronousInvoker
implementation of Invoker
, shown in Listing 2, provides a synchronous architecture for the system. It references the processor directly.
Listing 2. SynchronousInvoker
...
public class SynchronousInvoker implements Invoker {
/**
* Synchronously invokes the request.
*/
public void invoke(Serializable request){
new MyProcessor().process(request);
}
}
JMSInvoker
, together with the MDB, provides an asynchronous architecture for the system. This implementation of the interface is shown in Listing 3. Instead of talking to the processor directly as SynchronousInvoker
does, JMSInvoker
just drops the message onto a Queue
or a topic, then returns control immediately.
Listing 3. JMSInvoker
...
/**
* A JMS MDB based asynchronous implementation of the invoker.
*/
public class JMSInvoker implements Invoker {
/**
* Asynchronously (JMS) invokes the specific request.
*/
private final static String ConnectionFactory = "jms/ClaimConnectionFactory";
private final static String Queue = "jms/ClaimQueue";
public void invoke(Serializable request){
try{
Context ctx = new InitialContext();
ConnectionFactory cf = (ConnectionFactory)
ctx.lookup(ConnectionFactory);
Destination outDest = (Destination) ctx.lookup(Queue);
Connection connection = cf.createConnection();
Session session = connection.createSession(false,Session.AUTO_ACKNOWLEDGE);
MessageProducer destSender = session.createProducer(outDest);
ObjectMessage ooutMessage = session.createObjectMessage(request);
destSender.send(ooutMessage);
destSender.close();
session.close();
connection.close();
}catch(Exception e){
String error = "Error in JMS invoker: " + e.getMessage();
}
}
}
Sitting in the EJB container, ProcessMDBBean
, shown in Listing 4, receives the message, casts the message to the request object, and passes it along to the processor.
Listing 4. ProcessMDBBean
...
/**
* Bean implementation class for Enterprise Bean: ProcessMDB
*/
public class ProcessMDBBean
implements
javax.ejb.MessageDrivenBean,
javax.jms.MessageListener {
...
/**
* onMessage
*/
public void onMessage(javax.jms.Message msg) {
java.io.Serializable request = null;
try {
//call the application processor
new MyProcessor().process(request);
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
This is where the real business logic goes. MyProcessor
, shown in Listing 5, processes the request object passed in.
Listing 5. MyProcessor
...
public class MyProcessor {
public void process(Serializable request){
//processing request here...
}
}
At the business layer, MyBO
, shown in Listing 6, retrieves an Invoker
instance from the InvokerFactory
, shown in Listing 7, and lets the Invoker
do the processing.
Listing 6. MyBO
...
public class MyBO
{
public String submitClaim(Claim myClaim){
/* Delegate call to invoker to invoke the specific request:
*
* Note: The invoker is a synchronous or asynchronous
* process based on the specific implementation used. */
InvokerFactory.getInstance().invoke(myClaim);
return myClaim.getClaimId();
}
}
Listing 7 shows the InvokerFactory
.
Listing 7. InvokerFactory
...
public class InvokerFactory {
private InvokerFactory(){};
private static HashMap map = new HashMap();
private static boolean _isSynchronous = true;
public static void setSynchronous(boolean syncFlag){
_isSynchronous = syncFlag;
}
public static boolean getSynchronous(){
return _isSynchronous;
}
public static synchronized Invoker getInstance() {
//load synchronous invoker
String syncClassName = PropertyReader.getProperties().getProperty("SYNC_INVOKER_CLASS").trim();
Invoker synchSingleton = map.get(syncClassName);
if (synchSingleton == null){
try{
synchSingleton = (Invoker) Class.forName(syncClassName).newInstance();
}
catch(Exception e){e.printStackTrace();}
map.put(syncClassName, synchSingleton);
}
//load asynchronous invoker
String asyncClassName = PropertyReader.getProperties().getProperty("ASYNC_INVOKER_CLASS").trim();
Invoker asynchSingleton = map.get(asyncClassName);
if (asynchSingleton == null){
try{
asynchSingleton = (Invoker) Class.forName(asyncClassName).newInstance();
}
catch(Exception e){e.printStackTrace();}
map.put(asyncClassName, asynchSingleton);
}
return _isSynchronous?synchSingleton:asynchSingleton;
}
}
The InvokerFactory
loads both Invoker
s initially, then returns the appropriate one based on the value of isSynchronous
. isSynchronous
can be changed at runtime.
Deployment processes
To fully take advantage of this approach, a technical lead in your development team should take responsibility for setting the Java EE and EJB module dependencies. At the beginning of the development process, the technical lead unchecks the EJB module dependency at the EAR module, and checks in the code to source control. Once the code has been checked out from source control, the rest of the development team can focus on developing the core business logic using a synchronous architecture. A screenshot of these settings in Rational Application Developer is shown in Figure 2; obviously the equivalent is possible in any IDE.
Figure 2. Setting the dependencies
At the end of the development process, when you're ready to test the application, the technical lead can mark the EJB module dependencies for the EAR module and set up the JMS provider. Depending on the server vendors chosen, the technical lead might have to do some MDB configuration tasks as well. When the rest of the development team checks out the code from the SCM this time, they can use admin tools to switch the nature of the workflow. For instance, a JSP page like the one shown in Figure 3 would allow developers working on this application to toggle between the two invoker options.
Figure 3. AdminPage.jsp
Benefits and limitations of using the Invoker interface
While this technique can save time and trouble during the development process, it isn't suitable for every application scenario. If the application's asynchronous architecture differs too much from the synchronous version, the time saved during development might be wasted, as any asynchronous-specific features will need to be tested out in the asynchronous environment. For instance, if a feature requires that a customer asynchronously send requests and then work on other tasks while waiting for the confirmation, you cannot test this feature in the synchronous sequence.
In conclusion
Using the Invoker
interface to decouple your architecture gives your application the flexibility to change from synchronous to asynchronous processing at runtime. This flexibility can help increase your development team's productivity by deferring JMS configuration and delegating it to a central resource, such as a technical lead.