|
Articles Index
How to take advantage of Java's flexibility and power
By Tim Stefanini
(August 1997)
When I think about the strengths of Java, I always think of two things: its
networking classes, and its ability to instantiate a class from a string.
These features get their support through part of the class loader
mechanism, which enables a web browser to load your applet's classes. In a
large development project, it can be really useful to be able to specify in
a .ini file, which support the services you would like to have running. To
demonstrate this, I have created two services: CapService, which
capitalizes a string, and ReverseService, which reverses a string. While
these examples are simple to illustrate, a service can also be a much
larger program, even a web server. The following classes demonstrate how
to build a dynamic service loader.
Our implementation begins with the definition of an interface--called
ServiceInterface (ServiceInterface.class)--for all of our Services:
public interface ServiceInterface
{ //++ Public Methods
public void StartUp();
public void ShutDown();
public void Activate();
public void Deactivate();
public boolean IsActive();
}//end interface ServiceInterface
|
A service is a process that provides some functionality to a client.
Services are usually multithreaded and communicate via network sockets.
This interface lets services be dynamically activated or deactivated. A
call to StartUp() initiates the main service thread.
This interface is implemented by the class GenericService, which is defined
as abstract, which of course means that you are not allowed to create an
instance of this class directly. But you can create a class that is
inherited from the abstract base class, which is generally a good technique,
because it documents what is generic in your class hierarchy:
public abstract class GenericService
implements Runnable,
Cloneable, ServiceInterface
(The opposite of abstract classes are final classes, which can't be
extended. Using final classes can increase performance, but a good rule of
thumb is that you should only use this if the class is really not
reusable.)
GenericService creates a thread, which manages a ServerSocket class. This
class is used by calling its Accept() method, which blocks until it
receives a connection and returns the socket that is used to communicate
with your client:
final public void run()
{
Socket ns = serverSocket.accept();
GenericService n =
(GenericService)clone();
}
|
At this point, the GenericService clones itself to create a thread to
process the request. This is a good technique for using an abstract base
class when you don't know how difficult the request is to process. In our
example, our service could probably process the request faster than the
overhead of spawning a new thread.
Once we have our abstract service class, we need only write some specific
services. To do this, all we have to specify is the method that processes a
service request. Here our CapService takes the input String and calls
.toUpperCase:
public class CapService extends
GenericService implements
PortInterface
public void serviceRequest()
throws IOException
{
try
{ System.out.println(
"in serviceRequest for server " +
getClass().getName() + "\n");
String foo = null;
while ( (foo =
clientInput.readLine()) != null)
clientOutput.print(
"server: " + foo.toUpperCase() + "
:server\r\n")
clientOutput.flush();
} //end try
catch (Exception e)
{
e.printStackTrace();
}//end catch
} //end serviceRequest()
|
I've provided a port interface to each service to provide an easy way
for our client to get access to it:
public static int GetPort()
{
return ClassPort;
} //end getPort();
Now that I've defined the services and tested to see that they work, I'll
start the design of the service loader. The first step is to make a service
definition class which will use the ServiceInterface that I've already
created:
class ServiceDef (
<A HREF="ServiceDef.class">ServiceDef.class>/A<)
{
public ServiceDef (
String ClassName, String ClassPath)
{
mstrClassName = ClassName;
mstrClassPath = ClassPath;
}//end constructor
public String ClassPath ()
{
return mstrClassPath;
}//end ClassPath()
public String ClassName ()
{
return mstrClassName;
}//end ClassName()
public ServiceInterface ServiceObj ()
{
return mService;
}//end ServiceObj()
protected void Load ()
{
try
{
ClassoClass = Class.forName (
mstrClassPath);
mService = (ServiceInterface)
oClass.newInstance ();
}//end try
catch (Exception e)
{
e.printStackTrace();
}//end catch
}//end Load()
//protected data members
protected String mstrClassName;
protected String mstrClassPath;
private ServiceInterface mService;
}//end class ServiceDef
|
This class maintains a private object--mService--that uses ServiceInterface
as its type. When you call the Load() method, use
Class.forName(), which
returns the class. Calling oClass.newInstance() is the same as saying "new
Class." So, Load() creates a new instance of the class and saves it in its
member variable mService.
The last two classes-- ServiceLoader and GenericClient-- show how to use
this system. ServiceLoader first constructs a vector of class names and
class paths called mServiceList. (It is important to specify the full
package path when calling Class.forName(). If CapService were in the
package util.serviceloader, you would have needed to specify
util.serviceloader.CapService as its classpath name.) The second step is to
useLoad() on each object in the vector to create the service. Then make the
call to Activate() and StartUp():
private void LoadServiceClasses()
{
try
{
int iCount = mServiceList.size();
for (int i = 0; i < iCount; i++)
{
ServiceDef oServiceDef = (ServiceDef)
mServiceList.elementAt(i);
oServiceDef.Load();
ServiceInterface oServiceObj =
oServiceDef.ServiceObj();
String strKey = oServiceDef.ClassName ();
System.out.println(
"Starting: " + strKey + " ...");
oServiceObj.Activate ();
oServiceObj.StartUp ();
}//end for
}//end try
catch (Exception e)
{
e.printStackTrace();
}//end catch
}//end LoadServiceClasses
|
In the client program, you need only create a service loader and then use
the services:
//example use of Dynamic Service Loading
public class GenericClient
{
public static void main(String[] args)
{
try
{
//Load and Start all Service Classes
new ServiceLoader();
|
The extra work in creating a PortInterface now becomes very useful because
you can now access the service easily and create a socket to it:
//Create Input and Output Sockets
// for Service1 - CAPS
//Get port for Service Type
int iServerPort = CapService.GetPort();
//Create Socket
Socket CapSocket = new Socket(
"localhost", iServerPort );
//Create Input and Output Sockets for
//Service2 - Reverse
int iServerPort2 =
ReverseService.GetPort();
Socket ReverseSocket =
new Socket("localhost", iServerPort2 );
//Use Caps Service
PushToService(
"This is a fine class", CapSocket);
//Use Reverse Service
PushToService("This is a fine class",
ReverseSocket);
|
Now, using services is easy. PushToService retrieves the input and output
streams from the socket, prints the result, and returns it as a
string. You can enhance this solution by managing byte[] data from the
sockets rather than strings, or adding a more sophisticated scheduling
interface to automatically start and stop services over time.
By building up a library of services, you provide a solution that is
dynamically configurable at runtime for each user environment. Java's
ability to create a class by name makes this easy. And Java applets
require sophisticated, dynamic class loading, Java comes with some very
powerful capabilities for client/server design built right into it. This
program is an interesting example that shows you how to take advantage of
these powers.
Download the zipped file which contains the necessary
files for Stefanini's generic service loader.
|