|
A Service Using ActivationThe major concepts in Activation are the activatable object itself (which extends java.rmi.activation.Activatable ) and the environment in which it runs, an ActivationGroup . A JVM may have an activation group associated with it. If an object needs to be activated and there is already a JVM running its group, then it is restarted within that JVM. Otherwise, a new JVM is started. An activation group may hold a number of cooperating objects. The next sections show how to create a service as an activatable object that starts life in a server that sets up the activation group. Issues related to activation, such as security and state maintenance, will also be discussed. The ServiceAn activable object subclasses from Activatable and uses a special two-argument constructor that will be called when the object needs to be reconstructed. There is a standard implementation of this constructor that just calls the superclass constructor: public ActivatableImpl(ActivationID id, MarshalledObject data) throws RemoteException { super(id, 0); } (The use of the marshalled object parameter is discussed later in the "Maintaining State" section). Adding this constructor is all that is normally needed to change a remote service (that implements UnicastRemoteObject ) into an activatable service. For example, an activatable version of the remote file classifier described in RMI Proxy for FileClassifier" section is as follows : package activation; import java.rmi.activation.Activatable; import java.rmi.activation.ActivationID; import java.rmi.MarshalledObject; import common.MIMEType; import common.FileClassifier; import rmi.RemoteFileClassifier; /** * FileClassifierImpl.java */ public class FileClassifierImpl extends Activatable implements RemoteFileClassifier { public MIMEType getMIMEType(String fileName) throws java.rmi.RemoteException { if (fileName.endsWith(".gif")) { return new MIMEType("image", "gif"); } else if (fileName.endsWith(".jpeg")) { return new MIMEType("image", "jpeg"); } else if (fileName.endsWith(".mpg")) { return new MIMEType("video", "mpeg"); } else if (fileName.endsWith(".txt")) { return new MIMEType("text", "plain"); } else if (fileName.endsWith(".html")) { return new MIMEType("text", "html"); } else // fill in lots of other types, // but eventually give up and return new MIMEType(null, null); } public FileClassifierImpl(ActivationID id, MarshalledObject data) throws java.rmi.RemoteException { super(id, 0); } } // FileClassifierImpl Note that an activatable object cannot have a default no-args constructor to initialize itself, since this new constructor is required for the object to be constructed by the activation system. The ServerThe server needs to create an activation group for the objects to run in. The main issue involved here is to set a security policy file. There are two security policies in activatable objects: the policy used to create the server and export the service, and the policy used to run the service. The activation group sets a policy file for running methods of the service object. The policy file for the server is set using the normal -Djava.security.policy=... argument to start the server. After setting various parameters, the activation group is set for the JVM by ActivationGroup.createGroup() . Remote objects that subclass UnicastRemoteObject are created in the normal way using a constructor on the server. Activatable objects are not constructed in the server but are instead registered with rmid , which will look after construction on an as-needed basis. In order to create activatable objects, rmid needs to know the class name and the location of the class files. The server wraps these up in an ActivationDesc , and registers this with rmid by using Activatable.register() . This returns an RMI stub object that can be registered with lookup services using the ServiceRegistrar.register() methods. This is also a little different from subclasses of UnicastRemoteObject , which pass an object that is converted to a stub by the RMI runtime. The required actions, in point form, are as follows:
Changes need to be made to servers that export activatable objects instead of unicast remote objects. The server in RMI Proxy for FileClassifier" section, creates a unicast remote object and exports its RMI proxy to lookup services by passing the remote object to the ServiceRegistrar.register() method. The changes for such servers to export activatable objects are as follows:
The file classifier server using an activatable service would look like this: package activation; import rmi.RemoteFileClassifier; import net.jini.discovery.LookupDiscovery; import net.jini.discovery.DiscoveryListener; import net.jini.discovery.DiscoveryEvent; import net.jini.core.lookup.ServiceRegistrar; import net.jini.core.lookup.ServiceItem; import net.jini.core.lookup.ServiceRegistration; import net.jini.core.lease.Lease; import java.rmi.RMISecurityManager; import java.rmi.MarshalledObject; import java.rmi.activation.ActivationDesc; import java.rmi.activation.ActivationGroupDesc; import java.rmi.activation.ActivationGroupDesc.CommandEnvironment; import java.rmi.activation.Activatable; import java.rmi.activation.ActivationGroup; import java.rmi.activation.ActivationGroupID; import java.util.Properties; import java.rmi.activation.UnknownGroupException; import java.rmi.activation.ActivationException; import java.rmi.RemoteException; /** * FileClassifierServer.java */ public class FileClassifierServer implements DiscoveryListener { static final protected String SECURITY_POLICY_FILE = "/home/jan/projects/jini/doc/policy.all"; // Don't forget the trailing '/'! static final protected String CODEBASE = "http://localhost/classes/"; // protected FileClassifierImpl impl; protected RemoteFileClassifier stub; public static void main(String argv[]) { new FileClassifierServer(argv); // stick around while lookup services are found try { Thread.sleep(10000L); } catch(InterruptedException e) { // do nothing } // the server doesn't need to exist anymore System.exit(0); } public FileClassifierServer(String[] argv) { // install suitable security manager System.setSecurityManager(new RMISecurityManager()); // Install an activation group Properties props = new Properties(); props.put("java.security.policy", SECURITY_POLICY_FILE); ActivationGroupDesc.CommandEnvironment ace = null; ActivationGroupDesc group = new ActivationGroupDesc(props, ace); ActivationGroupID groupID = null; try { groupID = ActivationGroup.getSystem().registerGroup(group); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } try { ActivationGroup.createGroup(groupID, group, 0); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } String codebase = CODEBASE; MarshalledObject data = null; ActivationDesc desc = null; try { desc = new ActivationDesc("activation.FileClassifierImpl", codebase, data); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } try { stub = (RemoteFileClassifier) Activatable.register(desc); } catch(UnknownGroupException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } LookupDiscovery discover = null; try { discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS); } catch(Exception e) { System.err.println(e.toString()); System.exit(1); } discover.addDiscoveryListener(this); } public void discovered(DiscoveryEvent evt) { ServiceRegistrar[] registrars = evt.getRegistrars(); RemoteFileClassifier service; for (int n = 0; n < registrars.length; n++) { ServiceRegistrar registrar = registrars[n]; // export the proxy service ServiceItem item = new ServiceItem(null, stub, null); ServiceRegistration reg = null; try { reg = registrar.register(item, Lease.FOREVER); } catch(java.rmi.RemoteException e) { System.err.print("Register exception: "); e.printStackTrace(); // System.exit(2); continue; } try { System.out.println("service registered at " + registrar.getLocator().getHost()); } catch(Exception e) { } } } public void discarded(DiscoveryEvent evt) { } } // FileClassifierServer Running the ServiceThe service backend and the server must be compiled as usual, and in addition, an RMI stub object must be created for the service backend using the rmic compiler (in JDK 1.2, at least). The class files for the stub must be copied to somewhere where an HTTP server can deliver them to clients . This is the same as for any other RMI stubs. There is an extra step that must be performed for Activatable objects: the activation server rmid must be able to reconstruct a copy of the service backend (the client must be able to reconstruct a copy of the service's stub). This means that rmid must have access to the class files of the service backend, either from an HTTP server or from the file system. In the previous server, the codebase property in the ActivationDesc is set to an HTTP URL, so the class files for the service backend must be accessible to an HTTP server. Note that rmid does not get the class files for a service backend from the CLASSPATH , but from the codebase of the service. The HTTP server need not be on the same machine as the service backend. Before starting the service provider, an rmid process must be set running on the same machine as the service provider. An HTTP server must be running on a machine specified by the codebase property on the service. The service provider can then be started. This will register the service with rmid and will copy a stub object to any lookup services that are found. The server can then terminate. (As mentioned earlier, this will cause the service's lease to expire, but techniques to handle this are described later). In summary, there are typically three processes involved in getting an activatable service running:
While the service remains registered with lookup services, clients can download its RMI stub. The service will be created on demand by rmid . You only need to run the server once, since rmid keeps information about the service in its own log files. SecurityThe JVM for the service will be created by rmid and will be running in the same environment as rmid . Such things as the current directory for the service will be the same as for rmid , not from where the server ran. Similarly, the user ID for the service will be the user ID of rmid . This is a potential security problem in multi-user systems. For example, any user on a Unix system could write a service that attempts to read the shadow password file on the system, as an activatable service. Once registered with rmid , this same user could write a client that calls the appropriate methods on the service. If rmid is running in privileged mode, owned by the super-user of the system, then the service will run in that same mode and will happily read any file in the entire file system! For safety, rmid should probably be run using the user ID nobody , much like the recommendations for HTTP servers. Some of the security issues with rmid have been addressed in JDK 1.3. These were discussed in Chapter 12, and they allow a security policy to be associated with each activatable service. Non-Lazy ServicesThe types of services discussed in this chapter so far are "lazy" services, activated on demand when their methods are called. This reduces memory use at the expense of starting up a new JVM when required. Some services need to be continuously alive but can still benefit from the logging mechanism of rmid . If rmid crashes and is restarted, or the machine is rebooted and rmid restarts, then the server is able to use its log files to restart any "active" services registered with it, as well as to restore "lazy" services on demand. By making services non-lazy and ensuring that rmid is started on reboot, you can avoid messing around with boot configuration files. Maintaining StateAn activatable object is created afresh each time a method is called on it, using its two-argument constructor. The default action, calling super(id, O) will result in the object being created in the same state on each activation. However, method calls on objects (apart from get...() methods) usually result in a change of state of the object. Activatable objects will need some way of reflecting this change on each activation, and saving and restoring state using a disk file typically does this. When an object is activated, one of the parameters passed to it is a MarshalledObject instance. This is the same object that was passed to the activation system in the ActivationDesc parameter to Activation.register() . This object does not change between different activations, so it cannot hold changing state, but only data, which is fixed for all activations. A simple use for it is to hold the name of a file that can be used for state. Then, on each activation the object can restore state by reading stored information. On each subsequent method call that changes state, the information in the file can be overwritten. The mutable file classifier example was discussed in Chapter 14 ”it could be sent addType() and removeType() messages. It begins with a given set of MIME type/ file extension mappings. State here is very simple; it is just a matter of storing all the file extensions and their corresponding MIME types in a Map . If we turn this into an activatable object, we store the state by just storing the map. This map can be saved to disk using ObjectOutputStream.writeObject() , and it can be retrieved by ObjectInputStream.readObject() . More complex cases might need more complex storage methods. The very first time a mutable file classifier starts on a particular host, it should build its initial state file. There are a variety of methods that could be used. For example, if the state file does not exist, then the first activation could detect this and construct the initial state at that time. Alternatively, a method such as init() could be defined, to be called once after the object has been registered with the activation system. The "normal" way of instantiating an object ”through a constructor ”doesn't work very well with activatable objects. If a constructor for a class doesn't start by calling another constructor with this(...) or super(...) , then the no-argument superclass constructor super() is called. However, the class Activatable doesn't have a no-args constructor, so you can't subclass from Activatable and have a constructor such as FileClassifierMutable(String stateFile) that doesn't use the activation system. You can avoid this problem by not inheriting from Activatable and registering explicitly with the activation system, like this: public FileClassifierMutable(ActivationID id, MarshalledObject data) throws java.rmi.RemoteException { Activatable.exportObject(this, id, 0); // continue with instantiation Nevertheless, this is a bit clumsy: you create an object solely to build up initial state, and then discard it because the activation system will recreate it on demand. The technique we'll use here is to create initial state if the attempt to restore state from the state file fails for any reason when the object is activated. This is done in the restoreMap() method called from the constructor FileClassifierMutable(ActivationID id , MarshalledObject data) . The name of the file is extracted from the marshalled object passed in as parameter. package activation; import java.io.*; import java.rmi.activation.Activatable; import java.rmi.activation.ActivationID; import java.rmi.MarshalledObject; import net.jini.core.event.RemoteEventListener; import net.jini.core.event.RemoteEvent; import net.jini.core.event.EventRegistration; import java.rmi.RemoteException; import net.jini.core.event.UnknownEventException ; import javax.swing.event.EventListenerList; import common.MIMEType; import common.MutableFileClassifier; import mutable.RemoteFileClassifier; import java.util.Map; import java.util.HashMap; /** * FileClassifierMutable.java */ public class FileClassifierMutable extends Activatable implements RemoteFileClassifier { /** * Map of String extensions to MIME types */ protected Map map = new HashMap(); /** * Permanent storage for the map while inactive */ protected String mapFile; /** * Listeners for change events */ protected EventListenerList listenerList = new EventListenerList(); public MIMEType getMIMEType(String fileName) throws java.rmi.RemoteException { System.out.println("Called with " + fileName); MIMEType type; String fileExtension; int dotIndex = fileName.lastIndexOf('.'); if (dotIndex == 1 dotIndex + 1 == fileName.length()) { // can't find suitable suffix return null; } fileExtension= fileName.substring(dotIndex + 1); type = (MIMEType) map.get(fileExtension); return type; } public void addType(String suffix, MIMEType type) throws java.rmi.RemoteException { map.put(suffix, type); fireNotify(MutableFileClassifier.ADD_TYPE); saveMap(); } public void removeMIMEType(String suffix, MIMEType type) throws java.rmi.RemoteException { if (map.remove(suffix) != null) { fireNotify(MutableFileClassifier.REMOVE_TYPE); saveMap(); } } public EventRegistration addRemoteListener(RemoteEventListener listener) throws java.rmi.RemoteException { listenerList.add(RemoteEventListener.class, listener); return new EventRegistration(0, this, null, 0); } // Notify all listeners that have registered interest for // notification on this event type. The event instance // is lazily created using the parameters passed into // the fire method. protected void fireNotify(long eventID) { RemoteEvent remoteEvent = null; // Guaranteed to return a non-null array Object[] listeners = listenerList.getListenerList(); // Process the listeners last to first, notifying // those that are interested in this event for (int i = listeners.length - 2; i >= 0; i -= 2) { if (listeners[i] == RemoteEventListener.class) { RemoteEventListener listener = (RemoteEventListener)listeners[i+1]; if (remoteEvent == null) { remoteEvent = new RemoteEvent(this, eventID, 0L, null); } try { listener.notify(remoteEvent); } catch(UnknownEventException e) { e.printStackTrace(); } catch(RemoteException e) { e.printStackTrace(); } } } } /** * Restore map from file. * Install default map if any errors occur */ public void restoreMap() { try { FileInputStream istream = new FileInputStream(mapFile); ObjectInputStream p = new ObjectInputStream(istream); map = (Map) p.readObject(); istream.close(); } catch(Exception e) { e.printStackTrace(); // restoration of state failed, so // load a predefined set of MIME type mappings map.put("gif", new MIMEType("image", "gif")); map.put("jpeg", new MIMEType("image", "jpeg")); map.put("mpg", new MIMEType("video", "mpeg")); map.put("txt", new MIMEType("text", "plain")); map.put("html", new MIMEType("text", "html")); this.mapFile = mapFile; saveMap(); } } /** * Save map to file. */ public void saveMap() { try { FileOutputStream ostream = new FileOutputStream(mapFile); ObjectOutputStream p = new ObjectOutputStream(ostream); p.writeObject(map); p.flush(); ostream.close(); } catch(Exception e) { e.printStackTrace(); } } public FileClassifierMutable(ActivationID id, MarshalledObject data) throws java.rmi.RemoteException { super(id, 0); try { mapFile = (String) data.get(); } catch(Exception e) { e.printStackTrace(); } restoreMap(); } } // FileClassifierMutable The difference between the server for this service and the last one is that we now have to prepare a marshalled object for the state file and register it with the activation system. Here the filename is hard-coded, but it could be given as a command line argument (as services such as reggie do). package activation; import mutable.RemoteFileClassifier; import net.jini.discovery.LookupDiscovery; import net.jini.discovery.DiscoveryListener; import net.jini.discovery.DiscoveryEvent; import net.jini.core.lookup.ServiceRegistrar; import net.jini.core.lookup.ServiceItem; import net.jini.core.lookup.ServiceRegistration; import net.jini.core.lease.Lease; import java.rmi.RMISecurityManager; import java.rmi.MarshalledObject; import java.rmi.activation.ActivationDesc; import java.rmi.activation.ActivationGroupDesc; import java.rmi.activation.ActivationGroupDesc.CommandEnvironment; import java.rmi.activation.Activatable; import java.rmi.activation.ActivationGroup; import java.rmi.activation.ActivationGroupID; import java.util.Properties; import java.rmi.activation.UnknownGroupException; import java.rmi.activation.ActivationException; import java.rmi.RemoteException; /** * FileClassifierServerMutable.java */ public class FileClassifierServerMutable implements DiscoveryListener { static final protected String SECURITY_POLICY_FILE = "/home/jan/projects/jini/doc/policy.all"; // Don't forget the trailing '/'! static final protected String CODEBASE = "http://localhost/classes/"; static final protected String LOG_FILE = "/tmp/file_classifier"; // protected FileClassifierImpl impl; protected RemoteFileClassifier stub; public static void main(String argv[]) { new FileClassifierServerMutable(argv); // stick around while lookup services are found try { Thread.sleep(10000L); } catch(InterruptedException e) { // do nothing } // the server doesn't need to exist anymore System.exit(0); } public FileClassifierServerMutable(String[] argv) { // install suitable security manager System.setSecurityManager(new RMISecurityManager()); // Install an activation group Properties props = new Properties(); props.put("java.security.policy", SECURITY_POLICY_FILE); ActivationGroupDesc.CommandEnvironment ace = null; ActivationGroupDesc group = new ActivationGroupDesc(props, ace); ActivationGroupID groupID = null; try { groupID = ActivationGroup.getSystem().registerGroup(group); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } try { ActivationGroup.createGroup(groupID, group, 0); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } String codebase = CODEBASE; MarshalledObject data = null; try { data = new MarshalledObject(LOG_FILE); } catch(java.io.IOException e) { e.printStackTrace(); } ActivationDesc desc = null; try { desc = new ActivationDesc("activation.FileClassifierMutable", codebase, data); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } try { stub = (RemoteFileClassifier) Activatable.register(desc); } catch(UnknownGroupException e) { e.printStackTrace(); System.exit(1); } catch(ActivationException e) { e.printStackTrace(); System.exit(1); } catch(RemoteException e) { e.printStackTrace(); System.exit(1); } LookupDiscovery discover = null; try { discover = new LookupDiscovery(LookupDiscovery.ALL_GROUPS); } catch(Exception e) { System.err.println(e.toString()); System.exit(1); } discover.addDiscoveryListener(this); } public void discovered(DiscoveryEvent evt) { ServiceRegistrar[] registrars = evt.getRegistrars(); RemoteFileClassifier service; for (int n = 0; n < registrars.length; n++) { ServiceRegistrar registrar = registrars[n]; // export the proxy service ServiceItem item = new ServiceItem(null, stub, null); ServiceRegistration reg = null; try { reg = registrar.register(item, Lease.FOREVER); } catch(java.rmi.RemoteException e) { System.err.print("Register exception: "); e.printStackTrace(); // System.exit(2); continue; } try { System.out.println("service registered at " + registrar.getLocator().getHost()); } catch(Exception e) { } } } public void discarded(DiscoveryEvent evt) { } } // FileClassifierServerMutable This example used a simple way of storing state. Sun uses a far more complex system in many of its services, such as reggie ”a "reliable log" in the package com.sun.jini.reliableLog . However, this package is not a part of standard Jini, so it may change or even be removed in later versions of Jini. There is nothing to stop you from using it, though, if you need a robust storage mechanism. |