Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
The JMS Distributed Object Framework (JMS-DOF) is an experiment in supporting distributed object communication using the Java Messaging Service. The framework is intended to provide an alternative to technologies like CORBA and RMI in distributed applications that are centered around JMS-based communication.
One of the benefits of using JMS for distributed object communication is scalability. For example, you may be providing a stateless data service to your clients. With JMS-DOF you can run multiple copies of the service, each consuming messages from a common queue of requests. The services will pull requests from the JMS queue when the service is not busy and a request is available. Although the client request and the reply will be communicated using asynchronous JMS messages, the client sees a synchronous object interface. There are definite advantages to this loosely coupled approach. New service replicas can be transparently added as system load increases. Also, If a replicated service crashes, it will not directly affect the client.
Some of the features of the DOF include:
Version 0.2 adds the following features...
Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
DOF applications are organized as servers containing one or more distributed objects (also called services).
Writing a serviceTo implement a service in this distributed object framework, a developer simply defines an interface and implements it. For example, a login/authentication service might provide the following interface:
public interface LoginService {
AccessToken authenticate(String user, String password)
throws AuthenticationException;
}
The service implementer might define
the implementation to be:
public class LoginServiceImpl implements LoginService {
public AccessToken authenticate(String user, String password)
throws AuthenticationException
{
if (user.startsWith("A")) {
return new AccessToken("WXYZ");
} else {
throw new AuthenticationException("invalid user");
}
}
}
From a distributed object (service)
developer perspective, that's it. Notice there is no special base class or application-specific
code that needs to be written to support distributed access to the service. The
object implementation is the same whether it is used locally, remotely, or a accessed
in both contexts. To make this object available for access via JMS, the server
initialization code would register it with a JMS-specific implementation of the
RequestDispatcher interface. For
example (exception handling not shown),
public static void main(String args[]) {
// ... initialize JMS, etc.
JmsRequestDispatcher dispatcher =
new JmsRequestDispatcher(queueSession);
dispatcher.registerObject(
"LoginService-1",
new LoginServiceImpl(),
queueSession.createQueue("LoginRequest"));
queueConnection.start();
}
The login service object identifier is "LoginService-1" and the requests will be received from the JMS queue called "LoginRequest". At this point, the object is available to remote clients. Again, the fact that the service is made available to remote clients via JMS is transparent to the developer of the distributed object. Since each object instance is registered with a unique identifier, multiple implementations of the same interface can be provided from the same server. This probably wouldn't make sense for a LoginService, but might be appropriate for a server providing multiple implementations of an abstract stock exchange interface.
It's also possible to create multiple servers that provide the same service instance (e.g. LoginService-1). Each server is listening to the same queue. From the client's perspective, there is only one distributed object. The multiple servers supports load balancing since an available server will attempt to pull the next message from the JMS queue. It can also provide some degree of fault tolerance since some of the replicated servers could crash and the remaining servers can still process requests from the same queue.
Accessing a service
To use the remote object, a client would obtain a distributed object reference by some means. For this example, the client initialization routine creates the reference explicitly. The reference could also be provided through a distributed JNDI server, etc.
LoginService loginService; // available to clients
public static void main(String[] args) {
// ... initialize JMS
JmsResponseDispatcher responseDispatcher =
new JmsResponseDispatcher(queueSession,
queueSession.createTemporaryQueue());
loginService =
(LoginService)DistributedObjectProxyFactory.create(
LoginService.class, "LoginService-1",
new JmsQueueTransport(responseDispatcher, queueSession, "LoginRequest"));
queueConnection.start();
}
This code creates a JmsResponseDispatcher
and uses it to create a JmsQueueTransport. The interface and the distributed object
instance identifier is used to create a dynamic proxy for accessing the remote
object using the queue called "LoginRequest". Any number of distributed object
proxies may share the same response dispatcher. Alternatively, multiple dispatchers
may be used if needed. To address threading issues, an Oswego
Executor implementation can be
passed as an optional construction parameter of the response dispatcher. If an
application needs pooled multithreaded response handling, a PooledExecutor
could be used. This approach eliminates the risk that a slow callback handler
will block the response dispatcher. Without an executor, the callbacks execute
in the JMS response handling thread.
A client accessing the LoginService will do so through the specified interface in the same manner as making a call to a local object.
AccessToken token;
try {
token = loginService.authenticate("user", "password");
} catch (AuthenticationException ex) {
log.error(ex);
}
In this code, the token is a local object.
Methods may also return distributed object references to remote objects and pass
those references as parameters to subsequent remote or local calls. Also notice
that the AuthenticationException
is effectively the exception thrown by the server code. Of course, it's a local
copy of the remote exception therefore stack trace information is not available
to the client.
No application-specific code was required to implement this synchronous call in the client. In fact, the same code could be calling a local object. It's impossible to tell from the example code. Under the hood the framework does the following:
This transparency is provided without application-specific classes or automated code generation. Much of this functionality is made possible through the java.lang.reflect.Proxy class.
On the server side, the following actions take place...
If an exception is thrown, it is sent to the client instead of the return value. A timeout is communicated to the client as an exception.
Asynchronous Calls
Asynchronous calls are executed by invoking the "call" method on a proxy and supplying a callback method. The callback method thread depends on the how the response dispatcher was configured. In this example, a stock quote service is accessed asynchronously.
public class QuotePrinter implements ResponseCallback {
public void handleResponse(Response response) {
Double price = (Double)response.getResult();
System.out.println("(async) IBM price: "+price);
}
}
// The async call
((DistributedObjectProxy)quoteService).call(
"getQuote(Ljava.lang.String;)", new Object[]{ "IBM" }, new QuotePrinter());
The type cast is needed because the quoteService implements its business object interface and the DistributedObjectProxy interface but the interfaces are not inherited from each other. The method signature passed to the asynchronous call is a slightly modified version of method signatures used by JNI. There is no return value specification and classes use periods as package delimiters rather than '/'. Other than that, it's the same syntax.
Exceptions are passed back through the response object. A flag indicates a normal response versus an exceptional one.
Custom Marshalling
The DOF is designed to allow flexible strategies for marshalling requests and replies. The request is filtered through an ObjectTransformer and then, assuming a JMS transport, passed to a MessageTransformer. The ObjectTransformer performs any required marshalling of the data itself (e.g. convert to XML, compress, encrypt). The MessageTransformer determines the type of JMS message to transport the data (e.g. ObjectMessage, BytesMessage, ...). The object transformers can be chained (e.g. convert to XML, then encrypt). In this version of the software, the DOF is configured to use an ObjectMessage and serialization of the request. Although it's straightforward to modify the code to install a different transformer chain, future implementations will support an XML-based configuration of this feature.
Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
Required Libraries
The jar files are built using the Ant script (build.xml) in the build subdirectory. You should be in this directory when you run Ant.
The default build will not create the EJB-related classes and jars. To build the EJB jar file use the ejb-jar target. This will create a deployable J2EE application jar with a MessageDrivenBean-based dispatcher.
To build the servlet-based dispatcher, use the http-jar target. This will build a deployable web application archive with the servlet.
The build-all target will build everything.
Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
Several simple examples are included with the code. The examples are in the com.technoetic.dof.examples package.
LoginService
An example that returns a non-distributed value and throws an exception.
StockQuoteService
An example that shows simple getting and setting of non-distributed data.
LocatorService
An example of getting and setting distributed objects.
WorkflowService
An example of setting distributed objects and chaining of distributed objects.
Enterprise Java Bean
Examples of accessing a simple EJB using a MessageDrivenBean and HttpServlet. A sample EJB (stock quote serivce) is in a J2EE application jar created by the test-ejb-jar Ant task.
To build the examples, use the build-examples target of the Ant script. The QueueConnectionHelper.properties file in examples/com/technoetic/dof/examples must be modified with the values appropriate for your JMS broker. To run the examples, add the examples directory to your CLASSPATH. After configuration is complete, start your JMS broker, run Server (Server.java) and after it connects to the broker, run the Client (Client.java). To see chaining between multiple workflow objects, run Server2 (Server2.java) before running the client.
Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
The DOF uses JUnit for unit testing. Run com.technoetic.dof.DistributedObjectTestSuite in a test runner to run the unit tests. Use the unit-test target of the Ant build file to run the unit tests. The test results are written to a file in the test directory.
NOTE: As of Ant 1.4.1, the Junit library must be in Ant's classpath for the junit task to work.
Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project |
This DOF is designed for extensibility. Although it currently supports only JMS queues, it will be straightforward to add additional transports. There are no JMS dependencies in the core framework classes and the Transport abstraction is intended to support communication APIs other than JMS. It may also be possible to support method-specific transports with a small extension to the framework. This would allow a single proxy to map each of its methods to a specified transport. For example, some methods might use TCP sockets, others might use JMS, others might use JDBC, etc.
Currently planned extensions...
jmsdof@sourceforge.net |
January 12,
2001
|