Professional enterprise JAX-WS in no time at all?

My current client is talking about migrating to Java 1.6 and a Java EE 5 app server (we are currently still on 1.5 because our data center only supports an older app server). One reason for doing so is that this stack supports JAX-WS. Not knowing much about JAX-WS, I decided it was time to take a look. The Java API for XML Web Services (JAX-WS) is basically a specification of how to deploy and use web services in the latest Java runtime. My first question was "whats so good about it compared to Apache Axis 1.4", which I’ve used successfully plenty of times in the past. Not only does JAX-WS offer improved performance as its based on StAX (a more efficient streaming pull parser for XML), but its also a standard. Axis isn’t a standard, even though it is extensively used. JAX-WS is partially part of Java SE 1.6 and the bits which are not part of it, namely the server side implementation, can be theoretically exchanged without breaking anything, because all implementations implement the given specs. So, no vendor lockin; and you get choice over implementations. What more could one ask for…

So I went with what I knew, and downloaded Axis2 which is an implementation of JAX-WS among other things and started to migrate a simple web service which had run under Axis 1.4. But it wasn’t as simple as I had hoped.

The requirement was to create a web service based on an existing Java "service" class, which supports incoming and outgoing attachments, security, exception handling, complex data types, and integration with Spring.

To turn an exsiting class into a web service in JAX-WS you simply add some annotations:

    @WebService(serviceName = "TestService", portName="Port")
    public class TestService {
    .
    .
   
That is all you need to do in order to deploy it to your server, as a fully functioning web service – great! Every public method is exposed. If you download the Axis2 WAR and deploy that to your web container, when you start the web app, it searches the entire classpath of the webapp for classes with such annotations and automatically deploys them. The URL depends upon the servlet mappings which are in the webapps web.xml, but the Axis2 default is to deploy the AxisServlet to "/services" so your web service URL becomes:

    http://localhost:8089/services/TestService.Port
   
You can view the WSDL under:

    http://localhost:8089/services/TestService.Port?wsdl
   
But to turn this service into something more useful, there are several steps required… Firstly, lets add support for sessions. To get access to the session serverside, you need a web service "context". Within the web service class (TestService in the download, see bottom), you can define a class attribute:

    /** injected by container */
    @Resource
    private WebServiceContext context;

If you give it the @Resource annotation, the container automatically injects an instance at runtime. There are lots of things you can do with this context, and to get the session, you do the following call:

    MessageContext mc = context.getMessageContext();
    HttpServletRequest request = (HttpServletRequest)mc.get(MessageContext.SERVLET_REQUEST);
    HttpSession session = request.getSession();

Since this is a standard HttpSession, you can use it just as you would in any web application. On the client (see later) you need to add a little information to the request context:

    map.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);

See later on, where there is more information. In Axis 1.4, the servers message context is maintained across successive calls to the same session. In JAX-WS and Axis2, this is not the case, and just because you are within a session, setting a property on the message context does not mean its available on the next call in the same session. The only place to put session variables appears to be in the HTTPSession. Which does raise questions if you are running your service outside of HTTP, say over SMTP…

Now that you have the message context, you can also handle attachments, which are particularly useful when handling binary data transfer over web services. Any attachments sent to a web service can be read off the context:

    /**
     * demonstrates how to handle incoming attachments.
     */
    private void handleAttachmentsInRequest(MessageContext mc) throws ApplicationException {
        AttachmentsAdapter aa = (AttachmentsAdapter) mc.get(MessageContext.INBOUND_MESSAGE_ATTACHMENTS);
        if(aa != null && !aa.isEmpty()) {
            for(String name : aa.keySet()){
                DataHandler dh = aa.get(name);
                //... do something with this attachment... like forward it to someone using email...
                orchestrationService.echo("testws@maxant.co.uk", dh.getDataSource());
            }
        }
    }

