Saturday, 23 April 2011

Java: JAX-WS web services and clients

JAX-WS is built into Java 6. This makes it a low-dependency choice for writing SOAP-based web service code. This post covers the basics of JAX-WS development with a sample web service.

An understanding of the following is beneficial: Servlets; XML Schema Definition (XSD); Web Services Description Langauge (WSDL); XPath; JAXB.

Server code was tested on Glassfish 3; client code was tested on Java 6.

The code here describes a contract-first web service; it is possible to do this the other way round, starting with Java code and generating descriptors from it.

The contract

SOAP web service contracts are defined by WSDL files. XSD schemas define the forms of the request and response.

The MaintainAddress service contains one operation: lookupAddress. The WSDL reuses name and address formats defined in their own schemas. The sample client will POST an XML request over HTTP and receive an XML response from the Java EE server.

The request taking a name (forename and surname) as a parameter:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:main="http://demo.iae.ws/MaintainAddress/"
                  xmlns:name="http://demo.iae.ws/name">
   <soapenv:Header/>
   <soapenv:Body>
      <main:name>
         <name:forename>Joe</name:forename>
         <name:surname>Blogs</name:surname>
      </main:name>
   </soapenv:Body>
</soapenv:Envelope>

Sample response containing the person's full address:

<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/"
                  xmlns:main="http://demo.iae.ws/MaintainAddress/"
                  xmlns:add="http://demo.iae.ws/address"
                  xmlns:name="http://demo.iae.ws/name">
   <soapenv:Header/>
   <soapenv:Body>
      <main:response>
         <add:name>
            <name:forename>Joe</name:forename>
            <name:surname>Blogs</name:surname>
         </add:name>
         <add:location>12 Foo Street</add:location>
         <add:location>Bar Town</add:location>
         <add:location>Bazville</add:location>
         <add:code>NW1 123</add:code>
      </main:response>
   </soapenv:Body>
</soapenv:Envelope>

By loading the WSDL and these sample messages into a tool like soapUI we can test both services and clients as they're developed.

Java bindings

JAX-WS defines mechanisms for generating classes for mapping between Java types and XML messages. The bindings can be used in both server and client applications.

The wsimport tool in the JDK can be used for this purpose:

wsimport ../wscontract/src/main/resources/wsdl/MaintainAddress.wsdl
    -verbose
    -wsdllocation wsdl/MaintainAddress.wsdl
    -keep -s src/main/java
    -Xnocompile
    -b jaxb/xsdbindings.xml -b jaxb/wsdlbindings.xml

There are variants of this tool as Ant tasks, Maven plugins and IDE tooling.

JAX-WS utilises JAXB for the mappings and it is possible to specify your own bindings for the WSDL and any dependent XSDs. This is useful for resolving type collisions, especially when working with a large number of WSDLs where maintainers have been lax in keeping XML namespaces unique. XPath expressions are used for applying rules to the mapping process.

In the sample application, the address XML type in the http://demo.iae.ws/address namespace is remapped to a CustomerAddress class rather than the default name Address.

The web service

The web service code can just be added to a regular WAR application. No web.xml entries are needed; the Java EE 6 container will pick up the @WebService annotation via introspection; further annotations are defined in the MaintainAddress interface generated as part of the binding code.

@WebService(endpointInterface = "demo.ws.service.MaintainAddress", serviceName = "MaintainAddress")
public class MaintainAddressImpl implements MaintainAddress {
  @Override
  public CustomerAddress lookupAddress(Name parameters) {
    String forename = assertNotEmpty("forename", parameters.getForename());
    String surname = assertNotEmpty("surname", parameters.getSurname());
    CustomerAddress address = new CustomerAddress();
    Name name = new Name();
    name.setForename(forename);
    name.setSurname(surname);
    address.setName(name);
    List<String> location = address.getLocation();
    location.add("1 " + forename + "'s Street");
    location.add(forename + "'s Town");
    address.setCode("123");
    return address;
  }

  private String assertNotEmpty(String id, String s) {
    if (s == null || s.isEmpty()) {
      try {
        SOAPFactory fac = SOAPFactory.newInstance();
        SOAPFault sf =
            fac.createFault("Illegal argument: " + id, new QName(
                SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE, "Client"));
        throw new SOAPFaultException(sf);
      } catch (SOAPException e) {
        throw new IllegalStateException(e);
      }
    }
    return s;
  }
}

