Chapter 12. Agents and patterns

  • Introduction—Mobile agents—Callbacks—Mobile servers—Agents and design patterns—Adapter—Proxy—Client-server patterns—Singleton—Remote factory—Abstract remote—Session—Exercises—Remarks

In this chapter

We discuss mobile agents as a programming technique made possible by RMI, and their relationship to various existing “design patterns”. We then consider some fundamental design patterns as they relate to RMI, and introduce a few variants of our own—including the “Abstract Remote” pattern.

Introduction

An agent acts on your behalf. As in a spy novel, you send an agent somewhere, or leave him behind to carry out your instructions when you move.

Agents in RMI exploit two features of Java: serialization and polymorphism. Serialization is the mechanism used to transport objects by value: refer to Chapter 3. Polymorphism, from the ancient Greek, is the ability to appear in many forms. In object-oriented programming, it refers to the fact that, as a derived class satisfies the type signature of its base class, it can be used wherever the base class can be used, even though it may have quite different implementations of the base class's methods—a different form.

In Java, polymorphism includes implementations of interfaces as well as derivations of base classes.

Use of agents forms a major part of RMI design and programming.

In RMI, an agent is passed as a parameter to a remote method call, or returned as a result. A client sends an agent to a server as a parameter. Conversely, a server sends an agent to a client as a result.

There are several interesting kinds of agent in RMI:

  • Mobile agents

  • Callbacks

  • Mobile servers.

Table 12.1 summarizes these agents.

Table 12.1. Agents in RMI

Variant Properties Acts at Communicates with
Mobile agentSerializableTargetNone
Mobile serverSerializable, remote, not exportedTargetSource
CallbackRemote, exportedSourceTarget

These are discussed individually below.

Mobile agents

A mobile agent is an agent which can be moved.

A mobile agent is merely an RMI parameter or result which is serializable, and whose actual type is unknown to the recipient. Because serializable objects are transmitted by deep copy, the mobile agent acts at the target—the server if the agent is a parameter, the client if the agent is a result.

Using mobile agents requires an abstract agent class or interface known to both sender and receiver, and a concrete agent implementation known probably only to the sender. This is rather like the RMI requirement to define a remote interface and a remote object. The structure of a mobile agent is illustrated as skeletal Java code in Example 12.1.

Mobile agents are invoked via a method in an interface or base class known to the target and implemented or extended by the agent.

Example 12.1. Mobile agent

// Abstract Agent interface or abstract class
interface Agent
{
       public abstract void act();
}

class ConcreteAgent implements Agent, Serializable
{
      public void act() { ... }
}

// Sender sends its own implementation of Agent
class Sender
{
      public void send(Receiver rcvr)
      {
        rcvr.receive(new ConcreteAgent());
      }
}

// General scheme of a receiver
class Receiver
{
      // Let the agent act on receipt
      public void receive(Agent agent) { agent.act();}
}

Callbacks

A callback is an agent which is left behind: if you like, an “immobile agent”.

A callback is merely an RMI parameter or result which is an exported remote object. Because a remote object is transmitted by remote reference, it stays behind and acts at the source.

The structure of a callback is illustrated in Example 12.2.

Example 12.2. Callback

// Abstract Callback interface or abstract class
interface Callback extends Remote
{
       void callback(); throws RemoteException
}

class CallbackClient extends UnicastRemoteObject implements Callback
{
      public void run()
      {
        Receiver receiver = ...;
        receiver.receive(this);
      }
      public void callback() { ... }
}

// General scheme of a receiver as before
class Receiver
{
      // Let the agent act on receipt
      public void receive(Callback agent) { callback.callback();}
}

In this example the caller is its own callback, because it implements the callback interface, but the callback could have been a separate object.

Limitations—deadlocks

Callbacks which work correctly when the callback object is local—i.e. before RMI is introduced—can encounter deadlocks when the callback object is remote.

To demonstrate this problem, consider what happens in Example 12.2 if both the run and the callback methods in the CallbackClient class are synchronized.

When receiver is a remote object, such an implementation of the callback pattern will encounter a deadlock. The reason is that if receiver is a local object, the callback method is called on the same thread as the run method, and therefore no deadlock arises. If receiver is remote, RMI calls the callback method on a new thread created in receiver to deal with incoming calls. This thread blocks on trying to enter the synchronized callback method, because the original thread is still waiting inside the synchronized run method—so a deadlock occurs.

