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 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.
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).
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.
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.
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:
It locates the remote object to be called.
It calls the desired method, passing the supplied parameters.
It captures the return value or exception of the call.
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.
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.
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.
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:
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. }
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.
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.
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. }
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);
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.
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 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.
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
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.
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).
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.
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
Logged Activity | |
---|---|
| Server-side remote calls |
| Server-side remote references |
| Client-side remote calls |
| Client-side remote references |
| Distributed garbage collection |
|
|
| Transport layer |
| TCP binding and connection |
| 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
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.
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.
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.
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).
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.
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());
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.
Compile the source files for the interface, implementation, client, and server classes.
javac *.java
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
In the first console window, change to a directory that has no class files. Start the RMI registry.
In the second console window, change to the download
directory and start NanoHTTPD
.
In the third console window, change to the server
directory and start the server.
java -Djava.rmi.server.codebase=http://localhost:8080/ WarehouseServer
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. }
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.
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.
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.
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:
Compile all source files.
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
Start the RMI registry in the rmi
directory (which contains no class files).
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.
Start the NanoHTTPD
web server in the download
directory.
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.)
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. }
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.
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
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.
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
.
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.)
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.
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>
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.
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.
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.
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.
18.118.163.250