Saturday, August 15, 2009

Getting started with Spring Web Services

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:

  1. JDK 6, including the xjc command-line tool for generating XML binding classes from your schema.
  2. 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:

  1. Plugin configuration for building the project for Java 6
  2. Dependencies required for using the annotation features of Spring-WS
  3. 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:

  1. some way to map methods to the payload of the request message. In our case the job is handled by PayloadRootAnnotationMethodEndpointMapping in conjunction with the PayloadRoot annotation.
  2. 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: