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
- Java bindings
- The web service
- Client bindings
- Dynamic clients
- Error handling
- Sample code
- Documentation
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 thewsimport ... -wsdllocation wsdl/MaintainAddress.wsdl
flag, but it does't point to a real URL. Tutorials often use a remotehttp://...
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 API for XML-Based Web Services (JAX-WS) 2.2 (MR3)
- Web Services for Java EE, Version 1.3 (MR2)
- Java Platform, Enterprise Edition 6 (Java EE 6) Specification
- Java Servlet 3.0 Specification
Java APIs:
No comments:
Post a Comment
All comments are moderated