Equally, you can send attachments back to the client:

    /**
     * demonstrates returning attachments in the response.
     */
    private void setAttachmentOnResponse(MessageContext mc) {
        @SuppressWarnings("unchecked")
        Map<String, DataHandler> attachments = (Map<String, DataHandler>)
                                    mc.get(MessageContext.OUTBOUND_MESSAGE_ATTACHMENTS);
                                   
        //can be null, initialise if necessary
        if(attachments == null){
            attachments = new HashMap<String, DataHandler>();
            mc.put(MessageContext.OUTBOUND_MESSAGE_ATTACHMENTS, attachments);
        }

        DataHandler dh = new DataHandler("an attachment being returned from the client", "text/plain");
        attachments.put("RETURN_FROM_SERVER", dh); //provide a content ID, and the handler for the attachment
    }
   
The client code for this service is generated using the "wsimport.exe" tool which is included in JDK 1.6:

    wsimport.exe -d src -verbose -s src service.wsdl
   
where "service.wsdl" is a local file containing the WSDL definition of the service. You could provide a URL here too.

The client then calls the service in a few simple lines of code:

        TestService_Service service = new TestService_Service();
        TestService proxy = service.getPort();

        //add session support from the client side, see above
        Map<String, Object> map = ((BindingProvider)proxy).getRequestContext();
        map.put(BindingProvider.SESSION_MAINTAIN_PROPERTY, Boolean.TRUE);

        //call the service remotely!
        Foo foo = proxy.getInfo(bar);

The first problems that I had, were exactly here. I was using JDK 1.6.0_10 and although its not the absolute newest version, it is the tenth release. I spent some time before finding that there was a bug and attachments were simply not sent over the wire to the server. Attachment returned from the server were sent over the wire, but the API had no access to them.  I worked this out by using the TCP/IP Monitor which is part of Eclipse 3.5 (maybe even earlier). With it you can make your client call a server called a monitor, which acts as a proxy, and logs the call, before forwarding it to the real server. Although the web services were running on port 8089, the client call was modified from that above, to call port 11350:

        String url = "http://localhost:11350/services/TestService.Port";
        map.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url);
 
By opening the TCP/IP Monitor View in Eclipse and right clicking in the upper left pane, you can view the properties and add a new monitor for your server. Ensure the monitor has been started before running your client. Note that as soon as you add SSL, the monitor no longer works.

Two things were required to solve my "attachments problem". Firstly I started using JDK 1.6.0_14. This meant that attachments coming back from the server were visible. But one other thing was needed to ensure that the client actually sent the attachments to the server, namely a custom SOAP handler. A "simple" or "empty" soap handler was added to the service:

    //add the soap handler to the service client
    service.setHandlerResolver(new HandlerResolver() {
        @SuppressWarnings("unchecked")
        public List<Handler> getHandlerChain(PortInfo portInfo) {
            List<Handler> hs = new ArrayList<Handler>();
            //hs.add(handler);
            hs.add(new SimpleSOAPHandler());
            return hs;
        }
    });

The SimpleSOAPHandler class itself looked like this:

    /**
     * a soap handler which works as a workaround to make inbound attachments work.
     * only works with java 1.6.0_14 and above.
     * its pretty much blank, but its really needed otherwise attachments are not sent
     * by the client.
     */
    private static class SimpleSOAPHandler
                            implements javax.xml.ws.handler.soap.SOAPHandler<SOAPMessageContext> {
       
        public Set<QName> getHeaders() {
            //simply return non null
            Set<QName> qs = new TreeSet<QName>();
            return qs;
        }
       
        public boolean handleFault(SOAPMessageContext context) {
            //simply return true to continue processing
            return true;
        };
       
        public boolean handleMessage(SOAPMessageContext context) {
            //simply return true to continue processing
            return true;
        }
       
        public void close(MessageContext context) {
        }
    }

As you can see, all it does it pretty much nothing apart from acting positively and handling messages and faults successfully! But its presence ensured that attachments were indeed uploaded to the server. For JDK 1.6.0_10 where attachments in the response could not be read, a similar soap handler could also be used, since it is called every time an incoming or outgoing message is sent. The SOAPMessageContext gives access to the attachments. In the download for this blog entry (see bottom), there is an example of how to handle downloads for 1.6.0_10, but its a mess because you need to keep track of whether the message you are handling is a request or a response to the service.

