Section 12.4. Writing Web Service Clients


12.4. Writing Web Service Clients

A client of a service wants to use the service to get something done. When a service is implemented using SOAP, this means that the client needs to send SOAP messages to the service, targeted at specific operations and ports provided by the service, and if these ports and operations generate response messages, the client needs to receive these messages and interpret them.

In order to do this, the client needs a way to map the Java objects and data it has into the ports and operations and messages exposed by the SOAP service, and back again. You can accomplish this Jusing JAX-RPC and SAAJ in several ways, which can be roughly grouped into the following three categories:


Static proxy

This approach involves mapping the service's WSDL descriptor to a set of client-side Java code. All classes needed by the client are pregenerated by this mapping, and the client uses them to interact with the service.


Dynamic proxy

This approach is similar to the static proxy, except that the stub used by the client is generated dynamically by the JAX-RPC runtime instead of using a class file being generated offline by mapping the WSDL. The client only needs to have a Java interface available that is compatible with the WSDL description of the service.


Dynamic invocation interface (DII)

In this approach, the client dynamically constructs a call to the target service, specifying all the particulars of the service explicitly when configuring the service call.

In terms of the mapping we mentioned (from Java to the service's ports, operations, and messages and back), these three options essentially boil down to doing the mapping once and encoding it "statically" as a set of concrete Java classes that the client uses (the static proxy approach ); doing the same mapping dynamically at runtime, with the generated classes existing only at runtime (the dynamic proxy approach); or having the client do the mapping programmatically in its code, with no service interface classes being generated (the dynamic invocation interface (DII) approach).

Let's see each approach in detail, by creating clients that interact with a very simple web service that simply echoes a string back to the client. In its WSDL description, this service is named EchoService and has a single port named echo. This port has a single operation, also called echo, that accepts a message containing a single string and returns a message containing a single string. It is about as simple as a web service can get.

12.4.1. Static Proxy Approach

The static proxy approach for interacting with web services from a Java client is by far the simplest. It involves generating, prior to runtime, a set of Java artifacts mapped from the WSDL descriptor for the web service. The Java artifacts generated from the WSDL can be bundled with the client code, and the client code is very clean, with all the SOAP communication details hidden in generated Service and Stub implementations. In addition, since you have the actual mapped Java interfaces and classes on the client side, as a developer you can import these into your IDE and view/search/exercise these interfaces just like other Java code.

This approach is called the static proxy approach because the mapping performed by the web service engine results in a set of actual concrete Java class files. The client will use these classes as-is to interact with the service, and these classes implement a hardcoded mapping of service ports and operations to Java interfaces and methods, so it's in this sense that the mapping is static.

In Axis, the tool that generates these static proxy classes is called WSDL2Java. It's provided in two forms: a Java class, org.apache.axis.wsdl.WSDL2Java, that can be invoked from the command line to perform the mapping, and an Ant task, axis-wsdl2java, that can be used in your Ant build file to do the same.

If the WSDL for the Echo service were published at the URL http://mywebservices.com/services/echo?wsdl, we would then use WSDL2Java to generate client stubs from the command line:

     > java org.apache.axis.wsdl.WSDL2Java         -o /home/dev/generated-src         http://mywebservices.com/services/echo?wsdl 

This command generates a set of client-side Java classes mapped from the WSDL file and writes the Java source files into the /home/dev/generated-src directory that was specified using the -o option. In the case of our echo service, the following interfaces will be generated, according to the JAX-RPC mapping specifications:


Echo

The echo port in the WSDL is mapped to this interface, which extends java.rmi.Remote. JAX-RPC specifies that mapped class names should always be mixed-case, so echo becomes the interface name Echo. Each operation defined in the WSDL for this port type is mapped to a corresponding method on this interface, and the input and output messages for the operation are mapped to corresponding Java method arguments and return values. In our case, there is a single operation defined for the port, also named echo, that has an input message consisting of a single string and an output message consisting of a single string. This operation is mapped to an echo( ) method with a single String argument and a String return value. No SOAP faults are associated with this operation in the WSDL, so there are no service-specific exceptions in the echo( ) method declaration, only the RemoteException required for all RMI remote methods.


EchoService

In the WSDL, our service is named EchoService, so this Java interface is mapped from it. It extends the javax.xml.rpc.Service interface from the JAX-RPC API. For each port defined in the service, there is a get<portname>( ) method that returns an instance of the Java class mapped from that port. In our case, there is a getechoPort( ) method that returns an Echo stub. because the bound port is named echoPort in the service binding and in our WSDL. Note that the accessor methods are named after the name used for the port binding, not for the port definition itself. A single port can be bound to multiple protocols or physical endpoint URLs in the WSDL file, so the binding name is used to distinguish between them.

The web service engine also needs to provide concrete implementations of these mapped interfaces for the client to use. The names and implementation details of these concrete implementations are not specified by the JAX-RPC standard, so using these generated classes directly may tie your client code to a particular web service engine. In the case of Axis, the following concrete classes are generated from the WSDL:


EchoBindingStub

This is a concrete implementation of the Echo interface mapped from the port type. This class serves as a client-side stub: all of the methods mapped from the WSDL operations are implemented here as calls to the service endpoint.


EchoServiceLocator

This is an implementation of the EchoService interface mapped from the WSDL service. The get<portname>( ) methods declared in the interface are implemented here to construct an appropriate implementation of the interface mapped from the WSDL port type. In this case, the getecho( ) method from EchoService is implemented to return an instance of EchoBindingStub, the Axis concrete implementation of the Echo interface.

These interfaces and classes are mapped into packages according to the namespaces used in the WSDL for their corresponding XML entities. JAX-RPC does not specify this mappingit's left up to the web service engine to decide how to deal with this. If you don't tell it otherwise, Axis converts the namespace of each WSDL entity into a valid package name. For example, if the WSDL file used for the mapping specifies a namespace for its entities like so:

     <wsdl:definitions targetNamespace="http://myservices/services/echo">     . . . 

then Axis places all of the Java classes into the package myservices.services.echo. Often, this isn't the desired behaviora client will probably want to have the Java code fit into its own package structure. Axis allows you to configure the package mapping by specifying a properties file when invoking the WSDL2Java utility. You can specify a namespace-to-package mapping file using the -N option on the command line:

     > java org.apache.axis.wsdl.WSDL2Java         -o /home/dev/generated-src -N nsmap.properties         http://mywebservices.com/services/echo?wsdl 

The namespace-to-package mapping file is a Java properties file, with namespaces as the "property name" and the corresponding package name as the property value. A mapping file for our previous example might look like the following:

     http\://myservices/services/echo=com.myapp.soap.echoclient 

Here, we're saying that any entities found in the WSDL namespace http://myservices/services/echo should be mapped to the Java package com.myapp.soap.echoclient. Note the backslash in front of the colon in the namespace value: Java properties files use colons as an optional separator between names and values, so we have to escape the colon in the URL to prevent it from being interpreted this way. When setting up these package mapping files yourself, don't forget to escape the colon after the URL protocol and colons in front of any port numbers (e.g., http\://myservices.com\:8080).

It's important to remember that the only part of this mapping process that is specified in detail by the JAX-RPC standard is the mapped port type interface (Echo) and the name of the mapped service interface (EchoService), both of which are taken from the names and attributes of their corresponding WSDL entities. The concrete implementation classes of these interfaces can be named whatever the mapping tool wants to name them. In addition, the mapping tool can choose to map namespaces to packages in a totally different way if it chooses.

Given all this, the static proxy approach for building a client to a web service simply involves using the concrete stub class generated by the web service engine's tools. In the case of Axis, a client uses the EchoSoapBindingStub class to interact with the echo web service. Instances of the stub class are obtained using the concrete Service implementation as a factory, calling the getecho( ) method to generate a stub instance:

     try {                  // Get a stub to the remote service         Echo echo = new EchoServiceLocator( ).getechoPort( );         // Invoke the operation and collect the result         String resp = echo.echo(msg);         System.out.println("Sent: \"" + msg +                            "\", got response \"" + resp + "\"");     }     catch (ServiceException se) {         System.err.println("Failed to invoke service: " +                            se.getMessage( ));     }     catch (RemoteException re) {         System.err.println("Error invoking service: " +                            re.getMessage( ));     } 

The call to getechoPort( ) on the Service implementation generated by Axis returns an instance of EchoBindingStub, the proxy for the echo service. When we call the echo( ) method on the proxy, it internally constructs the appropriate SOAP message to the web service (based on the information gleaned from the WSDL), sends the message to the service endpoint, receives the SOAP response, extracts the string data, and returns it as the result of the method call. In the client code, though, this is simply a normal-looking method call on a Java object.

You may have noticed that nowhere in the client do we specify the URL location of the web service (the service endpoint) that is the target of our request. The getechoPort( ) method on the Service uses, by default, the endpoint URL found in the WSDL that was used to generate the client-side Java classes. The endpoint will be found in the service element of the WSDL description, for example:

     . . .     <wsdl:service name="EchoService">         <wsdl:port binding="impl:echoSoapBinding" name="echoPort">             <wsdlsoap:address location="http://myservices.com/services/echo"/>         </wsdl:port>     </wsdl:service>     . . . 

If the client needs to contact an alternate instance of the web service running at a different location, Axis generates an alternate getecho( ) method on the mapped Service class. This version accepts a URL to indicate the service endpoint:

     Echo echo =         new EchoServiceLocator( ).getechoPort(new URL(                                                "http://altserver.com/echo"));     String resp = echo2.echo(msg); 

12.4.2. Dynamic Proxy Approach

The dynamic proxy approach to interacting with a web service does all of the WSDL-to-Java mapping dynamically. The client asks the web service engine to generate a client stub class on the fly, at runtime. The client does this by creating a javax.xml.rpc.Service instance that references the WSDL descriptor for the target service. The client then uses the generic getPort( ) methods on the Service to dynamically generate a stub class that can be used to communicate with the target web service. In the dynamic proxy approach, since no mapped Java interfaces are generated from the WSDL, the client has to provide its own Java interface for the stub class, and this interface needs to be compatible with the service.

The dynamic proxy approach is useful in cases in which a WSDL descriptor for a service is available but generating mapped Java code beforehand isn't desired or possible for some reason. One reason for this is portability across Java web service engines. By using the dynamic proxy approach described next, you avoid using the engine-specific concrete class implementations of the mapped WSDL interfaces (for Axis, these are the EchoServiceLocator and EchoBindingStub classes described earlier). So the client code can be more easily run using different web service engines or even different versions of the same web service engine.

Returning to our echo web service, if we wanted to invoke it using the dynamic proxy approach, we would do the following:

     import javax.xml.rpc.ServiceFactory;     import javax.xml.rpc.Service;     . . .     // Define an inner class that serves as our stub interface to     // the echo service     interface MyEcho extends Remote {         public String echo(String msg) throws RemoteException;     }     . . .     public void invokeService( ) {         String msg = "Hi there";         try {             // The WSDL that describes the service we want to contact             String wsdlLoc =                  "http://myservices.com/services/echo?wsdl";             // The qualified name of the service within the WSDL doc             QName serviceName =                  new QName("http://myservices.com/services/echo",                           "EchoService");                          ServiceFactory sFactory = ServiceFactory.newInstance( );             Service service =                  sFactory.createService(new URL(wsdlLoc), serviceName);             MyEcho echo = (MyEcho)service.getPort(MyEcho.class);             // Invoke the operation and collect the result             String resp = echo.echo(msg);             System.out.println("Sent: \"" + msg +                                "\", got response \"" + resp + "\"");         }         catch (ServiceException se) {             System.err.println("Service generated error: " + se.getMessage( ));         }         catch (RemoteException re) {             System.err.println("Error invoking service: " + re.getMessage( ));         }         catch (MalformedURLException mue) {             System.err.println("Bad service endpoint");         }     }     . . . 

Here, our client code creates a ServiceFactory instance by invoking the static ServiceFactory.newInstance( )method that creates a ServiceFactory instance provided by the resident web service engine in use. We use the ServiceFactory to create a Service that represents the particular web service we're targeting. The createService( ) method is called to do this, passing in the URL for a WSDL document that contains the service description, plus the qualified name of the target service within the document. In our case, the WSDL defines the service within the namespace http://myservices.com/services/echo, and the service is named EchoService in the WSDL, so we pass in a QName, serviceName, that represents this qualified name within the document. Finally, we ask the Service to dynamically map the target port of the service to our Java interface for the port by calling the getPort( ) method on the Service. In the call, we pass in the class we want the generated stub instance to implement, MyEcho. Service will create a concrete stub class on the fly, this stub class will extend the class provided in the getPort( ) call, and it will be connected to the requested port within the target web service.

The stub interface that the client provides must extend (directly or indirectly) java.rmi.Remote, and its interface has to be compatible with the WSDL description of the target port. In other words, the methods on the Java interface have to correspond with the operations defined for the port in the WSDL, according to the JAX-RPC mapping rules. If the web service engine has any problems doing the dynamic proxy generation, the getPort( ) method throws a ServiceException.

You may notice that we did not specify the qualified name of the port when we called getPort( ) to generate the stub. We just passed in the stub class that we wanted the generated stub instance to implement. When this version of getPort( ) is invoked, the web service engine is responsible for determining the "best match" between the ports defined in the WSDL document and the stub interface passed into the getPort( ) call. In the case of Axis, it first looks for a port type whose name matches the name of the Java interface. If that fails, it tries to use the first port defined in the WSDL. In our example, the Java interface name (MyEcho) does not match the target port name in the WSDL (echo). But only one port is defined in the WSDL, so that one will be mapped by the Axis runtime and our client will work fine. A more robust approach (and the one you really should use when more than one port is present in the WSDL) is to use the alternate version of Service.getPort( ), which accepts the qualified name of the target port as well as the desired stub interface. In our case, we would invoke this version of getPort( ) like so:

     QName portName =         new QName("http://localhost:8080/JEnt-examples/services/echo",                  "echo");     MyEcho echo2 = (MyEcho)service.getPort(portName, MyEcho.class); 

12.4.3. Dynamic Invocation Interface (DII) Approach

The DII approach puts the most burden on the client, because all of the service mapping details are provided by the client. But it also offers the most flexibility, since your client can construct a call to any web service without needing to define a stub interface or generate any proxy code beforehand. There is no stub class involved at all in the DII approachthe client programmatically constructs the call to the service itself and is responsible for ensuring that the arguments included in the call correspond to the messages expected by the web service. The client is also responsible for knowing what kind of response to expect from the web service.

The DII approach is best suited to situations in which the client can't assume much about the service to be contacted or simply doesn't have the information about the service prior to runtime. One typical scenario suited for DII is a service that does not publish a WSDL descriptor. With DII, you can still talk to the service by programmatically specifying the service configuration. DII is also useful in contexts in which the web service configuration needs to be externalized from the client applicationeither in configuration files that are read at runtime or in the user's head. A web service IDE, for example, might provide a dynamic web service test wizard, allowing the user to specify all the web service details. The IDE can use DII to generate the actual web service call based on the user's runtime inputs.

The DII approach involves creating a Service instance, as we saw in the dynamic proxy case. But instead of asking the Service to map the service to a proxy class, we ask the Service to give us a raw javax.xml.rpc.Call object that we can configure with all of our service call specifics. Here's our client of the echo service rewritten to use DII to make the service call:

     // The endpoint, service, port, and operation we want to invoke     String endpoint =       "http://localhost:8080/JEnt-examples/services/echo";     String serviceName =    "EchoService";     String portName =       "echo";     String operationName =  "echo";     // Some namespaces we'll be using in our client call     String serviceNS =      "http://localhost:8080/JEnt-examples/services/echo";     String xsdNS =          "http://www.w3.org/2001/XMLSchema";     String jaxrpcNS =       "http://java.sun.com/jax-rpc-ri/internal";     // Create qualified names for the service, port, and operation     QName serviceQName = new QName(serviceNS, serviceName);     QName portQName = new QName(serviceNS, portName);     QName operationQName = new QName(serviceNS, operationName);     try {         // Make a service factory         ServiceFactory factory = ServiceFactory.newInstance( );         // Create a reference to the service         Service service = factory.createService(serviceQName);         // Create a call to a particular port on the service         Call call = service.createCall(portQName);         // Set the endpoint URL for the call         call.setTargetEndpointAddress(endpoint);         // Set the return type of the operation we want to invoke         call.setReturnType(new QName(xsdNS, "string"));         // Set the operation we want to invoke         call.setOperationName(operationQName);         // Set the type of the single parameter to the operation         call.addParameter("message", new QName(xsdNS, "string"),                           ParameterMode.IN);         // Create the actual operation arguments and put them in an         // object array         String msg = "Hi there";         Object[] opParams  = {msg};         // Invoke the operation and collect the result         String resp = (String)call.invoke(opParams);         System.out.println("Sent: \"" + msg +                            "\", got response \"" + resp + "\"");     }     catch (ServiceException se) {         System.err.println("Service generated an error: " + se.getMessage( ));     }     catch (RemoteException re) {         System.err.println("Communication error: " + re.getMessage( ));     } 

We start by declaring a number of string parameters that indicate the names of various namespaces as well as the qualified names of the service, port, and operation we intend to invoke. Then we create a ServiceFactory as before. But, when we create the Service, we simply provide the qualified name of the service, with no WSDL description to go along with it. We're constructing the service call ourselves, and there's no mapping to be done by the web service engine: Service needs to know only its qualified name. We'll supply all the other details previously extracted from the WSDL when we configure the call to the service.

Once we have the Service, we ask it to create a Call for us, providing the qualified name of the port that we want to use. Once the Call is created, we set all of its parametersthe qualified name of the operation we want to invoke, the return type of that operation, and the type of arguments that the operation expects. This is all done using XML namespaces and qualified names within these namespaces to match the configuration of the service. For example, we specify the operation we want to invoke by setting the operation name on the Call to the qualified name of the target operation:

     String serviceName =    "EchoService";     String operationName =  "echo";     . . .     QName operationQName = new QName(serviceNS, operationName);     . . .          call.setOperationName(operationQName); 

We set the return type of the operation in a similar fashion, but since the operation we want to invoke simply returns a string, the qualified name of the return type comes from a standard XML Schema namespace:

     String xsdNS =          "http://www.w3.org/2001/XMLSchema";     . . .          call.setReturnType(new QName(xsdNS, "string")); 

Once the Call is configured, we invoke the target operation by passing our arguments into the invoke( ) method. The arguments are provided in the form of an Object array, which allows for any kind of arguments that any web service operation might require:

     String msg = "Hi there";     Object[] opParams  = {msg};          String resp = (String)call.invoke(opParams); 

The return value of the invoke( ) call is the return value of the web service operation, in the form of an Object, that we then cast to the expected String.



Java Enterprise in a Nutshell
Java Enterprise in a Nutshell (In a Nutshell (OReilly))
ISBN: 0596101422
EAN: 2147483647
Year: 2004
Pages: 269

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net