When built with Maven, the sample WAR file will be called wsboundservice-0.0.1-SNAPSHOT.war. Deployed to my local test servers, the endpoint was http://localhost:8080/wsboundservice-0.0.1-SNAPSHOT/MaintainAddress and this URL is hardcoded in the sample client code. If you run the client code, you may have to change these values depending on where/how you deploy the service.

JBoss AS 6

The code works fine on Glassfish 3 but the statement "No web.xml entries are needed" doesn't apply on JBoss 6. The JBoss AS 6 WebServices Guide describes how to add the web service as a servlet mapping in the web.xml. If this change is made, the service works as expected (though note it will no longer work on Glassfish.)

This issue is being tracked in JBWS-3276.

Client bindings

This simple client application calls the web service and prints out the returned address:

  public static void main(String[] args) {
    String endpoint =
        "http://localhost:8080/wsboundservice-0.0.1-SNAPSHOT/MaintainAddress";
    URL wsdl = LookupAddress.class.getClassLoader()
        .getResource("wsdl/MaintainAddress.wsdl");
    String namespace = "http://demo.iae.ws/MaintainAddress/";
    String localname = "MaintainAddress";
    MaintainAddress_Service service =
        new MaintainAddress_Service(wsdl, new QName(namespace, localname));
    MaintainAddress port = service.getMaintainAddressSOAP();
    BindingProvider bindingProvider = (BindingProvider) port;
    bindingProvider.getRequestContext()
        .put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, endpoint);

    Name name = new Name();
    name.setForename("Joe");
    name.setSurname("Blogs");
    try {
      CustomerAddress address = port.lookupAddress(name);
      for (String line : address.getLocation()) {
        System.out.println(line);
      }
      System.out.println(address.getCode());
    } catch (SOAPFaultException e) {
      System.err.println(e);
    } catch (WebServiceException e) {
      System.err.println(e);
    }
  }

Notes:

  • The standard Java 6 API includes JAX-WS client support, so there are no special dependencies.
  • The client uses the same binding code API the server uses.
  • The default, zero-argument constructor for MaintainAddress_Service uses the annotated WSDL location. This was set by the wsimport ... -wsdllocation wsdl/MaintainAddress.wsdl flag, but it does't point to a real URL. Tutorials often use a remote http://... location for this. The downside is that the client is trusting the remote server with the contract definition and it causes network I/O when the application starts. The sample code loads the WSDL from the local classpath.
  • By default, the endpoint the client talks to will be picked up from the WSDL (which in the sample is a dummy value). This is a problem if you want to point your client at different hosts without editing the WSDL and rebuilding your binaries. The ENDPOINT_ADDRESS_PROPERTY property is used to change the endpoint dynamically.

Server-side web service thread safety is comparable to the servlet API. On the client side, greater care is required. JAX-WS implementations (of which there are a number) vary in how thread-safe their client code is. For portable code, do not use the same object instances (e.g. ports) concurrently.

Dynamic clients

The binding code is convenient if you want to work with bindings to Java types, but it is possible to use JAX-WS without it. The dynamic API leverages the W3C DOM API, so you can leverage the XML processing APIs that work with it.

The sample code below creates a request from an XML template file, populates it using XPath and transforms the service response to HTML using an XSLT stylesheet.

public class DynamicLookup {

  /**
   * Sets the request parameters using XPath
   */
  private static void setNames(Node node, String forename, String surname) {
    XPath xpath = XPathFactory.newInstance()
        .newXPath();
    xpath.setNamespaceContext(new NamespaceContextMap("ns",
        "http://demo.iae.ws/name"));
    try {
      Node forenameElement =
          (Node) xpath.evaluate("//ns:forename", node, XPathConstants.NODE);
      forenameElement.setTextContent(forename);
      Node surnameElement =
          (Node) xpath.evaluate("//ns:surname", node, XPathConstants.NODE);
      surnameElement.setTextContent(surname);
    } catch (XPathExpressionException e) {
      throw new IllegalStateException(e);
    }
  }

  /**
   * Creates a request from an XML template
   */
  private static SOAPMessage createRequest(String forename, String surname)
      throws SOAPException, IOException {
    InputStream template =
        DynamicLookup.class.getResourceAsStream("/xml/client.xml");
    try {
      MessageFactory mf =
          MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
      SOAPMessage message = mf.createMessage(new MimeHeaders(), template);
      SOAPBody body = message.getSOAPBody();
      setNames(body, forename, surname);
      return message;
    } finally {
      template.close();
    }
  }

