XSD Driven JAX-WS development practice
How
to create an extendable web service
When a web service needs to be
created, both logic and communication protocol have to be implemented. The main
focus during the development process is obviously given to the business logic
implementation. However, an experienced developer knows that he/she will be
maintaining this service at least a few times even when it goes to production.
There is no problem to change or modify the logic, but modifying the protocol
can cause very serious interoperability issues.
SOAP [Simple Object Access Protocol]
maintenance is even more complex because of the nature of SOAP which has the
interoperability feature as mandatory. When the developer has to extend the
service, the existing clients should not modify their software to use the
updated service. The client should not even notice any change.
There are two most commonly used
ways to create a SOAP web service - Java-WSDL and WSDL-Java. Java-WSDL is based
on the existing java classes and it makes them available to be used externally
as a web service. It’s the easiest way to create a web service. However, the
service becomes tightly coupled with the java classes and changing them later
can break the protocol for its consumers. WSDL-Java defines a SOAP protocol
first and only after the java classes are generated. This approach is better
for interoperability because the protocol is defined first. Also, it shows a
goal to maintain in order to keep it operable regardless how its implementation
looks like. WSDL-Java also requires a comprehensive knowledge of WSDL. Neither
approach is easy; both require good knowledge and experience to make
maintainable web servers.
There is another popular technique
which combines the two previous methods - JAX-WS. This method allows controlling SOAP protocol
over JAX-WS annotations. By using new or existing annotated java classes, the
developer can change the SOAP protocol.
SOAP is the most common approach to
business to business (B2B) XML transactions. XML is used over JMS or HTTP or
even over electronic mail. Every valid XML document should have its own schema:
DTD or XSD. DTD became an obsolete
standard and XSD is the most common XML schema definition. The service and its
client usually have XSD schemas since it’s a communication protocol between
client and server. This gives unified format of sending the data, their
description and validation.
Every software component should have
the input and output model (or one of them) and the black magic box (business
logic). Web service is such a component, with the exception that its models
have to be self–descriptive and extendable without harming its consumers.
Let’s have a look at how an
extendable web service could be built:
We
need to implement a weather service. This service should give weather data
based on the location.
The requirements are uncertain
because it’s a brand new service and there is only a main idea so far. It is a
very typical situation in an early stage of software development. Let’s make
the following judgments before commencing:
Based on the above requirements, we
can mock up the input and output XML models as a payload example with data.
<?xml
version="1.0" encoding="UTF-8"?>
<weatherRequest
xmlns="http://mydemo.org/weatherservice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">
<location xsi:type="locationByCityType">
<city>Punta Canna</city>
</location>
</weatherRequest>
Have a look at the location
XML tag. This is the subtype locationByCity and
it is an XML extension of the based type location. Basically, it is the same as
inheritance in java. The location is the base type, and its siblings could be locationByCity or locationByLatLon
or any other subtype. This introduces a little bit of overhead at the
beginning, but the service becomes extensible, backward compatible and
maintainable from day one.
Now let’s mock up the XML response:
<?xml
version="1.0" encoding="UTF-8"?>
<weatherResponse
xmlns="http://mydemo.org/weatherservice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">
<weatherConditions
xsi:type="weatherConditionsByFTemp">
<degrees>90</degrees>
</weatherConditions>
</weatherResponse>
The same idea remains here. We are going
to return a weather attribute, but so far there is only temperature. There is weatherConditions - a based type, and weatherConditionsByFTemp - the first subtype we are going to
implement.
Once we have these XMLs let’s create
the schema:
<?xml
version="1.0" encoding="UTF-8"?>
<schema xmlns="http://www.w3.org/2001/XMLSchema" targetNamespace="http://mydemo.org/weatherservice"
xmlns:tns="http://mydemo.org/weatherservice" elementFormDefault="qualified"
attributeFormDefault="unqualified">
<complexType
name="locationType">
</complexType>
<complexType
name="locationByCityType">
<complexContent>
<extension
base="tns:locationType">
<sequence>
<element
name="city" type="string"
/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="weatherRequest">
<complexType>
<sequence>
<element
name="location" type="tns:locationType" />
</sequence>
</complexType>
</element>
<complexType
name="weatherConditionsType">
</complexType>
<complexType
name="weatherConditionsByFTemp">
<complexContent>
<extension
base="tns:weatherConditionsType">
<sequence>
<element
name="degrees" type="float"
/>
</sequence>
</extension>
</complexContent>
</complexType>
<element name="weatherResponse">
<complexType>
<sequence>
<element
name="weatherConditions"
type="tns:weatherConditionsType"
/>
</sequence>
</complexType>
</element>
</schema>
Once the XSD schema is created, the unit
test can be implemented. There is an input and output model (two XMLs above),
so we have some data. JAX-WS JAXB will generate the necessary java classes from
the schema document. The unit test will be a service which transforms the input
java model into the output java model. It could be mocked up right in the unit
test class and moved to the service class later.
In order to generate the java
classes, let’s use a maven project, copy xml files into test/resources and our schema
to main/resources and use jaxb plugin
to generate the sources:
This is my pom.xml:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>demoservices</groupId>
<artifactId>weatherservice</artifactId>
<packaging>jar</packaging>
<version>0.0.1-SNAPSHOT</version>
<pluginRepositories>
<pluginRepository>
<id>maven2-repository.dev.java.net</id>
<name>Java.net Maven 2
Repository</name>
<url>http://download.java.net/maven/2</url>
</pluginRepository>
</pluginRepositories>
<dependencies>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.jvnet.jaxb2.maven2</groupId>
<artifactId>maven-jaxb2-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>generate</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.5</source>
<target>1.5</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
All the classes are created and
JAX-WS annotations are already in place. Now we need to create a service class
interface.
public
interface WeatherService {
WeatherResponse getWeatherByLocation(WeatherRequest weatherRequest);
}
At this stage we have the method
interface and test data, so it’s a good time to create the unit test
public
class
WeatherServiceTest {
private
class
TestResource<T> {
@SuppressWarnings("unchecked")
protected
T getObjectFromTestresource(JAXBContext
context, String xsd,
String xml) throws SAXException,
JAXBException {
Unmarshaller
unmarshaller = context.createUnmarshaller();
URL xsdUrl
= ClassLoader.getSystemResource(xsd);
URL xmlURL
= ClassLoader.getSystemResource(xml);
assertNotNull(xmlURL);
assertNotNull(xsdUrl);
// note: setting schema to null will turn validator
off
Schema schema
= SchemaFactory.newInstance(
XMLConstants.W3C_XML_SCHEMA_NS_URI).newSchema(xsdUrl);
unmarshaller.setSchema(schema);
return
(T) unmarshaller.unmarshal(xmlURL);
}
}
@Test
public
void
getWeatherByLocationTest() {
//
TODO:
Implement it
WeatherService
weatherService = null;
try {
JAXBContext
context;
context
= JAXBContext.newInstance(WeatherRequest.class);
WeatherRequest
weatherRequest = new TestResource<WeatherRequest>()
.getObjectFromTestresource(context, "weather.xsd",
"weatherRequest.xml");
context
= JAXBContext.newInstance(WeatherResponse.class);
WeatherResponse
weatherResponseMock = new TestResource<WeatherResponse>()
.getObjectFromTestresource(context, "weather.xsd",
"weatherResponse.xml");
WeatherConditionsByFTemp
tempMock = (WeatherConditionsByFTemp)
weatherResponseMock
.getWeatherConditions();
WeatherResponse
sResponse = weatherService
.getWeatherByLocation(weatherRequest);
WeatherConditionsByFTemp
temp = (WeatherConditionsByFTemp) sResponse
.getWeatherConditions();
assertTrue(temp.getDegrees() == tempMock.getDegrees());
} catch (JAXBException
e) {
fail(e.getMessage());
} catch (SAXException
e) {
fail(e.getMessage());
}
}
}
Let’s implement the interface:
public
class
WeatherServiceImpl implements WeatherService {
public
WeatherResponse getWeatherByLocation(WeatherRequest weatherRequest) {
WeatherResponse
response = new WeatherResponse();
LocationType
locationType = weatherRequest.getLocation();
if (locationType instanceof
LocationByCityType) {
LocationByCityType
locationByCityType = (LocationByCityType)
locationType;
if ("Punta Canna".equals(locationByCityType.getCity())) {
WeatherConditionsByFTemp
tmp = new WeatherConditionsByFTemp();
tmp.setDegrees(90);
response.setWeatherConditions(tmp);
}
}
return
response;
}
}
Note that the service is a generic
extendable service. Its input type is WeatherRequest.
It is the base type and if there is a subtype the workflow is different. By
defining new subtypes we can handle different scenarios under the same XML
protocol. And we can make sure that all the old and legacy client will work
perfectly on the newest implementation of the web service.
Let’s see now how the protocol could
be extended based on the subtypes.
First let’s mock up the request and
response:
<?xml
version="1.0" encoding="UTF-8"?>
<weatherRequest
xmlns="http://mydemo.org/weatherservice"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://mydemo.org/weatherservice weather.xsd">
<location xsi:type="locationByLatLng">
<lat>10</lat>
<lng>10</lng>
</location>
</weatherRequest>
Note, LocationByLatLng is a new subtype of Location.
And it has to be introduced into the XSD document.
<complexType
name="locationByLatLng">
<complexContent>
<extension
base="tns:locationType">
<sequence>
<element
name="lat" type="double"
/>
<element
name="lon" type="double" />
</sequence>
</extension>
</complexContent>
</complexType>
Maven process will generate source
again automatically for our input and output XML models. Then let’s modify the
unit test to desterilize these XML models into java.
@Test
public
void
getWeatherByLocationLatLngTest() {
//
TODO:
Implement it
WeatherService
weatherService = new WeatherServiceImpl();
try {
JAXBContext
context;
context
= JAXBContext.newInstance(WeatherRequest.class);
WeatherRequest
weatherRequest = new TestResource<WeatherRequest>()
.getObjectFromTestresource(context, "weather.xsd",
"weatherLatlonRequest.xml");
context
= JAXBContext.newInstance(WeatherResponse.class);
WeatherResponse
weatherResponseMock = new TestResource<WeatherResponse>()
.getObjectFromTestresource(context, "weather.xsd",
"weatherResponse.xml");
WeatherConditionsByFTemp
tempMock = (WeatherConditionsByFTemp)
weatherResponseMock
.getWeatherConditions();
WeatherResponse
sResponse = weatherService
.getWeatherByLocation(weatherRequest);
WeatherConditionsByFTemp
temp = (WeatherConditionsByFTemp) sResponse
.getWeatherConditions();
assertTrue(temp.getDegrees() == tempMock.getDegrees());
} catch (JAXBException
e) {
fail(e.getMessage());
} catch (SAXException
e) {
fail(e.getMessage());
}
}
And the last step is to modify our
implementation of the service
if (locationType instanceof
LocationByLatLng) {
LocationByLatLng
locationByLatLng = (LocationByLatLng)
locationType;
if (locationByLatLng.getLat()==10 && locationByLatLng.getLng()==10) {
WeatherConditionsByFTemp
tmp = new WeatherConditionsByFTemp();
tmp.setDegrees(90);
response.setWeatherConditions(tmp);
}
There are now two different ways of
using the web service. One is from our initial implementation (legacy) and the
second from the current one (latest). The goal has been achieved. Communication
protocol can be extended and be fully compatible with the previous versions.
Following this practice the
developer can introduce new service capabilities faster and without bringing
any trouble to existing consumers.
The source code is available at http://shpuroff.com/weatherservice.zip
December 2011
Alex Shpurov
Principal Software Designer
Route1, Toronto ON