6.4 JAX-RPC and J2EE 1.4 Application Clients

   

6.4 JAX-RPC and J2EE 1.4 Application Clients

So far, you have seen two different ways for a client to get a reference to the Service object that it needs before it can invoke the methods of a web service:

  • Direct instantiation of the Service implementation class. This is the technique used in Chapter 2. Although this works, it requires the application to know the name of the generated Service class, which makes it dependent on a particular JAX-RPC implementation.

  • Using a ServiceFactory to create a Service object, as shown earlier in this chapter. While this frees your code from dependency on the JAX-RPC implementation, the object you get back implements only the Service interface, not the actual interface defined by the web service (such as BookService ). Therefore, it doesn't have methods such as getBookQueryPort( ) that directly return references to the service endpoint interface.

If you are writing a J2SE application client, these are the only choices available to you. However, J2EE 1.4 allows container-resident clients to retrieve references to Service objects defined in their JNDI environment. Furthermore, these Service objects can be instances of generated classes such as BookService . By using this facility, you can write code that is vendor-independent (in the sense that it does not rely on the actual name of the generated Service class), while still having the convenience of using methods such as getBookQueryPort( ) . This section shows how to make use of this feature by demonstrating how to create a J2EE application client that works with the book web service developed in Chapter 2.

Unlike J2SE clients, J2EE application clients run inside a client container provided by the vendor of the application server in which the service is deployed. In order to build a J2EE application client, you need to package it into a JAR file, use the application server's deployment tools to deploy it to the target server, and finally run it under the control of the client container. Although very little in this process depends on whether you are writing a client for a web service, for the sake of completeness, I'll show you everything that is necessary to create and deploy a web service client for the J2EE 1.4 reference implementation. If you are using a different application server, the details of the deployment might change but the same application code should work, since the APIs and even some of the deployment descriptors are part of the J2EE 1.4 platform specification.

6.4.1 Adding a Web Service Reference to the JNDI Environment

The application client that you are going to see in this section uses the book web service developed in Chapter 2. J2EE 1.4 allows a container-based client to get a reference to the Service object for a deployed service from an entry in its JNDI environment. You create such an entry for an application client by including a file called webservicesclient.xml in the META-INF directory of its JAR file (which will be referred to here as the client JAR file ). Example 6-9 shows the content of this file for a client that needs to access the book web service.

Example 6-9. A webservicesclient.xml file for a client of the book web service
 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE webservicesclient PUBLIC 
  "-//IBM Corporation, Inc.//DTD J2EE Web services client 1.0//EN" 
  "http://www.ibm.com/standards/xml/webservices/j2ee/j2ee_web_services_client_1_
0.dtd">
<webservicesclient>
    <service-ref>
      <description>Book Service Reference</description>
      <service-ref-name>service/BookService</service-ref-name>
      <service-interface>ora.jwsnut.chapter2.bookservice.BookService
      </service-interface>
      <wsdl-file>BookService.wsdl</wsdl-file>
      <jaxrpc-mapping-file>META-INF/model</jaxrpc-mapping-file>
    </service-ref>
</webservicesclient> 

The service-ref element defines a reference to a web service that is deployed somewhere on an application server. The meanings of the elements nested inside service-ref are as follows :

service-ref-name

The service-ref-name child element determines where the reference to the service appears in the JNDI environment, relative to java:comp/env . In this case, the reference appears at java:comp/env/service/BookService , a name that was chosen to be consistent with the recommendation in the J2EE specification that all web service references should appear under java:comp/env/service .

service-interface

The fully qualified name of the generated Service class for the book web service. It is also possible to use the value javax.xml.rpc.Service here, in which case the reference bound in the JNDI environment is an instance of Service rather than BookService , and the application code needs to use one of the getPort( ) methods rather than getBookQueryPort( ) to gain access to the service endpoint.

wsdl-file

The WSDL definition for the service. The value of this element gives the location of the WSDL file relative to the root of the JAR file that contains the client application.

jaxrpc-mapping-file

The J2EE 1.4 mapping file that describes how to map from the WSDL definition to the corresponding Java service endpoint interface. This path is also relative to the root of the JAR file containing the application. As noted in Chapter 2, the reference implementation of J2EE 1.4 allows you to use a wscompile model file instead of a mapping file.

The information in this file is processed by the deployment tools, and results in the generation of a class that implements the Service interface, together with the client-side stubs that the application will need to call the service itself. Unlike previous examples, code generation takes place when the client is deployed, rather than as part of the process of writing the application itself.

6.4.2 Writing the J2EE Application Client