The same thing can happen more indirectly, if the callback object is separate from the calling object but eventually attempts to synchronize on the calling object.

Limitations—firewalls

For reasons discussed in Chapter 15, clients behind Internet firewalls cannot export RMI servers visible outside the firewall. This means that callbacks can only be used in the server-to-client direction. The server can send a callback to the client as a result, but the client can't send one to the server as a parameter. The symptom of the latter is a failure when the server tries to execute the callback.

Mobile servers

Consider the case when the agent is serializable and a remote object which is not currently exported at the moment of being passed or returned.

In this form the agent is really a mobile server or “call-forward”.

As we saw when discussing the properties of remote servers in §7.9, if we serialize a remote object which is not currently exported, it will be automatically exported when de-serialized. In other words, when such an object is unmarshalled as a parameter or result, it instantly starts functioning as an RMI server at the target.

The structure of a mobile server is identical to that of a callback as shown in Example 12.2, except that the callback object is not exported at the time of the receive call. Basically, a client can start a server in a remote JVM. Alternately, a server can start another server in the client (firewalls permitting—see §12.5.1).

The mobile server technique requires Java 2 JDK 1.2.2 or later.

Limitations—firewalls

For reasons discussed in Chapter 15, clients behind Internet firewalls cannot export RMI servers visible outside the firewall. This means that mobile servers can only be used in the opposite—client-to-server—direction. The client can send a mobile server to another server as a parameter, but the server can't send one to the client as a result. The symptom of the latter is a failure when the client tries to execute the “call-forward” into the mobile server.

Agents and design patterns

Agents are often adapters or proxies. These are design patterns in their own right, discussed in the following sections. Design patterns “describe simple and elegant solutions to specific problems in object-oriented software design”, according to the standard book on design patterns,[1] often referred to as the “Gang of Four” book, or even “GoF” for short.

In a way, agents are themselves design patterns. However, as agents can be adapters or proxies, the discussion can get confusing if structured that way.

Adapter

The adapter pattern converts “the interface of a class into another interface clients expect”.[2] An adapter class implements an interface, or extends a base class, expected by a client, and delegates all its methods to an internal object of a different class. The adapter pattern is used where the client and the real implementing class have different interfaces. This situation arises continually, and for all sorts of reasons—RMI semantics, development history, JDK changes, and so forth.

This is illustrated in skeletal Java code in Example 12.3.

Example 12.3. Adapter

interface Service {}

class ServiceImplementation implements Service {}

interface ExpectedInterface {}

class ServiceAdapter implements ExpectedInterface {}

An adapter retains a reference to the object whose interface it converts, so that—unknown to its own clients—it can communicate with it.

Serializable adapters to remote services are useful in RMI, as the following example shows.

Example adapter: remote input stream

Consider the frequently asked question “how do I implement a remote input stream?”.

A straightforward RMI implementation of a remote input stream, illustrated in Example 12.4, doesn't work.

Example 12.4. RemoteInputStream—non-working

public interface RemoteInputStream extends Remote
{
       int read(byte[] buffer, int offset, int count)
          throws RemoteException, IOException;
       // ...
}

Why not? There are two problems with this implementation.

Firstly, the semantics of the InputStream.read methods, which we are trying to imitate, are incompatible with those of remote methods. As we saw in §2.5, arguments to remote calls are passed by deep copy, and are not returned after modification at the remote end. This means that the byte[] arguments to the various InputStream.read methods will not receive result data. We have to organize an intermediate interface.

Secondly, we would like clients to receive a real InputStream, which they can use in the usual ways, including the ability to layer a BufferedInputStream, and perhaps a DataInputStream or ObjectInputStream, on top of it. The RemoteInputStream of Example 12.4 is not type-compatible with InputStream—there is no type-relation between them—so it can't be used in constructors for other input stream classes. The following client code based on Example 12.4 won't even compile:

RemoteInputStream     rin = ...;// acquired somehow
InputStream     in = new BufferedInputStream(rin);

Both these can be solved with a better-defined remote interface and a serializable adapter, as illustrated in Example 12.5.

Example 12.5. InputStreamAdapter

public interface RemoteInputStream extends Remote
{
       int     available()       throws RemoteException, IOException;
       void   close()            throws RemoteException, IOException;
       byte[] read(int count)    throws RemoteException, IOException;
       long   skip(long count)   throws RemoteException, IOException;
}

