This chapter delves into some of the more complex things that can happen with Jini applications.
Clients, servers and service locators can use code from a variety of sources. Which source it uses can depend on the structure of a client and a server. This section looks at some of the variations that can occur.
A service may require information about a client before it can
(or will) proceed. For example, a banking service may require
a user id and a PIN number. Using the techniques already discussed,
this could be done by the client collecting the information and
calling suitable methods such as void setName(String name)
in the service (or more likely, in the
service's proxy) running in the client.
public class Client {
String getName();
}
class Service {
void setName(String name);
}
A service may wish to have more control over the setting of names and passwords than this. For example, it may wish to run verification routines based on the pattern of keystroke entries. More mundanely, it may wish to set time limits on the period between entering the name and the password. Or it may wish to enforce some particular user interface to collect this information. Whatever, the service proxy may wish to perform some sort of input processing on the client side before communicating with the real service. This section explores what happens when the service proxy needs to find extra classes in order to perform this processing.
A standalone application to get a user name might use a GUI interface such as
package standalone;
import java.awt.*;
import java.awt.event.*;
/**
* NameEntry.java
*
*
* Created: Sun Mar 28 23:47:02 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class NameEntry extends Frame {
public NameEntry() {
super("Name Entry");
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
public void windowOpened(WindowEvent e) {}});
Label label = new Label("Name");
TextField name = new TextField(20);
add(label, BorderLayout.WEST);
add(name, BorderLayout.CENTER);
name.addActionListener(new NameHandler());
pack();
}
public static void main(String[] args) {
NameEntry f = new NameEntry();
f.show();
}
} // NameEntry
class NameHandler implements ActionListener {
public void actionPerformed(ActionEvent evt) {
System.out.println("Name was: " + evt.getActionCommand());
}
}
The classes used in this are
Frame, Label, TextField,
ActionListener, ActionEvent, BorderLayout,
WindowEvent, System
.
NameEntry, NameHandler
.
In moving to a Jini system, we have already seen that different components may only need access to a subset of the total set of classes. The same will apply here, but it will critically depend on how the application is changed into a Jini system.
We don't want to be overly concerned about program logic of what is done with the user name once it has been entered, as the interesting part in this section is the location of classes. All versions will need an interface definition, which we can make simply as
package common;
/**
* NameEntry.java
*
*
* Created: Mon Mar 29 11:36:25 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public interface NameEntry {
public void show();
} // NameEntry
Then the client can call upon an implementation to simply show()
itself and collect information in whatever way it chooses.
(Note: we don't want to get involved here in the ongoing discussion about the
most appropriate interface definition for GUI classes!)
The first version of an implementation is
package complex;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;
/**
* NameEntryImpl1.java
*
*
* Created: Mon Mar 29 11:38:33 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class NameEntryImpl1 extends Frame implements common.NameEntry,
ActionListener, java.io.Serializable
/*ServiceIDListener*/ {
public NameEntryImpl1() {
super("Name Entry");
/*
addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
public void windowOpened(WindowEvent e) {}});
*/
setLayout(new BorderLayout());
Label label = new Label("Name");
add(label, BorderLayout.WEST);
TextField name = new TextField(20);
add(name, BorderLayout.CENTER);
name.addActionListener(this);
// don't do this here!
// pack();
}
/**
* method invoked on pressing <return> in the TextField
*/
public void actionPerformed(ActionEvent evt) {
System.out.println("Name was: " + evt.getActionCommand());
}
/*
public void serviceIDNotify(ServiceID serviceID) {
System.out.println("got service ID " + serviceID.toString());
}
*/
public void show() {
pack();
super.show();
}
} // NameEntryImpl1
This creates the GUI elements in the constructor. When exported, this
entire user interface will be serialized and
exported. It isn't too big in this case (about 2,100 bytes),
but that is because the example
is small. A GUI with several hundred objects will be much larger.
This is overhead, which could be avoided because the
classes are all available on the client side, anyway.
Another problem with this code is that it firstly creates an object
on the server machine that has heavy reliance on environmental factors
on the server. It then removes itself from that environment and has to
re-establish itself on the target client environment. On my current system,
this shows by TextField
complaining that it cannot find
a whole bunch of fonts on my server machine. Well it doesn't matter here:
it gets moved to the client machine. (As it happens, the fonts aren't
available there either, so I end with two batches of complaint messages,
from the server and from the client. I should only get the client
complaints.) It could matter if the service died
because of missing stuff on the server side, which would exist
on the client.
The client needs to know the interface class NameEntry
The server needs to know the class files for
NameEntry
Server1
NameEntryImpl1
The HTTP server needs to know the class files for
NameEntryImpl1
The second implementation minimises the amount of serialised code that
must be shipped around, by creating as much as possible on the client
side. We don't even need to declare the class as a subclass of
Frame
as that class also exists on the client side.
The client calls the interface method show()
, and all the
GUI creation is moved to there.
package complex;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;
/**
* NameEntryImpl2.java
*
*
* Created: Mon Mar 29 11:38:33 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class NameEntryImpl2 implements common.NameEntry,
ActionListener, java.io.Serializable
/*ServiceIDListener*/ {
public NameEntryImpl2() {
}
/**
* method invoked on pressing <return> in the TextField
*/
public void actionPerformed(ActionEvent evt) {
System.out.println("Name was: " + evt.getActionCommand());
}
/*
public void serviceIDNotify(ServiceID serviceID) {
System.out.println("got service ID " + serviceID.toString());
}
*/
public void show() {
Frame fr = new Frame("Name Entry");
fr.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
public void windowOpened(WindowEvent e) {}});
fr.setLayout(new BorderLayout());
Label label = new Label("Name");
fr.add(label, BorderLayout.WEST);
TextField name = new TextField(20);
fr.add(name, BorderLayout.CENTER);
name.addActionListener(this);
fr.pack();
fr.show();
}
} // NameEntryImpl2
There are some standard classes that cannot be serialised: one example
is the Swing JTextArea
class (as of Swing 1.1).
This has been frequently logged as a bug against Swing. Until this
is fixed, the only way one of these objects can be
used by a service is to create it on the client.
The client needs to know the interface class NameEntry
The server needs to know the class files for
NameEntry
Server2
NameEntryImpl2
NameEntryImpl2$1
anonymous class
, that acts as
the WindowListener
. The class file is produced by the
compiler. In version 1, this part of the code was commented out for simplicity.
The HTTP server needs to know the class files for
NameEntryImpl1
NameEntryImpl1$1
Apart from the standard classes and a common interface, the previous implementations just used a single class that was uploaded to the lookup service and then passed on to the client. A more realistic situation might require the uploaded service to access a number of other classes that could not be expected to be on the client machine. It is simple to modify the examples to use a server-side specific class for the action listener, instead of the class itself. This looks like
package complex;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import com.sun.jini.lookup.JoinManager;
import net.jini.core.lookup.ServiceID;
import com.sun.jini.lookup.ServiceIDListener;
import com.sun.jini.lease.LeaseRenewalManager;
/**
* NameEntryImpl3.java
*
*
* Created: Mon Mar 29 11:38:33 1999
*
* @author Jan Newmarch
* @version 1.0
*/
public class NameEntryImpl3 implements common.NameEntry,
java.io.Serializable
/*ServiceIDListener*/ {
public NameEntryImpl3() {
}
/*
public void serviceIDNotify(ServiceID serviceID) {
System.out.println("got service ID " + serviceID.toString());
}
*/
public void show() {
Frame fr = new Frame("Name Entry");
fr.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {System.exit(0);}
public void windowOpened(WindowEvent e) {}});
fr.setLayout(new BorderLayout());
Label label = new Label("Name");
fr.add(label, BorderLayout.WEST);
TextField name = new TextField(20);
fr.add(name, BorderLayout.CENTER);
name.addActionListener(new NameHandler());
fr.pack();
fr.show();
}
} // NameEntryImpl3
class NameHandler implements ActionListener {
/**
* method invoked on pressing <return> in the TextField
*/
public void actionPerformed(ActionEvent evt) {
System.out.println("Name was: " + evt.getActionCommand());
}
} // NameHandler
This version uses a class NameHandler
that only exists on
the server machine. When the client attempts to deserialise the
NameEntryImpl3
instance it will fail to find this class,
and be unable to complete deserialisation. How is this resolved?
Well, in the same way as before, by making it available through the HTTP
server.
The client needs to know the interface class NameEntry
The server needs to know the class files for
NameEntry
Server1
NameEntryImpl1
NameEntryImpl1$1
NameHandler
NameHandler
class file is another one produced by the compiler.
The HTTP server needs to know the class files for
NameEntryImpl1
NameEntryImpl1$1
NameHandler
In all of the simple examples using explicit registration, a single thread was used. That is, as a service locator was discovered, the registration process commenced in the same thread. Now this registration may take some time, so it would be preferable to do this in a separate thread. This is also recommended in the specification.
Running another thread is not a difficult procedure. Basically we have to
define a new class that extends Thread
and move most of
the registration into its run
method. This is done in the
following version of the ``option 2'' file classifier server, using an inner class
package complex;
import option2.FileClassifierImpl;
import java.rmi.RMISecurityManager;
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 com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
/**
* FileClassifierServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.1
* added LeaseRenewalManager
* moved sleep() from constructor to main()
*/
public class FileClassifierServer implements DiscoveryListener,
LeaseListener {
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new FileClassifierServer();
// keep server running forever to
// - allow time for locator discovery and
// - keep re-registering the lease
try {
Thread.currentThread().sleep(Lease.FOREVER);
} catch(java.lang.InterruptedException e) {
// do nothing
}
}
public FileClassifierServer() {
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();
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
new RegisterThread(registrar).start();
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
/**
* an inner class to register the service in its own thread
*/
class RegisterThread extends Thread {
ServiceRegistrar registrar;
RegisterThread(ServiceRegistrar registrar) {
this.registrar = registrar;
}
public void run() {
ServiceItem item = new ServiceItem(null,
new FileClassifierImpl(),
null);
ServiceRegistration reg = null;
try {
reg = registrar.register(item, Lease.FOREVER);
} catch(java.rmi.RemoteException e) {
System.err.println("Register exception: " + e.toString());
}
System.out.println("service registered");
// set lease renewal in place
leaseManager.renewUntil(reg.getLease(), Lease.FOREVER,
FileClassifierServer.this);
}
}
} // FileClassifierServer
If you use a JoinManager
to handle lookup and registration, then
it essentially does this for you: creates a new thread to handle
registration. Thus the examples in the chapter on ``JoinManager'' do not
need any modification, as the JoinManager
already uses the
concepts of this section.
In the option 3 example of ``Simple Examples'', a proxy of our design was
created. However, when it came to communicating back to the original
service, a remote method call using RMI was used, and for this the proxy
had to locate an RMI stub (or RMI proxy) using Naming.lookup()
.
Thus we ended up with two proxies on the client!
This section investigates one of the two possibilities in reducing the number of proxies, and that is to reuse the RMI proxy as the only service proxy. To do this, perform the RMI proxy lookup on the server side instead of the client side. Then instead of exporting our own proxy, export the proxy returned from the lookup. There is then no need for our own proxy at all!
A version of the option 3 file classifier to do this is
package option3;
import java.rmi.Naming;
import java.net.InetAddress;
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 com.sun.jini.lease.LeaseRenewalManager;
import com.sun.jini.lease.LeaseListener;
import com.sun.jini.lease.LeaseRenewalEvent;
import java.rmi.RMISecurityManager;
/**
* FileClassifierServer.java
*
*
* Created: Wed Mar 17 14:23:44 1999
*
* @author Jan Newmarch
* @version 1.1
* added LeaseRenewalManager
* moved sleep() from constructor to main()
*/
public class FileClassifierServerRMI implements DiscoveryListener, LeaseListener {
// this is just a name - can be anything
// impl object forces search for Stub
static final String serviceName = "FileClassifier";
String registeredName;
protected FileClassifierImpl impl;
protected LeaseRenewalManager leaseManager = new LeaseRenewalManager();
public static void main(String argv[]) {
new FileClassifierServerRMI();
// no need to keep server alive, RMI will do that
}
public FileClassifierServerRMI() {
try {
impl = new FileClassifierImpl();
} catch(Exception e) {
System.err.println("New impl: " + e.toString());
System.exit(1);
}
// register this with RMI registry
System.setSecurityManager(new RMISecurityManager());
try {
Naming.rebind("rmi://localhost/" + serviceName, impl);
} catch(java.net.MalformedURLException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
} catch(java.rmi.RemoteException e) {
System.err.println("Binding: " + e.toString());
System.exit(1);
}
System.out.println("bound");
// find where we are running
String address = null;
try {
address = InetAddress.getLocalHost().getHostName();
} catch(java.net.UnknownHostException e) {
System.err.println("Address: " + e.toString());
System.exit(1);
}
registeredName = "//" + address + "/" + serviceName;
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;
try {
Object obj = Naming.lookup(registeredName);
service = (RemoteFileClassifier) obj;
} catch(Exception e) {
e.printStackTrace();
return;
}
for (int n = 0; n < registrars.length; n++) {
ServiceRegistrar registrar = registrars[n];
// export the proxy service
ServiceItem item = new ServiceItem(null,
service,
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) {
}
leaseManager.renewFor(reg.getLease(), Lease.FOREVER, this);
}
}
public void discarded(DiscoveryEvent evt) {
}
public void notify(LeaseRenewalEvent evt) {
System.out.println("Lease expired " + evt.toString());
}
} // FileClassifierServer