  /**
   * Transforms the XML response to HTML
   */
  private static void toHtml(Node node, OutputStream out)
      throws TransformerException, IOException {
    InputStream stylesheet =
        DynamicLookup.class.getResourceAsStream("/xml/tohtml.xslt");
    try {
      Transformer transformer = TransformerFactory.newInstance()
          .newTransformer(new StreamSource(stylesheet));
      transformer.transform(new DOMSource(node), new StreamResult(out));
    } finally {
      stylesheet.close();
    }
  }

  public static void main(String[] args) throws SOAPException, IOException,
      TransformerException {
    String endpoint =
        "http://localhost:8080/wsboundservice-0.0.1-SNAPSHOT/MaintainAddress";
    URL wsdl = DynamicLookup.class.getClassLoader()
        .getResource("wsdl/MaintainAddress.wsdl");
    String namespace = "http://demo.iae.ws/MaintainAddress/";
    Service service =
        Service.create(wsdl, new QName(namespace, "MaintainAddress"));
    QName portName = new QName(namespace, "lookupAddress");
    service.addPort(portName, SOAPBinding.SOAP11HTTP_BINDING, endpoint);
    Dispatch<SOAPMessage> dispatch =
        service.createDispatch(portName, SOAPMessage.class,
            Service.Mode.MESSAGE);

    SOAPMessage request = createRequest("Joe", "Blogs");
    try {
      SOAPMessage response = dispatch.invoke(request);
      toHtml(response.getSOAPBody(), System.out);
    } catch (SOAPFaultException e) {
      System.err.println(e);
    } catch (WebServiceException e) {
      System.err.println(e);
    }
  }
}

If you're wondering about the benefits of using JAX-WS over some other HTTP mechanism, JAX-WS will save you the trouble of converting SOAP errors to exceptions.

This code uses a NamespaceContext implementation from an earlier post on XPath.

Error handling

The sample web service checks for empty inputs; if an empty forename or surname is submitted, it throws an exception.

  private String assertNotEmpty(String id, String s) {
    if (s == null || s.isEmpty()) {
      try {
        SOAPFactory fac = SOAPFactory.newInstance();
        SOAPFault sf =
            fac.createFault("Illegal argument: " + id, new QName(
                SOAPConstants.URI_NS_SOAP_1_1_ENVELOPE, "Client"));
        throw new SOAPFaultException(sf);
      } catch (SOAPException e) {
        throw new IllegalStateException(e);
      }
    }
    return s;
  }

This will be returned as a SOAP fault message:

<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
   <S:Body>
      <S:Fault xmlns:ns4="http://www.w3.org/2003/05/soap-envelope">
         <faultcode>S:Client</faultcode>
         <faultstring>Illegal argument: forename</faultstring>
         <detail>
            <ns2:exception class="javax.xml.ws.soap.SOAPFaultException" note="To disable this feature, set com.sun.xml.ws.fault.SOAPFaultBuilder.disableCaptureStackTrace system property to false" xmlns:ns2="http://jax-ws.dev.java.net/">
               <message>Illegal argument: forename</message>
               <ns2:stackTrace>
                  <!-- stack trace elided -->
               </ns2:stackTrace>
            </ns2:exception>
         </detail>
      </S:Fault>
   </S:Body>
</S:Envelope>

This exception will be caught on the client as a SOAPFaultException. The Client fault code indicates that the cause of the error was known to have originated in the client. The SOAP 1.1 specification details the allowed fault codes. Any other exception thrown by your service code will be returned as a SOAP fault; for example, a NullPointerException in your code would be returned with a fault code of Server.

Clients should handle WebServiceException to take care of any other I/O, server or network errors. If a client uses JAX-WS asynchronous features, it should also handle ExecutionException. It is possible to add checked exceptions to your binding code by explicitly declaring a SOAP fault in the WSDL though you will always have to handle the other exception types detailed here.

Sample code

All the sources are available in a public Subversion repository.

Repository: http://illegalargumentexception.googlecode.com/svn/trunk/code/java/
License: MIT
Project: jaxws

Documentation

Web standards:

Java specifications:

Java APIs:

No comments:

Post a Comment

All comments are moderated