In a previous installment, I showed you how I used ActiveMQ's
JCAContainer to expose a class that only implements
javax.jms.MessageListener
could be exposed as a message-driven
POJO (hereafter referred to as MDP).
Aside from sparking a heated
and unnecessary JBoss vs. Spring debate on The ServerSide (which I
refuse to participate in), that article demonstrated that a simple
JavaBean could be granted the powers of
message-driven EJBs without requiring an EJB container or the
lifecycle methods mandated by javax.ejb.MessageDrivenBean
.
In that article, I questioned the validity of referring to the
MDP as a "POJO" because it still must implement
javax.jms.MessageListener
. I wrote it off as a minor annoyance at the
time. Since then, James has been quite
helpful and has shown me how to achieve MDPs with pure POJOs (not requiring any
external interfaces of any kind). The trick is to use Lingo. Let me show you how...
Getting started
As before, there are several JARs you'll need in your classpath. The following list is what I'm using to develop and implement Lingo-style MDPs:
- spring.jar (I'm using the 1.2.2 version)
- log4j-1.2.9.jar
- commons-logging-1.0.3.jar
- activemq-core-3.1-SNAPSHOT.jar
- concurrent-1.3.4.jar
- geronimo-spec-jms-1.1-rc4.jar
- geronimo-spec-j2ee-management-1.0-rc4.jar
- lingo-1.0-SNAPSHOT.jar
As a service to my readers, I've bundled up all of these JARs along with my example code and made it available here.
Writing the POJO
The first thing you'll need for a MDP is a POJO. The following HelloServiceImpl class mimics the functionality of the MDP from my previous article:
package com.habuma.mdpojo.lingo; public class HelloServiceImpl implements HelloService { public void sayHello(String name) { System.out.println("Hello " + name + "!"); } }
You'll notice that (unlike my previous example) this class doesn't implement any interface from an external framework or platform. In fact, the only interface it implements is its own service interface:
package com.habuma.mdpojo.lingo; public interface HelloService { public void sayHello(String name); }
So far there's nothing special about this POJO. In fact, it could be used as a regular non-MDP bean if you'd like. That's the beauty of the Lingo approach: Your MDP doesn't even know that it's message-driven!
Exporting the JMS service
Turning the POJO into an MDP involves a service exporter. If you've
ever used Spring to export POJOs as RMI, Hessian/Burlap, or
HttpInvoker services, you're already familiar with service
exporters. (If not, then chapter 6 of Spring in
Action may be of interest to you.) In the case of Lingo MDPs,
the service exporter is Lingo's JmsServiceExporter
. I've
declared the JmsServiceExporter
bean (in mdp-service.xml) as
follows:
<bean id="server" class="org.logicblaze.lingo.jms.JmsServiceExporter"> <property name="service"> <bean class="com.habuma.mdpojo.lingo.HelloServiceImpl" /> </property> <property name="serviceInterface" value="com.habuma.mdpojo.lingo.HelloService" /> <property name="connectionFactory"> <ref bean="connectionFactory" /> </property> <property name="destination"> <ref bean="destination" /> </property> </bean>
The "service" property tells the JmsServiceExporter
which POJO will
be exported. Here I've wired HelloServiceImpl
in as an
inner-bean, but there's no reason it couldn't have been declared as a
regular <bean>
and wired in as a reference. The
"serviceInterface" tells the JmsServiceExporter
the interface of the
exposed POJO.
The "connectionFactory" and "destination" properties wire in the JMS connection factory and a message queue that the service should be exposed to. Assuming that ActiveMQ will be running on the localhost, listening on port 61616, the following beans will do:
<bean id="connectionFactory" class="org.activemq.ActiveMQConnectionFactory"> <property name="brokerURL" value="tcp://localhost:61616" /> </bean> <bean id="destination" class="org.activemq.message.ActiveMQQueue"> <constructor-arg index="0" value="Hello.Queue" /> </bean>
The same "connectionFactory" and "destination" beans will also be needed on the client side. So that I don't have to duplicate these declarations, I've placed them in a separate Spring application context file called "mdp-common.xml" that will be shared by both the MDP and the client.
Starting the MDP
With JmsServiceExporter
declared, we're ready to start up the
application context. The following class will do the trick:
public class StartMDPojo { public static void main(String[] args) { new FileSystemXmlApplicationContext( new String[] {"mdp-common.xml", "mdp-service.xml"}); } }
Make sure ActiveMQ is running, then fire up StartMDPojo
. If
everything goes well, your MDP will be added as a new client to
ActiveMQ and will be ready to service requests. (Note: I had to use
ActiveMQ 3.1-SNAPSHOT to get this to work. I had some trouble running Lingo
against ActiveMQ 3.0.)
Sending a message
Sending a message to the MDP is also fairly painless. Just as with
accessing other remoting options in Spring, we'll use a proxy factory
bean. In this case, we'll use Lingo's JmsProxyFactoryBean
(declared in
mdp-client.xml):
<bean id="client" class="org.logicblaze.lingo.jms.JmsProxyFactoryBean"> <property name="serviceInterface" value="com.habuma.mdpojo.lingo.HelloService"/> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="destination" /> </bean>
If you've used any of Spring's proxy factory beans for accessing
other remote services (such as RmiProxyFactoryBean
or
HessianProxyFactoryBean
), then
JmsProxyFactoryBean
should seem
somewhat familiar. Just as with the other remoting options,
JmsProxyFactoryBean
wraps the details of
accessing the remote service in a proxy that implements the POJO's
service interface. From the client's perspective, the
remote service appears to be just another local POJO.
The properties used to configure JmsProxyFactoryBean
are fairly
self-explanatory. Again, the "serviceInterface" defines the interface
that the proxy will implement. And the "connectionFactory" and
"destination" properties tell the proxy which message queue to send
the message to.
In a typical application, the "client" bean would be wired into a
HelloService
property of some other bean. But for the
sake of simplicity, our example will pull
the "client" directly out of the application context and call it's
sayHello()
method:
public class MDPojoClient { public static void main(String[] args) { ApplicationContext ctx = new FileSystemXmlApplicationContext( new String[] {"mdp-common.xml", "mdp-client.xml"}); HelloService client = (HelloService) ctx.getBean("client"); client.sayHello("Craig"); } }
Notice that the MDPojoClient
isn't any the wiser that the "client"
bean that it is using is actually an MDP. For all it knows, it's
dealing with some local POJO that implements
HelloService
. In fact, the only place you'll find any
references to JMS is in the configuration files. The actual Java
source code is blissfully ignorant of the fact that there's any JMS
work going on behind the scenes. This is important because it makes it
easy to swap out the MDP implementation with some other implementation
(perhaps an RMI implementation, an EJB implementation...or even a mock
implementation when unit-testing).
When you run MDPojoClient
and the
sayHello()
method is invoked, the inner-workings of the
JMS proxy will place a message into the "Hello.Queue". On the MDP
side, the service exporter will consume the message and send it on to
the actual implementation of sayHello()
.
One-way methods
If you were paying close attention, you may have noticed that the
call to sayHello()
blocked--that is, it was a synchronous
call. Can Lingo not do asynchronous calls?
Lingo decides whether a method is synchronous or asynchronous by
consulting a Metadata
Strategy. The default (and currently the only) metadata strategy
is SimpleMetadataStrategy
. By default, SimpleMetadataStrategy
assumes that all
calls are synchronous. But by configuring a new
LingoRemoteInvocationFactory
with your
JmsProxyFactoryBean
, you can change all of that. What you
need to do is register a LingoRemoteInvocationFactory
bean in mdp-client.xml like this:
<bean id="invocationFactory" class="org.logicblaze.lingo.LingoRemoteInvocationFactory"> <constructor-arg> <bean class="org.logicblaze.lingo.SimpleMetadataStrategy"> <constructor-arg value="true" /> </bean> </constructor-arg> </bean>
As you can see, I've told Spring to instantiate
LingoRemoteInvocationFactory
using a single-argument
constructor, passing a SimpleMetadataStrategy
instance. The SimpleMetadataStrategy
itself is declared
as an inner-bean. Its "oneWayForVoidMethods" property is set to "true"
to indicate that void
methods are one-way and shouldn't
block.
Just declaring the "invocationFactory" bean is not quite
enough. You must also wire it into the "remoteInvocationFactory"
property of the JmsProxyFactoryBean
:
<bean id="client" class="org.logicblaze.lingo.jms.JmsProxyFactoryBean"> <property name="serviceInterface" value="com.habuma.mdpojo.lingo.HelloService"/> <property name="connectionFactory" ref="connectionFactory" /> <property name="destination" ref="destination" /> <property name="remoteInvocationFactory" ref="invocationFactory" /> </bean>
Now if you run MDPojoClient
the call will return
immediately, even if the sayHello()
method takes longer
to execute. Try it...put a Thread.sleep(10000)
in the
sayHello()
method and some appropriate logging to see
that the call returns immediately while the sayHello()
method stalls for 10 seconds.
But what about...?
What if the method isn't void
? Can the call still be
asynchronous? In short, no. If the method being invoked returns
something, the client must wait for the return value. (In
theory, you could write your own metadata strategy to ignore returned
values, but I'm not sure of what value that would be.)
What if you need an asynchronous method to reply to the client? All
you must do is create an instance of
java.util.EventListener
, pass it to the one-way method as
an argument, then have the one-way method place its results into the
listener. (Perhaps I'll write up an article showing this in more detail
some other time).
What about JBoss message-driven POJOs? How do they compare to the
Lingo approach? Well, I've only glanced over the Trailblazer
examples, so I can't speak as an expert on the topic. I'm still
forming my opinions at this point, but I'm a bit troubled that a
supposed "POJO" approach requires me to use a
org.jboss.annotation.ejb.Consumer
annotation. What about
using this annotation is better than implementing
javax.jms.MessageListener
?
(Don't read this question as an attack on JBoss...I'm legitimately
asking.)
Before I go, one other nit: There are some out there who read my
last article and referred to the MDP technique as the "Spring
approach". It is true that Spring is involved, but to be precise, the
technique presented in that article is actually the ActiveMQ
approach to MDP. Likewise, the technique presented in this article is the
Lingo approach to MDP. Spring itself currently does not have its own
MDP solution.
As I mentioned before, I'll be discussing MDPs (both ActiveMQ-style and Lingo-style) among other things in a presentation that I'll be giving at the LoneStar Software Symposium in Austin, TX next month. Be sure to say "hi" if you're there.
Any typos, goof-ups, blunders, snaffoos, or bloopers contained in this article are merely a figment of your imagination. Boo-boos and flub-ups, however, are probably my fault. Please let me know if you find any so that I may correct them.