public class InputStreamAdapter extends InputStream implements Serializable
{
       private RemoteInputStream    rin;

       // constructors, serialVersionUID etc not shown
       public int   read() throws IOException
       {
         byte[] buffer = new byte[1];
         int result = read(buffer);
         return (result > 0) ? (buffer[0] & 0xff) : result;
       }
       public int   read(byte[] buffer, int offset, int count) throws IOException
       {
         byte[] result = rin.read(count);
         System.arrayCopy(result, 0, buffer, offset, result.length);
         return result.length;
       }
       public int   read(byte[] buffer) throws IOException
       {
         return read(buffer, 0, buffer, length);
       }
       public int   available() throws IOException
       {
         return rin.available();
       }
       public long skip(long count) throws IOException
       {
         return rin.skip(long);
       }
}

This solution has the following elements:

  • the class we must be type-compatible with—in this case, InputStream

  • the intermediate remote interface RemoteInputStream

  • the adapter class InputStreamAdapter

  • a server which implements RemoteInputStream

  • the real InputStream obtained by the server, probably a FileInputStream or an input stream attached to a socket.

Note that InputStreamAdapter is type-compatible with InputStream, which it extends. This means that remote methods can be declared to receive or return InputStream objects: their implementations actually receive or return InputStreamAdapter objects.

Only the adapter and the real RemoteInputStream server know about the RemoteInputStream interface. Clients and other remote interfaces are written in terms of InputStream objects.

This solution takes advantage of the fact that RemoteException extends IOException. There are objections to using this technique: see the discussion in §12.8.2.

To complete the example, we only need to do the following:

  • provide a remote service implementation of RemoteInputStream

  • add a method to some remote interface which returns an InputStream

  • implement this method in a remote server to return an InputStreamAdapter attached to the RemoteInputStream implementation.

Proxy

A proxy is a surrogate for another object, with which it is usually type-compatible.[3] That is, a proxy is usually another implementation of an interface implemented by the object being substituted, or another extension of one of its base classes.

This is illustrated in skeletal Java code in Example 12.6.

Example 12.6. Proxy

interface Service {}

class ServiceImplementation implements Service {}

class ServiceProxy implements Service {}

A proxy retains a reference to the object being substituted, so that, unknown to its own clients, it can communicate with it.

The proxy pattern has all kinds of uses in situations where some sort of intermediate control is required over accesses to the object being substituted.

A number of interesting specializations of the proxy pattern are described below.

Smart proxy

A smart proxy is a proxy which contains some intelligence—does more than just delegate. Really, most proxies are smart proxies, or there isn't much point in having them.

A smart proxy can be used to conceal a remote interface. The client may think it's using a non-remote interface, but the object it's using may really be a smart proxy which delegates to a remote extension of the interface. This very useful mechanism allows distributing part of the implementation to the client and part to an RMI server, as the following example shows.

Example proxy: remote output stream

Consider the frequently-asked question “how do I implement a remote output stream?”. It is useful to examine this problem, as it has interesting structural features and deals with a well-known interface. However, for the actual desirability of remote output streams, see §12.15.

A straightforward RMI implementation of a remote output stream might look like Example 12.7.

Example 12.7. RemoteOutputStream

public interface RemoteOutputStream extends Remote
{
       int write(byte[] buffer, int offset, int count)
              throws RemoteException, IOException;
       // ...
}

Unlike the first attempt at the remote input stream above, this solution will actually work. The remote input stream's problem with the semantics of the byte[] parameter does not arise, as the data is being sent in the same direction as the call.

However, as in the case of the remote input stream example above, we would like clients to receive a real OutputStream, which they can use in the usual ways—including the ability to stack a BufferedOutputStream and/or a DataOutputStream or ObjectOutputStream on top of it—but this RemoteOutputStream is not inherited from OutputStream. There is no type-relation between them, so a RemoteOutputStream cannot function polymorphically as an OutputStream.

To solve this, we need an intermediate class. Unlike the remote input stream example, we aren't changing the interface, so the intermediate class is a proxy rather than an adapter. The intermediate class must extend OutputStream, which suggests that it must be serialized to the client and act there as a Serializable Proxy. Our solution now looks like Example 12.8.

Example 12.8. RemoteOutputStream—improved

public interface RemoteOutputStream extends Remote
{
       void  close()       throws RemoteException, IOException;
       void  flush()       throws RemoteException, IOException;
       void  write(int ch) throws RemoteException, IOException;
       void  write(byte[] buffer)
                           throws RemoteException, IOException;
       void write(byte[] buffer, int offset, int count)
                          throws RemoteException, IOException;
}