The fact that even with JDK 1.6.0_14 you still need the SOAP handler, when it does nothing, demonstrates that the reference implementation of JAX-WS is still buggy. There is absolutely no need for it, and it is a shame that its required because Sun normally delivers a much better quality of software which simply works out of the box, and does so logically without you needing to fiddle with workarounds.

The next set of problems came as soon as I made my service slightly more complicated so that it received and sent complex data types (ie. not just int, String, etc. but object models). The client classes generated by wsimport.exe did not always contain fields from the object models…

This was somewhat unfortunate but the solution is to start annotating those missing fields with JAX-Binding annotations so that wsimport generates them. JAX-WS only specifies the web service specs. The data which is passed over to the web service as parameters is handled by the Java API for XML Binding (JAX-B), and this provides its own annotations for telling it how to marshal, unmarshal (serialize) and generate classes. For any missing fields, simply add the following annotation:

    /**
     * simply an array of codePoints, to show that we can return
     * complex structures from web services.
     */
    @XmlElement(name="chars")
    protected List<Integer> chars = new ArrayList<Integer>();

What was unpleasant about this is that there is nothing special about this field (apart from it being a List, but in other examples that was not a problem) and I was only aware that it was not mapped, because the generated client API didn’t let me access it! Furthermore, I had to add the name attribute of the annotation, otherwise I got errors about duplicate fields being mapped. It might have been acceptable if all fields had such annotations, but for example in the Foo class from which the above code snippet is taken, if I annotated the other field "answer", then I got the following exception when requesting the WSDL from the server:

    Caused by: com.sun.xml.internal.bind.v2.runtime.IllegalAnnotationsException:
        1 counts of IllegalAnnotationExceptions
        Class has two properties of the same name "answer"

This error occurred regardless of whether I gave the "answer" field a named attribute with the correct name, an unnamed annotation or even one with a different name:

    @XmlElement(name="answer")
    private String answer;

    @XmlElement
    private String answer;

    @XmlElement(name="answer2")
    private String answer;

All of the above failed. Compared to Axis 1.4 where these problems did not occur, this was frustrating. I had similar problems with exceptions (see shortly, regarding the fields in the fault bean). Again, this appears to be as very buggy, and generates unnecessary work for developers.

