Adding Web Services to bigrez.com


Adding Web Services to bigrez.com

We now have all of the knowledge we need to design the Web Service interface to bigrez.com . Because the Web Service interface is intended to support global reservation systems, we need to provide two Web Services: one to find a set of available hotel rooms matching the search criteria and one to book a reservation.

Our first design decision is whether these services should be synchronous or asynchronous. In general, we believe that application-to-application integration Web Services should be asynchronous to decouple the availability and scalability of the calling application from the Web Service. For booking reservations , this makes perfect sense. For querying our reservation system for matching hotels when a travel agent or user is waiting for an immediate response so that they can choose a hotel and book a reservation, we have chosen to make this Web Service synchronous to improve the response time that the user experiences. Let s look at the query service first.

Best Practice  

Use asynchronous Web Services for application-to-application integration. Synchronous Web Services are good for Web Services interacting with humans .

The next design decision we need to make is determining what operations are necessary to support the query service. Is one operation that takes the search criteria and returns all of the information about the hotel enough, or do we want to break this into two operations, one to return the list of matching hotels with limited information and one to get detailed information about a specific hotel? To answer this question, we need to look at how we expect the system to be used. Based on what our global reservation systems partners have told us, we expect that most people booking rooms will want to choose the hotel based on location and then check for availability and view more detailed information about a particular hotel. Therefore, to save the overhead associated with returning all of the availability and detailed information for each matching hotel, we will return only the minimum amount of information needed to identify a particular hotel in the first operation. We will include a second operation that returns availability and other detailed information for a particular hotel. These types of decisions can be key to building scalable Web Services.

Best Practice  

Proper Web Service design requires understanding how the services and the data that they return will be used. In general, Web Services should be coarse-grained, but care must be taken to avoid returning more information than is required by most clients .

For our asynchronous reservation Web Service, we need to determine what facility we are going to provide for the client to correlate the responses with the actual requests . Because, at the time of writing, there is no Web Services standard for accomplishing this, we need to create our own, keeping in mind that we do not want to compromise interoperability. Therefore, we want to include the correlation IDs as part of the main SOAP messages.

We could place the burden on the client for providing a correlation ID, but this would require us to augment that correlation ID with information about the client to prevent the possibility of multiple clients providing the same ID. Therefore, we take the responsibility of generating the correlation IDs. For this to work, we need to return the correlation ID to the client as the response to the call to book the reservation. This means that, in order for the reservation processing itself to be asynchronous but the returning of the correlation ID to be synchronous, we use a synchronous Web Service that generates a correlation ID, places the reservation request and correlation ID into a JMS message, writes the JMS message to a queue, and returns the correlation ID to the client. At that point, a message-driven bean picks up the reservation request, places the reservation, and creates a confirmation message for the client.

Once the reservation is made, we need to return the confirmation message to the client. There are multiple ways to accomplish this, some of which are the following:

Client polling.    Having the client poll for the request makes our lives as programmers easier. We simply dump the responses onto a JMS queue or into a database and provide another synchronous Web Service that retrieves the response. We place the burden on the client for asking for the response. The problem is that this puts undue load on the server if the clients are frequently asking for a response before it is available. When the system is experiencing heavy load, the problem only gets worse because the system may have to process multiple poll requests before a particular response is ready.

Client-side callback Web Service.     Having the client provide a Web service that we call to return the response when it is available reduces the load that a polling solution places on the server. This solution is much more scalable but requires that the client be capable of hosting a Web Service. While requiring end-user client applications to have a Web Service for callback purposes may not be reasonable, certainly large applications like a global reservation system will be able to provide such a facility.

We must make sure that our business logic for booking the reservation is decoupled from the process for returning the responses. This is critical because we cannot allow our reservation transaction processing to be slowed down, or caused to fail, by the availability or response time of callback Web Services we invoke to return the reservation confirmation message. Therefore, our reservation transaction writes the confirmation message to a JMS queue. We use another MDB to dequeue the message and invoke the callback Web Service to deliver the confirmation message.

Best Practice  

Whenever possible, return asynchronous responses to callers via a callback Web Service provided by the caller. When using asynchronous processing, always decouple the sending of the response from the core business transaction.