public class OutputStreamProxy extends OutputStream implements Serializable
{
       private RemoteOutputStream rout;

       // constructors, serialVersionUID etc not shown

       public void write(byte[] buffer, int offset, int count) throws IOException
       {
         rout.write(buffer, offset, count);
       }
       // etc for other write() methods, close(), and flush()
}

This solution has the following elements:

  • the class we are trying to be type-compatible with—in this case, OutputStream

  • the intermediate remote interface RemoteOutputStream

  • the proxy class OutputStreamProxy

  • a remote server which implements RemoteOutputStream

  • the real OutputStream obtained by the server, probably a FileOutputStream or an output stream attached to a socket.

Our RemoteOutputStream interface exports all the same methods as OutputStream. Fortuitously, all the methods of OutputStream already throw IOException, which is a base class of RemoteException, satisfying the rule that all methods in a remote interface must be declared to throw RemoteException or one of its base classes.

For the same reason, it was possible for us to change the method signatures in OutputStreamProxy to add RemoteException to the exceptions already declared to be thrown by the base class OutputStream. This is really handy, because by the rules of Java we normally can't do this.[4] It works in this case because all the methods of OutputStream already throw IOException, which, as we just discussed, is a base class of RemoteException.[5]

In general we won't be so lucky—we won't have such a convenient base class to extend. In the general case, we will have to catch RemoteException in the methods of OutputStreamProxy and throw an exception acceptable to the rules of Java (possibly—at worst—a RuntimeException).

From the standpoint of purity, one would frown on taking advantage of IOException in this way. Generally speaking, remote interfaces should be purpose-designed, not inherited via this sort of trickery.

To complete the example, we only need to do the following:

  • provide a remote service implementation of RemoteOutputStream

  • add a method to some remote interface which returns an OutputStream

  • implement this method in a remote server to return an OutputStreamProxy attached to the RemoteOutputStream implementation.

The remote output stream example demonstrates a general technique which is useful where clients expect an instance of some pre-existing class which is not already represented by a remote interface.

Looking closely at the RemoteInputStream and RemoteOutputStream solutions, there is structurally very little difference between them. The difference in fact is the difference between an adapter and a proxy: the Adapter has a different interface from the class “behind” it, where the proxy has the same interface. This only arose because we had to invent an interface for RemoteInputStream, whereas we were able to imitate an existing interface for RemoteOutputStream.

As a matter of fact we could have invented an interface for RemoteOutputStream too, in which case both solutions would be adapters. From a purely formal point of view, this is exactly what we did, because RemoteOutputStream is not actually identical to—the same thing as—OutputStream, it's just an interface with exactly the same methods.

Remote proxy

A remote proxy is “a local representative for an object in a different address space”.[6]

RMI is itself an instance of this pattern. An RMI stub is a remote proxy—a local representative for the remote object—and it contains, or conceals, the mechanism necessary to communicate with that object. For local purposes, it implements the same remote interfaces as the remote object, so it can be used locally by the client as though it is indeed the remote object.

Smart remote proxy

In the smart remote proxy pattern, the client thinks it's talking to a remote interface, which it is, but it's not talking directly to an RMI stub. Instead, it is talking to a local object which implements the remote interface and which is acting as a proxy to the RMI stub. This is illustrated in skeletal Java code in Example 12.9.

Example 12.9. Smart remote proxy

public interface RemoteService extends java.rmi.Remote{}

public class ServiceImplementation implements RemoteService{}

public class ServiceProxy implements RemoteService,Serializable{}

When the client acquires an object of type RemoteService, it receives an instance of ServiceProxy, as opposed to receiving a reference to a ServiceImplementation. The ServiceProxy is transmitted whole—by serialization—whereas the ServiceImplementation is transmitted as a remote stub, by the semantics of RMI results.

As usual, the ServiceProxy would hold a reference to the object it is the proxy for, in this case a ServiceImplementation. This reference is constructed prior to transmission, at the server, probably on construction of the ServiceProxy.

This pattern is useful where the server needs actions to be performed at the client as well as at the server. The pattern has obvious security implications, as its implementation is provided entirely by the server. The client—or its authors—may not even be aware that proxy code is being executed locally.

An RMI stub for an activatable remote object is really itself a smart remote proxy. The Jini Lookup service also uses this pattern.