As far as using the service endpoint interface of the book web service is concerned , it does not matter whether you implement the client as a J2SE application, a J2EE client, or as part of an EJB or a servlet ” you still invoke the same methods with the same arguments. The difference lies in the way that container-resident clients obtain a Service object. The J2SE client in Chapter 2 was obliged to know the name of the Service implementation class:

  BookService_Impl service = new BookService_Impl(  );  BookQuery bookQuery = (BookQuery)service.getBookQueryPort(  );
((Stub)bookQuery)._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, args[0]); 

A container-resident client that includes a service-ref element in its webservicesclient.xml file can, instead, obtain a Service object from its JNDI environment. Here's how such a client would access the Service object declared in Example 6-9:

 InitialContext ctx = new InitialContext(  );
BookService service = (BookService)PortableRemoteObject.narrow(
    ctx.lookup("java:comp/env/service/BookService"),
    BookService.class);
BookQuery bookQuery = (BookQuery)service.getBookQueryPort(  );
((Stub)bookQuery)._setProperty(Stub.ENDPOINT_ADDRESS_PROPERTY, args[0]); 

Notice that this code deals only with the implementation-independent BookService and BookQuery interfaces; it is up to the deployment tools to provide suitable implemention classes and make them available to the application at runtime. Not only does this lack of dependence on concrete implementation classes make the code more portable, it also removes the need for these classes to be generated while the application is being developed. In fact, the only information that the developer needs about the service is contained in its WSDL definition, from which the only classes needed to create the application (i.e., the service endpoint interface and the other classes that it uses) can be generated by using the -import option of the wscompile utility:

 wscompile -import -f:norpcstructures  -d output/interface config.xml 

The -import option requires a config.xml file that specifies the location of the WSDL definition. Used on its own, this option results in the generation of the service endpoint interface and additional classes that know how to build the SOAP requests for the interface methods. Since we don't want the SOAP message creation classes, we use the -f:norpcstructures option, so that only the implementation-independent service endpoint interface classes ( BookQuery , BookInfo , and BookServiceException) are generated.

6.4.3 Packaging and Deploying the Application Client

Unlike a J2SE client, a J2EE application client needs to be packaged and deployed to an application server. The purpose of the deployment is not to make the client available for remote access ” rather, it is to give the deployment tools the opportunity to create the appropriate server-dependent client-side stubs as well as other information that will be needed to run the application from a client machine.

To deploy the application, you need to create a client JAR file that contains the files shown in Table 6-4.

Table 6-4. Files required for the book web service application client JAR file

File type

Filename

Service endpoint interface

ora.jwsnut.chapter2.bookservice.BookQuery

 

ora.jwsnut.chapter2.bookservice.BookInfo

 

ora.jwsnut.chapter2.bookservice.BookServiceException

Service interface

ora.jwsnut.chapter2.bookservice.BookService

Application implementation

ora.jwsnut.chapter6.client.BookServiceAppClient

WSDL file

BookService.wsdl

Deployment descriptors

META-INF/ application-client .xml

 

META-INF/mapping.xml or META-INF/model

 

META-INF/webservicesclient.xml

Manifest file

META-INF/MANIFEST.MF

The client JAR file is actually used in two ways:

  • For deployment to the server in order to create the stubs

  • As the source for the class files for the application client at runtime

The WSDL file and the deployment descriptors are required at deployment time, whereas the class files and the manifest file are needed when the application is started on the client system. You've already seen the webservicesclient.xml file and the mapping.xml file (or the wscompile model file, which may be substituted for it in the case of the J2EE 1.4 reference implementation) as well as what they contain. These files are specific to the J2EE 1.4 web service implementation. The other deployment file, application-client.xml , is a generic file that is used to describe a J2EE application client and does not contain anything related to web services.

Having created the client JAR file, you can either deploy it directly or wrap it in an EAR file and deploy that instead. Whichever choice you make, you need to arrange for the file containing the generated client stubs to be returned and stored on the client system so that they can be used when the application is executed. You can build and deploy the client JAR file for this example by making chapter6\appclient your working directory and typing the command:

 ant appclient-deploy 

Having built the client JAR file (called appclient.jar ) and packaged it inside an Enterprise Archive file (called appclient.ear ), the appclient-deploy target of the Ant buildfile deploys it to the server using the following command:

 deploytool -deployModule -id BooksAppClient appclient.ear stubs.jar 

Following successful deployment, the generated stubs are written to a file called stubs.jar . The JAR also contains a file called sun-j2ee-ri.xml that contains information generated by the deployment tools intended for the application container within which the client is executed. The content of this file, which is specific to the J2EE 1.4 reference implementation, is shown in Example 6-10. [3]

[3] Actually, deployment of the application client is only one way to generate the required stubs. Since the JAR file for an application client can be included in the same EAR as the WAR file or EJB JAR file containing a web service implementation, it is often convenient to use deploytool as shown here to create the stubs when the service itself is deployed. If you want to keep the application separate, however, you could choose to use the j2eec command instead, which can also generate the stubs and the sun-j2ee-ri.xml file, and does not require access to the target application server. Of course, like deploytool , j2eec works only with the J2EE reference implementation. To create stubs for a third-party application server, you need to use the vendor's equivalent of j2eec . The j2eec command is described in Chapter 8.

