Chapter 10. Distributed Objects

<feature><title></title> <objective>

THE ROLES OF CLIENT AND SERVER

</objective>
<objective>

REMOTE METHOD CALLS

</objective>
<objective>

THE RMI PROGRAMMING MODEL

</objective>
<objective>

PARAMETERS AND RETURN VALUES IN REMOTE METHODS

</objective>
<objective>

REMOTE OBJECT ACTIVATION

</objective>
<objective>

WEB SERVICES AND JAX-WS

</objective>
</feature>

Periodically, the programming community starts thinking of “objects everywhere” as the solution to all its problems. The idea is to have a happy family of collaborating objects that can be located anywhere. When an object on one computer needs to invoke a method on an object on another computer, it sends a network message that contains the details of the request. The remote object computes a response, perhaps by accessing a database or by communicating with additional objects. Once the remote object has the answer to the client request, it sends the answer back over the network. Conceptuatlly, this process sounds quite simple, but you need to understand what goes on under the hood to use distributed objects effectively.

In this chapter, we focus on Java technologies for distributed programming, in particular the Remote Method Invocation (RMI) protocol for communicating between two Java virtual machines (which might run on different computers). We then briefly visit the JAX-WS technology for making remote calls to web services.

The Roles of Client and Server

The basic idea behind all distributed programming is simple. A client computer makes a request and sends the request data across a network to a server. The server processes the request and sends back a response for the client to analyze. Figure 10-1 shows the process.

Transmitting objects between client and server

Figure 10-1. Transmitting objects between client and server

We would like to say at the outset that these requests and responses are not what you would see in a web application. The client is not a web browser. It can be any application that executes business rules of any complexity. The client application might or might not interact with a human user, and if it does, it can have a command-line or Swing user interface. The protocol for the request and response data allows the transfer of arbitrary objects, whereas traditional web applications are limited by using HTTP for the request and HTML for the response.

What we want is a mechanism by which the client programmer makes a regular method call, without worrying about sending data across the network or parsing the response. The solution is to install a proxy object on the client. The proxy is an object located in the client virtual machine that appears to the client program as if it was the remote object. The client calls the proxy, making a regular method call. The client proxy contacts the server, using a network protocol.

Similarly, the programmer who implements the service doesn’t want to fuss with client communication. The solution is to install a second proxy object on the server. The server proxy communicates with the client proxy, and it makes regular method calls to the object implemeting the service (see Figure 10-2).

Remote method call with proxies

Figure 10-2. Remote method call with proxies

How do the proxies communicate with each other? That depends on the implementation technology. There are three common choices:

  • The Java RMI technology supports method calls between distributed Java objects.

  • The Common Object Request Broker Architecture (CORBA) supports method calls between objects of any programming language. CORBA uses the binary Internet Inter-ORB Protocol, or IIOP, to communicate between objects.

  • The web services architecture is a collection of protocols, sometimes collectively described as WS-*. It is also programming-language neutral. However, it uses XML-based communication formats. The format for transmitting objects is the Simple Object Access Protocol (SOAP).

If the communicating programs are implemented in Java code, then the full generality and complexity of CORBA or WS-* is not required. Sun developed a simple mechanism, called RMI, specifically for communication between Java applications.

It is well worth learning about RMI, even if you are not going to use it in your own programs. You will learn the mechanisms that are essential for programming distributed applications, using a straightforward architecture. Moreover, if you use enterprise Java technologies, it is very useful to have a basic understanding of RMI because that is the protocol used to communicate between enterprise Java beans (EJBs). EJBs are server-side components that are composed to make up complex applications that run on multiple servers. To make effective use of EJBs, you will want to have a good idea of the costs associated with remote calls.

Unlike RMI, CORBA and SOAP are completely language neutral. Client and server programs can be written in C, C++, C#, Java, or any other language. You supply an interface description to specify the signatures of the methods and the types of the data your objects can handle. These descriptions are formatted in a special language, called Interface Definition Language (IDL) for CORBA and Web Services Description Language (WSDL) for web services.

For many years, quite a few people believed that CORBA was the object model of the future. Frankly, though, CORBA has a reputation—sometimes deserved—for complex implementations and interoperability problems, and it has only reached modest success. We covered interoperability between Java and CORBA for five editions of this book, but dropped it for lack of interest. Our sentiments about CORBA are similar to those expressed by French president Charles De Gaulle about Brazil: It has a great future . . . and always will.

Web services had a similar amount of buzz when they first appeared, with the promise that they are simpler and, of course, founded in the goodness of the World Wide Web and XML. However, with the passing of time and the work of many committees, the protocol stack has become less simple, as it acquired more of the features that CORBA had all along. The XML protocol has the advantage of being (barely) human-readable, which helps with debugging. On the other hand, XML processing is a significant performance bottleneck. Recently, the WS-* stack has lost quite a bit of its luster and it too is gaining a reputation—sometimes deserved—for complex implementations and interoperability problems.

We close this chapter with an example of an application that consumes a web service. We have a look at the underlying protocol so that you can see how communication between different programming languages is implemented.

Remote Method Calls

The key to distributed computing is the remote method call. Some code on one machine (called the client) wants to invoke a method on an object on another machine (the remote object). To make this possible, the method parameters must somehow be shipped to the other machine, the server must be informed to locate the remote object and execute the method, and the return value must be shipped back.

Before looking at this process in detail, we want to point out that the client/server terminology applies only to a single method call. The computer that calls the remote method is the client for that call, and the computer hosting the object that processes the call is the server for that call. It is entirely possible that the roles are reversed somewhere down the road. The server of a previous call can itself become the client when it invokes a remote method on an object residing on another computer.

Stubs and Parameter Marshalling

When client code wants to invoke a method on a remote object, it actually calls an ordinary method on a proxy object called a stub. For example,

Warehouse centralWarehouse = get stub object;
double price = centralWarehouse.getPrice("Blackwell Toaster");

The stub resides on the client machine, not on the server. It knows how to contact the server over the network. The stub packages the parameters used in the remote method into a block of bytes. The process of encoding the parameters is called parameter marshalling. The purpose of parameter marshalling is to convert the parameters into a format suitable for transport from one virtual machine to another. In the RMI protocol, objects are encoded with the serialization mechanism that is described in Chapter 1. In the SOAP protocol, objects are encoded as XML.

To sum up, the stub method on the client builds an information block that consists of

  • An identifier of the remote object to be used.

  • A description of the method to be called.

  • The parameters.

The stub then sends this information to the server. On the server side, a receiver object performs the following actions:

  1. It locates the remote object to be called.

  2. It calls the desired method, passing the supplied parameters.

  3. It captures the return value or exception of the call.

  4. It sends a package consisting of the marshalled return data back to the stub on the client.

The client stub unmarshals the return value or exception from the server. This value becomes the return value of the stub call. Or, if the remote method threw an exception, the stub rethrows it in the virtual machine of the caller. Figure 10-3 shows the information flow of a remote method invocation.

Parameter marshalling

Figure 10-3. Parameter marshalling

This process is obviously complex, but the good news is that it is completely automatic and, to a large extent, transparent for the programmer.

The details for implementing remote objects and for getting client stubs depend on the technology for distributed objects. In the following sections, we have a close look at RMI.

The RMI Programming Model

To introduce the RMI programming model, we start with a simple example. A remote object represents a warehouse. The client program asks the warehouse about the price of a product. In the following sections, you will see how to implement and launch the server and client programs.

Interfaces and Implementations

The capabilities of remote objects are expressed in interfaces that are shared between the client and server. For example, the interface in Listing 10-1 describes the service provided by a remote warehouse object:

Example 10-1. Warehouse.java

 1. import java.rmi.*;
 2.
 3. /**
 4.    The remote interface for a simple warehouse.
 5.    @version 1.0 2007-10-09
 6.    @author Cay Horstmann
 7. */
 8. public interface Warehouse extends Remote
 9. {
10.    double getPrice(String description) throws RemoteException;
11. }

Interfaces for remote objects must always extend the Remote interface defined in the java.rmi package. All the methods in those interfaces must also declare that they will throw a RemoteException. Remote method calls are inherently less reliable than local calls—it is always possible that a remote call will fail. For example, the server might be temporarily unavailable, or there might be a network problem. Your client code must be prepared to deal with these possibilities. For these reasons, you must handle the RemoteException with every remote method call and specify the appropriate action to take when the call does not succeed.

Next, on the server side, you must provide the class that actually carries out the work advertised in the remote interface—see Listing 10-2.

Example 10-2. WarehouseImpl.java

 1. import java.rmi.*;
 2. import java.rmi.server.*;
 3. import java.util.*;
 4.
 5. /**
 6.  * This class is the implementation for the remote Warehouse interface.
 7.  * @version 1.0 2007-10-09
 8.  * @author Cay Horstmann
 9.  */
10. public class WarehouseImpl extends UnicastRemoteObject implements Warehouse
11. {
12.    public WarehouseImpl() throws RemoteException
13.    {
14.       prices = new HashMap<String, Double>();
15.       prices.put("Blackwell Toaster", 24.95);
16.       prices.put("ZapXpress Microwave Oven", 49.95);
17.    }
18.
19.    public double getPrice(String description) throws RemoteException
20.    {
21.       Double price = prices.get(description);
22.       return price == null ? 0 : price;
23.    }
24.
25.    private Map<String, Double> prices;
26. }

Note

Note

The WarehouseImpl constructor is declared to throw a RemoteException because the superclass constructor can throw that exception. This happens when there is a problem connecting to the network service that tracks remote objects.

You can tell that the class is the target of remote method calls because it extends UnicastRemoteObject. The constructor of that class makes objects remotely accessible. The “path of least resistance” is to derive from UnicastRemoteObject, and all service implementation classes in this chapter do so.

Occasionally, you might not want to extend the UnicastRemoteObject class, perhaps because your implementation class already extends another class. In that situation, you need to manually instantiate the remote objects and pass them to the static exportObject method. Instead of extending UnicastRemoteObject, call

UnicastRemoteObject.exportObject(this, 0);

in the constructor of the remote object. The second parameter is 0 to indicate that any suitable port can be used to listen to client connections.

Note

Note

The term “unicast” refers to the fact that the remote object is located by making a call to a single IP address and port. This is the only mechanism that is supported in Java SE. More sophisticated distributed object systems (such as JINI) allow for “multicast” lookup of remote objects that might be on a number of different servers.

The RMI Registry

To access a remote object that exists on the server, the client needs a local stub object. How can the client request such a stub? The most common method is to call a remote method of another remote object and get a stub object as a return value. There is, however, a chicken-and-egg problem here: The first remote object has to be located some other way. For that purpose, the JDK provides a bootstrap registry service.

A server program registers at least one remote object with a bootstrap registry. To register a remote object, you need a RMI URL and a reference to the implementation object.

RMI URLs start with rmi: and contain an optional host name, an optional port number, and the name of the remote object that is (hopefully) unique. An example is:

rmi://regserver.mycompany.com:99/central_warehouse

By default, the host name is localhost and the port number is 1099. The server tells the registry at the given location to associate or “bind” the name with the object.

Here is the code for registering a WarehouseImpl object with the RMI registry on the same server:

WarehouseImpl centralWarehouse = new WarehouseImpl();
Context namingContext = new InitialContext();
namingContext.bind("rmi:central_warehouse", centralWarehouse);

The program in Listing 10-3 simply constructs and registers a WarehouseImpl object.

Example 10-3. WarehouseServer.java

 1. import java.rmi.*;
 2. import javax.naming.*;
 3.
 4. /**
 5.   * This server program instantiates a remote warehouse object, registers it with the naming
 6.   * service, and waits for clients to invoke methods.
 7.   * @version 1.12 2007-10-09
 8.   * @author Cay Horstmann
 9.   */
10.
11. public class WarehouseServer
12. {
13.    public static void main(String[] args) throws RemoteException, NamingException
14.    {
15.       System.out.println("Constructing server implementation...");
16.       WarehouseImpl centralWarehouse = new WarehouseImpl();
17.
18.       System.out.println("Binding server implementation to registry...");
19.       Context namingContext = new InitialContext();
20.       namingContext.bind("rmi:central_warehouse", centralWarehouse);
21.
22.       System.out.println("Waiting for invocations from clients...");
23.    }
24. }

Note

Note

For security reasons, an application can bind, unbind, or rebind registry object references only if it runs on the same host as the registry. This prevents hostile clients from changing the registry information. However, any client can look up objects.

A client can enumerate all registered RMI objects by calling:

Enumeration<NameClassPair> e = namingContext.list("rmi://regserver.mycompany.com");

NameClassPair is a helper class that contains both the name of the bound object and the name of its class. For example, the following code displays the names of all registered objects:

while (e.hasMoreElements())
   System.out.println(e.nextElement().getName());

A client gets a stub to access a remote object by specifying the server and the remote object name in the following way:

String url = "rmi://regserver.mycompany.com/central_warehouse";
Warehouse centralWarehouse = (Warehouse) namingContext.lookup(url);

Note

Note

Because it is notoriously difficult to keep names unique in a global registry, you should not use this technique as the general method for locating objects on the server. Instead, there should be relatively few named remote objects registered with the bootstrap service. These should be objects that can locate other objects for you.

The code in Listing 10-4 shows the client that obtains a stub to the remote warehouse object and invokes the remote getPrice method. Figure 10-4 shows the flow of control. The client obtains a Warehouse stub and invokes the getPrice method on it. Behind the scenes, the stub contacts the server and causes the getPrice method to be invoked on the WarehouseImpl object.

Calling the remote getDescription method

Figure 10-4. Calling the remote getDescription method

Example 10-4. WarehouseClient.java

 1. import java.rmi.*;
 2. import java.util.*;
 3. import javax.naming.*;
 4.
 5. /**
 6.  * A client that invokes a remote method.
 7.  * @version 1.0 2007-10-09
 8.  * @author Cay Horstmann
 9.  */
10. public class WarehouseClient
11. {
12.    public static void main(String[] args) throws NamingException, RemoteException
13.    {
14.       Context namingContext = new InitialContext();
15.
16.       System.out.print("RMI registry bindings: ");
17.       Enumeration<NameClassPair> e = namingContext.list("rmi://localhost/");
18.       while (e.hasMoreElements())
19.          System.out.println(e.nextElement().getName());
20.
21.       String url = "rmi://localhost/central_warehouse";
22.       Warehouse centralWarehouse = (Warehouse) namingContext.lookup(url);
23.
24.       String descr = "Blackwell Toaster";
25.       double price = centralWarehouse.getPrice(descr);
26.       System.out.println(descr + ": " + price);
27.    }
28. }

Deploying the Program

Deploying an application that uses RMI can be tricky because so many things can go wrong and the error messages that you get when something does go wrong are so poor. We have found that it really pays off to test the deployment under realistic conditions, separating the classes for client and server.

Make two separate directories to hold the classes for starting the server and client.

server/
   WarehouseServer.class
   Warehouse.class
   WarehouseImpl.class

client/
   WarehouseClient.class
   Warehouse.class

When deploying RMI applications, one commonly needs to dynamically deliver classes to running programs. One example is the RMI registry. Keep in mind that one instance of the registry will serve many different RMI applications. The RMI registry needs to have access to the class files of the service interfaces that are being registered. When the registry starts, however, one cannot predict all future registration requests. Therefore, the RMI registry dynamically loads the class files of any remote interfaces that it has not previously encountered.

Dynamically delivered class files are distributed through standard web servers. In our case, the server program needs to make the Warehouse.class file available to the RMI registry, so we put that file into a third directory that we call download.

download/
   Warehouse.class

We use a web server to serve the contents of that directory.

When the application is deployed, the server, RMI registry, web server, and client can be located on four different computers—see Figure 10-5. However, for testing purposes, we will use a single computer.

Server calls in the Warehouse application

Figure 10-5. Server calls in the Warehouse application

Note

Note

For security reasons, the rmiregistry service that is part of the JDK only allows binding calls from the same host. That is, the server and rmiregistry process need to be located on the same computer. However, the RMI architecture allows for a more general RMI registry implementation that supports multiple servers.

To test the sample application, use the NanoHTTPD web server that is available from http://elonen.iki.fi/code/nanohttpd. This tiny web server is implemented in a single Java source file. Open a new console window, change to the download directory, and copy NanoHTTPD.java to that directory. Compile the source file and start the web server, using the command

java NanoHTTPD 8080

The command-line argument is the port number. Use any other available port if port 8080 is already used on your machine.

Next, open another console window, change to a directory that contains no class files, and start the RMI registry:

rmiregistry

Caution

Caution

Before starting the RMI registry, make sure that the CLASSPATH environment variable is not set to anything, and double-check that the current directory contains no class files. Otherwise, the RMI registry might find spurious class files, which will confuse it when it should download additional classes from a different source. There is a reason for this behavior; see http://java.sun.com/javase/6/docs/technotes/guides/rmi/codebase.html. In a nutshell, each stub object has a codebase entry that specifies from where it was loaded. That codebase is used to load dependent classes. If the RMI registry finds a class locally, it will set the wrong codebase.

Now you are ready to start the server. Open a third console window, change to the server directory, and issue the command

java -Djava.rmi.server.codebase=http://localhost:8080/ WarehouseServer

The java.rmi.server.codebase property points to the URL for serving class files. The server program communicates this URL to the RMI registry.

Have a peek at the console window running NanoHTTPD. You will see a message that demonstrates that the Warehouse.class file has been served to the RMI registry.

Caution

Caution

It is very important that you make sure that the codebase URL ends with a slash (/).

Note that the server program does not exit. This seems strange—after all, the program just creates a WarehouseImpl object and registers it. Actually, the main method does exit immediately after registration, as you would expect. However, when you create an object of a class that extends UnicastRemoteObject, a separate thread that keeps the program alive indefinitely is started. Thus, the program stays around to allow clients to connect to it.

Finally, open a fourth console window, change to the client directory, and run

java WarehouseClient

You will see a short message, indicating that the remote method was successfully invoked (see Figure 10-6).

Testing an RMI application

Figure 10-6. Testing an RMI application

Note

Note

If you just want to test out basic program logic, you can put your client and server class files into the same directory. Then you can start the RMI registry, server, and client in that directory. However, because RMI class loading is the source of much grief and confusion, we felt it best to show you the correct setup for dynamic class loading right away.

Logging RMI Activity

If you start the server with the option

-Djava.rmi.server.logCalls=true WarehouseServer &

then the server logs all remote method calls on its console. Try it—you’ll get a good impression of the RMI traffic.

If you want to see additional logging messages, you have to configure RMI loggers, using the standard Java logging API. (See Volume I, Chapter 11 for more information on logging.)

Make a file logging.properties with the following content:

handlers=java.util.logging.ConsoleHandler
.level=FINE
java.util.logging.ConsoleHandler.level=FINE
java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter

You can fine-tune the settings by setting individual levels for each logger rather than setting the global level. Table 10-1 lists the RMI loggers. For example, to track the class loading activity, you can set

sun.rmi.loader.level=FINE

Table 10-1. RMI Loggers

Logger Name

Logged Activity

sun.rmi.server.call

Server-side remote calls

sun.rmi.server.ref

Server-side remote references

sun.rmi.client.call

Client-side remote calls

sun.rmi.client.ref

Client-side remote references

sun.rmi.dgc

Distributed garbage collection

sun.rmi.loader

RMIClassLoader

sun.rmi.transport.misc

Transport layer

sun.rmi.transport.tcp

TCP binding and connection

sun.rmi.transport.proxy

HTTP tunneling

Start the RMI registry with the option

-J-Djava.util.logging.config.file=directory/logging.properties

Start the client and server with

-Djava.util.logging.config.file=directory/logging.properties

Here is an example of a logging message that shows a class loading problem: The RMI registry cannot find the Warehouse class because the web server has been shut down.

FINE: RMI TCP Connection(1)-127.0.1.1: (port 1099) op = 80
Oct 13, 2007 4:43:30 PM sun.rmi.server.LoaderHandler loadProxyClass
FINE: RMI TCP Connection(1)-127.0.1.1: interfaces = [java.rmi.Remote, Warehouse], codebase =
"http://localhost:8080/"
Oct 13, 2007 4:43:30 PM sun.rmi.server.LoaderHandler loadProxyClass
FINE: RMI TCP Connection(1)-127.0.1.1: proxy class resolution failed
java.lang.ClassNotFoundException: Warehouse

Parameters and Return Values in Remote Methods

At the start of a remote method invocation, the parameters need to be moved from the virtual machine of the client to the virtual machine of the server. After the invocation has completed, the return value needs to be transferred in the other direction. When a value is passed from one virtual machine to another other, we distinguish two cases: passing remote objects and passing nonremote objects. For example, suppose that a client of the WarehouseServer passes a Warehouse reference (that is, a stub through which the remote warehouse object can be called) to another remote method. That is an example of passing a remote object. However, most method parameters will be ordinary Java objects, not stubs to remote objects. An example is the String parameter of the getPrice method in our first sample application.

Transferring Remote Objects

When a reference to a remote object is passed from one virtual machine to the other, the sender and recipient of the remote object both hold a reference to the same entity. That reference is not a memory location (which is only meaningful in a single virtual machine), but it consists of a network address and a unique identifier for the remote object. This information is encapsulated in a stub object.

Conceptually, passing a remote reference is quite similar to passing local object references within a virtual machine. However, always keep in mind that a method call on a remote reference is significantly slower and potentially less reliable than a method call on a local reference.

Transferring Nonremote Objects

Consider the String parameter of the getPrice method. The string value needs to be copied from the client to the server. It is not difficult to imagine how a copy of a string can be transported across a network. The RMI mechanism can also make copies of more complex objects, provided they are serializable. RMI uses the serialization mechanism described in Chapter 1 to send objects across a network connection. This means that any classes that implement the Serializable interface can be used as parameter or return types.

Passing parameters by serializing them has a subtle effect on the semantics of remote methods. When you pass objects into a local method, object references are transferred. When the method applies a mutator method to a parameter object, the caller will observe that change. But if a remote method mutates a serialized parameter, it changes the copy, and the caller will never notice.

To summarize, there are two mechanisms for transferring values between virtual machines.

  • Objects of classes that implement the Remote interface are transferred as remote references.

  • Objects of classes that implement the Serializable interface but not the Remote interface are copied using serialization.

All of this is automatic and requires no programmer intervention. Keep in mind that serialization can be slow for large objects, and that the remote method cannot mutate serialized parameters. You can, of course, avoid these issues by passing around remote references. That too comes at a cost: Invoking methods on remote references is far more expensive than calling local methods. Being aware of these costs allows you to make informed choices when designing remote services.

Note

Note

Remote objects are garbage-collected automatically, just as local objects are. However, the distributed collector is signifcantly more complex. When the local garbage collector finds that there are further local uses of a remote reference, it notifies the distributed collector that the server is no longer referenced by this client. When a server is no longer used by any clients, it is marked as garbage.