Virtual proxy

A virtual proxy “creates expensive objects on demand”,[7] where “expensive” means “expensive to create”—objects which consume a lot of memory, say, or which take an appreciable time to initialize.

RMI activation is an example of this pattern. An activatable stub is a virtual proxy for an activatable server, which takes appreciable time to be activated.

RMI activation is also an example of the smart reference pattern.[8]

Activatable proxy

There are several ways to turn an existing RMI server (typically a UnicastRemoteObject) into an activatable service. The most obvious way is to change the existing remote object so that it is activatable:

  • if it extends UnicastRemoteObject, change it to extend Activatable instead

  • otherwise, export it with Activatable.exportObject instead of UnicastRemoteObject.exportObject.

In both cases you must also alter the remote object class to export the required activation constructor, and call the appropriate base-class constructors.

A better, less obvious way is to leave the existing RMI server alone, and implement an activatable server “in front” of it which acts as an activatable proxy, forwarding all remote method calls to the original unaltered remote object. You must also arrange to bind the activatable proxies instead of the old non-activatable ones in the RMI registry or whatever naming service you are using.

This second method appeals because it is easier to implement in environments with strong source-code-control régimes. It also appeals because it cleanly separates the requirements of activation from the requirements of implementing the remote server, at the small cost of an extra method call from the activatable proxy to the original remote object. This method call may be either local or via RMI to yet another host machine.

In either method, you must also create an application setup program to register the activatable server.

Other uses of proxies

It is possible to design RMI systems in which objects have remote or local behaviour depending on context: local clients use the local object directly; remote clients use it via RMI; and different behaviour is required depending on whether the object is being used by a local or remote client.

You should avoid such a context-dependent design, in which the object must continually ask itself “context” questions such as:

  • “am I servicing a remote client now?”

  • “am I doing so directly or indirectly?”

  • “should I throw a local or remote exception?”

This situation is a prime candidate for the application of the proxy pattern, so that local clients use the local object directly, and remote clients use it via a remote proxy. In this case, the remote proxy is a remote server which implements the same interface, and which delegates to the local object. When the local and remote versions are split up in this way, the remote version can be certain that it is servicing a remote client, and the local version can be certain that it is servicing a local client.

Each can act accordingly, without complex context decisions having to be made at run-time.

Client-server patterns

Client-server

Client-server is a pattern of communication between entities which take on different roles. The client entity initiates the connection and makes a request; the server entity only receives requests and returns replies. The client normally terminates the connection.

RMI is inherently a client-server architecture. You must have a client and you must have a server.

It should be observed that in any object-oriented program, every object is a server, except data-only objects and the initial object; if it provides callbacks, the initial object is a server too. “The client/server relationship between objects, however, is not completely useful. Virtually all objects in an object-oriented system are suppliers of functionality. Objects that do not serve functionality are called data objects. Because objects tend to be suppliers as well as consumers, the overall architecture tends to shift from being client/server to server/server. ”[9]

Client-dispatcher-server

In the client-dispatcher-server pattern,[10] a “dispatcher” mediates between a client and a server. The client communicates with the dispatcher to access a service by name, and the dispatcher forwards the request to the server registered under that name.

This pattern can be used to provide “ location transparency”, so that clients need not be aware of the actual network location of servers. It can also be used to implement load-balancing.

RMI is itself an instance of this pattern. Under the covers, stubs (clients) communicate directly with the RMI runtime system (dispatcher) which dispatches the call to the appropriate remote objects (servers) by means of a fixed object name (object ID). In a way, the RMI registry is another example of this pattern.

Peer-to-peer

Peer-to-peer is a pattern of communication between entities which are peers of each other—there is no master/slave or client/server relationship between them. Either peer is entitled to initiate and terminate the conversation, and either is entitled to submit requests and return replies. We mention this pattern only to point out that RMI does not support it.

Singleton

A singleton is a class of which exactly one instance can exist.[11] A singleton is often used to encapsulate a process which must be sequentialized among multiple users, for example a printer spooler, or to represent an external resource of which only one instance exists, such as a file system or a database. java.lang.Runtime is an example of a singleton class built into Java, representing the Java runtime system itself.

The singleton pattern appears in several important ways in RMI.

Stateless servers

