Using the contract-first approach to define a web service offers some advantages in contrast to the code-first approach.
In the following tutorial we’re going to take a look at some details of this approach and we’re going to implement a real SOAP service using JAX-WS, Maven and the Eclipse IDE.
Finally we’re going to run our service implementation on an embedded Jetty instance and we’re going to take a look at soapUI and how to test our service using this neat tool.
Prerequisites
For the following tutorial you’re will need to have the following software installed …
-
soapUI (installation details follow below)
Contract-First vs Code-First
First some thoughts on the different approach in the development of a new webservice or simply said – what is the advantage of using contract-first?
-
It’s independent of your programming language used .. perhaps the server and client are created using different programming languages or the decision for a specific language has not been made yet
-
It is more descriptive than a generated WSDL file, you’re able to apply more restrictions and rules here that you otherwise needed to implement in some method in your concrete programming language
-
If the server and the client part are implemented by different teams, both sides are able to begin implementing quickly
-
So what’s the disadvantage of contract first? Creating a SOAP web-service using JAX-WS and some annotations it is reeeelly easy and done quick just as described in my following article: “https://www.hascode.com/2010/09/creating-a-soap-service-using-jax-ws-annotations/[Creating a SOAP Service using JAX-WS Annotations]“
The Goal
The goal of this tutorial is to create a SOAP web-service following the contract-first approach. This means we’re going to define the service interface using xml schema first and create stub classes from the WSDL afterwards.
In this tutorial we’re going to implement a book service that allows its clients to query for books.
Creating a new project
Like in the rest of my other tutorials we’re going to create a “mavenized” project first …
-
Create a new, simple Maven project in Eclipse
-
Otherwise execute the following line in your console
mvn archetype:generate
Creating the schema
Now that we’ve got a project we’re going to define the XML schema for our web-service’s request and responses..
We’re using the Eclipse schema editor here – it’s really helpful to quickly create a schema.
-
We’re creating a new schema file in src/main/resources/schema named bookService.xsd in Eclipse via “New > Other > XML Schema File”
-
The final schema file looks like this
<?xml version="1.0" encoding="UTF-8"?> <schema xmlns="http://www.w3.org/2001/XMLSchema" xmlns:tns="https://www.hascode.com/bookService/1.0" targetNamespace="https://www.hascode.com/bookService/1.0" elementFormDefault="qualified"> <element name="BookServiceRequest" type="tns:BookServiceRequestType"> </element> <complexType name="BookServiceRequestType"> <sequence> <element name="limit" type="int"/> </sequence> </complexType> <complexType name="BookServiceResponseType"> <sequence> <element name="book" type="tns:BookType" maxOccurs="unbounded" minOccurs="0"/> </sequence> </complexType> <element name="BookServiceResponse" type="tns:BookServiceResponseType"> </element> <complexType name="BookType"> <sequence> <element name="title" type="string" maxOccurs="1" minOccurs="1"/> <element name="author" type="string" maxOccurs="1" minOccurs="1"/> <element name="published" type="date" maxOccurs="1" minOccurs="1"/> </sequence> </complexType> </schema>
-
This is how the schema types look like in my schema editor
Creating the WSDL
Now that we have defined our DTO types and the SOAP request and response type, we’re going to create a valid WSDL file using the xml schema …
-
We’re creating a new file named bookService.wsdl in src/main/resources/wsdl
<?xml version="1.0" encoding="UTF-8"?> <wsdl:definitions xmlns:tns="https://www.hascode.com/bookService/1.0" xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" name="bookService" targetNamespace="https://www.hascode.com/bookService/1.0"> <wsdl:types> <xsd:schema targetNamespace="https://www.hascode.com/bookService/1.0"> <xsd:import schemaLocation="../schema/bookService.xsd" namespace="https://www.hascode.com/bookService/1.0"/> </xsd:schema> </wsdl:types> <wsdl:message name="BookServiceRequest"> <wsdl:part name="BookServiceRequest" element="tns:BookServiceRequest"/> </wsdl:message> <wsdl:message name="BookServiceResponse"> <wsdl:part name="BookServiceResponse" element="tns:BookServiceResponse"/> </wsdl:message> <wsdl:portType name="BookServicePortType"> <wsdl:operation name="fetchBooks"> <wsdl:input name="BookServiceRequest" message="tns:BookServiceRequest"/> <wsdl:output name="BookServiceResponse" message="tns:BookServiceResponse"/> </wsdl:operation> </wsdl:portType> <wsdl:binding name="BookServiceBinding" type="tns:BookServicePortType"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="fetchBooks"> <soap:operation style="document" soapAction="https://www.hascode.com/bookService/fetchBooks"/> <wsdl:input name="BookServiceRequest"> <soap:body use="literal"/> </wsdl:input> <wsdl:output name="BookServiceResponse"> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="BookService"> <wsdl:port name="BookServicePort" binding="tns:BookServiceBinding"> <soap:address location="/service/bookService"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
-
If you want to you may use Eclipse’s WSDL editor here – it looks like this
Creating the Stub Classes using JAX-WS and Maven
Now we need to create stub classes from the WSDL file above. Luckily there is the JAX-WS Maven plugin that allows us to generate these files and add them to the project’s sources …
-
First we’re adding dependencies for javaee-web and jax-ws and configure the jax-ws maven plugin in our pom.xml – my final version looks like this one
<?xml version="1.0"?> <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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.hascode.tutorial</groupId> <artifactId>ws</artifactId> <version>0.0.1-SNAPSHOT</version> <name>Book Lookup Web Service</name> <description>hasCode.com Contract-First-Webservices Tutorial</description> <packaging>war</packaging> <repositories> <repository> <id>java.net2</id> <name>Repository hosting the jee6 artifacts</name> <url>http://download.java.net/maven/2</url> </repository> </repositories> <pluginRepositories> <pluginRepository> <id>java.net2</id> <name>Repository hosting the jee6 artifacts</name> <url>http://download.java.net/maven/2</url> </pluginRepository> </pluginRepositories> <dependencies> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>6.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-rt</artifactId> <version>2.2</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>2.0.2</version> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.1.1</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>jetty-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>build-helper-maven-plugin</artifactId> <executions> <execution> <id>add-source</id> <phase>generate-sources</phase> <goals> <goal>add-source</goal> </goals> <configuration> <sources> <source>${basedir}/target/generated/src/main/java</source> </sources> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxws-maven-plugin</artifactId> <version>1.12</version> <configuration> <wsdlDirectory>${basedir}/src/main/resources/wsdl</wsdlDirectory> <packageName>com.hascode.tutorial.ws.service</packageName> <keep>true</keep> <sourceDestDir>${basedir}/target/generated/src/main/java</sourceDestDir> </configuration> <executions> <execution> <goals> <goal>wsimport</goal> </goals> </execution> </executions> </plugin> </plugins> <finalName>bookService</finalName> </build> </project>
-
Now generate the sources from WSDL using
mvn generate-sources
-
The generated classes are stored in target/generated .. as an example – my generated BookType class looks like this
package com.hascode.ws.generated; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.XmlSchemaType; import javax.xml.bind.annotation.XmlType; import javax.xml.datatype.XMLGregorianCalendar; /** * <p>Java class for BookType complex type. * * <p>The following schema fragment specifies the expected content contained within this class. * * <pre> * <complexType name="BookType"> * <complexContent> * <restriction base="{http://www.w3.org/2001/XMLSchema}anyType"> * <sequence> * <element name="title" type="{http://www.w3.org/2001/XMLSchema}string"/> * <element name="author" type="{http://www.w3.org/2001/XMLSchema}string"/> * <element name="published" type="{http://www.w3.org/2001/XMLSchema}date"/> * </sequence> * </restriction> * </complexContent> * </complexType> * </pre> * * */ @XmlAccessorType(XmlAccessType.FIELD) @XmlType(name = "BookType", propOrder = { "title", "author", "published" }) public class BookType { @XmlElement(required = true) protected String title; @XmlElement(required = true) protected String author; @XmlElement(required = true) @XmlSchemaType(name = "date") protected XMLGregorianCalendar published; [.. setter / getter ..] }
Writing the Server Implementation
In the next step we’re going to write our own implementation of the service interface ..
-
In the package com.hascode.tutorial.ws.service we’re creating our implementation of the generated service interface BookServicePortType in a class named BookServicePortTypeImpl
package com.hascode.tutorial.ws.service; import java.util.GregorianCalendar; import javax.jws.WebService; import javax.xml.datatype.DatatypeConfigurationException; import javax.xml.datatype.DatatypeFactory; import com.hascode.ws.BookServicePortType; import com.hascode.ws.BookServiceRequestType; import com.hascode.ws.BookServiceResponseType; import com.hascode.ws.BookType; @WebService(endpointInterface="com.hascode.ws.service.BookServicePortType") public class BookServicePortImpl implements BookServicePortType { @Override public BookServiceResponseType fetchBooks( BookServiceRequestType bookServiceRequest) { final BookServiceResponseType response = new BookServiceResponseType(); for(int i=0;i< bookServiceRequest.getLimit();i++){ final BookType book = new BookType(); book.setAuthor("Elvis "+i); try { book.setPublished(DatatypeFactory.newInstance() .newXMLGregorianCalendar(new GregorianCalendar(2011,8,14))); } catch (DatatypeConfigurationException e) { } book.setTitle("Programming Java Edition #"+i); response.getBook().add(book); } return response; } }
-
As you see, we’re going to add as much books to the response as the user told us using the limit parameter in the request
Creating the Server Instance
Now we want to query for some books but we need a server first ..
-
Create the directory src/main/webapp/WEB-INF if it does not exist yet
mkdir -p src/main/webapp/WEB-INF
-
Add the following web.xml to the JAX-WS context listeners
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <session-config> <session-timeout> 40 </session-timeout> </session-config> <listener> <listener-class> com.sun.xml.ws.transport.http.servlet.WSServletContextListener </listener-class> </listener> <servlet> <description>JAX-WS endpoint</description> <display-name>The JAX-WS servlet</display-name> <servlet-name>jaxws</servlet-name> <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>jaxws</servlet-name> <url-pattern>/bookService</url-pattern> </servlet-mapping> </web-app>
-
In the next step we’re adding a file named sun-jaxws.xml to specify the endpoint and the mapping to our implementation from the preceding step
<?xml version="1.0" encoding="UTF-8"?> <endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0"> <endpoint name="bookService" implementation="com.hascode.tutorial.ws.service.BookServicePortImpl" url-pattern="/bookService" /> </endpoints>
-
Now we’re starting an embedded Jetty instance to run our server instance
mvn jetty:run
-
You should by now be able to get an overview of the running web-services by opening the following address in your browser _http://localhost:8080/bookService
-
Our web-services WSDL is now delivered at the following address: http://localhost:8080/bookService?wsdl and looks like this
<?xml version='1.0' encoding='UTF-8'?><!-- Published by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.2-12/14/2009 02:16 PM(ramkris)-. --><!-- Generated by JAX-WS RI at http://jax-ws.dev.java.net. RI's version is JAX-WS RI 2.2-12/14/2009 02:16 PM(ramkris)-. --> <definitions xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd" xmlns:wsp="http://www.w3.org/ns/ws-policy" xmlns:wsp1_2="http://schemas.xmlsoap.org/ws/2004/09/policy" xmlns:wsam="http://www.w3.org/2007/05/addressing/metadata" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://service.ws.tutorial.hascode.com/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://schemas.xmlsoap.org/wsdl/" targetNamespace="http://service.ws.tutorial.hascode.com/" name="BookServicePortImplService"> <import namespace="https://www.hascode.com/bookService/1.0" location="http://localhost:8080/bookService?wsdl=1" /> <binding xmlns:ns1="https://www.hascode.com/bookService/1.0" name="BookServicePortImplPortBinding" type="ns1:BookServicePortType"> <soap:binding transport="http://schemas.xmlsoap.org/soap/http" style="document" /> <operation name="fetchBooks"> <soap:operation soapAction="https://www.hascode.com/bookService/fetchBooks" /> <input> <soap:body use="literal" /> </input> <output> <soap:body use="literal" /> </output> </operation> </binding> <service name="BookServicePortImplService"> <port name="BookServicePortImplPort" binding="tns:BookServicePortImplPortBinding"> <soap:address location="http://localhost:8080/bookService" /> </port> </service> </definitions>
Installing soapUI
soapUI is my favourite tool for testing and debugging SOAP web services. You may use it as a standalone app or as Eclipse plugin .. the last one is what I am going to use here ..
Install soapUi using the following update site
Testing the Web-Service
Now that we finally got a running web service, we’re going to test it. soapUI is my favourite tool for this …
-
Start soapUI, select File > New soapUI Project. Enter what you want as project name but use the URL from above as WSDL: http://localhost:8080/bookService?wsdl
-
As you can see, soapUI has detected the methods available .. to start a new request, simply double-click on fetchBooks > Request 1 in the left navigation and enter a value of 4____in the request’s xml so that it looks like this one:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="https://www.hascode.com/bookService/1.0"> <soapenv:Header/> <soapenv:Body> <ns:BookServiceRequest> <ns:limit>4</ns:limit> </ns:BookServiceRequest> </soapenv:Body> </soapenv:Envelope>
Figure 2. Create a new request in soapUI -
Now click on the green icon “Submit request to specified endpoint url” and enjoy the result delivered from our web-service
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/"> <S:Body> <BookServiceResponse xmlns="https://www.hascode.com/bookService/1.0"> <book> <title>Programming Java Edition #0</title> <author>Elvis 0</author> <published>2011-09-14+02:00</published> </book> <book> <title>Programming Java Edition #1</title> <author>Elvis 1</author> <published>2011-09-14+02:00</published> </book> <book> <title>Programming Java Edition #2</title> <author>Elvis 2</author> <published>2011-09-14+02:00</published> </book> <book> <title>Programming Java Edition #3</title> <author>Elvis 3</author> <published>2011-09-14+02:00</published> </book> </BookServiceResponse> </S:Body> </S:Envelope>
Figure 3. SOAP Request result in soapUI
Creating a SOAP client using the JAX-WS Maven Plugin
If you’d like to know how to create a SOAP client from the WSDL in a few minutes using JAX-WS, please take a look at my tutorial “https://www.hascode.com/2010/04/create-a-soap-client-using-the-jax-ws-maven-plugin/[Create a SOAP client using the JAX-WS Maven Plugin]“.
Tutorial Sources
I have put the source from this tutorial on my GitHub repository – download it there or check it out using Mercurial:
git clone https://github.com/hascode/contract-first-webservice-tutorial.git
Troubleshooting
-
“Plugin execution not covered by lifecycle configuration: org.codehaus.mojo:jaxws-maven-plugin:1.12:wsimport (execution: default, phase: generate-sources) Maven Project Build Lifecycle Mapping Problem” – Downgrade your m2eclipse Plugin version or use the quick fix offered and tell m2eclipse shut up and ignore this “problem. Detailed information on this can be found on this wiki page.
<?xml version="1.0"?> <pluginManagement> <plugins> <!--This plugin's configuration is used to store Eclipse m2e settings only. It has no influence on the Maven build itself.--> <plugin> <groupId>org.eclipse.m2e</groupId> <artifactId>lifecycle-mapping</artifactId> <version>1.0.0</version> <configuration> <lifecycleMappingMetadata> <pluginExecutions> <pluginExecution> <pluginExecutionFilter> <groupId> org.codehaus.mojo </groupId> <artifactId> jaxws-maven-plugin </artifactId> <versionRange> [1.12,) </versionRange> <goals> <goal>wsimport</goal> </goals> </pluginExecutionFilter> <action> <ignore/> </action> </pluginExecution> </pluginExecutions> </lifecycleMappingMetadata> </configuration> </plugin> </plugins> </pluginManagement>