Our next example program will illustrate the transfer of remote and serializable objects. We change the Warehouse interface as shown in Listing 10-5. Given a list of keywords, the warehouse returns the Product that is the best match.

Example 10-5. Warehouse.java

 1. import java.rmi.*;
 2. import java.util.*;
 3.
 4. /**
 5.    The remote interface for a simple warehouse.
 6.    @version 1.0 2007-10-09
 7.    @author Cay Horstmann
 8. */
 9. public interface Warehouse extends Remote
10. {
11.    double getPrice(String description) throws RemoteException;
12.    Product getProduct(List<String> keywords) throws RemoteException;
13. }

The parameter of the getProduct method has type List<String>. A parameter value must belong to a serializable class that implements the List<String> interface, such as ArrayList<String>. (Our sample client passes a value that is obtained by a call to Arrays.asList. Fortunately, that method is guaranteed to return a serializable list as well.)

The return type Product encapsulates the description, price, and location of the product—see Listing 10-6.

Note that the Product class is serializable. The server constructs a Product object, and the client gets a copy (see Figure 10-7).

Copying local parameter and result objects

Figure 10-7. Copying local parameter and result objects

Example 10-6. Product.java

 1. import java.io.*;
 2.
 3. public class Product implements Serializable
 4. {
 5.    public Product(String description, double price)
 6.    {
 7.       this.description = description;
 8.       this.price = price;
 9.    }
10.
11.    public String getDescription()
12.    {
13.       return description;
14.    }
15.
16.    public double getPrice()
17.    {
18.       return price;
19.    }
20.
21.    public Warehouse getLocation()
22.    {
23.       return location;
24.    }
25.
26.    public void setLocation(Warehouse location)
27.    {
28.       this.location = location;
29.    }
30.
31.    private String description;
32.    private double price;
33.    private Warehouse location;
34. }

 

However, there is a subtlety. The Product class has an instance field of type Warehouse, a remote interface. The warehouse object is not serialized, which is just as well as it might have a huge amount of state. Instead, the client receives a stub to a remote Warehouse object. That stub might be different from the centralWarehouse stub on which the getProduct method was called. In our implementation, we will have two kinds of products, toasters and books, that are located in different warehouses.

Dynamic Class Loading

There is another subtlety to our next sample program. A list of keyword strings is sent to the server, and the warehouse returns an instance of a class Product. Of course, the client program will need the class file Product.class to compile. However, whenever our server program cannot find a match for the keywords, it returns the one product that is sure to delight everyone: the Core Java book. That object is an instance of the Book class, a subclass of Product.

When the client was compiled, it might have never seen the Book class. Yet when it runs, it needs to be able to execute Book methods that override Product methods. This demonstrates that the client needs to have the capability of loading additional classes at runtime. The client uses the same mechanism as the RMI registry. Classes are served by a web server, the RMI server class communicates the URL to the client, and the client makes an HTTP request to download the class files.

Whenever a program loads new code from another network location, there is a security issue. For that reason, you need to use a security manager in RMI applications that dynamically load classes. (See Chapter 9 for more information on class loaders and security managers.)

Programs that use RMI should install a security manager to control the activities of the dynamically loaded classes. You install it with the instruction

System.setSecurityManager(new SecurityManager());

Note

Note

If all classes are available locally, then you do not actually need a security manager. If you know all class files of your program at deployment time, you can deploy them all locally. However, it often happens that the client or server program evolves and new classes are added over time. Then you benefit from dynamic class loading. Any time you load code from another source, you need a security manager.

By default, the SecurityManager restricts all code in the program from establishing network connections. However, the program needs to make network connections to three remote locations:

  • The web server that loads remote classes.

  • The RMI registry.

  • Remote objects.

To allow these operations, you supply a policy file. (We discussed policy files in greater detail in Chapter 9.) Here is a policy file that allows an application to make any network connection to a port with port number of at least 1024. (The RMI port is 1099 by default, and the remote objects also use ports ≥ 1024. We use port 8080 for dowloading classes.)

grant
{
   permission java.net.SocketPermission
      "*:1024-65535", "connect";
};

You need to instruct the security manager to read the policy file by setting the java.security.policy property to the file name. You can use a call such as

System.setProperty("java.security.policy", "rmi.policy");

Alternatively, you can specify the system property setting on the command line:

-Djava.security.policy=rmi.policy

To run the sample application, be sure that you have killed the RMI registry, web server, and the server program from the preceding sample. Open four console windows and follow these steps.

  1. Compile the source files for the interface, implementation, client, and server classes.

    javac *.java
  2. Make three directories, client, server, and download, and populate them as follows:

    client/
       WarehouseClient.class
       Warehouse.class
       Product.class
       client.policy
    server/
       Warehouse.class
       Product.class
       Book.class
       WarehouseImpl.class
       WarehouseServer.class
       server.policy
    download
       Warehouse.class
       Product.class
       Book.class
  3. In the first console window, change to a directory that has no class files. Start the RMI registry.

  4. In the second console window, change to the download directory and start NanoHTTPD.

  5. In the third console window, change to the server directory and start the server.

    java -Djava.rmi.server.codebase=http://localhost:8080/ WarehouseServer
  6. In the fourth console window, change to the client directory and run the client.

    java WarehouseClient

Listing 10-7 shows the code of the Book class. Note that the getDescription method is overridden to show the ISBN. When the client program runs, it shows the ISBN for the Core Java book, which proves that the Book class was loaded dynamically. Listing 10-8 shows the warehouse implementation. A warehouse has a reference to a backup warehouse. If an item cannot be found in the warehouse, the backup warehouse is searched. Listing 10-9 shows the server program. Only the central warehouse is entered into the RMI registry. Note that a remote reference to the backup warehouse can be passed to the client even though it is not included in the RMI registry. This happens whenever no keyword matches and a Core Java book (whose location field references the backup warehouse) is sent to the client.

Example 10-7. Book.java

 1. /**
 2.  * A book is a product with an ISBN number.
 3.  * @version 1.0 2007-10-09
 4.  * @author Cay Horstmann
 5.  */
 6. public class Book extends Product
 7. {
 8.    public Book(String title, String isbn, double price)
 9.    {
10.       super(title, price);
11.       this.isbn = isbn;
12.    }
13.
14.    public String getDescription()
15.    {
16.       return super.getDescription() + " " + isbn;
17.    }
18.
19.    private String isbn;
20. }

Example 10-8. WarehouseImpl.java

 1. import java.rmi.*;
 2. import java.rmi.server.*;
 3. import java.util.*;
 4.
 5. /**
 6.  * This class is the implementation for the remote Warehouse interface.
 7.  * @version 1.0 2007-10-09
 8.  * @author Cay Horstmann
 9.  */
10. public class WarehouseImpl extends UnicastRemoteObject implements Warehouse
11. {
12.    /**
13.     * Constructs a warehouse implementation.
14.     */
15.    public WarehouseImpl(Warehouse backup) throws RemoteException
16.    {
17.       products = new HashMap<String, Product>();
18.       this.backup = backup;
19.    }
20.
21.    public void add(String keyword, Product product)
22.    {
23.       product.setLocation(this);
24.       products.put(keyword, product);
25.    }
26.
27.    public double getPrice(String description) throws RemoteException
28.    {
29.       for (Product p : products.values())
30.          if (p.getDescription().equals(description)) return p.getPrice();
31.       if (backup == null) return 0;
32.       else return backup.getPrice(description);
33.    }
34.
35.    public Product getProduct(List<String> keywords) throws RemoteException
36.    {
37.       for (String keyword : keywords)
38.       {
39.          Product p = products.get(keyword);
40.          if (p != null) return p;
41.       }
42.       if (backup != null)
43.          return backup.getProduct(keywords);
44.       else if (products.values().size() > 0)
45.          return products.values().iterator().next();
46.       else
47.          return null;
48.    }
49.
50.    private Map<String, Product> products;
51.    private Warehouse backup;
52. }