Exception handling was the next feature that needed to be added, as it allows decent handling of business logic problems. In Axis 1.4 you simply added a throws clause to your service method and when you generated the client, the exception was mapped and the client could catch the exception. The fact that the call was remote was virtually transparent. For lower level problems like network problems, there was the RemoteException class which you could catch to deal with such problems. In JAX-WS the exception handling mechanism has been changed. Web Service specs allow for errors to be handled client side by receiving web faults in the response, as opposed to exceptions. The response to a web service call has a header, a fault and a body. If an exception is thrown, it is mapped to a fault. In JAX-WS this is no longer that transparent, and an exception class uses a "fault bean". The exception is what is thrown, but the bean is a class holding the information about that exception, like a custom internationalised message, or a unique error code. When defining the exception, you use an annotation to state which bean class to use:

    @WebFault(
            name="AppExceptionWebFault",
            faultBean="uk.co.maxant.testws.webservices.ApplicationExceptionFaultBean"
    )
    public class ApplicationException extends Exception {

The bean class itself is nothing special:

    public class ApplicationExceptionFaultBean implements Serializable {

        private static final long serialVersionUID = 1L;
       
        //doesnt work without this annotation!
        @XmlElement(name="msg")
        private String msg;

        /** required by jax-ws */
        public ApplicationExceptionFaultBean(){
        }
       
        public ApplicationExceptionFaultBean(String msg){
            this.msg = msg;
        }

        public String getMsg(){
            return msg;
        }
    }

Note how the bean class, as well as the exception, and indeed anything which is sent over the wire, has a default constructor. In the bean, the fields which are to be marshalled are also annotated using JAX-B annotations.

As far as integrating with Spring, there is nothing too complicated. Spring was configured to auto-wire the internal services. A Service Locator was written which loads and caches the application context. Finally a Dependency Resolver was written which uses reflection to inject service beans from Spring into fields in the web service. Since the web service runs outside of the transaction and security context of Spring, it is important that the web service delegate all calls to a façade inside Spring. In the example which you can download, this façade is the orchestration service, which typically would define transaction demarcation, security requirements, and delegate to lower level calls within other Spring services. There is much more information about this in the upcoming white paper on GWT (Google Web Toolkit), available at http://www.maxant.co.uk/whitepapers.jsp. Additionally I made reuse of the maxant Spring Security extension, available at http://www.maxant.co.uk/tools.jsp. Spring itself also contains an implementation of JAX-WS and can also be configured to load the application context at runtime. Although I did not investigate this here, it may be more useful than the dependency resolver / service locator used in this example.

The final hurdle to overcome in the simple requirements listed at the start of this blog posting, which is now getting rather long because of all the problems I encountered, was security.

Any professional enterprise service needs to consider security. There are typically three important facets, namely encryption to ensure the message is not readable by unauthorized eyes; authentication to ensure the caller is who they say they are; and authorization to ensure the caller is allowed to use the functionality on offer by the service. While standards like WS-Security cover these facets, it is simpler just to use existing mechanisms for all three. Not only that, but with all the problems I had encountered so far, I didn’t want additional headaches trying to integrate message-level security when there are simpler ways. Because we are running our service inside a Java EE web container, why not simply use SSL to encrypt transmissions, and simple basic authentication to check the user and their roles? JAX-WS clients generated by the wsimport.exe tool allow you to set the URL to the web service so that it goes over SSL for encryption, as well as provide security credentials to authenticate/authorize:

    String url = "https://localhost:8444/services/TestService.Port";
    .
    .
    map.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, url);
    .
    .
    //login info
    map.put(BindingProvider.USERNAME_PROPERTY, "andy@maxant.co.uk");
    map.put(BindingProvider.PASSWORD_PROPERTY, "asdf");
   
When the call to the server is made, these properties are passed into the HTTP header:

    POST /services/TestService.Port HTTP/1.1
    Authorization: Basic YW5keUBtYXhhbnQuY28udWs6YXNkZg==