As a general rule, “singleton equals stateless”. When designing RMI servers, one of the first questions is “do I supply a new instance to each new client, or do I supply the same instance to all clients?” The answer depends on whether or not the server accumulates state about a client. If it does, the client needs its own instance of the server; if it does not, all clients can use the same server instance—a singleton.

If the answer is “new instance per client”, you must be designing a server with per-client state. (This might be a time to use the session pattern: see §12.13.)

If the answer is “same instance”, you are really designing a stateless server which can handle any client. There are considerable advantages in designing your servers to be stateless:

  • clients are relatively immune to errors in the server

  • clients don't have to establish/re-establish a lot of session context before the server can service them

  • one server instance can service any number of clients, conserving memory.

Servers in the registry

The kind of server which is bound to the RMI registry is most usually a singleton. A server which is made available to any client via a name in the registry is normally stateless and therefore a singleton, by the “singleton equals stateless” principle.

Activatables

The result of registering an Activatable—an activatable stub—represents a singleton ActivationID at the server host. The activatable represented by the ActivationID—the tuple of {groupId, class, location, initial data}—is only instantiated once per host. Unless you have deliberately registered the same ActivationDesc more than once, or registered more than one ActivationDesc for a server class in the same activation group, only one instance of the Activatable class will execute in the activation group.

Remote factory

The remote factory pattern is an extension of the factory pattern introduced in the GoF book, such that the class implementing the factory is itself a remote object.[12]

This is illustrated in skeletal Java code, reusing some of the examples above, in Example 12.10.

A banking example is shown in Example 12.11.

The objects returned by the factory can be anything the server chooses, as long as they conform with the required type-signature. In particular, they can be hidden derived classes of the declared types, or agents, proxies, or adapters as described above, or remote or concrete factories for further types.

Example 12.10. Remote factory

public interface RemoteFactory extends Remote
{
       InputStream     createInputStream(...)  throws RemoteException;
       OutputStream    createOutputStream(...) throws RemoteException;
       Session         createSession(...)      throws RemoteException;
}

Example 12.11. Remote factory for banking

public interface RemoteBankingFactory extends Remote
{
       Customer  createCustomer(...) throws RemoteException;
       Account   createAccount(Customer customer)
                                   throws RemoteException;
}

The remote factory pattern is useful:

  • when implementing polymorphism

  • in code mobility situations

  • to avoid the class-versioning problem

  • to avoid the rollout problem when modifying classes used by clients.

The remote factory pattern also provides an answer to the perennial question “what object should be bound in the RMI registry?”—the factory should be bound.

Abstract remote

The abstract remote pattern uses an abstract class which implements an associated remote interface.[13]

This is illustrated in skeletal Java code in Example 12.12.

Example 12.12. Abstract remote

public interface RemoteService extends java.rmi.Remote
{
       // ...
}

public abstract class AbstractService
       implements RemoteService
{
       public AbstractService() throws RemoteException{}
}

public class ConcreteService extends AbstractService
{
       // ...
}

Like all abstract/concrete patterns, this pattern separates the abstract class from its concrete implementation class(es). The point of the pattern in RMI is that the abstract remote class is entirely sufficient to be processed by rmic, generating all required stubs (and skeletons, if any). The concrete implementation class(es) need never be processed by rmic: implementations can be varied arbitrarily, after which they only need to be recompiled. The stubs (and skeletons, if any) need only be regenerated when the remote interface changes.

This is a very neat way to simplify the build procedure for an application, and to reduce the amount of new class code to be reinstalled after a server change. It also provides an opportunity to vary server implementations. i.e. to use server-side polymorphism.

Note that the abstract remote class need not provide any code except constructors. In particular, it need not reiterate the remote method declarations of the remote interface.

By the rules of Java, methods declared in an interface are already abstract. In an abstract class which implements the interface, they continue to be abstract unless explicitly re-declared as non-abstract methods (with implementations).

As a matter of fact, java.rmi.activation.ActivationGroup is itself an instance of this pattern. It is an abstract class whose actual implementation is elsewhere: the stub is generated from ActivationGroup.

Session

A session is the state associated with a series of interactions between a single client and a server. This can be implemented by allocating a new instance of a server per client; this server can then accumulate the client's state in its local variables. More generally, an explicit Session object can be created per client, to act as a dispatcher between the client and a number of servers, with facilities for the servers to access the session and query or modify its state. Sessions often begin with a login event and end with a logout or session expiry event.

This is illustrated in skeletal Java code in Example 12.13.