Now that we have settled on the high-level interactions, let s drill down to the next level and define the interfaces. We want to ensure that our interfaces have the right level of abstraction and are sufficiently decoupled from the implementation that even if we were to change the implementation technology, the interfaces would remain unchanged. As a result, let s start by defining the messages that are to be exchanged and any custom data types we need to represent those messages. One way to do that is to start with WSDL, which is how we chose to start our example. You could also accomplish the same thing by starting with a Java interface and data types. Regardless of how you choose to start, the important point is to make sure that you do not expose your business logic to the data structures used by the Web Service and vice versa. While not following this recommendation can simplify the job of providing a Web Service interface to your business logic, it effectively couples your business logic to the Web Service interface. Any changes to the Web Service interface require touching all of the business logic that uses the affected data structure. In our example, we take this one step further and decouple the WSDL operation names from the business methods we invoke.

Best Practice  

Resist the temptation to expose business methods and data types to your Web Service interface directly. Failure to do so can make it difficult and costly to evolve Web Service interfaces as the business requirements for your application change.

In our example, we used wsdl2service to generate a Java interface that the Web Service endpoint needs to implement. At this point, we need to decide what back-end component type to use to implement our Web Service. Using a regular Java object is attractive because wsdl2service generates a normal Java interface, as opposed to one appropriate for use by a stateless session bean. We also need the transactional semantics of an EJB to manipulate the entity bean references and relationships that our business methods return efficiently . Using a stateless session bean directly as our Web Service would require us to make manual modifications to the web-services .xml deployment descriptor generated by wsdl2service . Therefore, we decided to use a regular Java object that delegates interactions with the business components to a stateless session bean. While this adds an extra layer to the architecture, the Java class that implements the endpoint is so lightweight that it doesn t add any measurable amount of overhead. In addition, it simplifies the development and maintenance of our application because it allows us to use the WebLogic Server Ant tasks to automate Java type and deployment descriptor generation completely. Figure 15.5 shows the high-level architecture of our design.

click to expand
Figure 15.5:  High-level bigrez.com Web Service architecture.

Our BigRezWebServiceImpl class provides the three methods for finding properties, checking availability, and booking a reservation that our Web Service exposes and delegates these calls to the BigRezWebServiceSessionBean EJB. For the first two methods, our EJB simply calls directly to the back-end business logic EJBs and returns a response based on the business-logic response. In the bookReservation() method, our EJB creates a JMS ObjectMessage and places it onto the GRS_RequestQueue . From there, our JMSReservationRequestBean MDB picks up the request, invokes the back-end business logic, and writes the confirmation response to the GRSResponseQueue ”all as part of a JTA transaction. The fact that this whole process is part of a single transaction makes the application vulnerable to the poison message problem we discussed in Chapter 9.

Let s take a moment to make sure that we understand the problem before we describe the way we chose to address it. Because our Web Service is accepting messages that we are simply packaging up and placing in a queue, it is possible that the data provided may prevent the business logic from successfully booking the reservation. These failures can happen for multiple reasons but generally can be separated into two categories.

First, system-level failures, such as the database going down, prevent messages from being processed successfully. We call these system exceptions, and the normal way to deal with system exceptions is to re-enqueue the message by rolling back the transaction so that JMS redelivers the message later in hopes that the temporary system problems will be resolved in a timely manner. WebLogic JMS also allows us to specify an error queue to which the messages can be routed should redelivery continue to fail. This mechanism works well and meets all of our requirements for handling system exceptions.

Second, the processing of some messages may fail because of business rules, such as invalid credit card information or no availability for the chosen dates. We call these business exceptions, and the normal way to deal with business exceptions is to send an error message back to the caller to inform it that the request failed. A problem occurs, though, when the business logic requires the transaction to be rolled back, as would be the case when a partially completed reservation fails during credit card validation after the transaction has modified some data. Because the transaction is usually marked for rollback before the business logic returns control to the controller (that is, JMSReservationRequestBean in our example), it is not possible for us to write the error message to the response queue as a part of the transaction. We must allow the transaction to roll back.

If we allow the transaction to roll back, the message containing the bad request is placed back on the input queue for redelivery. If we allow this, we place additional processing load on the system, plus we end up having both system and business exceptions on our error queue. Additionally, the client receives no response to its request while all of this redelivery and error-queue processing is taking place, causing the client to resend the bad request, thereby making the problem worse. How can we deal with this?

There are multiple ways to deal with this problem ”some of them create other problems, but others require more work for the application programmer. We will consider only those approaches that do not create more problems. We considered two possibilities:

  • Reordering the business logic to try to catch all business problems before updates are made

  • Tracking the message IDs that failed for business reasons and writing the error response in a separate transaction

