JMS Distributed Object Framework SourceForge Logo


Introduction | Usage | Build | Examples | Tests | Future Work | Download | Project

Introduction

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

Usage

DOF applications are organized as servers containing one or more distributed objects (also called services).

Writing a service

To 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:

  1. marshals the request
  2. generates a correlation id for the JMS message
  3. blocks the current thread
  4. sends the request via JMS
  5. waits for responses, (possibly times out)
  6. correlates responses (for all calling threads) using correlation ids
  7. unmarshals the server response
  8. propagates the server response to the calling thread
  9. unblocks the calling thread
  10. returns the function result
  11. (or throws an exception depending on the server response)

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...

  1. the request is unmarshalled
  2. the target object and method are located
  3. the method is invoked on the target
  4. the local result is marshaled
  5. the result is send to the client using JMS and the correlation id

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

Build

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

Examples

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

Testing

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

Future Work

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