|
Efficient and easy to use web services: XFire in practice
Efficient and easy to use web services: XFire in practiceTomasz Sztelak Web services have proved the long-awaited solution for communication between distributed applications running on a variety of platforms and created using a variety of programming languages. The use of XML-based industry standards such as SOAP, WSDL and UDDI has made it possible to exchange data and services between applications running not only within such frameworks as J2EE or Microsoft .NET, but also applications written in practically any other language, whether compiled (for example C++, Pascal, Cobol) or interpreted (Perl, Python, PHP and others). The rapid development of web service technologies was spurred by the fact that web services are loosely coupled with the applications they serve logic to, so adding a web service has very little effect on existing application components, making it possible to gradually integrate web services with existing functionality. The approach also allows existing capabilities to be served up using web services without costly modifications to the core system, and with a minimum risk of failure. Web services provide a way to integrate disparate applications into one large distributed system, effectively eliminating the need to deploy costly middleware to translate between the various applications' communication methods. Large web-based corporations were quick to notice the benefits of using web services and start serving functionality using this technology - Google, Amazon, eBay, Yahoo and Allegro (largest Polish auction site) to name but a few. The level of interest in the technology is best attested by the fact that over 50 000 programmers are currently registered in the Amazon web services program. The eBay program currently has over 8000 corporate and individual participants, with the 600 applications currently using the eBay API accounting for some 40% of all transactions.
Figure 1. Web service architecture Problems with existing toolkitsA quick web search brings up a multitude of web service development tools. Some of the most popular include:
However, selecting a specific application to suit your needs is not an easy task. Each of the above makes it possible to serve up application logic as a web service, but each also has disadvantages that can make it cumbersome or impossible to use. To start with, let's try to define the features that any sensible development tool should have:
Unfortunately, most of the tools listed above fail in one or more of these areas. Small and medium businesses usually cannot afford to buy costly (though very good) commercial packages, such as WASP or Weblogic Server. The performance of several solutions is also questionable. The typical problem is that SOAP messages are processed using DOM, which usually involves first building a document to represent the XML content of the message and only then creating an application object corresponding to this data. This results not only in lower performance, but also considerable memory overhead, with the document tree frequently taking up many times the space of the data itself. Such performance issues apply to Axis 1.x (Axis 2 will use much faster stream-based XML processing using StAX) and JAX-RPC. Moreover, few commercial offerings shine on ease of use, often requiring complicated configuration and additional operations during installation, or providing awkward APIs that make it much harder to properly understand the tool and develop applications.
Figure 2. Service description in WSDL format Why XFire?The XFire project came about in response to problems with developing web services using available tools (including the ones listed above). The aim was to create a free toolkit with all the functionality of commercial solutions, but without their shortcomings. Here is how XFire scores on the features outlined above:
A web service in 5 minutesWe'll start by creating the server side. To create a web service, you'll need any application server (such as Tomcat 5.x, http://tomcat.apache.org), an XFire distribution package (xfire-all-1.0-M6.zip or later, available at http://xfire.codehaus.org/Download) and any Java IDE (such as Eclipse, http://www.eclipse.org). Deploying a web service requires a suitable application directory structure to be created, as shown in Listing 1. Now copy the following files to the application's library subdirectory (WEB-INF/lib): xfire-all-1.0-SNAPSHOT.jar, stax-api-1.0.jar, wsdl4j-1.4.jar, xerces-2.4.0.jar, xml-apis.jar, jdom-1.0.jar, annogen-0.1.0.jar, qdox-1.5.jar, xbean-spring-2.0-SNAPSHOT.jar, spring-1.2.4.jar, stax-1.1.2-dev.jar. Listing 1. Application directory structure
xfire
- WEB-INF/
-classes/ - service classes
-META-INF/
-xfire/
-services.xml - service definition
-lib/ - libraries used by service code
web.xml
Initial configuration for XFire proceeds in a similar manner to other products of this sort, requiring the definition of a servlet to provide an access point for receiving client requests and passing them on to the relevant classes. The definition resides in the web.xml file in the application directory, as shown in Listing 2. Assuming that you're running Tomcat on its default port and the application is called XFire, all client requests sent to http://localhost:8080/xfire/services will be treated as web service requests and passed to the XFire servlet. Listing 2. The web.xml file
xml version="1.0" encoding="ISO-8859-1"?>
<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.2//EN"
"http://java.sun.com/j2ee/dtds/web-app_2_2.dtd">
<web-app>
<servlet>
<servlet-name>XFireServletservlet-name>
<display-name>XFire Servletdisplay-name>
<servlet-class>org.codehaus.xfire.transport.http.XFireConfigurableServletservlet-class>
servlet>
<servlet-mapping>
<servlet-name>XFireServletservlet-name>
<url-pattern>/services/*url-pattern>
servlet-mapping>
web-app>
Next, we need to provide an interface (Listing 3) and implementation (Listing 4) for the actual application class. Listing 3. Class interface for the sample application
package pl.sdj.petstore; public interface PetStore { // buy a pet with the specified name long buyPet(String name); // check order status String getOrderStatus(long orderId); } Listing 4. Class implementation for the sample application
package pl.sdj.petstore.impl; import pl.sdj.petstore.PetStore; public class PetStoreImpl implements PetStore { public String getOrderStatus(long orderId) { // "mouse" is the only pet in stock if (orderId == "mouse".hashCode()) { return "ordered"; } return "unknown"; } public long buyPet(String name) { // return hashcode as order id return (name == null ? -1 : name.hashCode()); } } As a final configuration step, we need to indicate which application classes should be served up as web services. This is done by creating the file services.xml in the META-INF/xfire directory, with the content shown in Listing 5. The elements used in the file are as follows:
Listing 5. Service configuration in the META-INF/xfire/services.xml file
<beans xmlns="http://xfire.codehaus.org/config/1.0"> <service> <name>PetStorename> <namespace>http://www.sdj.plnamespace> <serviceClass>pl.sdj.petstore.PetStoreserviceClass> <implementationClass>pl.sdj.petstore.impl.PetStoreImplimplementationClass> service> beans> Once the application has been built and deployed on the server, we should have a fully functional web service. To check if everything is as it should be, open the URL http://localhost:8080/xfire/services/PetStore?wsdl in a web browser and you should see a WSDL description of the web service. As you can see, a fully functional web service can be deployed with just a few lines of configuration. Simple though they are, the XFire configuration mechanisms actually provide control of virtually all aspects of application operation. Listing 6 shows the elements available within the basic configuration framework. Listing 6. Configuration file structure
<beans xmlns="http://xfire.codehaus.org/config/1.0"> <xfire> ..<inHandlers> <handler handlerClass=""/> inHandlers> <outHandlers> <handler handlerClass=""/> outHandlers> <faultHandlers> <handler handlerClass=""/> faultHandlers> xfire> <service> <name /> <serviceClass /> <implementationClass/> <namespace /> <serviceFactory/> <style/> <soapVersion /> <properties> <property key="key">valueproperty> properties> <inHandlers> <handler handlerClass="" />inHandlers> <outHandlers> <handler handlerClass="" />outHandlers> <faultHandlers> <handler handlerClass="" />faultHandlers> service> beans> Adding functionalityNow that we know how a web service works and what configuration parameters are available, it would be a good idea to extend its functionality a bit, for example by allowing only clients from a specific IP to call the web service methods. This capability can be added through the use of handler classes, executed each time a service method call is received. Handler objects can be registered for all incoming messages (using inHandler objects to handle method calls), outgoing messages (using outHandler objects to handle method call results) and error notifications (using faultHandler objects to pass information about any errors during a method call). Handler classes can be registered both for specific services and globally for all existing services. Handlers can also be placed at various phases of message processing (defined as Phase objects), allowing handler logic to be hooked into the parsing phase (PARSE), into the service selection phase (DISPATCH), just before the method call (PRE-INVOKE) or at any other predefined point. This means that additional message-related functionality can easily be added, including SOAP message encryption or signing, user authentication or service API version control. Listing 7 presents a sample handler class. The invoke() method is the most important part, as it is called for each message. In this example, the method retrieves the IP of the calling client from the HTTP request (an HTTPServletRequest object) and compares it to the permitted IP for the web service, as defined in the configuration file. The getPhase() method returns information on the phase of the message processing chain that the class should apply to (incoming message processing involves the following phases: TRANSPORT, PARSE, PRE_DISPATCH, DISPATCH, POLICY, USER, PRE_INVOKE, SERVICE). Listing 7. Sample handler class implementation
package pl.sdj.handlers; import org.codehaus.xfire.MessageContext; import org.codehaus.xfire.handler.AbstractHandler; import org.codehaus.xfire.transport.http.XfireServletController; public class IPRestrictionHandler extends AbstractHandler { public void invoke(MessageContext ctx) throws Exception { // get remote client IP String remoteIp = XfireServletController.getRequest().getRemoteAddr(); // get permitted client IP String allowedIP = (String) ctx.getService().getProperty("allowedIP"); System.out.println("Request to call method "+ctx.getInMessage().getAction()+" from IP "+remoteIp); // compare IPs if( !remoteIp.equals(allowedIP)){ // if the IP are different, end message processing throw new XFireException("Your IP is not permitted"); } } // specifies the handler's place in the processing chain public String getPhase() { return Phase.USER; } } Note that testing handler code within the IDE requires the servletapi-2.3.jar file from the XFire package to be added to the application libraries. Now we simply register the handler class for our service by adding a suitable handler element to the services.xml file, as shown in Listing 8. From now on, we can be sure that unauthorised clients will not be able to access the functionality of our web service. Listing 8. Extended service configuration
<beans xmlns="http://xfire.codehaus.org/config/1.0"> <service> <name>PetStorename> <namespace>http://www.sdj.plnamespace> <serviceClass>pl.sdj.petstore.PetStoreserviceClass> <implementationClass>pl.sdj.petstore.impl.PetStoreImplimplementationClass> <properties> <property key="allowedIP">127.0.0.1property> properties> <inHandlers> <handler handlerClass="pl.sdj.handlers.IPRestrictionHandler" /> inHandlers> service> beans> Creating a web service clientWe will now build a simple command-line client application for our web service. To run the client, you will need the following libraries from the XFire distribution package: commons-discovery-0.2.jar, commons-logging-1.0.4.jar, log4j-1.2.8.jar, wsdl4j-1.5.1.jar, jdom-1.0.jar, servletapi-2.3.jar, stax-1.1.2-dev.jar, stax-api-1.0.jar, xerces-2.4.0.jar, xfire-all-1.0-SNAPSHOT.jar, xml-apis.jar, javamail-1.3.2.jar, commons-httpclient-3.0-rc3.jar, commons-codec-1.3.jar, as well as the actual client application class using the PetStore service API, as shown in Listing 9. Listing 9. Service client implementation
public class XfireClient { // service name private static final String NAME = "PetStore"; // service namespace private static final String NAMESPACE ="http://www.sdj.pl"; // service interface private static final Class SERVICECLASS = PetStore.class; // service URL private static final String SERVICE_ADDRESS = "http://127.0.0.1:8080/xfire/services/PetStore"; // service instance private PetStore service; public void init() { // service properties definition Service serviceModel = new ObjectServiceFactory().create(SERVICECLASS, NAME, NAMESPACE, null); try { // associate the local service instance with the remote service service = (PetStore) new XfireProxyFactory().create(serviceModel, SERVICE_ADDRESS); } catch (MalformedURLException e) { throw new RuntimeException("Wrong URL: "+e.getMessage()); } } public void buyPet() throws Exception { System.out.println("Buying 'mouse'"); long orderId = service.buyPet("mouse"); System.out.println("Order ID: " + orderId ); String status = service.getOrderStatus(orderId); System.out.println("Order status: " + status); } public static void main(String[] args) throws Exception { XFireClient client = new XFireClient(); client.init(); client.buyPet(); } } The most important part of the client class is the init() method, which creates a service object whose API mirrors that of the remote service object. The local object must be initialised using the exact same values as the server-side one, so the factory method call should include the following element values taken from services.xml: ObjectServiceFactory().create(, , , properties); The properties argument allows additional parameters to be specified, including the SOAP protocol version or message generation method. Now we need to create a proxy (an XFireProxy object) to delegate method calls to the remote service. The proxy factory method call takes two arguments: the local service object and the URL of the remote service to be invoked. From now on, any calls to the local service object will yield the same results as method calls for the corresponding remote service. Running the client class from the command line should give the result shown in Listing 10.
Listing 10. Client call res
|
|






