Thursday, March 15, 2007

Migrating Smoothly from rpc/encoded to document/literal Web Services with Spring-WS

I've recently had to migrate Axis 1.x based web services to Spring WS. The Axis web services were rpc/encoded whereas the new ones use document/literal style. Rpc/encoded style is not portable, generates bulky XML which is difficult to validate. However, I think they are still common for different reasons. In this context, migrating clients to the new service implies rewriting parts of their code. This could be difficult to manage if the service is public and the clients are heterogeneous, specially if they are not under your direct control. I thought about a simple way to achieve a smoother migration with Spring WS that I would like to share in this post.

The Idea

The basic idea is to detect the incoming rpc/encoded requests and to transform them to the new structure on the fly using XSLT. The responses to the transformed requests have to be transformed back again the XML expected by the client. This way the clients can use the new web service only by pointing to its endpoint. Because of the added overhead by XSLT, this solution is probably not long-term. It is more suitable in a transitory period to allow each client to adapt its code according to its own schedule and constraints.
Spring WS has a transforming interceptor that can take care of the transformation part. I will extend it so that:

  • It recognizes and transforms rpc/encoded requests. Document/literal requests will not be altered.
  • It marks the transformed requests in order to transform back their responses.

An Example

I will use a familiar example: an online store. The OrderRequest class represents a client's order and contains a DeliveryInfo object containing an address and a delivery date, and a list of items to order. An Item has a reference and a quantity. The server responds with the reference of the order.

For the sake of brevity, I'll show only the interesting parts. Here's and example of the rpc/encoded payload of an order with 2 items:

<caf:placeOrder soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <o xsi:type="caf:OrderRequest">
        <info xsi:type="test:DeliveryInfo">
            <address xsi:type="xsd:string">abc</address>
            <deliveryDate xsi:type="xsd:date">2007-04-01</deliveryDate>
        </info>
        <items xsi:type="test:ArrayOfItem" soapenc:arrayType="test:Item[1]">
            <item>
                <quantity xsi:type="xsd:int">10</quantity>
                <ref xsi:type="xsd:string">123</ref>
            </item>
            <item>
                <quantity xsi:type="xsd:int">5</quantity>
                <ref xsi:type="xsd:string">345</ref>
            </item>
        </items>
    </o>
</caf:placeOrder>
And the response payload:
<ns1:placeOrderResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xmlns:ns1="http://cafebabe">
    <placeOrderReturn href="#id0"/>
</ns1:placeOrderResponse>
<multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:long" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">123456</multiRef>
Here's the target document/literal request payload:
<caf:orderRequest>
    <info>
        <address>abc</address>
        <deliveryDate>2007-04-01</deliveryDate>  
    </info>
    <items>
        <item>
            <quantity>10</quantity>
            <ref>123</ref>
        </item>
        <item>
            <quantity>5</quantity>
            <ref>345</ref>
        </item>
    </items>
</caf:orderRequest>
And the corresponding response:
<ns3:orderResponse xmlns:ns3="http://cafebabe">
    <orderId>123456</orderId>
</ns3:orderResponse> 
We need two XSLT stylesheets, one to transform the requests (rpc->document) and the other for the responses (document->rpc). First request.xsl:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:caf="http://cafebabe">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="no"/>
    <xsl:template match="/caf:placeOrder/o">
        <caf:orderRequest>
            <info>
                <address><xsl:value-of select="info/address"/></address>
                <deliveryDate><xsl:value-of select="info/deliveryDate"/></deliveryDate>
            </info>
            <items>
            <xsl:for-each select="items/item">
                <item>
                    <quantity><xsl:value-of select="quantity"/></quantity>
                    <ref><xsl:value-of select="ref"/></ref>
                </item>
            </xsl:for-each>
            </items>
        </caf:orderRequest>
    </xsl:template>
</xsl:stylesheet>
And response.xsl:
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0" xmlns:caf="http://cafebabe" xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <xsl:output method="xml" omit-xml-declaration="yes" indent="yes"/>
    <xsl:template match="/caf:orderResponse">
        <caf:placeOrderResponse soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
            <placeOrderReturn href="#id0"/>
        </caf:placeOrderResponse>
        <multiRef id="id0" soapenc:root="0" soapenv:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" xsi:type="xsd:long" xmlns:soapenc="http://schemas.xmlsoap.org/soap/encoding/">
            <xsl:value-of select="orderId"/>
        </multiRef>
    </xsl:template>
</xsl:stylesheet>
Basically, each stylesheet copies the target XML structure and extracts the values using XPath expressions.

The Transforming Interceptor

As I mentioned earlier, the transforming interceptor is based on PayloadTransformingInterceptor from Spring WS. Its basic outline is as follows:
public class RpcTransformingInterceptor extends PayloadTransformingInterceptor {
    private static final String SOAP_ENCODING = "http://schemas.xmlsoap.org/soap/encoding/";
...
  public boolean handleRequest(MessageContext messageContext, Object endpoint)
      throws Exception {
    WebServiceMessage message = messageContext.getRequest();

    if (isRpc(message)) {
      setRequestTransformed();
      return super.handleRequest(messageContext, endpoint);
    }
    return true;
  }