The problem with the first approach is that it is hard, if not impossible to achieve. Object orientation and encapsulation only make the problem worse because you would now need every component programmer to understand the global rules for making updates. Even if you could achieve this and do all of the checks up front, it is still possible for business errors to be encountered during the update phase of the transaction. Entity beans and caching only make this problem worse because the actual database updates may be delayed until the transaction commit phase, after the business logic has returned control to the container that started the transaction. Therefore, we have chosen the second approach.

To make the second approach work, we need to distinguish between system and business exceptions. Our business logic differentiates between these two types of errors by the exceptions it throws. We could have used any scheme that allowed us to differentiate between the two types of exceptions. We chose to throw a EJBException for system-level errors and a BigRezBusinessException , or a more specific subclass, for business-level errors. As you know, EJBException is a subclass of RuntimeException. The container automatically marks any active transaction for rollback when it encounters a RuntimeException . Our BigRezBusinessException is not a RuntimeException , so the container will not automatically mark the transaction for rollback; therefore, we need to do it explicitly in the application code.

For system exceptions, we allow the transaction to roll back so that JMS will redeliver the message later ”after the transient problem that caused the exception is resolved, we hope. System problems that last for an extended period of time will cause messages to be delivered multiple times until the redelivery limit is reached. These messages are moved to the error queue. Because the normal mechanism to process messages from the error queue is to move them back to the input queue, you need to make sure that the component that normally does this stops when system-level problems occur and restarts when the problems are resolved.

For business exceptions, we need to add some logic for delivering the error messages to the response queue and for purging request messages for which we have already generated error messages from the system. To generate the error message, we use our controller MDB, the JMSReservationRequestBean , to catch any BigRezBusinessException errors and generate the appropriate response message to send back to the caller. To get the message to the caller, we must write the message to the GRSResponseQueue in a separate transaction from the one used to dequeue the request. The easiest way to accomplish this is to create a JMSErrorResponseTrackingSessionBean that sets its transaction attributes to RequiresNew so that the calls to it are invoked in a separate transaction. By calling this JMSErrorResponseTrackingSessionBean , we write the error response to the GRSResponseQueue without having it removed when the original transaction rolls back.

Additionally, we need a mechanism to purge the original message from the system. Because the current transaction is going to roll back, the original message will be redelivered by JMS after the appropriate delay. We need to detect this redelivery and purge the message from the system rather than trying to process it again. To accomplish this, we maintain a history of messages that have already had their error responses written to the GRSResponseQueue but have yet to be removed from the GRSRequestQueue .

We use the JMSErrorResponseTrackingSessionBean to perform this tracking activity and help solve the redelivery problem. The first half of the solution requires writing some unique identifier for the request message to persistent storage when we write the error response to the GRSResponseQueue . For this, we simply write the JMS Message ID to a database table as part of the same transaction that sends the error response to the GRSResponseQueue . The second half of the solution is to detect when the current message has already been processed by the JMSErrorResponseTrackingSessionBean . This involves checking every message as soon as it is received to see if its JMS Message ID matches one in our database table. If it does, we use the current transaction to remove the message ID entry from our database table and commit the transaction, effectively removing the request message from the queue without processing it again. If the JMS Message ID is not in the tracking table, we dispatch the message to the business logic.

To return the response to the caller, we start with the WSDL that we and our global reservation system partners have agreed upon as the callback interface that they will provide for sending back confirmations and errors. Using the clientgen Ant task, we generate the client-specific jar file that contains the generated classes needed to use the JAX-RPC static invocation model. In the JMSReservationResponseBean , we invoke the Web Service using the JAX-RPC client directly from the onMessage() method. We use portable stubs for this to insulate our application from any version dependencies that the WebLogic Server Web Service run time might have.

Now, we are ready to look at how to build this application. While the build file is certainly more complex that the simple examples we have been looking at so far, there is nothing fundamentally different. If you refer to the ws-gen task, we start by using the auto-typing mechanism to generate the Java types, holders, and serializers for all of the WSDL custom types. Next, we use wsdl2service to generate the web-services.xml deployment descriptor and the Java interface class that the Web Service must implement. We also use the WSDL to generate the client-specific jar file to package with the Web Service. Before going on to do anything else, we must generate the Java types and the client jar file for the callback Web Service because our JMSReservationResponseBean uses these classes. While we are at it, we go ahead and generate the Java interface for the callback Web Service as well because we provide a simple implementation of it that we will use for testing. After this, we simply run EJBGen , compile, and package up the application for deployment.




Mastering BEA WebLogic Server. Best Practices for Building and Deploying J2EE Applications
Mastering BEA WebLogic Server: Best Practices for Building and Deploying J2EE Applications
ISBN: 047128128X
EAN: 2147483647
Year: 2003
Pages: 125

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