JAX-WS is the standard API of the Java platform not only for the creation of web service providers but also for building web service clients. In the following I will show how to build and test a web service client using the JAX-WS reference implementation (RI) in conjunction with the Spring framework.
The example: A client for a simple shop web service
As example a simple client for an exemplary shop web service shall be built, that allows to search for products by their id. The WSDL looks as follows (this is a slightly simplified version of the WSDL from the shop service example which is part of the maven-instant-ws project):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
<?xml version="1.0" encoding="UTF-8"?>
<definitions
name="Products"
targetNamespace="http://www.gmorling.de/jaxwsonspring/products"
xmlns="http://schemas.xmlsoap.org/wsdl/"
xmlns:tns="http://www.gmorling.de/jaxwsonspring/products"
xmlns:products="http://www.gmorling.de/jaxwsonspring/products/types"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/">
<types>
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema">
<xsd:import namespace="http://www.gmorling.de/jaxwsonspring/products/types"
schemaLocation="products.xsd" />
</xsd:schema>
</types>
<message name="GetProductByIdRequestMessage">
<part name="body" element="products:GetProductByIdRequest" />
</message>
<message name="GetProductByIdResponseMessage">
<part name="body" element="products:GetProductByIdResponse" />
</message>
<portType name="ProductsPortType">
<operation name="GetProductById">
<input message="tns:GetProductByIdRequestMessage" />
<output message="tns:GetProductByIdResponseMessage" />
</operation>
</portType>
<binding name="ProductsSoapBinding" type="tns:ProductsPortType">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" />
<operation name="GetProductById">
<soap:operation soapAction="GetProductById" />
<input>
<soap:body use="literal" />
</input>
<output>
<soap:body use="literal" />
</output>
</operation>
</binding>
<service name="ProductsService">
<port name="ProductsPort" binding="tns:ProductsSoapBinding">
<soap:address location="TODO" />
</port>
</service>
</definitions>
|
The WSDL basically defines a single operation, GetProductById, that takes a GetProductByIdRequest object and returns a GetProductByIdResponse object. These types are specified in a separate XML schema:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
<?xml version="1.0" encoding="UTF-8"?>
<schema targetNamespace="http://www.gmorling.de/jaxwsonspring/products/types"
xmlns="http://www.w3.org/2001/XMLSchema" xmlns:products="http://www.gmorling.de/jaxwsonspring/products/types">
<!-- GetProductById -->
<element name="GetProductByIdRequest">
<complexType>
<sequence>
<element name="Id" type="int" />
</sequence>
</complexType>
</element>
<element name="GetProductByIdResponse">
<complexType>
<sequence>
<element type="products:Product" name="Product" minOccurs="0" />
</sequence>
</complexType>
</element>
<!-- General-purpose types -->
<complexType name="Product">
<sequence>
<element name="Id" type="int" />
<element name="Name" type="string" />
<element name="Price" type="decimal" />
<element name="Size" type="string" minOccurs="0" />
</sequence>
</complexType>
</schema>
|
The request type is just a wrapper for an int parameter representing a product id, while the response type contains a Product element, which itself has a name, price etc.
Generating proxy classes
JAX-WS provides a tool called wsimport which takes the WSDL of a web service and generates proxy classes for the WSDL's service and port definitions. These can then be used to access the web service endpoint.
With the help of the JAX-WS Maven plugin the wsimport tool can easily be used in Maven based projects. Just configure the wsimport goal of the plugin in your project's pom.xml as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
...
<build>
...
<plugins>
...
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>jaxws-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>wsimport</goal>
</goals>
</execution>
</executions>
<configuration>
<wsdlDirectory>${basedir}/src/main/resources/wsdl</wsdlDirectory>
</configuration>
</plugin>
...
</plugins>
...
</build>
...
|
The WSDL to be processed can either be fetched directly from the actual web service endpoint or from a local directory (by specifying the wsdlDirectory property as shown in the example). I recommend to stick with the latter approach. That way your project can be built even if the service to be accessed is not available from your development environment.
During the "generate-sources" build lifecycle phase the plugin will generate
- proxy classes for all service and port type declarations contained within the WSDL files in the specified directory
- JAXB binding classes for all schema types used in the operations of that services
Using the proxy classes generated from the shop service WSDL it is not too hard to build a simple shop client:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
package de.gmorling.jaxwsonspring;
import javax.xml.ws.BindingProvider;
import de.gmorling.jaxwsonspring.shop.products.ProductsPortType;
import de.gmorling.jaxwsonspring.shop.products.ProductsService;
import de.gmorling.jaxwsonspring.shop.products.types.GetProductByIdRequest;
public class ShopClient {
private final static String END_POINT_URL = "http://localhost:8080/shopserver/Products";
public String getProductNameByid(int productId) {
GetProductByIdRequest request = new GetProductByIdRequest();
request.setId(productId);
ProductsService productsService = new ProductsService();
ProductsPortType productsPort = productsService.getProductsPort();
((BindingProvider) productsPort).getRequestContext().put(
BindingProvider.ENDPOINT_ADDRESS_PROPERTY, END_POINT_URL);
return productsPort.getProductById(request).getProduct().getName();
}
}
|
All you have to to do is to instantiate the ProductsService, retrieve the ProductsPort from it, set the endpoint address and call any of the port's operations.
Portability issues
This works basically pretty well, but I ran into a problem, as I tried to execute my project's binary on a different machine suddenly the WSDL of the service couldn't be found. Searching the web a little bit I found out, that JAX-WS RI parses the WSDL each time a Service instance is created.
The WSDL's location is taken from an annotation of the generated Service class (ProductsService in this case), where it is unfortunately specified as an absolute path. That causes the Service initialization to fail if the project is moved to another directory or to another system, where the WSDL doesn't exist at the expected location.
This problem can be solved by specifying the WSDL location relatively when instantiating the Service class. This complicates the process of obtaining web service port references a little bit, therefore I thought it might be a good idea to make use of dependency injection (DI). That way the rather ugly API for setting the endpoint address can be hidden from the caller as well and DI finally allows to inject mock port implementations in unit tests.
Spring's JaxWsPortProxyFactoryBean
At first I considered building a custom solution using the Spring DI container, but then I stumbled upon Spring's JaxWsPortProxyFactoryBean that already provides DI services for JAX-WS client ports.
Using that bean a JAX-WS port can be configured within a Spring application context as follows:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="productsPort" class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
<property name="serviceInterface" value="de.gmorling.jaxwsonspring.shop.products.ProductsPortType" />
<property name="wsdlDocumentUrl" value="wsdl/products.wsdl" />
<property name="namespaceUri" value="http://www.gmorling.de/jaxwsonspring/shop/products" />
<property name="serviceName" value="ProductsService" />
<property name="endpointAddress" value="http://localhost:8080/jaxws-on-spring-server/Products" />
</bean>
</beans>
|
So basically you have to configure the type of the port to be injected (the attribute name "serviceInterface" seams a bit irritating to me), the URL of the WSDL (e.g. identifying a classpath resource), namespaceUri and serviceName as specified in the WSDL file and finally the address of the endpoint to be used.
A word on thread safety
When configured as shown above Spring will use the singleton scope for the productsPort bean. That means the bean will exist only once and potentially be accessed from multiple threads at the same time.
Unfortunately the JAX-WS specification doesn't clearly say whether the generated service and port classes are thread-safe or not. Therefore one generally should assume that they aren't.
But after some searching, I found a comment by one of the JAX-WS RI developers stating, that the proxy classes of JAX-WS RI are thread-safe, as long as the request context of a port instance isn't modified. Assuming that no user of the bean modifies its request context (e.g. by re-setting the endpoint address) we are fine with the singleton scope for now.
Injecting JAX-WS client ports
Now let's rewrite the ShopClient class by having the productsPort bean injected into it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
package de.gmorling.jaxwsonspring;
import javax.annotation.Resource;
import de.gmorling.jaxwsonspring.shop.products.ProductsPortType;
import de.gmorling.jaxwsonspring.shop.products.types.GetProductByIdRequest;
public class ShopClient {
@Resource
private ProductsPortType productsPort;
public String getProductNameByid(int productId) {
GetProductByIdRequest request = new GetProductByIdRequest();
request.setId(productId);
return productsPort.getProductById(request).getProduct().getName();
}
}
|
Of course we need to configure ShopClient as Spring bean as well:
1
2
3
|
...
<bean id="shopClient" class="de.gmorling.jaxwsonspring.ShopClient" />
...
|
When creating the shopClient bean Spring will process the @Resource annotation (which stems from JSR 250: "Common Annotations for the JavaTM Platform") by populating the productsPort field with the bean of the same name.
Mocking web service requests in unit tests
Leveraging dependency the ShopClient class is greatly simplified now. As last step let's create a unit test for it.
To do so we should work with a mock implementation of the ProductsPortType interface. Working with a mock instead of accessing the real shop web service does not only increase the performance of the unit test. It also ensures, that the test result isn't dependent on the service's availability or its proper functioning.
For creating the mock we will use the freely available Mockito framework in the following (alternatively we could work with EasyMock or JMockit).
In the Spring application context for the unit test we specify that the productsPort bean shall be created by calling the method org.mockito.Mockito#mock(), which expects the class object of the object to be mocked as parameter:
1
2
3
4
5
6
7
8
9
10
11
12
|
<?xml version="1.0" encoding="UTF-8"?>
<beans
xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="productsPort" class="org.mockito.Mockito" factory-method="mock" >
<constructor-arg index="0" value="de.gmorling.jaxwsonspring.shop.products.ProductsPortType" />
</bean>
<bean id="shopClient" class="de.gmorling.jaxwsonspring.ShopClient" />
</beans>
|
Next we have to define, how the mock shall behave, when it is called by the code under test. For doing so we inject the productsPort bean into our test class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package de.gmorling.jaxwsonspring;
import static org.junit.Assert.*;
import static org.mockito.Mockito.*;
import java.math.BigDecimal;
import javax.annotation.Resource;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import de.gmorling.jaxwsonspring.shop.products.ProductsPortType;
import de.gmorling.jaxwsonspring.shop.products.types.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class ShopClientTest {
@Resource
private ProductsPortType productsPort;
@Resource
private ShopClient shopClient;
@Before
public void instructMock() {
GetProductByIdResponse response = new GetProductByIdResponse();
Product product = new Product();
product.setId(1);
product.setName("Jeans-Hose");
product.setPrice(new BigDecimal("89.99"));
product.setSize("L");
response.setProduct(product);
when(productsPort.getProductById(
any(GetProductByIdRequest.class))).thenReturn(response);
}
@Test
public void getProductName() {
assertEquals("Jeans-Hose", shopClient.getProductNameByid(1));
}
}
|
In the instructMock() method we first create a sample response object. Then we specify, that whenever the getProductById() of the mock is called, this response object shall be returned.
As the productsPort bean has singleton scope, the same instance of the bean will be injected into the shopClient bean. That way the shopClient bean will finally return the expected product name within the actual test method.
Conclusion
JAX-WS is a powerful API not only for the creation of web service providers but also for building web service clients. Unfortunately the devil is in the details when not handled properly, JAX-WS will try to read WSDL files using absolute file pathes and setting end point addresses is not very intuitive as well.
Luckily the Spring framework comes to the rescue by enabling the creation of web service ports using dependency injection. That way obtaining port references is not only greatly simplified, it also allows mocking actual web service requests within unit tests.
The complete source code from this post can be downloaded here. As always I'd be happy about any comments and ideas for improvement.