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.
39 comments:
Very nice.
About your portability issue, NetBeans 6.7 creates a local copy of the WSDL and maps the remote reference to it. Would this have helped? See the DZone article.
Hi,
thanks for your feedback and pointing to the article.
Using a catalog file surely works, though I still would prefer, if no absolute path names were involved at all, which would render the mapping unnecessary.
Greets, Gunnar
Hi Gunnar,
thanks for point this out, recently used this for one of my applications: http://www.ralfebert.de/blog/java/campaign_monitor_java_spring/
Greetings,
Ralf
Ralf, great to hear, that the post was helpful for your.
This is extremely helpful! I am using as a the base for a new project.
BTW, a few things are out of date... I would be happy to update them.
hamish, thanks for your feedback. Could you go a bit more into detail, which part you think is outdated? I'd update it then.
Great article. Just what I was looking for.
Hi, is there a way to enable logging for SOAP request/response using this approach. I was searching a lot, but I didn't find any solutions.
Thanks,
Nikola
Have you looked at Metro's documentation already:
https://jax-ws.dev.java.net/guide/Logging.html
This article has been very helpful. Thanks so much!
Is there any way to autogenerate the serviceInterface that I must use for the JaxWsPortProxyFactoryBean's bean?
Thanks in advance, great article.
Guthikonda, have you searched for the missing dependencies on the web? Maybe you have to add some repo (e.g. java.net) to your settings.xml or POM. I'll update the project when I'm finding the time, just not sure, when this will be.
Mounisha, seems like your are missing JAX-WS on your classpath. Either run on Java 6 or add an implementation such as CXF, Axis or Metro to you project.
Guthikonda: I updated the sample project (current Maven dependencies etc.) and put it to GitHub. You can find it here. Just don't forget the JBoss and java.net Maven repositories to your settings.xml.
Hi Gunnar,
very very Thanks for helping hands. i guess in the updated sample(prev attachment --jagadeesh) project have lot of changes so can u please explain me how run and flow of the program please.
Thanks
siri
Anonymous: the project at GitHub is pretty much the same as the code listings in this post.
It contains two tests:
* ShopClientMockTest sets up a mock for the client's port using Mockito and tests the client against this mock
* ShopClientIntegrationTest launches a mock server on localhost for the client's WSDL using the JAX-WS endpoint API and tests two clients (one using plain JAX-WS, one using Spring's JAX-WS client integration) against this mock server
Both tests are set up using Spring, so you should have a look at the application contexts used to get an understanding of the wiring.
Hi Gunnar,
After a long search i got this cake.
please explain mvn commands to run this application.i am new to maven.i have one more doubt.. before you running unit tests r u starting jetty server and after completion of running unit tests you are stoping?
txd
java.lang.NoClassDefFoundError: com/sun/tools/ws/Invoker
at org.codehaus.mojo.jaxws.WsImportMojo.wsImport(WsImportMojo.java:273)
at org.codehaus.mojo.jaxws.WsImportMojo.processLocalWsdlFiles(WsImportMo
jo.java:235)
at org.codehaus.mojo.jaxws.WsImportMojo.execute(WsImportMojo.java:191)
at org.codehaus.mojo.jaxws.MainWsImportMojo.execute(MainWsImportMojo.jav
a:15)
at org.apache.maven.plugin.DefaultPluginManager.executeMojo(DefaultPlugi
nManager.java:490)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoals(Defa
ultLifecycleExecutor.java:694)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalWithLi
fecycle(DefaultLifecycleExecutor.java:556)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoal(Defau
ltLifecycleExecutor.java:535)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeGoalAndHan
dleFailures(DefaultLifecycleExecutor.java:387)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.executeTaskSegmen
ts(DefaultLifecycleExecutor.java:348)
at org.apache.maven.lifecycle.DefaultLifecycleExecutor.execute(DefaultLi
fecycleExecutor.java:180)
at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:328)
at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:138)
at org.apache.maven.cli.MavenCli.main(MavenCli.java:362)
at org.apache.maven.cli.compat.CompatibleMain.main(CompatibleMain.java:6
0)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.
java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAcces
sorImpl.java:25)
at java.lang.reflect.Method.invoke(Method.java:585)
at org.codehaus.classworlds.Launcher.launchEnhanced(Launcher.java:315)
at org.codehaus.classworlds.Launcher.launch(Launcher.java:255)
at org.codehaus.classworlds.Launcher.mainWithExitCode(Launcher.java:430)
at org.codehaus.classworlds.Launcher.main(Launcher.java:375)
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 2 seconds
[INFO] Finished at: Sat Jan 01 04:00:06 IST 2011
[INFO] Final Memory: 6M/11M
[INFO] ------------------------------------------------------------------------how to solve the above issue plz explain.
@Mounisha: Just open the project in the IDE of your choice and run the tests I mentioned. As I wrote the integration test itself publishes an endpoint for the service and runs the client against it, so there is no need to manually start Jetty or any other web container.
@Naidu: Seems like you are missing JAX-WS. Either run the project on Java 6 (which comprises JAX-WS) or add the dependency to your setup.
Hi Gunnar how to solve below issue?
SEVERE: WSSERVLET11: failed to parse runtime descriptor: java.lang.LinkageError:
JAXB 2.0 API is being loaded from the bootstrap classloader, but this RI (from
jar:file:/C:/Documents%20and%20Settings/hari.HARI-19A1A35ECE/.m2/repository/com/
sun/xml/bind/jaxb-impl/2.1.9/jaxb-impl-2.1.9.jar!/com/sun/xml/bind/v2/model/impl
/ModelBuilder.class) needs 2.1 API. Use the endorsed directory mechanism to plac
e jaxb-api.jar in the bootstrap classloader. (See http://java.sun.com/j2se/1.5.0
/docs/guide/standards/)
java.lang.LinkageError: JAXB 2.0 API is being loaded from the bootstrap classloa
der, but this RI (from jar:file:/C:/Documents%20and%20Settings/hari.HARI-19A1A35
ECE/.m2/repository/com/sun/xml/bind/jaxb-impl/2.1.9/jaxb-impl-2.1.9.jar!/com/sun
/xml/bind/v2/model/impl/ModelBuilder.class) needs 2.1 API. Use the endorsed dire
ctory mechanism to place jaxb-api.jar in the bootstrap classloader. (See http://
java.sun.com/j2se/1.5.0/docs/guide/standards/)
at com.sun.xml.bind.v2.model.impl.ModelBuilder.(ModelBuilder.jav
a:173)
at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getTypeInfoSet(JAXBContex
tImpl.java:432)
Mounisha, just do as written in the stack trace you posted: Add the current JAXB JARs to you JVM's endorsed dir. I really recommend to have a look at JAX WS RI's reference documentation, this and similar questions should all be answered there.
Mounisha: as I wrote before, please refer to the reference guides for JAX-WS RI and it's Maven plugin, all your questions are answered there.
Also please understand that I'm maintaining this blog in my spare time and while I really like to help in understanding the contents presented here, I don't have the time to provide assistance on the general usage of the technologies or frameworks discussed. You should find plenty of help in reference guides, forums, mailing lists etc.
Hi Gunnar Morling,
Thanks for sharing your knowledge.I have question,Is this will support (jax-ws.xml)single endpoint interface will serve multiple webservice calls?. for example in your article you implemented one req and response in xsd. is this will work for multiple request and responses with single xsd?
Nice article, thanks for sharing.
Very useful thanks
Thanks a lots for this time saving article. Very useful.
If you are using JAXB then the JAXBContext object must be single instance. The Service Object generated by wsimport must also be a single instance.
Failure to do so can be a major memory leak. I've tested on Tomcat and iPlanet web server with Spring 3.1 and 3.2 and this is always the case.
I am using JAXBContext and my specific Service object directly, not wrapping in spring. So if you code inline there is a major issue, upwards of 1 million classes after about 3000 service calls cannot be recycled.
This is a known issue. Put these two objects as statics and no issues.
Food for thought.
Very detailed explanation.
Thanks a bunch from all the newbies like me!
very nice. for more java examples, visit http://java2novice.com site
hey guy, i have a trouble. in my wsdl i have two operation but the plugin just take the first operation to the interfaz and the second operation not. do you know why??
tanks
Good Work man.... Thank you for helping me mock this stuff..
Thanks for the article. Useful one will share it with others.
Post a Comment