Implementing the Backend Components
WebLogic permits three choices for a backend component: a simple Java class, a stateless session EJB, or a JMS destination. Remember, a web service is composed of a number of operations, and you can implement each operation using any one of these backend component types. Quite often, the operations of a web service are implemented using the remote methods of a stateless session EJB. In a sense, the web service simply wraps the corresponding EJB interface. Stateless session EJBs allow you to encapsulate well-defined business processes, and web services built around a stateless session EJB can provide SOAP clients with an elegant conduit to this business functionality.
Thus, stateless session EJBs are a good choice for implementing the operations of a web service, especially if it is process-oriented and needs to benefit from other J2EE services such as the support for distributed transactions, persistence, security, and concurrency. A Java class is a suitable backend for a process-oriented web service. Creating a Java class requires less effort than building a stateless session EJB. In general, you should consider using standard Java classes for implementing the operations of a web service if you don't need the support for additional EJB features such as persistence, security, transactions, and concurrency. However, Java classes used as backends for web service operations have certain limitations, which we examine in the next section.
Web service operations can also wrap JMS actions. For example, a client can invoke an operation that triggers the delivery of a message to a JMS destination. A JMS consumer say, an MDB then can process the message. Alternatively, the client can invoke an operation that pulls messages from a JMS queue. In this case, some JMS producer regularly feeds messages into the JMS queue. The client then can poll the web service for a response that wraps the received message. JMS backends are ideal because they allow you to decouple the web service from its actual implementation. Moreover, if the underlying application is built over the asynchronous messaging paradigm, web services allow you to transparently expose your existing JMS destinations to other SOAP clients. BEA, however, discourages the use of JMS-backed web services.
19.3.1 Java Class Backends
A Java class is quite possibly the simplest backend that you can create for a web service. As we have seen already, WebLogic's servicegen Ant task can easily manufacture a deployable EAR file that packages a web service from any Java class that exposes one or more public methods. In this case, each operation of the web service is bound to a public method of the Java class, so when a client invokes the web service operation, it actually is handled by the associated Java method. However, you do need to obey certain rules when creating a Java class that serves as a backend for a web service operation:
- The Java class must define a default, no-argument constructor. WebLogic uses the default constructor to create an instance of the class.
- WebLogic creates a single instance of the Java class that handles all requests to the web service. This means that whenever a client invokes an operation that is backed by a method of a Java class, WebLogic uses the same instance to service the SOAP request. For this reason, you must write thread-safe Java code.
- Any method that is exposed as a web service operation must be declared public.
Most importantly, because the backend Java class runs within WebLogic's J2EE framework, the Java class must never start any threads. Once you create the Java classes, you need to modify the web-services.xml descriptor file and list all the Java components:
The names that you specify for these Java components are important. They are used later when you bind an operation to a method in the component. The following portion from the web-services.xml descriptor file shows how to associate web service operations with Java methods:
Notice how we've used multiple Java classes to implement the different operations of the same web service. One important use of the operation element is that it lets you describe the parameters and return values of a web service operation. The following fragment from the web-services.xml descriptor file describes the signature of a web service operation that is implemented via the method String methodX(java.lang.String):
Use the params element to explicitly specify the list of parameters and return values for the operation. If you omit the params element, WebLogic introspects the target method to automatically determine how the web service operation ought to be invoked. Each parameter is specified using the param element, while the return value is specified using the return-param element. A single web service operation may declare, at most, one return value. For each parameter (or return value), you can specify the name, the associated datatype, and whether the parameter is located in the "header" or the "body" of the SOAP message. By default, parameters and return values are located in the body of the SOAP message. For each parameter, you can use the style attribute to specify the direction of flow: in, out, or in-out. The following XML fragment shows how to specify an input-output parameter of integer type located in the body of the SOAP message:
Later in this chapter, we examine how the operation element lets you associate a chain of SOAP handlers with a web service operation.
Note that you don't need to explicitly declare each operation of the web service. If the web service is implemented by a stateless session EJB or a standard Java class, WebLogic can automatically introspect the backend component and construct a list of operations using all of the public methods in the backend component. The following portion of the web-services.xml descriptor file shows how you can expose all the public methods of ClassA as web service operations:
19.3.2 Stateless Session EJB Backends
Stateless session EJBs provide another alternative for implementing web services. They allow you to build web services over existing business operations encapsulated within a J2EE application. Furthermore, stateless session EJBs can benefit from all the standard J2EE services such as support for object pooling, persistence, transactions, security, and concurrency. It also is neater from an architectural viewpoint because the web service is designed based on an EJB interface rather than being based on the EJB's implementation class.
Assume that you've generated an EJB JAR that packages one or more stateless session EJBs, and the EJB JAR has been assembled into a J2EE enterprise application. This means that all stateless session EJBs are available to any deployed web applications as well. Once again, you need to first modify the web-services.xml descriptor file for a web application and make these stateless session EJBs available to your web services. The following portion from the web-services.xml descriptor shows how to use the stateless-ejb element to specify a stateless session EJB that will be used to implement the operations of the web service:
Here we used the ejb-link element to refer to the stateless session EJBs FooEJB and FooBarEJB, both bundled within the myEJBServices.jar EJB JAR file. You also could use the jndi-name element to refer to a particular EJB through its JNDI name:
In this case, the EJB deployment descriptor weblogic-ejb-jar.xml in myEJBServices.jar must assign the JNDI names for all EJBs packaged within the EJB JAR:
FooEJB myEJBs.FooEJB FooBarEJB myEJBs.FooBarEJB
Refer to Chapter 10 for more information on how to implement stateless session EJBs. Now that you've declared the stateless session EJBs, you can map the web service operations to the desired EJB methods:
Once again, if the web service operation is asynchronous, the corresponding EJB method must be declared to return void. Similarly, if the EJB method uses custom data types for the parameters and return values, you need to generate the serialization classes that convert these datatypes between their XML and Java representations.
You also can use the servicegen Ant task to create a web service that blindly exposes all EJB methods of a stateless session EJB. The following build script shows how to invoke the servicegen task to generate this web service:
ejbJar="build_dir/myEJBServices.jar" targetNamespace="${namespace}" serviceName="FooService" serviceURI="/FooService" generateTypes="True" expandMethods="True" />
The expandMethods="True" attribute indicates whether servicegen should create a separate operation element for each EJB method in the web-services.xml descriptor file, or simply refer to all EJB methods using the generic method="*"/>. The attribute generateTypes="True" instructs servicegen to also generate the serialization classes, the schema definitions, and the type mappings for any custom datatypes used as parameters or return values of the EJB's methods.
19.3.3 JMS Backends
Two kinds of JMS actions can be triggered when a client invokes a web service operation: the operation may send data to a JMS destination, or it may receive data from a JMS queue. If the web service defines an operation that sends data to a JMS destination, you also need some JMS consumer to process any messages delivered to the JMS destination. So, when a web service receives a SOAP request from a client, the parameters encapsulated in the request are sent to the target JMS queue or topic. Any JMS consumers listening on the JMS destination will receive the message, extract the incoming parameters from the message body, and process it accordingly. If the web service defines an operation that receives data from a JMS queue, you also need some JMS producer to feed messages to the queue. A web service client must then poll the web service at regular intervals for a SOAP response in order to receive the messages placed on the queue.
|
In the case of an operation supported by a JMS backend, the only refinement to the event sequence illustrated in Figure 19-1 is how the operation is handled. When the client invokes a JMS-implemented operation, WebLogic converts the incoming XML data to its Java representation using the appropriate serializer classes. It then wraps the resulting Java object within a javax.jms.ObjectMessage instance and places this object on the JMS destination. Thus, any JMS listener bound to the JMS destination needs to process the ObjectMessage instances that are delivered to the JMS topic or queue. A similar process occurs in reverse when a client invokes a web service operation that receives data from a JMS queue.
Note that although WebLogic's JMS backends provide an easy way to interface with a JMS queue, the resulting code is proprietary and won't be portable. Writing your own interface to a JMS queue using a stateless session bean or Java class isn't that much more difficult, and the result will be more portable and flexible.
19.3.3.1 Designing a JMS backend
If the operation sends data to a JMS destination, you need to decide whether you want to use a JMS topic or a queue. A JMS queue provides support for point-to-point messaging, whereby the message is delivered to exactly one JMS consumer. A JMS topic provides support for publish/subscribe messaging, whereby the message is delivered to a number of recipients. Once you've decided on the type of JMS destination, you need to implement the listener that retrieves messages from the JMS destination and processes them. For instance, you could implement an MDB that processes all the messages delivered to the JMS destination. The MDB could even delegate some of the handling to other EJBs. The operation completes its handling once the backend listener processes the message delivered to the JMS destination.
Furthermore, you need to decide whether the operation sends data to a JMS destination or receives data from a JMS queue. The same operation cannot do both simultaneously. If you want the client to be able to both send and receive data, you need to define two web service operations:
- A send operation that is associated with a JMS destination to which a JMS listener is attached (say, an MDB). Chapter 8 examines the various ways to implement a listener for a JMS destination.
- A receive operation that is associated with a JMS queue. After the JMS listener retrieves and processes the incoming message, it delivers a response message to this JMS queue.
So, for instance, you could implement an MDB that listens to a JMS topic (associated with the send operation) for an incoming message, process it, and then place a response on the JMS queue (associated with the receive operation). Thus, in order to simulate typical request-response behavior, you need to set up two JMS destinations, one for receiving request messages from the client and another JMS queue for delivering response messages for the client. And, in order to support any web service operation that is implemented by a JMS backend, you need to complete the following tasks:
- Create the JMS consumers (or producers) that retrieve (or deliver) messages to the JMS destination.
- Configure the JMS destinations that the web service will use to receive data from a client or send back data to the client.
- Configure the JMS connection factory that is used by the web service to connect to WebLogic's JMS server.
Chapter 8 provides more information on how to use the Administration Console to set up various JMS resources for WebLogic.
19.3.3.2 Building a web service over a JMS backend
Once again, the servicegen Ant task lets you automatically create a deployable EAR that packages a web service that interacts with a JMS destination. It also can be used to generate the necessary support for a client JAR that lets you statically invoke the web service. The following portion of a build script shows how to assemble a web service that sends XML data to a JMS queue:
JMSAction="send" JMSDestination="InQueue" JMSDestinationType="queue" JMSConnectionFactory="oreilly.myConnectionFactory" JMSOperationName="makeUpper" targetNamespace="${namespace}" serviceName="SimpleJMS" serviceURI="/SimpleJMS" expandMethods="True">
Here, we've instructed the servicegen task to create a web service that exposes a single operation, makeUpper, that sends data to a JMS queue. Because the web service wraps a JMS backend, you need to supply values for the following JMS-specific attributes of the service element:
JMSAction
This attribute indicates whether the web service sends data to the JMS destination or receives data from the JMS destination.
JMSDestination
This attribute specifies the JNDI name of the JMS destination that sits behind the web service.
JMSDestinationType
This attribute indicates whether the JMS destination is a queue or a topic.
JMSConnectionFactory
This attribute specifies the JNDI name of the connection factory used to connect to the JMS destination.
JMSOperationName
This attribute specifies the name of the web service operation in the generated WSDL file. If no value is specified, the name of the operation defaults to the value of the JMSAction attribute.
Another attribute that we didn't use in the preceding example is the JMSMessageType attribute, which specifies the fully qualified name of the Java class that defines the type for the single parameter to the send (or receive) operation. By default, the web service sends or receives JMS messages that wrap a java.lang.String parameter. If you have specified a nonbuilt-in data type for the web service, then you also should include the generateTypes="True" attribute when you invoke the servicegen task.
The Ant task must be run from within a staging directory that contains a JAR file that bundles together the compiled Java classes for any JMS producers and consumers. This JAR file should be placed directly under this staging directory, along with other EJB JARs that package any MDBs. The generated EAR file bundles together all of these JAR files, and a WAR file that also includes the appropriate web-services.xml descriptor file. Remember, the client subelement is optional and should be used only when you also need to generate the client JAR during the assembly of the web service. Of course, you also can use the clientgen task to generate the client JAR file.
|
The following portion from an Ant script shows how you can generate a web service that receives data from a JMS queue:
JMSAction="receive" JMSDestination="OutQueue" JMSDestinationType="queue" JMSConnectionFactory="oreilly.myConnectionFactory" JMSOperationName="getMakeUpperResult" targetNamespace="${namespace}" serviceName="SimpleJMSResult" serviceURI="/SimpleJMSResult" expandMethods="True">
Given these two web services, you now can simulate a call to the makeUpper( ) method using the two JMS queues. Example 19-7 lists the code for an MDB that retrieves JMS messages from InQueue, processes the incoming parameter, and then sends back the result in a JMS message to OutQueue.
Example 19-7. Using an MDB to implement the makeUpper( ) operation
// Assume the MDB is listening for messages delivered to the JMS queue "InQueue" // and uses the JMS connection factory bound to oreilly.myConnectionFactory public void onMessage(Message message) { ObjectMessage o = (ObjectMessage) message; if (o != null) { String inparam = (String) o.getObject( ); String result = inparam.toUpperCase( ); //send message response to the "OutQueue" QueueConnectionFactory factory = (QueueConnectionFactory) ctx.lookup("oreilly.myConnectionFactory"); Queue queue = (Queue) ctx.lookup("OutQueue"); QueueConnection qconn = factory.getQueueConnection( ); QueueSession qs = wconn.createQueueSession(false, Session.AUTO_ACKNOWLEDGE); QueueSender qsender = qs.createSender(queue); Message response = qsession.createObjectMessage(new String(result)); qconn.start( ); qsender.send(response); qsender.close( ); qs.close( ); qconn.close( ); } }
Once you've deployed the web services, you can use the generated client JAR to invoke the web services:
import com.oreilly.wlguide.webservices.simple.SimpleClient.SimpleJMS; import com.oreilly.wlguide.webservices.simple.SimpleClient.SimpleJMSPort; import com.oreilly.wlguide.webservices.simple.SimpleClient.SimpleJMS_Impl; //Send JMS message to "InQueue" SimpleJMS wsj = new SimpleJMS_Impl(args[0]); SimpleJMSPort p = wsj.getSimpleJMSPort( ); p.makeUpper("Hello World"); SimpleJMSResult wsj2 = new SimpleJMSResult_Impl(args[0]); SimpleJMSResultPort p2 = wsj2.getSimpleJMSResultPort( ); //Keep polling "OutQueue" for a response while (!quit) { String result = (String) p2.getMakeUpperResult( ); if (result != null) { System.out.println("TextMessage:" + result); if (result.equals("")) { quit = true; System.out.println("Done!"); } continue; } try { Thread.sleep(1000); } catch (Exception ignore) {} }
Remember, web services implemented by a JMS backend either accept a single parameter or return a single value. If the web service needs to support a custom datatype, then you should use the JMSMessageType="com.foo.bar.myType" attribute of the service element when invoking the servicegen Ant task. In this case, you must ensure that the Java type is available to the classpath before invoking the Ant task.
19.3.3.3 Configuring the web-services.xml descriptor
Let's examine the changes you need to make to the web-services.xml descriptor file in order to describe a web service implemented by a JMS backend. Once again, you need to use the components element to describe the JMS destinations that live behind the web service:
Here, the jms-send-destination element defines the JMS destination (InQueue) to which a send operation can deliver its messages, whereas the jms-receive-queue element defines the JMS queue (OutQueue) from which a receive operation can retrieve the response messages. In both cases, you need to specify the fully qualified JNDI names of the JMS destination, and the JMS connection factory used by the web service to connect to the destination. Once you've set up the JMS backends in the web-services.xml descriptor file, you can define the web service operations in terms of these backend components:
component="JMSSend" name="makeUpper" invocation-style="one-way"> component="JMSReceive" name="getMakeUpperResult" invocation-style="request-response">
Notice how the send operation declares a single inbound parameter, while the receive operation defines a single return value. This is an essential requirement of JMS-implemented web service operations the operation must define either a single inbound parameter or return value. The send operation is marked as asynchronous because the client doesn't expect a response when it invokes the send operation. On the other hand, the receive operation must be invoked in the typical request-response fashion.
19.3.4 Raising a SOAPFaultException
Web services can propagate exceptions back to the client. Any stateless session EJB or Java class used to implement a web service operation should throw a javax.xml.rpc.soap.SOAPFaultException exception to indicate an error during processing. WebLogic then serializes the exception to the appropriate XML structure and generates a SOAP response that wraps this SOAP fault before sending the message back to the client. Remember, asynchronous operations cannot propagate SOAP faults back to the client because no response is ever returned to the client. If the backend component throws any other type of Java exception, then WebLogic still tries its best to map the exception to a SOAP fault. However, in order to ensure that the client receives accurate information on any exceptions raised by the web service, the backend should explicitly throw a SOAPFaultException.