Introduction—Mobile agents—Callbacks—Mobile servers—Agents and design patterns—Adapter—Proxy—Client-server patterns—Singleton—Remote factory—Abstract remote—Session—Exercises—Remarks
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.
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 agent | Serializable | Target | None |
Mobile server | Serializable, remote, not exported | Target | Source |
Callback | Remote, exported | Source | Target |
These are discussed individually below.
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();} }
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.
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.
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.
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.
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 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.
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.
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:
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.
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.
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
andRemoteOutputStream
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 forRemoteInputStream
, whereas we were able to imitate an existing interface forRemoteOutputStream
.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, becauseRemoteOutputStream
is not actually identical to—the same thing as—OutputStream
, it's just an interface with exactly the same methods.
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.
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.
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]
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:
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.
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:
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 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]
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 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.
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.
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.
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.
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.
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:
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.
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
.
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 { // ... }
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
.
18.221.123.73