The server can then authenticate using a realm, so long as the relevant security constraints are set in the webapps web.xml:

    <security-constraint> <!-- allows only authorized users to have access, and forces HTTPS -->
        <display-name>SecureServices</display-name>
        <web-resource-collection>
            <web-resource-name>SecureServices</web-resource-name>
            <url-pattern>/services/*</url-pattern>
        </web-resource-collection>
        <auth-constraint>
            <role-name>registered</role-name>
        </auth-constraint>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>

    <login-config>
        <auth-method>BASIC</auth-method>
    </login-config> 

    <security-role>
        <role-name>registered</role-name>
    </security-role>

Well that's the theory, but there were a number of complications. Firstly, notice in the HTTP header shown above that the Basic Authentication token has the key "Authorization", with a capital "A". Now take a quick look at the Tomcat 5.5.9 code:

    MessageBytes authorization =
        request.getCoyoteRequest().getMimeHeaders()
        .getValue("authorization");

It searches for the header with a lower case key… The internet RfC specifies that the keys are case insensitive, so this is actually a bug in Tomcat. But this is the first time I have ever come across this problem. Using Eclipse’s TCP/IP Monitor, you can repeateldy retest this bug as well as change the key to lowercase to work around it. But since the web service client is entirely generated, my only solution was to try upgrading Tomcat. Instead of researching which version fixed the bug, I simply took the latest version, 6.0.20. The authenticators in this latest version also work a little differently, so only authenticate against URLs which define a security constaint. Older versions of Tomcat authenticate any time the HTTP header contains the basic authentication key. This was just something else which made this little project into a much bigger one.

The next problem with security related to the requirement to authenticate. The web service client which is generated needs the WSDL at runtime in order to initiate, probably because it does so using a dynamic proxy. This is also different from Axis 1.4. The problem is that the WSDL document is serverside, and inaccessible unless you specify the username/password. But the client already fails when you create the proxy, so before you have access to the message context into which to add security properties. Basically said, you need to put the WSDL some place else, so that the client can access it without the username/password. The easiest way is to just download it and save it, but that has its own problems becuase the XSD is imported into the WSDL via a reference:

    <xsd:import namespace="http://webservices.testws.maxant.co.uk/"
            schemaLocation="TestService.Port?xsd=TestService_schema1.xsd"/>
   
This was changed to:

    <xsd:import namespace="http://webservices.testws.maxant.co.uk/"
            schemaLocation="testwsmaxantcouk.services.TestService.Port.xsd"/>

where the WSDL and XSD files were located in the project root folder:

    testwsmaxantcouk.services.TestService.Port.wsdl
    testwsmaxantcouk.services.TestService.Port.xsd

The WSDL location is then passed to the service constructor:

    QName serviceName = new QName("http://webservices.testws.maxant.co.uk/", "TestService");
    String wsdlFile = "file:///testwsmaxantcouk.services.TestService.Port.wsdl";   
    URL wsdlLoc = new URL(wsdlFile);
    TestService_Service service = new TestService_Service(wsdlLoc , serviceName);

This works fine, but these are all manual steps which are in my view completely unnecessary! Surely someone on the JAX-WS team at Sun thought this through at some stage? Apparently not. Alternatively there are very good reasons for this, but I have not found them published anywhere…

I attempted to automate the process of downloading the WSDL and XSD, by creating an Apache Ant script. The idea was to use the "get" task which you can use with a username and password to download a secured file to a local file. The final problem I faced with security relates to SSL and this automated build. Since I was testing against a self certified SSL certificate, ie. one which was not signed by a Certificate Authority (CA), I had to install the certificate in the trust store so that I avoided "PKIX" errors, whereby Java throws an exception because it does not trust the unsigned certificate. To do this, download the security certificate with your browser by examining it and exporting it. Once you have it in a local file, install it into the following store:

    %JAVA_HOME%\jre\lib\security\cacerts

This is your JREs trust store, as opposed to the key store located at:

    %user.home%/.keystore

The keystore is what holds private data, trusted client certificates, or indeed the certificates which your server provides to clients in order to open an SSL connection. The truststore is where you tell your JRE which certificates to trust. To import the certificate to the trust store, so that Java stops throwing exceptions when trying to connect to a server it does not trust, run this command:

    keytool -keystore %trustStore% -import -file fileExportedFromBrowser

It asks you if you are sure, to which you respond yes! The truststore password, if you have never changed it is "changeit".

If you use a different truststore than the default one, or indeed keystore, you can set it up via System properties:

    -Djavax.net.ssl.trustStore=someTrustStore
    -Djavax.net.ssl.trustStorePassword=somePassword
    -Djavax.net.ssl.keyStore=someKeyStore
    -Djavax.net.ssl.keyStorePassword=someOtherPassword

In fact, I didn’t get as far as fully automating the WSDL download because I still had issues whereby Java needs a HostVerifier. This problem came about because SSL is based on IP Address, and I was developing against the domain "testwsmaxantcouk" which was mapped to localhost IP address 127.0.0.1 in my hosts file. Java tries to verify that the requested domain name matches the IP adderss, I think using a reverse DNS lookup, which of course fails in my case because the IP address maps to more than one domain name.

So as you can see by the length of this blog entry, implementing JAX-WS is by far not as simple if you need to create a professional web service using any of the following:

  • exceptions
  • security
  • complex data types / structures
  • attachments

As such, I would advise you to create some stringent proof of concepts if you need to migrate to this technology set and check your constraints on which versions of Java, application server and JAX-WS implementation you are allowed to use, perhaps because of enterprise architecture requiring common platforms for all your applications.

The Eclipse project, all libraries and source code created as part of blog project can be downloaded here.
   
© Ant Kutschera 2009

Some references:

http://java.sun.com/j2se/1.5.0/docs/tooldocs/solaris/keytool.html

http://mail-archives.apache.org/mod_mbox/tomcat-users/200910.mbox/%3C2185441d0910021517w201a497fo83625c30062f7355@mail.gmail.com%3E

http://java.sun.com/developer/EJTechTips/2006/tt0527.html