This guide walks you through the process of consuming a SOAP-based web service with Spring.

What you’ll build

You will build a client that fetches weather data from a remote, WSDL-based web service using SOAP. You can find out more about the weather service at http://wiki.cdyne.com/index.php/CDYNE_Weather

The service provides weather forecasts based on a zipcode. You will be able to use your own zip code.

What you’ll need

How to complete this guide

Like most Spring Getting Started guides, you can start from scratch and complete each step, or you can bypass basic setup steps that are already familiar to you. Either way, you end up with working code.

To start from scratch, move on to Set up the project.

To skip the basics, do the following:

When you’re finished, you can check your results against the code in gs-consuming-web-service/complete.

Set up the project

First you set up a basic build script. You can use any build system you like when building apps with Spring, but the code you need to work with Gradle and Maven is included here. If you’re not familiar with either, refer to Building Java Projects with Gradle or Building Java Projects with Maven.

Create the directory structure

In a project directory of your choosing, create the following subdirectory structure; for example, with mkdir -p src/main/java/hello on *nix systems:

└── src
    └── main
        └── java
            └── hello

Create a Gradle build file

Below is the initial Gradle build file. But you can also use Maven. The pom.xml file is included right here. If you are using Spring Tool Suite (STS), you can import the guide directly.

If you look at pom.xml, you’ll find it has a specific version of maven-compiler-plugin. This is NOT recommended in general. Instead, it’s meant to solve an issue with our CI system that defaulted to a very old (pre-Java5) version of this plugin. build.gradle
configurations {
    jaxb
}

