I needed to quickly implement a simple (document/literal wrapped) web service, so I thought I would try Spring Web Services. Unfortunately, just following the tutorial did not result a working webapp. I had to do some searching and reading of the source code to figure out a few problems. Here are my notes.
Like every other part of the Spring portfolio, Spring Web Services provides a wide variety of ways to accomplish the same task. I chose the following options:
- Use the
@Endpoint
and@PayloadRoot
annotations to map methods to SOAP operations. - Use JAXB 2 for XML binding.
This combination seemed to require the least amount of coding and configuration directly related to web services, allowing me to focus on the actual service implementation.
Set up your environment
There are only two things you absolutely need to get started with Spring Web Services:
- JDK 6, including the
xjc
command-line tool for generating XML binding classes from your schema. - Maven (http://maven.apache.org) for managing dependencies and building the project
Maven eliminates the need to download Spring Web Services or any of its dependencies directly.
Generate the project skeleton
Maven makes it really easy to create the basic skeleton for the project:
mvn archetype:create \ -DarchetypeGroupId=org.springframework.ws \ -DarchetypeArtifactId=spring-ws-archetype \ -DgroupId=org.xocoatl.ws \ -DartifactId=web-service
This command downloads the required dependencies to your local repository and builds the following directory structure:
$ ls -R web-service/ web-service/: pom.xml src web-service/src: main web-service/src/main: resources webapp web-service/src/main/resources: web-service/src/main/webapp: WEB-INF web-service/src/main/webapp/WEB-INF: spring-ws-servlet.xml web.xml
This structure just follows the Maven conventions for a webapp.
Write the web service contract
Spring Web Services only supports a contract-first service development model, so the next task is to write the XML schema that defines your service. Create web-service/src/main/webapp/WEB-INF/web-service.xsd
and write your contract:
<?xml version="1.0" encoding="UTF-8"?> <xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:tns="urn:ws.xocoatl.org" elementFormDefault="qualified" targetNamespace="urn:ws.xocoatl.org"> <xsd:element name="EchoRequest"> <xsd:complexType> <xsd:sequence> <xsd:element name="Message" type="xsd:string" /> </xsd:sequence> </xsd:complexType> </xsd:element> <xsd:element name="EchoResponse"> <xsd:complexType> <xsd:sequence> <xsd:element name="Message" type="xsd:string" /> </xsd:sequence> </xsd:complexType> </xsd:element> </xsd:schema>
At minimum, your schema will include types representing the payload of the request and response messages for each operation in your service. Spring-WS includes the ability to automatically generate a WSDL that includes your schema. As I discovered, however, the default WSDL generator requires that your payload types end in Request
or Response
. This seems to go against the typical convention of having your request payload elements match the operation name (i.e. Echo
rather than EchoRequest
.
Generate the XML binding classes
Once you have defined your contract, you can use the xjc
command-line tool to generate the source code for the XML binding classes that your service will use. The simplest thing is to just add this to the maven build configuration. Here is how my pom.xml
wound up:
<?xml version="1.0" encoding="UTF-8"?> <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>org.xocoatl.ws</groupId> <artifactId>web-service</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>Web Service</name> <build> <plugins> <!-- use JDK 6 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.6</source> <target>1.6</target> </configuration> </plugin> <!-- use the command-line xjc tool that comes with JDK 6 to generate XML binding classes from schema --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>exec-maven-plugin</artifactId> <version>LATEST</version> <executions> <execution> <id>mkdir</id> <phase>generate-sources</phase> <goals> <goal>exec</goal> </goals> <configuration> <executable>mkdir</executable> <arguments> <argument>-p</argument> <argument>target/generated-sources/xjc</argument> </arguments> </configuration> </execution> <execution> <id>xjc</id> <phase>generate-sources</phase> <goals> <goal>exec</goal> </goals> </execution> </executions> <configuration> <executable>xjc</executable> <arguments> <argument>-p</argument> <argument>org.xocoatl.schema</argument> <argument>-d</argument> <argument>target/generated-sources/xjc</argument> <argument>src/main/webapp/WEB-INF/web-service.xsd</argument> </arguments> </configuration> </plugin> <plugin> <groupId>org.mortbay.jetty</groupId> <artifactId>maven-jetty-plugin</artifactId> <version>LATEST</version> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-oxm</artifactId> <version>1.5.7</version> </dependency> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-oxm-tiger</artifactId> <version>1.5.7</version> </dependency> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-ws-core</artifactId> <version>1.5.7</version> </dependency> <dependency> <groupId>org.springframework.ws</groupId> <artifactId>spring-ws-core-tiger</artifactId> <version>1.5.7</version> </dependency> </dependencies> </project>
There is also a maven plugin to invoke xjc
, but maven reported errors while trying to download it, and with JDK 6 it is easy to just execute the command-line tool directly. Note that it is necessary to create the output directory first, for some reason xjc
will not do this for you. Windows users may need to be tweak this step.
This task is bound to the generate-sources
phase of the build lifecycle, so if necessary it will always run before compiling your own code.
While editing the POM file, I made a few other additions:
- Plugin configuration for building the project for Java 6
- Dependencies required for using the annotation features of Spring-WS
- Plugin configuration for running the webapp in Jetty during development
Now you are ready to actually generate the XML binding classes:
$ mvn generate-sources [INFO] Scanning for projects... [INFO] ------------------------------------------------------------------------ [INFO] Building Web Service [INFO] task-segment: [generate-sources] [INFO] ------------------------------------------------------------------------ [INFO] [exec:exec {execution: mkdir}] [INFO] [exec:exec {execution: default}] [INFO] parsing a schema... [INFO] compiling a schema... [INFO] org/xocoatl/schema/EchoRequest.java [INFO] org/xocoatl/schema/EchoResponse.java [INFO] org/xocoatl/schema/ObjectFactory.java [INFO] org/xocoatl/schema/package-info.java [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESSFUL [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2 seconds [INFO] Finished at: Sat Aug 15 21:07:19 PDT 2009 [INFO] Final Memory: 7M/79M [INFO] ------------------------------------------------------------------------
The generated source code is written to target/generated-sources/xjc
, where it is available for building with your own code.
Implement the endpoint
The next step is to implement the endpoint itself. Maven requires that your source code reside in src/main/java
. I created a file at src/main/java/org/xocoatl/ws/EchoEndoint.java
for my implementation:
package org.xocoatl.ws; import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.PayloadRoot; import org.xocoatl.schema.EchoRequest; import org.xocoatl.schema.EchoResponse; @Endpoint public class EchoEndpoint { @PayloadRoot(localPart = "EchoRequest", namespace = "urn:ws.xocoatl.org") public EchoResponse echo(EchoRequest echoRequest) { EchoResponse echoResponse = new EchoResponse(); echoResponse.setMessage(echoRequest.getMessage()); return echoResponse; } }
Usually your endpoint will delegate to an underlying service to perform some logic. The code in the endpoint simply extracts data from the request, passes it to the service, and then builds an appropriate response.
Write the Spring configuration
By convention, the Spring configuration resides in src/main/webapp/WEB-INF/spring-ws-servlet.xml
. The samples in the Spring-WS distribution are a tossed salad of various approaches, but this is the minimal configuration that seems to be necessary for the annoation + JAXB approach:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:sws="http://www.springframework.org/schema/web-services" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/web-services http://www.springframework.org/schema/web-services/web-services-1.5.xsd"> <bean id="echoEndpoint" class="org.xocoatl.ws.EchoEndpoint" /> <bean class="org.springframework.ws.server.endpoint.adapter.GenericMarshallingMethodEndpointAdapter"> <constructor-arg ref="marshaller" /> <constructor-arg ref="marshaller" /> </bean> <bean id="marshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="contextPath" value="org.xocoatl.schema" /> </bean> <bean class="org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping" /> <bean id="schema" class="org.springframework.xml.xsd.SimpleXsdSchema"> <property name="xsd" value="/WEB-INF/web-service.xsd" /> </bean> <bean id="echo" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition"> <property name="schema" ref="schema" /> <property name="portTypeName" value="Echo" /> <property name="locationUri" value="http://localhost:8080/" /> <property name="targetNamespace" value="urn:ws.xocoatl.org" /> </bean> </beans>
Every endpoint requires two form of configuration:
- some way to map methods to the payload of the request message. In our case the job is handled by
PayloadRootAnnotationMethodEndpointMapping
in conjunction with thePayloadRoot
annotation. - an adapter for reading the request and writing the response messages. In our case we are using JAXB marshalling and unmarshalling for these tasks.
Note that the id of the DefaultWsdl11Definition
bean winds up in the URL of the WSDL.
Run the application
Once the configuration is complete, you can run the application from the command line:
$ mvn jetty:run
At this point you should be able to browse to http://localhost:8080/echo.wsdl
. You should also be able to load the WSDL into a tool such as soapUI and try out the operation.
No comments:
Post a Comment