Example 10-9. WarehouseServer.java

 1. import java.rmi.*;
 2. import javax.naming.*;
 3.
 4. /**
 5.   * This server program instantiates a remote warehouse objects, registers it with the naming
 6.   * service, and waits for clients to invoke methods.
 7.   * @version 1.12 2007-10-09
 8.   * @author Cay Horstmann
 9.   */
10.
11. public class WarehouseServer
12. {
13.    public static void main(String[] args) throws RemoteException, NamingException
14.    {
15.       System.setProperty("java.security.policy", "server.policy");
16.       System.setSecurityManager(new SecurityManager());
17.
18.       System.out.println("Constructing server implementation...");
19.       WarehouseImpl backupWarehouse = new WarehouseImpl(null);
20.       WarehouseImpl centralWarehouse = new WarehouseImpl(backupWarehouse);
21.
22.       centralWarehouse.add("toaster", new Product("Blackwell Toaster", 23.95));
23.       backupWarehouse.add("java", new Book("Core Java vol. 2", "0132354799", 44.95));
24.
25.       System.out.println("Binding server implementation to registry...");
26.       Context namingContext = new InitialContext();
27.       namingContext.bind("rmi:central_warehouse", centralWarehouse);
28.
29.       System.out.println("Waiting for invocations from clients...");
30.    }
31. }

Remote References with Multiple Interfaces

A remote class can implement multiple interfaces. Consider a remote interface ServiceCenter.

public interface ServiceCenter extends Remote
{
   int getReturnAuthorization(Product prod) throws RemoteException;
}

Now suppose a WarehouseImpl class implements this interface as well as the Warehouse interface. When a remote reference to such a service center is transferred to another virtual machine, the recipient obtains a stub that has access to the remote methods in both the ServiceCenter and the Warehouse interface. You can use the instanceof operator to find out whether a particular remote object implements an interface. Suppose you receive a remote object through a variable of type Warehouse.

Warehouse location = product.getLocation();

The remote object might or might not be a service center. To find out, use the test

if (location instanceof ServiceCenter)

If the test passes, you can cast location to the ServiceCenter type and invoke the getReturnAuthorization method.

Remote Objects and the equals, hashCode, and clone Methods

Objects inserted in sets must override the equals method. In the case of a hash set or hash map, the hashCode method must be defined as well. However, there is a problem when trying to compare remote objects. To find out if two remote objects have the same contents, the call to equals would need to contact the servers containing the objects and compare their contents. Like any remote call, that call could fail. But the equals method in the class Object is not declared to throw a RemoteException, whereas all methods in a remote interface must throw that exception. Because a subclass method cannot throw more exceptions than the superclass method it replaces, you cannot define an equals method in a remote interface. The same holds for hashCode.

Instead, the equals and hashCode methods on stub objects simply look at the location of the remote objects. The equals method deems two stubs equal if they refer to the same remote object. Two stubs that refer to different remote objects are never equal, even if those objects have identical contents. Similarly, the hash code is computed only from the object identifier.

For the same technical reasons, remote references do not have a clone method. If clone were to make a remote call to tell the server to clone the implementation object, then the clone method would need to throw a RemoteException. However, the clone method in the Object superclass promised never to throw any exception other than CloneNotSupportedException.

To summarize, you can use remote references in sets and hash tables, but you must remember that equality testing and hashing do not take into account the contents of the remote objects. You simply cannot clone remote references.

Remote Object Activation

In the preceding sample programs, we used a server program to instantiate and register objects so that clients could make remote calls on them. However, in some cases, it might be wasteful to instantiate lots of remote objects and have them wait for connections, whether or not client objects use them. The activation mechanism lets you delay the object construction so that a remote object is only constructed when at least one client invokes a remote method on it.

To take advantage of activation, the client code is completely unchanged. The client simply requests a remote reference and makes calls through it.

However, the server program is replaced by an activation program that constructs activation descriptors of the objects that are to be constructed at a later time, and binds receivers for remote method calls with the naming service. When a call is made for the first time, the information in the activation descriptor is used to construct the object.

A remote object that is used in this way should extend the Activatable class instead of the UnicastRemoteObject class. Of course, it also implements one or more remote interfaces. For example,

class WarehouseImpl
   extends Activatable
   implements Warehouse
{
   . . .
}

Because the object construction is delayed until a later time, it must happen in a standardized form. Therefore, you must provide a constructor that takes two parameters:

  • An activation ID (which you simply pass to the superclass constructor).

  • A single object containing all construction information, wrapped in a MarshalledObject.

If you need multiple construction parameters, you must package them into a single object. You can always use an Object[] array or an ArrayList for this purpose.

When you build the activation descriptor, you will construct a MarshalledObject from the construction information like this:

MarshalledObject<T> param = new MarshalledObject<T>(constructionInfo);

In the constructor of the implementation object, use the get method of the MarshalledObject class to obtain the deserialized construction information.

T constructionInfo = param.get();

To demonstrate activation, we modify the WarehouseImpl class so that the construction information is a map of descriptions and prices. That information is wrapped into a MarshalledObject and unwrapped in the constructor:

public WarehouseImpl(ActivationID id, MarshalledObject<Map<String, Double>> param)
      throws RemoteException, ClassNotFoundException, IOException
{
   super(id, 0);
   prices = param.get();
   System.out.println("Warehouse implementation constructed.");
}

By passing 0 as the second parameter of the superclass constructor, we indicate that the RMI library should assign a suitable port number to the listener port.

This constructor prints a message so that you can see that the warehouse object is activated on demand.

Note

Note

Your remote objects don’t actually have to extend the Activatable class. If they don’t, then place the static method call

Activatable.exportObject(this, id, 0)

in the constructor of the server class.

Now let us turn to the activation program. First, you need to define an activation group. An activation group describes common parameters for launching the virtual machine that contains the remote objects. The most important parameter is the security policy.

Construct an activation group descriptor as follows:

Properties props = new Properties();
props.put("java.security.policy", "/path/to/server.policy");
ActivationGroupDesc group = new ActivationGroupDesc(props, null);

The second parameter describes special command options. We don’t need any for this example, so we pass a null reference.

Next, create a group ID with the call

ActivationGroupID id = ActivationGroup.getSystem().registerGroup(group);

Now you are ready to construct activation descriptors. For each object that should be constructed on demand, you need the following:

  • The activation group ID for the virtual machine in which the object should be constructed.

  • The name of the class (such as "WarehouseImpl" or "com.mycompany.MyClassImpl").

  • The URL string from which to load the class files. This should be the base URL, not including package paths.

  • The marshalled construction information.

For example,

MarshalledObject param = new MarshalledObject(constructionInfo);
ActivationDesc desc = new ActivationDesc(id, "WarehouseImpl",
                                         "http://myserver.com/download/", param);

Pass the descriptor to the static Activatable.register method. It returns an object of some class that implements the remote interfaces of the implementation class. You can bind that object with the naming service:

Warehouse centralWarehouse = (Warehouse) Activatable.register(desc);
namingContext.bind("rmi:central_warehouse", centralWarehouse);

Unlike the server programs of the preceding examples, the activation program exits after registering and binding the activation receivers. The remote objects are constructed only when the first remote method call occurs.

Listings 10-10 and 10-11 show the code for the activation program and the activatable warehouse implementation. The warehouse interface and the client program are unchanged.