buildscript {
    repositories {
        maven { url "http://repo.spring.io/libs-release" }
        mavenLocal()
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.1.4.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'eclipse'
apply plugin: 'idea'
apply plugin: 'spring-boot'

repositories {
    mavenLocal()
    mavenCentral()
    maven { url 'http://repo.spring.io/libs-release' }
}

// tag::wsdl[]
task genJaxb {
    ext.sourcesDir = "${buildDir}/generated-sources/jaxb"
    ext.classesDir = "${buildDir}/classes/jaxb"
    ext.schema = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl"

    outputs.dir classesDir

    doLast() {
        project.ant {
            taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask",
                    classpath: configurations.jaxb.asPath
            mkdir(dir: sourcesDir)
            mkdir(dir: classesDir)

            xjc(destdir: sourcesDir, schema: schema,
                    package: "hello.wsdl") {
                arg(value: "-wsdl")
                produces(dir: sourcesDir, includes: "**/*.java")
            }

            javac(destdir: classesDir, source: 1.6, target: 1.6, debug: true,
                    debugLevel: "lines,vars,source",
                    classpath: configurations.jaxb.asPath) {
                src(path: sourcesDir)
                include(name: "**/*.java")
                include(name: "*.java")
            }

            copy(todir: classesDir) {
                fileset(dir: sourcesDir, erroronmissingdir: false) {
                    exclude(name: "**/*.java")
                }
            }
        }
    }
}
// end::wsdl[]

dependencies {
    compile("org.springframework.boot:spring-boot-starter")
    compile("org.springframework.ws:spring-ws-core")
    compile(files(genJaxb.classesDir).builtBy(genJaxb))

    jaxb "com.sun.xml.bind:jaxb-xjc:2.1.7"
}

jar {
	from genJaxb.classesDir
}

task wrapper(type: Wrapper) {
    gradleVersion = '1.11'
}

task afterEclipseImport {
    dependsOn genJaxb
}

The Spring Boot gradle plugin provides many convenient features:

  • It collects all the jars on the classpath and builds a single, runnable "über-jar", which makes it more convenient to execute and transport your service.

  • It searches for the public static void main() method to flag as a runnable class.

  • It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies. You can override any version you wish, but it will default to Boot’s chosen set of versions.

If you read Producing a SOAP web service, you might be wondering why this guide doesn’t use spring-boot-starter-ws? That Spring Boot starter is only for server-side web services. That starter brings on board things like embedded Tomcat, which isn’t need to make a web call.

Generate domain objects based on a WSDL

The interface to a SOAP web service is captured in a WSDL. JAXB provides an easy means to generate Java classes from a WSDL (or rather: the XSD contained in the <Types/> section of the WSDL). The WSDL for the weather service can be found at http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl.

To generate Java classes from the WSDL in maven, you need the following plugin setup:

<plugin>
    <groupId>org.jvnet.jaxb2.maven2</groupId>
    <artifactId>maven-jaxb2-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>generate</goal>
            </goals>
        </execution>
    </executions>
    <configuration>
        <schemaLanguage>WSDL</schemaLanguage>
        <generatePackage>hello.wsdl</generatePackage>
        <forceRegenerate>true</forceRegenerate>
        <schemas>
            <schema>
                <url>http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl</url>
            </schema>
        </schemas>
    </configuration>
</plugin>

This setup will generate classes for the WSDL found at the specified URL, putting those classes in the hello.wsdl package.

To do the same with gradle, you will need the following in your build file:

task genJaxb {
    ext.sourcesDir = "${buildDir}/generated-sources/jaxb"
    ext.classesDir = "${buildDir}/classes/jaxb"
    ext.schema = "http://wsf.cdyne.com/WeatherWS/Weather.asmx?wsdl"

    outputs.dir classesDir

    doLast() {
        project.ant {
            taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask",
                    classpath: configurations.jaxb.asPath
            mkdir(dir: sourcesDir)
            mkdir(dir: classesDir)

            xjc(destdir: sourcesDir, schema: schema,
                    package: "hello.wsdl") {
                arg(value: "-wsdl")
                produces(dir: sourcesDir, includes: "**/*.java")
            }

            javac(destdir: classesDir, source: 1.6, target: 1.6, debug: true,
                    debugLevel: "lines,vars,source",
                    classpath: configurations.jaxb.asPath) {
                src(path: sourcesDir)
                include(name: "**/*.java")
                include(name: "*.java")
            }

            copy(todir: classesDir) {
                fileset(dir: sourcesDir, erroronmissingdir: false) {
                    exclude(name: "**/*.java")
                }
            }
        }
    }
}

As gradle does not have a JAXB plugin (yet), it involves an ant task, which makes it a bit more complex than in maven.

In both cases, the JAXB domain object generation process has been wired into the build tool’s lifecycle so there are no extra steps to run.

Create a weather service client

To create a web service client, you simply have to extend the WebServiceGatewaySupport class and code your operations:

src/main/java/hello/WeatherClient.java


package hello;

import java.text.SimpleDateFormat;

import org.springframework.ws.client.core.support.WebServiceGatewaySupport;
import org.springframework.ws.soap.client.core.SoapActionCallback;

import hello.wsdl.Forecast;
import hello.wsdl.ForecastReturn;
import hello.wsdl.GetCityForecastByZIP;
import hello.wsdl.GetCityForecastByZIPResponse;
import hello.wsdl.Temp;

public class WeatherClient extends WebServiceGatewaySupport {

	public GetCityForecastByZIPResponse getCityForecastByZip(String zipCode) {
		GetCityForecastByZIP request = new GetCityForecastByZIP();
		request.setZIP(zipCode);

		System.out.println();
		System.out.println("Requesting forecast for " + zipCode);

		GetCityForecastByZIPResponse response = (GetCityForecastByZIPResponse) getWebServiceTemplate().marshalSendAndReceive(
				request,
				new SoapActionCallback(
						"http://ws.cdyne.com/WeatherWS/GetCityForecastByZIP"));

		return response;
	}

	public void printResponse(GetCityForecastByZIPResponse response) {
		ForecastReturn forecastReturn = response.getGetCityForecastByZIPResult();

		if (forecastReturn.isSuccess()) {
			System.out.println();
			System.out.println("Forecast for " + forecastReturn.getCity() + ", "
					+ forecastReturn.getState());

			SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
			for (Forecast forecast : forecastReturn.getForecastResult().getForecast()) {
				System.out.print(format.format(forecast.getDate().toGregorianCalendar().getTime()));
				System.out.print(" ");
				System.out.print(forecast.getDesciption());
				System.out.print(" ");
				Temp temperature = forecast.getTemperatures();
				System.out.print(temperature.getMorningLow() + "\u00b0-"
						+ temperature.getDaytimeHigh() + "\u00b0 ");
				System.out.println();
			}
		} else {
			System.out.println("No forecast received");
		}
	}

}

The client contains two methods: getCityForecastByZip which does the actual SOAP exchange, and printResponse which prints the received response. We will focus on the former method.

In this method, both the GetCityForecastByZIP and the GetCityForecastByZIPResponse classes are derived from the WSDL and were generated in the JAXB generation process described in the previous step. It creates the GetCityForecastByZIP request object and set it with the zipCode parameter. After printing out the zip code, it uses the WebServiceTemplate supplied by the WebServiceGatewaySupport base class to do the actual SOAP exchange. It passes the GetCityForecastByZIP request object, as well as a SoapActionCallback to pass on a SOAPAction header with the request, as the WSDL described that it needed this header in the <soap:operation/> elements. It casts the response into a GetCityForecastByZIPResponse object, which is then returned.

Configuring web service components

Spring WS uses Spring Framework’s OXM module which has the Jaxb2Marshaller to serialize and deserialize XML requests.

src/main/java/hello/WeatherConfiguration.java


package hello;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.oxm.jaxb.Jaxb2Marshaller;

@Configuration
public class WeatherConfiguration {

	@Bean
	public Jaxb2Marshaller marshaller() {
		Jaxb2Marshaller marshaller = new Jaxb2Marshaller();
		marshaller.setContextPath("hello.wsdl");
		return marshaller;
	}

	@Bean
	public WeatherClient weatherClient(Jaxb2Marshaller marshaller) {
		WeatherClient client = new WeatherClient();
		client.setDefaultUri("http://wsf.cdyne.com/WeatherWS/Weather.asmx");
		client.setMarshaller(marshaller);
		client.setUnmarshaller(marshaller);
		return client;
	}

}

The marshaller is pointed at the collection of generated domain objects and will use them to both serialize and deserialize between XML and POJOs.

The weatherClient is created and configured with the URI of the weather service shown up above. It is also configured to use the JAXB marshaller.

Make the application executable

This application is packaged up to run from the console and retrieve a single weather forecast for a given zip code.

src/main/java/hello/Application.java


package hello;

import org.springframework.boot.SpringApplication;
import org.springframework.context.ApplicationContext;

import hello.wsdl.GetCityForecastByZIPResponse;

public class Application {

	public static void main(String[] args) {
		ApplicationContext ctx = SpringApplication.run(WeatherConfiguration.class, args);

		WeatherClient weatherClient = ctx.getBean(WeatherClient.class);

		String zipCode = "94304";
		if (args.length > 0) {
			zipCode = args[0];
		}
		GetCityForecastByZIPResponse response = weatherClient.getCityForecastByZip(zipCode);
		weatherClient.printResponse(response);
	}

}

The main() method defers to the SpringApplication helper class, providing Application.class as an argument to its run() method. This tells Spring to read the annotation metadata from Application and to manage it as a component in the Spring application context.

This application is hard coded to look up zip code 94304, Palo Alto, CA. Towards the end of this guide, you’ll see how to plug in a different zip code without editing the code.

Build an executable JAR

You can build a single executable JAR file that contains all the necessary dependencies, classes, and resources. This makes it easy to ship, version, and deploy the service as an application throughout the development lifecycle, across different environments, and so forth.

./gradlew build

Then you can run the JAR file:

java -jar build/libs/gs-consuming-web-service-0.1.0.jar

If you are using Maven, you can run the application using mvn spring-boot:run. Or you can build the JAR file with mvn clean package and run the JAR by typing:

java -jar target/gs-consuming-web-service-0.1.0.jar
The procedure above will create a runnable JAR. You can also opt to build a classic WAR file instead.

Run the service

If you are using Gradle, you can run your service at the command line this way:

./gradlew clean build && java -jar build/libs/gs-consuming-web-service-0.1.0.jar
If you are using Maven, you can run your service by typing mvn clean package && java -jar target/gs-consuming-web-service-0.1.0.jar.

You can alternatively run the app directly from Gradle like this:

./gradlew bootRun
With mvn, you can run mvn spring-boot:run.

Logging output is displayed. The service should be up and running within a few seconds.

Requesting forecast for 94304

Forecast for Palo Alto, CA
2013-01-03 Partly Cloudy °-57°
2013-01-04 Partly Cloudy 41°-58°
2013-01-05 Partly Cloudy 41°-59°
2013-01-06 Partly Cloudy 44°-56°
2013-01-07 Partly Cloudy 41°-60°
2013-01-08 Partly Cloudy 42°-60°
2013-01-09 Partly Cloudy 43°-58°

You can plug in a different zip code by typing java -jar build/libs/gs-consuming-web-service-0.1.0.jar 34769

Requesting forecast for 34769

Forecast for Saint Cloud, FL
2014-02-18 Sunny 51°-79°
2014-02-19 Sunny 55°-81°
2014-02-20 Sunny 59°-84°
2014-02-21 Partly Cloudy 63°-85°
2014-02-22 Partly Cloudy 63°-84°
2014-02-23 Partly Cloudy 63°-82°
2014-02-24 Partly Cloudy 62°-80°

Summary

Congratulations! You’ve just developed a client to consume a SOAP-based web service with Spring.