In this article we shortly describe how to setup Web Service using JAX-WS technology and maven. We will create a client project, a service project and a shared common project (which contains JAX-WS generated classes). The projects' directory structure will be:
Actually, when I've started the project, I have realised that using wsgen → WSDL → wsimport scheme is a bit complicated and excessive: you will be generated the same set of JAXB beans both for server and client, which you might want to share between Web Service and Java Web Client. So what we can do:
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building My DB Service
[INFO] task-segment: [clean, install]
[INFO] ------------------------------------------------------------------------
[INFO] [resources:resources]
...
[INFO] [compiler:compile]
[INFO] Compiling 4 source files to my-db-service\target\classes
[INFO] [jaxws:wsgen {execution: default}]
Note: ap round: 1
[ProcessedMethods Class: org.mycompany.service.db.service.MyDBServiceImpl]
[should process method: addConcept hasWebMethods: true ]
[endpointReferencesInterface: false]
[declaring class has WebSevice: true]
[returning: true]
[WrapperGen - method: addConcept(java.lang.String,java.lang.String,org.mycompany.service.db.service.ConceptType)]
[method.getDeclaringType(): org.mycompany.service.db.service.MyDBServiceImpl]
[requestWrapper: org.mycompany.service.db.service.jaxws.AddConcept]
[ProcessedMethods Class: java.lang.Object]
org\mycompany\service\db\service\jaxws\AddConcept.java
org\mycompany\service\db\service\jaxws\AddConceptResponse.java
org\mycompany\service\db\service\jaxws\MyDBServiceExceptionBean.java
Note: ap round: 2
[INFO] [war:war]
...Note, that due to limitation, that JSR-181 service endpoint scanner, implemented by Sun, does not implement merging of annotation properties of implementation and interface, we have to duplicate some of information in @WebService annotation of implementation class (in particular, add targetNamespace and mention endpointInterface):
package org.mycompany.service.db.service; import java.util.Arrays; import java.util.Date; import javax.jws.WebService; import org.mycompany.service.Concept; import org.mycompany.service.ConceptType; import org.mycompany.service.db.jaxws.MyDBService; import org.mycompany.service.db.jaxws.MyDBServiceException; @WebService(name = "MyDBService", endpointInterface = "org.mycompany.service.db.jaxws.MyDBService", targetNamespace = "http://service.mycompany.org/") public class MyDBServiceImpl implements MyDBService { /** * This method simply creates a new {@link Concept} instance based on passed arguments. */ @Override public Concept addConcept(String id, String creator, ConceptType type) throws MyDBServiceException { return new Concept(id, new Date(), new Concept.Types(Arrays.asList(type)), creator); } }
Now we are ready to do the reverse thing: generate beans and Web Service interface, complete the implementation and write a sample client for the service. In order to automate wsimport process we will use JAX-WS Maven Plugin.
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxws-maven-plugin</artifactId> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>wsimport</goal> </goals> <configuration> <sourceDestDir>${project.build.sourceDirectory}</sourceDestDir> <keep>true</keep> <!-- verbose> true</verbose --> <wsdlDirectory>wsdl</wsdlDirectory> <wsdlFiles> <wsdlFile>MyDBService.wsdl</wsdlFile> </wsdlFiles> </configuration> </execution> </executions> </plugin>
After execution we see:
> mvn install
[INFO] Scanning for projects...
[INFO] ------------------------------------------------------------------------
[INFO] Building My DB Common
[INFO] task-segment: [install]
[INFO] ------------------------------------------------------------------------
[INFO] [jaxws:wsimport {execution: default}]
[INFO] Processing: wsdl\MyDBService.wsdl
...
parsing WSDL...
generating code...
org\mycompany\service\AddConcept.java
org\mycompany\service\AddConceptResponse.java
org\mycompany\service\Concept.java
org\mycompany\service\ConceptType.java
org\mycompany\service\MyDBImplService.java
org\mycompany\service\MyDBService.java
org\mycompany\service\MyDBServiceException.java
org\mycompany\service\MyDBServiceException_Exception.java
org\mycompany\service\ObjectFactory.java
org\mycompany\service\package-info.java
...
OK, what we get as the result:
<jaxws:bindings version="2.0" ... > <jaxws:package name="org.mycompany.service.db.jaxws" /> </jaxws:bindings>
<jaxb:bindings version="1.0" ...> <jaxb:schemaBindings> <jaxb:package name="org.mycompany.service.db.jaxb" /> </jaxb:schemaBindings> </jaxb:bindings>
package org.mycompany.service.db.jaxb; import java.util.Calendar; import java.util.Date; import java.util.GregorianCalendar; import javax.xml.bind.DatatypeConverter; public class XSDateTimeCustomBinder { public static Date parseDateTime(String dateTime) { return DatatypeConverter.parseDate(dateTime).getTime(); } public static String printDateTime(Date date) { final Calendar cal = new GregorianCalendar(); cal.setTime(date); return DatatypeConverter.printDateTime(cal); } }
and use this class in JAXB javaType customization in jaxws/jaxb-binding.xml3):
<jaxb:bindings version="1.0" ...> <jaxb:globalBindings> <jaxb:javaType name="java.util.Date" xmlType="xsd:dateTime" parseMethod="org.mycompany.service.db.jaxb.XSDateTimeCustomBinder.parseDateTime" printMethod="org.mycompany.service.db.jaxb.XSDateTimeCustomBinder.printDateTime" /> </jaxb:globalBindings> </jaxb:bindings>
The complete file looks like this4):
<jaxb:bindings version="1.0" xmlns:jaxb="http://java.sun.com/xml/ns/jaxb" xmlns:xsd="http://www.w3.org/2001/XMLSchema" schemaLocation="../wsdl/MyDBService.xsd" node="/xsd:schema"> <jaxb:globalBindings> <jaxb:javaType name="java.util.Date" xmlType="xsd:dateTime" parseMethod="org.mycompany.service.db.jaxb.XSDateTimeCustomBinder.parseDateTime" printMethod="org.mycompany.service.db.jaxb.XSDateTimeCustomBinder.printDateTime" /> </jaxb:globalBindings> <jaxb:schemaBindings> <jaxb:package name="org.mycompany.service.db.jaxb" /> </jaxb:schemaBindings> </jaxb:bindings>
Also we need to mention it in pom.xml5):
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxws-maven-plugin</artifactId> <version>1.12</version> <executions> <execution> <phase>generate-sources</phase> <goals> <goal>wsimport</goal> </goals> <configuration> ... <bindingDirectory>jaxws</bindingDirectory> </configuration> </execution> </executions> </plugin>
The further customizations involve JAXB2 plugins. These plugins are injected into generation process and benefit from high flexibility of code manipulations to generate more programmer-friendly code6). Unfortunately, due to issue#45 it is not possible to inject JAXB plugins into JAX-WS using jaxws-maven-plugin. Also due to some conflicts with build-in Java 6 JAXB implementation, JAXB plugins do not work (see here). As a workaround I have reallocated most popular plugins to com.sun.tools.xjc.addon package. The complete JAX-WS maven plugin + JAXB plugins with fake version 1.12.2 can be downloaded from here7).
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>jaxws-maven-plugin</artifactId> <version>1.12.2</version> <executions> <execution> ... <configuration> <xjcArgs> <xjcArg>-Xxew</xjcArg> <xjcArg>-instantiate early</xjcArg> <xjcArg>-delete</xjcArg> <xjcArg>-Xcommons-lang</xjcArg> <xjcArg>-Xcommons-lang:ToStringStyle=SHORT_PREFIX_STYLE</xjcArg> <xjcArg>-Xdefault-value</xjcArg> <xjcArg>-Xfluent-api</xjcArg> <xjcArg>-Xvalue-constructor</xjcArg> </xjcArgs> </configuration> </execution> </executions> </plugin>
Note that -Xcommons-lang should be mentioned twice: once to activate the plugin and another time to pass the plugin the actual arguments.
After re-running code generation we get better possibilities to code:
/* * This class is not compilable after -Xxew is applied. */ /* public static Concept createDummyConcept_Normal() { final Concept concept = new Concept(); final Concept.Types types = new Concept.Types(); concept.setCreator("robot"); concept.setCreationDateTime(new Date()); concept.setTypes(types); types.getType().add(ConceptType.CREATURE); types.getType().add(ConceptType.HUMAN); types.getType().add(ConceptType.WOMAN); return concept; } */ public static Concept createDummyConcept_ListAccess() { final Concept concept = new Concept(); concept.setCreator("computer"); concept.setCreationDateTime(new Date()); // -Xxew demo: concept.getTypes().add(ConceptType.CREATURE); concept.getTypes().add(ConceptType.HUMAN); concept.getTypes().add(ConceptType.WOMAN); return concept; } public static Concept createDummyConcept_Construtor() { // -Xvalue-constructor demo: return new Concept("99", new Date(), "neighbour", Arrays.asList(ConceptType.CREATURE, ConceptType.HUMAN, ConceptType.WOMAN)); } public static Concept createDummyConcept_FluentAPI() { // -Xfluent-api demo: return new Concept().withId("01").withCreationDateTime(new Date()).withCreator("author").withTypes( ConceptType.CREATURE, ConceptType.CHILD); }
Further improvement comes from the fact that staring from v2.1.4 of JAX-WS RI it is not necessary to run wsgen to generate wrapper classes for input/output arguments of webservice methods8). So all we need is to generate JAXB beans for our datatypes XSD. We will also suppress generation of file headers with timestamp information, as it makes life with source version control easier. That can be done with the help of the following pom.xml:
<project ...> ... <dependencies> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.4</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.jvnet.jaxb2.maven2</groupId> <artifactId>maven-jaxb2-plugin</artifactId> <version>0.7.2</version> <executions> <execution> <goals> <goal>generate</goal> </goals> <configuration> <!--verbose> true</verbose--> <generateDirectory>${project.build.sourceDirectory}</generateDirectory> <schemaDirectory>xsd</schemaDirectory> <bindingDirectory>jaxb</bindingDirectory> <removeOldOutput>false</removeOldOutput> <forceRegenerate>false</forceRegenerate> <extension>true</extension> <args> <arg>-no-header</arg> <!-- suppress generation of a file header with timestamp --> <arg>-Xxew</arg> <arg>-instantiate early</arg> <arg>-delete</arg> <arg>-Xcommons-lang</arg> <arg>-Xcommons-lang:ToStringStyle=SHORT_PREFIX_STYLE</arg> <arg>-Xdefault-value</arg> <arg>-Xfluent-api</arg> <arg>-Xvalue-constructor</arg> </args> <plugins> <plugin> <groupId>dk.conspicio.jaxb.plugins</groupId> <artifactId>jaxb-xew-plugin</artifactId> <version>1.3</version> </plugin> <plugin> <groupId>org.jvnet.jaxb2_commons.tools.xjc.plugin</groupId> <artifactId>jaxb-commons-lang-plugin</artifactId> <version>1.0</version> </plugin> <plugin> <groupId>org.jvnet.jaxb2_commons.tools.xjc.plugin</groupId> <artifactId>jaxb-default-value-plugin</artifactId> <version>1.0</version> </plugin> <plugin> <groupId>org.jvnet.jaxb2_commons.tools.xjc.plugin</groupId> <artifactId>jaxb-fluent-api-plugin</artifactId> <version>2.1.8</version> </plugin> <plugin> <groupId>org.jvnet.jaxb2_commons.tools.xjc.plugin</groupId> <artifactId>jaxb-value-constructor-plugin</artifactId> <version>1.0</version> </plugin> <plugin> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.4</version> </plugin> </plugins> </configuration> </execution> </executions> </plugin> </plugins> </build> <pluginRepositories> <pluginRepository> <id>maven2-repository.dev.java.net</id> <url>http://download.java.net/maven/2/</url> </pluginRepository> </pluginRepositories> </project>
Notes:
First, let's create deployment descriptors. For client, web.xml is empty (using default JSP servlet) and for server we need a special servlet, that will handle requests for services and route them to corresponding service endpoint. So the files to be placed to webapp/WEB-INF folder are:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <listener> <listener-class>com.sun.xml.ws.transport.http.servlet.WSServletContextListener</listener-class> </listener> <servlet> <servlet-name>WSServlet</servlet-name> <servlet-class>com.sun.xml.ws.transport.http.servlet.WSServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>WSServlet</servlet-name> <url-pattern>/my-service</url-pattern> </servlet-mapping> <session-config> <session-timeout>60</session-timeout> </session-config> </web-app>
<?xml version="1.0" encoding="UTF-8"?> <endpoints xmlns="http://java.sun.com/xml/ns/jax-ws/ri/runtime" version="2.0"> <endpoint name="MyDBService" implementation="org.mycompany.service.db.service.MyDBServiceImpl" url-pattern="/my-service" /> </endpoints>
And now we have to include jaxws-rt into our package, that actually holds the servlet and also tune maven-war-plugin to get WEB-INF resources from webapp folder:
<project> <groupId>org.mycompany.db-service</groupId> <artifactId>my-db-service</artifactId> <packaging>war</packaging> <version>0.1-SNAPSHOT</version> ... <dependencies> <dependency> <groupId>${project.groupId}</groupId> <artifactId>my-db-common</artifactId> <version>0.1-SNAPSHOT</version> </dependency> <dependency> <groupId>com.sun.xml.ws</groupId> <artifactId>jaxws-rt</artifactId> <version>2.1.4</version> <scope>runtime</scope> </dependency> </dependencies> <build> <sourceDirectory>src</sourceDirectory> <plugins> <plugin> <artifactId>maven-war-plugin</artifactId> <configuration> <warSourceDirectory>webapp</warSourceDirectory> </configuration> </plugin> ... </plugins> </build> </project>
We gonna use Tomcat Maven Plugin to deploy our Web Service and a client for it. First we need to add the tomcat manager username and password to maven configuration file9):
<settings> <servers> <server> <id>local-server-id</id> <username>tomcat</username> <password>Ab1nXmI</password> </server> </servers> </settings>
This user should have «manager» rights in Tomcat server config:
<tomcat-users> ... <user username="tomcat" password="Ab1nXmI" roles="manager"/> </tomcat-users>
Now we add the plugin to pom.xml for service (and for client, but changing the deployment context path):
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>tomcat-maven-plugin</artifactId> <configuration> <!-- Server credentials are stored in settings.xml: --> <server>local-server-id</server> <url>http://local-server.net:8080/manager</url> <!-- Deployment context: --> <path>/my-manager</path> </configuration> </plugin>
and use mvn tomcat:deploy or later mvn tomcat:redeploy10) to do the job:
... [INFO] ------------------------------------------------------------------------ [INFO] Building RDF Storage Web Service [INFO] task-segment: [tomcat:redeploy] [INFO] ------------------------------------------------------------------------ ... [INFO] [tomcat:redeploy] [INFO] Deploying war to http://local-server.net:8080/my-manager [INFO] OK - Undeployed application at context path /my-manager [INFO] OK - Deployed application at context path /my-manager [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------
This approach is relatively simple to implement:
<?xml version="1.0" encoding="UTF-8"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <!-- Context loader creates a Spring web contexts and puts it into servlet context. --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/application.context.xml</param-value> </context-param> <!-- This servlet lookups the Spring web context in servlet context and process the bindings. --> <servlet> <servlet-name>jaxws-servlet</servlet-name> <servlet-class>com.sun.xml.ws.transport.http.servlet.WSSpringServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>jaxws-servlet</servlet-name> <url-pattern>/test-ws</url-pattern> </servlet-mapping> </web-app>
WEB-INF/application.context.xml
<?xml version="1.0"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:ws="http://jax-ws.dev.java.net/spring/core" xmlns:wss="http://jax-ws.dev.java.net/spring/servlet" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://jax-ws.dev.java.net/spring/core https://jax-ws.dev.java.net/spring/core.xsd http://jax-ws.dev.java.net/spring/servlet https://jax-ws.dev.java.net/spring/servlet.xsd"> <wss:binding url="/test-ws"> <wss:service> <ws:service bean="#testWS"></ws:service> </wss:service> </wss:binding> <bean id="testWS" class="org.mycompany.service.db.service.MyDBServiceImpl"> ... </bean> </beans>
If you wish to use WSSpringServlet in more complicated configurations (e.g. when the service is wrapped into AOP proxy to support transactions) first head this note.
Q: What is «XPath evaluation of … results in empty target node»?
A: This happens if XPath expression is not matched any node (the expression is incorrect or you are trying to match in the wrong schema file) ( see here some more information)
Q: Why JAX-WS generates faultInfo?
A: See here
Дискуссия