To launch this program, follow these steps:

  1. Compile all source files.

  2. Distribute class files as follows:

    client/
       WarehouseClient.class
       Warehouse.class
    server/
       WarehouseActivator.class
       Warehouse.class
       WarehouseImpl.class
       server.policy
    download/
       Warehouse.class
       WarehouseImpl.class
    rmi/
       rmid.policy
  3. Start the RMI registry in the rmi directory (which contains no class files).

  4. Start the RMI activation daemon in the rmi directory.

    rmid -J-Djava.security.policy=rmid.policy

    The rmid program listens to activation requests and activates objects in a separate virtual machine. To launch a virtual machine, the rmid program needs certain permissions. These are specified in a policy file (see Listing 10-12). You use the -J option to pass an option to the virtual machine running the activation daemon.

  5. Start the NanoHTTPD web server in the download directory.

  6. Run the activation program from the server directory.

    java -Djava.rmi.server.codebase=http://localhost:8080/ WarehouseActivator

    The program exits after the activation receivers have been registered with the naming service. (You might wonder why you need to specify the codebase as it is also provided in the constructor of the activation descriptor. However, that information is only processed by the RMI activation daemon. The RMI registry still needs the codebase to load the remote interface classes.)

  7. Run the client program from the client directory.

    java WarehouseClient

    The client will print the familiar product description. When you run the client for the first time, you will also see the constructor messages in the shell window of the activation daemon.

Example 10-10. WarehouseActivator.java

 1. import java.io.*;
 2. import java.rmi.*;
 3. import java.rmi.activation.*;
 4. import java.util.*;
 5. import javax.naming.*;
 6.
 7. /**
 8.   * This server program instantiates a remote warehouse object, registers it with the naming
 9.   * service, and waits for clients to invoke methods.
10.   * @version 1.12 2007-10-09
11.   * @author Cay Horstmann
12.   */
13.
14. public class WarehouseActivator
15. {
16.    public static void main(String[] args) throws RemoteException, NamingException,
17.          ActivationException, IOException
18.    {
19.       System.out.println("Constructing activation descriptors...");
20.
21.       Properties props = new Properties();
22.       // use the server.policy file in the current directory
23.       props.put("java.security.policy", new File("server.policy").getCanonicalPath());
24.       ActivationGroupDesc group = new ActivationGroupDesc(props, null);
25.       ActivationGroupID id = ActivationGroup.getSystem().registerGroup(group);
26.
27.       Map<String, Double> prices = new HashMap<String, Double>();
28.       prices.put("Blackwell Toaster", 24.95);
29.       prices.put("ZapXpress Microwave Oven", 49.95);
30.
31.       MarshalledObject<Map<String, Double>> param = new MarshalledObject<Map<String, Double>>(
32.             prices);
33.
34.       String codebase = "http://localhost:8080/";
35.
36.       ActivationDesc desc = new ActivationDesc(id, "WarehouseImpl", codebase, param);
37.
38.       Warehouse centralWarehouse = (Warehouse) Activatable.register(desc);
39.
40.       System.out.println("Binding activable implementation to registry...");
41.       Context namingContext = new InitialContext();
42.       namingContext.bind("rmi:central_warehouse", centralWarehouse);
43.       System.out.println("Exiting...");
44.    }
45. }

Example 10-11. WarehouseImpl.java

 1. import java.io.*;
 2. import java.rmi.*;
 3. import java.rmi.activation.*;
 4. import java.util.*;
 5.
 6. /**
 7.  * This class is the implementation for the remote Warehouse interface.
 8.  * @version 1.0 2007-10-20
 9.  * @author Cay Horstmann
10.  */
11. public class WarehouseImpl extends Activatable implements Warehouse
12. {
13.    public WarehouseImpl(ActivationID id, MarshalledObject<Map<String, Double>> param)
14.          throws RemoteException, ClassNotFoundException, IOException
15.    {
16.       super(id, 0);
17.       prices = param.get();
18.       System.out.println("Warehouse implementation constructed.");
19.    }
20.
21.    public double getPrice(String description) throws RemoteException
22.    {
23.       Double price = prices.get(description);
24.       return price == null ? 0 : price;
25.    }
26.
27.    private Map<String, Double> prices;
28. }

Example 10-12. rmid.policy

1. grant
2. {
3.    permission com.sun.rmi.rmid.ExecPermission
4.       "${java.home}${/}bin${/}java";
5.    permission com.sun.rmi.rmid.ExecOptionPermission
6.       "-Djava.security.policy=*";
7. }

Web Services and JAX-WS

In recent years, web services have emerged as a popular technology for remote method calls. Technically, a web service has two components:

  • A service that can be accessed with the SOAP transport protocol

  • A description of the service in the WSDL format

SOAP is an XML protocol for invoking remote methods, similar to the protocol that RMI uses for the communication between clients and servers. Just as you can program RMI applications without knowing anything about the details of the RMI protocol, you don’t really need to know any details about SOAP to call a web service.

WSDL is an interface description language. It too is based on XML. A WSDL document describes the interface of a web service: the methods that can be called, and their parameter and return types. In this section, we generate a WSDL document from a service implemented in Java. This document contains all the information that a client program needs to invoke the service, whether it is written in Java or another programming language. In the next section, we write a Java program that invokes the Amazon e-commerce service, using the WSDL provided by Amazon. We have no idea in which language that service was implemented.

Using JAX-WS

There are several toolkits for implementing web services in Java. In this section, we discuss the JAX-WS technology that is included in Java SE 6 and above.

With JAX-WS, you do not provide an interface for a web service. Instead, you annotate a class with @WebService, as shown in Listing 10-13. Note also the @WebParam annotation of the description parameter. It gives the parameter a humanly readable name in the WSDL file. (This annotation is optional. By default, the parameter would be called arg0.)

Example 10-13. Warehouse.java

 1. package com.horstmann.corejava;
 2. import java.util.*;
 3. import javax.jws.*;
 4.
 5. /**
 6.  * This class is the implementation for a Warehouse web service
 7.  * @version 1.0 2007-10-09
 8.  * @author Cay Horstmann
 9.  */
10.
11. @WebService
12. public class Warehouse
13. {
14.    public Warehouse()
15.    {
16.       prices = new HashMap<String, Double>();
17.       prices.put("Blackwell Toaster", 24.95);
18.       prices.put("ZapXpress Microwave Oven", 49.95);
19.    }
20.
21.    public double getPrice(@WebParam(name="description") String description)
22.    {
23.       Double price = prices.get(description);
24.       return price == null ? 0 : price;
25.    }
26.
27.    private Map<String, Double> prices;
28. }

In RMI, the stub classes were generated dynamically, but with JAX-WS, you run a tool to generate them. Change to the base directory of the Webservices1 source and run the wsgen class as follows:

wsgen -classpath . com.horstmann.corejava.Warehouse

Note

Note

The wsgen tool requires that the class that provides the web service is contained in a package other than the default package.

The tool generates two rather mundane classes in the com.horstmann.corejava.jaxws package. The first class encapsulates all parameters of the call:

public class GetPrice
{
    private String description;
    public String getDescription() { return this.description; }
    public void setDescription(String description) { this.description = description; }
}

The second class encapsulates the return value:

public class GetPriceResponse
{
    private double _return;
    public double get_return() { return this._return; }
    public void set_return(double _return) { this._return = _return; }
}

Typically, one has a sophisticated server infrastructure for deploying web services, which we do not discuss here. The JDK contains a very simple mechanism for testing a service. Simply call the Endpoint.publish method. A server is started on the given URL—see Listing 10-14.

Example 10-14. WarehouseServer.java

 1. package com.horstmann.corejava;
 2.
 3. import javax.xml.ws.*;
 4.
 5. public class WarehouseServer
 6. {
 7.    public static void main(String[] args)
 8.    {
 9.       Endpoint.publish("http://localhost:8080/WebServices/warehouse", new Warehouse());
10.    }
11. }

At this point, you should compile the server classes, run wsgen, and start the server:

java com.horstmann.corejava.WarehouseServer

Now point your web browser to http://localhost:8080/WebServices/warehouse?wsdl. You will get this WSDL file:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:tns="http://corejava.horstmann.com/"
  xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
  targetNamespace="http://corejava.horstmann.com/" name="WarehouseService">
  <types>
    <xsd:schema>
      <xsd:import schemaLocation="http://localhost:8080/WebServices/warehouse?xsd=1"
      namespace="http://corejava.horstmann.com/"></xsd:import>
    </xsd:schema>
  </types>
  <message name="getPrice">
    <part element="tns:getPrice" name="parameters"></part>
  </message>
  <message name="getPriceResponse">
    <part element="tns:getPriceResponse" name="parameters"></part>
  </message>
  <portType name="Warehouse">
    <operation name="getPrice">
      <input message="tns:getPrice"></input>
      <output message="tns:getPriceResponse"></output>
    </operation>
  </portType>
  <binding name="WarehousePortBinding" type="tns:Warehouse">
    <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"></soap:binding>
    <operation name="getPrice">
      <soap:operation soapAction=""></soap:operation>
      <input><soap:body use="literal"></soap:body></input>
      <output><soap:body use="literal"></soap:body></output>
    </operation>
  </binding>
  <service name="WarehouseService">
    <port name="WarehousePort" binding="tns:WarehousePortBinding">
      <soap:address location="http://localhost:8080/WebServices/warehouse"></soap:address>
    </port>
  </service>
</definitions>