  public boolean handleResponse(MessageContext messageContext, Object endpoint)
        throws Exception {
    if (isRequestTransformed())
      return super.handleResponse(messageContext, endpoint);
    return true;
    }
...
}
First we need to mark each transformed request. Spring 2 supports request scope which is just what we need. I will define a simple inner class BooleanHolder in my RpcTransformingInterceptor and add a request-scoped property transformed:
public class RpcTransformingInterceptor extends PayloadTransformingInterceptor {
  public static class BooleanHolder {

    private boolean value = false;

    public boolean getValue() {
      return value;
    }

    public void setValue(boolean value) {
      this.value = value;
    }
  }

  private BooleanHolder transformed;

  private void setRequestTransformed(){
    transformed.setValue(true);
  }

  private boolean isRequestTransformed(){
    return transformed.getValue();
  }
We have to write a bean definition in the Spring context:
<bean id="transformed" scope="request" class="cafebabe.ws.RpcTransformingInterceptor$BooleanHolder">
    <aop:scoped-proxy />
</bean>
For earlier versions of Spring, it is possible to use a ThreadLocalTargetSource instead.

Now, we have to read the encoding style of the payload. If it is equal to http://schemas.xmlsoap.org/soap/encoding/ the request will be transformed. Working with Sun's implementation of SAAJ 1.3 I found out that calling getSaajMessage().getSOAPBody().getEncodingStyle() returns null. We will have to read the encodingStyle directly from the payload. Spring WS gives access to the payload via WebServiceMessage.getPayloadSource(). Because we are only reading a small part of the message, StaX seems appropriate for the job:

...
private static XMLInputFactory inputFactory = XMLInputFactory.newInstance();
...
private boolean isRpc(WebServiceMessage message)
            throws XMLStreamException {
  XMLStreamReader parser = inputFactory.createXMLStreamReader(message.getPayloadSource());
  parser.nextTag();
  String encodingStyle = parser.getAttributeValue(
      "http://schemas.xmlsoap.org/soap/envelope/"
      , "encodingStyle");
  return SOAP_ENCODING.equals(encodingStyle);
}
I also implemented a DOM version:
...
private static TransformerFactory transformerFactory = 
    TransformerFactory.newInstance();
...
private boolean isRpc(WebServiceMessage message)
        throws TransformerException {
  Transformer transformer = transformerFactory.newTransformer();
  DOMResult domResult = new DOMResult();
  transformer.transform(message.getPayloadSource(), domResult);
  Element rootElement = (Element) domResult.getNode().getFirstChild();
  String encodingStyle = rootElement.getAttributeNS(
        "http://schemas.xmlsoap.org/soap/envelope/"
        , "encodingStyle");
  return SOAP_ENCODING.equals(encodingStyle);
}
That's it! Now the interceptor will transform the rpc/encoded message in a transparent way for the client and the server. Make sure that you place it first in the chain of interceptors, at least before the validation interceptor if you use one.

Performance and other Considerations

I used JAXB 2 for Java-XML binding with Spring WS and soapUI for all the tests. I also used the default message factory (SaajSoapMessageFactory).
There are several StaX parsers. Only Woodstox worked for me. For example, SJSXP and StaX-RI threw exceptions when calling createXMLStreamReader (message.getPayloadSource()). Actually, Woodstox threw an exception when I tried to use AxiomSoapMessageFactory but worked fine with SAAJ.

Obviously the transformation overhead depends on the complexity of the XML. However, I wanted to measure the overhead added by the interceptor to examine each request, even non transformed ones. I wanted also to compare between the DOM version and StaX version. I conducted the tests with two kinds of messages: a small message containing 2 items and a bigger message containing 400 items (size around 47k). I set soapUI to launch 5 concurrent threads for 5 minutes with a test delay of 1 second. I repeated the tests 5 times and calculated the average. Please note that the results are relative as there is no business logic executed by the service (100% of the time is spent in the framework and XML processing). Here are the results:
Small payloadOverheadBig payloadOverhead
No interceptor~435 tpsN/A~35 tpsN/A
DOM (no transformation)~400 tps8%~23 tps34%
StaX (no transformation)~415 tps5%~34 tps3%
StaX (with transformation)~377 tps13%~20 tps43%
Knowing that SAAJ messages are DOM documents I was surprised by the bad results of the DOM version. Transforming a DomSource to a DomResult seems to duplicate the whole document (I checked with a debugger and found different object ids) so it is a costly operation. However, StaX doesn't add a significant overhead and its performance doesn't degrade with larger messages.

The Bottomline

  • The interceptor doesn't add significant overhead to examine incoming messages. Compliant requests will not suffer a loss of performance.
  • Performace tests should be conducted per case to determine if the overhead of the XSL transformation is acceptable or not.
The source code can be downloaded here.