Example 6-10. The sun-j2ee-ri.xml file generated for the book web service application client
 <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE j2ee-ri-specific-information
     PUBLIC "-//Sun Microsystems Inc.//DTD J2EE Reference Implementation 1.4//EN"
     "http://localhost:8000/sun-j2ee-ri_1_4.dtd">
<j2ee-ri-specific-information>
  <rolemapping/>
  <app-client>
    <module-name>appclient.jar</module-name>
    <service-ref>  <service-ref-name>service/BookService</service-ref-name>
      <service-impl-class>ora.jwsnut.chapter2.bookservice.BookService_Impl
      </service-impl-class>  <service-qname>
        <namespaceURI>urn:jwsnut.chapter2.bookservice/wsdl/BookQuery
        </namespaceURI>
        <localpart>BookService</localpart>
      </service-qname>
    </service-ref>
  </app-client>
</j2ee-ri-specific-information> 

The most interesting part of this file is highlighted in bold; these two lines instruct the application container to bind an instance of the class ora.jwsnut.chapter2.bookservice.BookService_Impl into the application's JNDI environment at java:comp/env/service/BookService . The JNDI location was obtained by the deployment tools from the service-ref element in the webservicesclient.xml file shown in Example 6-9. The class name, however, was not specified in that file ” it is dependent on the JAX-RPC implementation and corresponds to one of the generated artifacts in the stubs.jar file. In fact, the stubs.jar file contains compiled versions of all of the implementation-dependent files listed in Table 6-1. The difference between this case and the discussion earlier in this chapter is that these classes were compiled and generated by the deployment tools rather than as part of the development of the client-side application itself.

6.4.4 Running the Application Client

Although creating a J2EE application client has the benefit of removing vendor-specific dependencies from the code, there are two important points that are worth bearing in mind:

  • This technique works only when the application can be deployed into a J2EE application server so that the appropriate stubs can be generated. This is not as limiting as it may appear, however, as it doesn't necessarily mean that you use only the resulting combination of client and stubs to talk to the container in which the application was deployed. On the contrary, since the stubs simply generate SOAP messages, it should be possible to use them to connect to any implementation of the same web service, whether it is hosted by a J2EE-based application server or in a .NET environment.

  • You can't just run the application client using a simple java command. Instead, you need to run it under the control of an application client container. Furthermore, because the stubs file contains information that is currently not part of the J2EE platform specification, you will almost certainly have to use the client container provided by your application server vendor.

You can run the application client for the book web service by typing the command:

 ant appclient-run 

This command uses the J2EE command-line utility runclient , which invokes the reference implementation's client container. Here is the actual command that gets executed by this Ant buildfile target:

 runclient -client appclient.jar  -stubs stubs.jar 
    http://localhost:8000/Books/BookQuery 

The appclient.jar file is, of course, the client JAR file containing only the vendor-independent application implementation (the content of which was listed in Table 6-4), while stubs.jar contains all of the files that depend on the target application server. By using these two JAR files together, it should be possible to supply the address of any implementation of the book web service and successfully interwork with it, provided that the underlying JAX-RPC implementation is capable of interworking with the server that it is directed to connect to. In case you were wondering how the runclient utility knows which class in the client JAR contains the main( ) method of the application itself, this information is provided by a Main-Class entry in the MANIFEST.MF file of the client JAR:

 Main-Class: ora.jwsnut.chapter6.client.BooksAppClient 

Although in this example the client JAR was deployed to the application server in order for the stubs to be generated, the deployed module is not used further. In fact, you can undeploy it from the application server and still run the application. To prove this, use the following commands:

 ant appclient-undeploy
ant appclient-run 

You'll see that the application continues to work even after being "undeployed."

6.4.5 Web Service References for Servlets and EJBs

It is worth noting that J2EE 1.4 allows both EJBs and servlets to act as clients of web services that may reside in the same application server or, more likely, at a remote location. This facility makes it possible for a server-side component to satisfy some or all of a client request by delegating it to an external web service, without the client needing to be aware that this is the case.

The deployment information for EJBs and web applications that need to communicate with web services must include a service-ref element that describes the service endpoint interface and points to its WSDL definition. This element appears in the web.xml file in the case of a servlet or in the ejb-jar.xm l file for an EJB. As with the service-ref element in the webservicesclient.xml file (which is not used for servlets or EJBs), this element results in an appropriate Service object being bound into the component's JNDI environment at runtime. A servlet or EJB uses the same techniques as the application client just shown to access the Service object and invoke web service methods.