This description tells us that an operation getPrice is provided. Its input is a tns:getPrice and its output is a tns:getPriceResponse. (Here, tns is the namespace alias for the target namespace, http://corejava.horstmann.com.)

To understand these types, point your browser to http://localhost:8080/WebServices/warehouse?xsd=1. You will get this XSL document:

<xs:schema targetNamespace="http://corejava.horstmann.com/" version="1.0">
   <xs:element name="getPrice" type="tns:getPrice"/>
   <xs:element name="getPriceResponse" type="tns:getPriceResponse"/>
   <xs:complexType name="getPrice">
      <xs:sequence><xs:element name="description" type="xs:string" minOccurs="0"/></xs:sequence>
   </xs:complexType>
   <xs:complexType name="getPriceResponse">
      <xs:sequence><xs:element name="return" type="xs:double"/></xs:sequence>
   </xs:complexType>
</xs:schema>

Now you can see that getPrice has a description element of type string, and getPriceResponse has a return element of type double.

Note

Note

The WSDL file does not specify what the service does. It only specifies the parameter and return types.

A Web Service Client

Let’s turn to implementing the client. Keep in mind that the client knows nothing about the server except what is contained in the WSDL. To generate Java classes that can communicate with the server, you generate a set of client classes, using the wsimport utility.

wsimport -keep -p com.horstmann.corejava.server http://localhost:8080/WebServices/warehouse?wsdl

The -keep option keeps the source files, in case you want to look at them. The following classes and interfaces are generated:

GetPrice
GetPriceResponse
Warehouse
WarehouseService
ObjectFactory

You already saw the GetPrice and GetPriceResponse classes.

The Warehouse interface defines the remote getPrice method:

public interface Warehouse
{
    @WebMethod public double getPrice(@WebParam(name = "description") String description);
}

You only need to know one thing about the WarehouseService class: its getPort method yields a stub of type Warehouse through which you invoke the service—see Listing 10-15.

You can ignore the ObjectFactory class as well as the file package-info.java that defines a package-level annotation. (We discuss annotations in detail in Chapter 11.)

Note

Note

You can use any convenient package for the generated classes. If you look closely, you will notice that the GetPrice and GetPriceResponse classes are in different packages on the server and client. This is not a problem. After all, neither the server nor the client know about each other’s Java implementation. They don’t even know whether the other is implemented in Java.

Example 10-15. WarehouseClient.java

 1. import java.rmi.*;
 2. import javax.naming.*;
 3. import com.horstmann.corejava.server.*;
 4.
 5. /**
 6.  * The client for the warehouse program.
 7.  * @version 1.0 2007-10-09
 8.  * @author Cay Horstmann
 9.  */
10. public class WarehouseClient
11. {
12.    public static void main(String[] args) throws NamingException, RemoteException
13.    {
14.       WarehouseService service = new WarehouseService();
15.       Warehouse port = service.getPort(Warehouse.class);
16.
17.       String descr = "Blackwell Toaster";
18.       double price = port.getPrice(descr);
19.       System.out.println(descr + ": " + price);
20.    }
21. }

Now you are ready to run the client program. Double-check that the server is still running, open another shell window, and execute

java WarehouseClient

You will get the familiar message about the price of a toaster.

Note

Note

You might wonder why there is no equivalent of a RMI registry. When you locate a remote object for RMI, the client need not know on which server the object is located. It merely needs to know how to locate the registry. However, to make a web service call, the client needs the URL of the server. It is hardwired into the WarehouseService class.

We used a network sniffer to see how the client and server actually communicate (see Figure 10-8). The client sends the following request to the server:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://corejava.horstmann.com/">
   <soapenv:Body>
      <ns1:getPrice><description>Blackwell Toaster</description></ns1:getPrice>
   </soapenv:Body>
</soapenv:Envelope>
Analyzing SOAP traffic

Figure 10-8. Analyzing SOAP traffic

The server responds:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
   xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:ns1="http://corejava.horstmann.com/">
   <soapenv:Body>
      <ns1:getPriceResponse><return>24.95</return></ns1:getPriceResponse>
   </soapenv:Body>
</soapenv:Envelope>

In this section, you have seen the essentials about web services:

  • The services are defined in a WSDL document, which is formatted as XML.

  • The actual request and response methods use SOAP, another XML format.

  • Clients and servers can be written in any language.

The Amazon E-Commerce Service

To make the discussion of web services more interesting, we look at a concrete example: the Amazon e-commerce web service, described at http://www.amazon.com/gp/aws/landing.html. The e-commerce web service allows a programmer to interact with the Amazon system for a wide variety of purposes. For example, you can get listings of all books with a given author or title, or you can fill shopping carts and place orders. Amazon makes this service available for use by companies that want to sell items to their customers, using the Amazon system as a fulfillment back end. To run our example program, you will need to sign up with Amazon and get a free developer token that lets you connect to the service.

Alternatively, you can adapt the technique described in this section to any other web service. The site http://www.xmethods.com lists many freely available web services that you can try.

Let us look more closely at the WSDL for the Amazon E-Commerce Service (located at http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl). It describes an ItemSearch operation as follows:

<operation name="ItemSearch">
   <input message="tns:ItemSearchRequestMsg"/>
   <output message="tns:ItemSearchResponseMsg"/>
</operation>

...

<message name="ItemSearchRequestMsg">
   <part name="body" element="tns:ItemSearch"/>
</message>
<message name="ItemSearchResponseMsg">
   <part name="body" element="tns:ItemSearchResponse"/>
</message>

Here are the definitions of the ItemSearch and ItemSearchResponse types:

<xs:element name="ItemSearch">
   <xs:complexType>
      <xs:sequence>
         <xs:element name="MarketplaceDomain" type="xs:string" minOccurs="0"/>
         <xs:element name="AWSAccessKeyId" type="xs:string" minOccurs="0"/>
         <xs:element name="SubscriptionId" type="xs:string" minOccurs="0"/>
         <xs:element name="AssociateTag" type="xs:string" minOccurs="0"/>
         <xs:element name="XMLEscaping" type="xs:string" minOccurs="0"/>
         <xs:element name="Validate" type="xs:string" minOccurs="0"/>
         <xs:element name="Shared" type="tns:ItemSearchRequest" minOccurs="0"/>
         <xs:element name="Request" type="tns:ItemSearchRequest" minOccurs="0"
            maxOcurs="unbounded"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>
<xs:element name="ItemSearchResponse">
   <xs:complexType>
      <xs:sequence>
         <xs:element ref="tns:OperationRequest" minOccurs="0"/>
         <xs:element ref="tns:Items" minOccurs="0" maxOccurs="unbounded"/>
      </xs:sequence>
   </xs:complexType>
</xs:element>

Using the JAX-WS technology, the ItemSearch operation becomes a method call:

void itemSearch(String marketPlaceDomain, String awsAccessKeyId,
   String subscriptionId, String associateTag, String xmlEscaping, String validate,
   ItemSearchRequest shared, List<ItemSearchRequest> request,
   Holder<OperationRequest> opHolder, Holder<List<Items>> responseHolder)

The ItemSearchRequest parameter type is defined as

<xs:complexType name="ItemSearchRequest">
   <xs:sequence>
      <xs:element name="Actor" type="xs:string" minOccurs="0"/>
      <xs:element name="Artist" type="xs:string" minOccurs="0"/>
      . . .
      <xs:element name="Author" type="xs:string" minOccurs="0"/>
      . . .
      <xs:element name="ResponseGroup" type="xs:string" minOccurs="0" maxOccurs="unbounded"/>
      . . .
      <xs:element name="SearchIndex" type="xs:string" minOccurs="0"/>
      . . .
</xs:complexType>

This description is translated into a class.

public class ItemSearchRequest
{
   public ItemSearchRequest() { ... }
   public String getActor() { ... }
   public void setActor(String newValue) { ... }
   public String getArtist() { ... }
   public void setArtist(String newValue) { ... }
   ...
   public String getAuthor() { ... }
   public void setAuthor(String newValue) { ... }
   ...
   public List<String> getResponseGroup() { ... }
   ...
   public void setSearchIndex(String newValue) { ... }
   ...
}

To invoke the search service, construct an ItemSearchRequest object and call the itemSearch method of the “port” object.

ItemSearchRequest request = new ItemSearchRequest();
request.getResponseGroup().add("ItemAttributes");
request.setSearchIndex("Books");
Holder<List<Items>> responseHolder = new Holder<List<Items>>();
request.setAuthor(name);
port.itemSearch("", accessKey, "", "", "", "", request, null, null, responseHolder);

The port object translates the Java object into a SOAP message, passes it to the Amazon server, translates the returned message into a ItemSearchResponse object, and places the response in the “holder” object.

Note

Note

The Amazon documentation about the parameters and return values is extremely sketchy. However, you can fill out forms at http://awszone.com/scratchpads/index.aws to see the SOAP requests and responses. Those help you guess what parameter values you need to supply and what return values you can expect.

Our sample application (in Listing 10-16) is straightforward. The user specifies an author name and clicks the Search button. We simply show the first page of the response (see Figure 10-9). This shows that the web service is successful. We leave it as the proverbial exercise for the reader to extend the functionality of the application.

Connecting to a web service

Figure 10-9. Connecting to a web service

To run this program, you first generate the client-side artifact classes:

wsimport -p com.horstmann.amazon
   http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl

Then edit the AmazonTest.java file to include your Amazon key, compile, and run:

javac AmazonTest.java
java AmazonTest

Example 10-16. AmazonTest.java

  1. import com.horstmann.amazon.*;
  2. import java.awt.*;
  3. import java.awt.event.*;
  4. import java.util.List;
  5. import javax.swing.*;
  6. import javax.xml.ws.*;
  7.
  8. /**
  9.   * The client for the Amazon e-commerce test program.
 10.   * @version 1.10 2007-10-20
 11.   * @author Cay Horstmann
 12.   */
 13.
 14. public class AmazonTest
 15. {
 16.    public static void main(String[] args)
 17.    {
 18.       JFrame frame = new AmazonTestFrame();
 19.       frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 20.       frame.setVisible(true);
 21.    }
 22. }
 23.
 24. /**
 25.  * A frame to select the book author and to display the server response.
 26.  */
 27. class AmazonTestFrame extends JFrame
 28. {
 29.    public AmazonTestFrame()
 30.    {
 31.       setTitle("AmazonTest");
 32.       setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
 33.
 34.       JPanel panel = new JPanel();
 35.
 36.       panel.add(new JLabel("Author:"));
 37.       author = new JTextField(20);
 38.       panel.add(author);
 39.
 40.       JButton searchButton = new JButton("Search");
 41.       panel.add(searchButton);
 42.       searchButton.addActionListener(new ActionListener()
 43.          {
 44.             public void actionPerformed(ActionEvent event)
 45.             {
 46.                result.setText("Please wait...");
 47.                new SwingWorker<Void, Void>()
 48.                   {
 49.                      @Override
 50.                      protected Void doInBackground() throws Exception
 51.                      {
 52.                         String name = author.getText();
 53.                         String books = searchByAuthor(name);
 54.                         result.setText(books);
 55.                         return null;
 56.                      }
 57.                   }.execute();
 58.             }
 59.          });
 60.
 61.       result = new JTextArea();
 62.       result.setLineWrap(true);
 63.       result.setEditable(false);
 64.
 65.       if (accessKey.equals("your key here"))
 66.       {
 67.          result.setText("You need to edit the Amazon access key.");
 68.          searchButton.setEnabled(false);
 69.       }
 70.
 71.       add(panel, BorderLayout.NORTH);
 72.       add(new JScrollPane(result), BorderLayout.CENTER);
 73.    }
 74.
 75.    /**
 76.     * Calls the Amazon web service to find titles that match the author.
 77.     * @param name the author name
 78.     * @return a description of the matching titles
 79.     */
 80.    private String searchByAuthor(String name)
 81.    {
 82.       AWSECommerceService service = new AWSECommerceService();
 83.       AWSECommerceServicePortType port = service.getPort(AWSECommerceServicePortType.class);
 84.       ItemSearchRequest request = new ItemSearchRequest();
 85.       request.getResponseGroup().add("ItemAttributes");
 86.       request.setSearchIndex("Books");
 87.
 88.       Holder<List<Items>> responseHolder = new Holder<List<Items>>();
 89.       request.setAuthor(name);
 90.       port.itemSearch("", accessKey, "", "", "", "", request, null, null, responseHolder);
 91.
 92.       List<Item> response = responseHolder.value.get(0).getItem();
 93.
 94.       StringBuilder r = new StringBuilder();
 95.       for (Item item : response)
 96.       {
 97.          r.append("authors=");
 98.          List<String> authors = item.getItemAttributes().getAuthor();
 99.          r.append(authors);
100.          r.append(",title=");
101.          r.append(item.getItemAttributes().getTitle());
102.          r.append(",publisher=");
103.          r.append(item.getItemAttributes().getPublisher());
104.          r.append(",pubdate=");
105.          r.append(item.getItemAttributes().getPublicationDate());
106.          r.append("
");
107.       }
108.       return r.toString();
109.    }
110.
111.    private static final int DEFAULT_WIDTH = 450;
112.    private static final int DEFAULT_HEIGHT = 300;
113.
114.    private static final String accessKey = "12Y1EEATQ8DDYJCVQYR2";
115.
116.    private JTextField author;
117.    private JTextArea result;
118. }

 

This example shows that calling a web service is fundamentally the same as making any other remote method call. The programmer calls a local method on a proxy object, and the proxy connects to a server. Because web services are springing up everywhere, this is clearly an interesting technology for application programmers.

You have now seen the RMI mechanism, a sophisticated distributed programming model for Java programs that is used extensively in the Java EE architecture. You have also had an introduction into web services, which allow you to connect clients and servers, independent of the programming language. In the next chapter, we turn to a different aspect of Java programming: interacting with “native” code in a different programming language on the same machine.

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

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