The “Secure Sockets Layer” described in §16.5 implements this pattern, although not as an RMI subsystem. It provides “secure sessions” manifested by SSLSession objects; these can be expired or explicitly closed by servers and can accumulate state on behalf of the client.

Example 12.13. Session

public interface Login extends Remote
{
       Session    login(...) throws RemoteException, ...;
}

public interface Session extends Remote
{
       AnyService        getAnyService() throws RemoteException;
       AnotherService    getAnotherService() throws RemoteException;
       // client logout
       void logout() throws RemoteException;
       // service decides to invalidate the session, e.g. on expiry
       void   invalidate() throws RemoteException;
}

public interface AnyService extends Remote
{
       // ...
}

public interface AnotherService extends Remote
{
       // ...
}

Exercises

1:

Write a server which implements the RemoteInputStream interface.

2:

Write a server which implements the RemoteOutputStream interface.

3:

Implement a trivial file retrieval system using the RemoteInputStream and RemoteOutputStream discussed above and implementations of the following interfaces:

public interface RemoteFileFactory extends Remote
{
  RemoteFile    getRootFile() throws RemoteException;
}

public interface RemoteFile extends Remote
{
  // These methods mimic java.io.File
  boolean isDir() throws RemoteException, IOException;
  boolean isFile() throws RemoteException, IOException;
  long    lastModified() throws RemoteException, IOException;
  long    length() throws RemoteException, IOException;
  RemoteFile[]    listFiles()
          throws RemoteException, IOException;
  RemoteFile[]    listFiles(FilenameFilter)
          throws RemoteException, IOException;

  // I/O accessors
  InputStream        getInputStream()
           throws RemoteException, IOException;
  // Controversial ...
  OutputStream        getOutputStream()
           throws RemoteException, IOException;
  }

Obviously RemoteFile is a remote adapter for a java.io.File at the server. The interfaces in this exercise provide a means of obtaining an initial RemoteFile representing some directory, and of traversing that directory to any depth obtaining RemoteFiles contained in it. Once a desired file has been found, an InputStream can be obtained for it, and its contents read. Also, an OutputStream can be obtained for it, and its contents overwritten.

4:

Write a client for the previous exercise which recursively traverses RemoteFiles, starting with one returned by the RemoteFileFactory, until it finds a file of a certain name, and prints out the contents of that file. For example, the client could print out its own source code.

Remarks on the examples and exercises

The examples and exercises appear to provide a framework for a simple software or data distribution system. Please note that they are provided for illustrative purposes only, with the intention of demonstrating adapters and proxies for familiar interfaces, rather than solving the remote I/O problem. As a matter of fact, they exhibit some impractical, indeed undesirable, features for that purpose.

  • RemoteInputStream provides nothing that can't be done with a simple HTTP URL to the same file, assuming that an HTTP server exists at the remote.

  • RemoteOutputStream provides write access to servers: this is generally quite undesirable on security grounds.

  • the system would create large numbers of RemoteFile objects at the server when traversing a directory structure of any size.

  • The read and skip methods of RemoteInputStream, and the write methods of RemoteOutputStream, are not idempotent: they rely on the server's and client's notions of current stream position staying in synchroniszation.[14]

  • The JavaSpaces technology provides a higher-level means of achieving the same objectives as these exercises.



[1] Gamma, Helm, Johnson, and Vlissides: Design Patterns: Elements of Reusable Object-Oriented Software.

[2] ibid., pp. 139-150.

[3] Gamma et al., pp. 207-218.

[4] Java Language Specification, §8.4.4.

[5] Strictly speaking we didn't even have to add RemoteException to the method declarations, as it extends IOException and so is already implicitly declared. We have done so as a matter of style.

[6] Gamma et al., p. 208.

[7] Gamma et al., p. 208.

[8] ibid., p. 209.

[9] Thiruvathukal, Thomas, and Korczynski, Reflective Remote Method Invocation.

[10] Sommerlad and Stal, “The Client-Dispatcher-Server Design Pattern”, in Vlissides, Coplien, and Kerth, eds., Pattern Languages of Program Design 2.

[11] Gamma et al., pp. 127 ff.

[12] Gamma et al., pp. 107-116.

[13] Maso, Re: Different classes that implement the same remote interface.

[14] You would solve these problems by adding a long startpos parameter to all these methods, so that only the client would have to maintain its current position; but this would destroy the purpose of the examples, which is symmetry with InputStream and OutputStream.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.123.73