Is it possible to easily convert the REST WS example (http://spring.io/guides/gs/rest-service/) to a SOAP WS? What needs to be done to convert that example?
1 Answers
Yes, it is easy possible to convert this (simple) example to SOAP.
Migrate
To get from here to (something similar like) this (guide|repository), we need to:
adjust build.gradle to:
plugins { id 'org.springframework.boot' version '2.6.0' // update to latest id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.stackoverflow' // re-brand ;) version = '0.0.1-SNAPSHOT' sourceCompatibility = '1.8' repositories { mavenCentral() } // complete chunk from https://github.com/spring-guides/gs-producing-web-service/tree/main/complete/build.gradle sourceSets { main { java { srcDir 'src/main/java' srcDir 'build/generated-sources/jaxb' } } } task genJaxb { ext.sourcesDir = "${buildDir}/generated-sources/jaxb" ext.schema = "src/main/resources/greeting.xsd" // replace countries.xsd from soap stackoverflow with our xsd outputs.dir sourcesDir doLast() { project.ant { taskdef name: "xjc", classname: "com.sun.tools.xjc.XJCTask", classpath: configurations.jaxb.asPath mkdir(dir: sourcesDir) xjc(destdir: sourcesDir, schema: schema) { arg(value: "-wsdl") produces(dir: sourcesDir, includes: "**/*.java") } } } } compileJava.dependsOn genJaxb configurations { jaxb } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation('org.springframework.boot:spring-boot-starter-test') // addtional dependencies implementation 'org.springframework.boot:spring-boot-starter-web-services' implementation 'wsdl4j:wsdl4j' jaxb('org.glassfish.jaxb:jaxb-xjc') } // end of chunk test { useJUnitPlatform() }
, which is: original build.gradle + instructions for jaxb + additional dependencies.
settings.gradle we change (correctly but not obligatory) to:
rootProject.name = 'soap-service'
We introduce src/main/resources/greeting.xsd with following content:
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:tns="http://stackoverflow.com/greeting-soap/" targetNamespace="http://stackoverflow.com/greeting-soap/" elementFormDefault="qualified"> <xs:element name="getGreetingRequest"> <xs:complexType> <xs:sequence> <xs:element name="name" type="xs:string" minOccurs="0" nillable="true" default="from XSD" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="getGreetingResponse"> <xs:complexType> <xs:sequence> <xs:element name="greeting" type="tns:greeting"/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name="greeting"> <xs:sequence> <xs:element name="id" type="xs:long"/> <xs:element name="content" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:schema>
, trying to mimic the REST endpoint and model as SOAP.
(Best) we delete all (main & test) sources in:
- src/main/java/com/example/restservice
- src/test/java/com/example/restservice
, to replace them.
This will be our SpringBootApplication + Configuration:
package com.stackoverflow.soapservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.core.io.ClassPathResource; import org.springframework.ws.config.annotation.EnableWs; import org.springframework.ws.transport.http.MessageDispatcherServlet; import org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition; import org.springframework.xml.xsd.SimpleXsdSchema; import org.springframework.xml.xsd.XsdSchema; @EnableWs @SpringBootApplication public class SOAPServiceApplication { public static final String NAMESPACE_URI = "http://stackoverflow.com/greeting-soap/"; public static void main(String[] args) { SpringApplication.run(SOAPServiceApplication.class, args); } @Bean public ServletRegistrationBean<MessageDispatcherServlet> messageDispatcherServlet(ApplicationContext applicationContext) { MessageDispatcherServlet servlet = new MessageDispatcherServlet(); servlet.setApplicationContext(applicationContext); servlet.setTransformWsdlLocations(true); return new ServletRegistrationBean<>(servlet, "/ws/*"); } @Bean(name = "greetings") public DefaultWsdl11Definition defaultWsdl11Definition(XsdSchema greetingSchema) { DefaultWsdl11Definition wsdl11Definition = new DefaultWsdl11Definition(); wsdl11Definition.setPortTypeName("GreetingPort"); wsdl11Definition.setLocationUri("/ws"); wsdl11Definition.setTargetNamespace(NAMESPACE_URI); wsdl11Definition.setSchema(greetingSchema); return wsdl11Definition; } @Bean public XsdSchema greetingsSchema() { return new SimpleXsdSchema(new ClassPathResource("greeting.xsd")); } }
The endpoint class:
package com.stackoverflow.soapservice; import com.stackoverflow.greeting_soap.GetGreetingRequest; import com.stackoverflow.greeting_soap.GetGreetingResponse; import com.stackoverflow.greeting_soap.Greeting; import static com.stackoverflow.soapservice.SOAPServiceApplication.NAMESPACE_URI; import java.util.concurrent.atomic.AtomicLong; import org.springframework.ws.server.endpoint.annotation.Endpoint; import org.springframework.ws.server.endpoint.annotation.PayloadRoot; import org.springframework.ws.server.endpoint.annotation.RequestPayload; import org.springframework.ws.server.endpoint.annotation.ResponsePayload; @Endpoint public class GreetingEndpoint { private static final String HELLO_TPL = "Hello, %s!"; private static final AtomicLong COUNTER = new AtomicLong(); @PayloadRoot(namespace = NAMESPACE_URI, localPart = "getGreetingRequest") @ResponsePayload public GetGreetingResponse sayHello(@RequestPayload GetGreetingRequest request) { GetGreetingResponse response = new GetGreetingResponse(); Greeting greeting = new Greeting(); greeting.setContent( String.format( HELLO_TPL, request.getName() == null ? "World" : request.getName() ) ); greeting.setId(COUNTER.incrementAndGet()); response.setGreeting(greeting); return response; } }
And a test:
package com.stackoverflow.soapservice; import com.stackoverflow.greeting_soap.GetGreetingRequest; import com.stackoverflow.greeting_soap.GetGreetingResponse; import java.util.HashMap; import javax.xml.bind.JAXBElement; import javax.xml.namespace.QName; import static org.assertj.core.api.Assertions.assertThat; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.TestConfiguration; import org.springframework.boot.web.server.LocalServerPort; import org.springframework.context.annotation.Bean; import org.springframework.oxm.jaxb.Jaxb2Marshaller; import org.springframework.ws.client.core.WebServiceTemplate; @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) class GreetingEndpointTest { private static final QName NS_NAME_QNAME = QName.valueOf( String.format("{%s}%s", SOAPServiceApplication.NAMESPACE_URI, "name") ); @TestConfiguration static class TestConfig { @Bean public Jaxb2Marshaller marshaller() { final Jaxb2Marshaller marshaller = new Jaxb2Marshaller(); marshaller.setPackagesToScan("com.stackoverflow.greeting_soap"); marshaller.setMarshallerProperties(new HashMap<String, Object>() { { put(javax.xml.bind.Marshaller.JAXB_FORMATTED_OUTPUT, true); } }); return marshaller; } @Bean public WebServiceTemplate wsTemplate(Jaxb2Marshaller marshaller) { return new WebServiceTemplate(marshaller); } } @LocalServerPort private int port = 0; @Autowired private WebServiceTemplate ws; @Test public void testSendAndReceive() { // Given: final String NAME = "Test"; GetGreetingRequest request = new GetGreetingRequest(); JAXBElement<String> elem = new JAXBElement<>(NS_NAME_QNAME, String.class, NAME); request.setName(elem); // When: GetGreetingResponse result = (GetGreetingResponse) ws.marshalSendAndReceive("http://localhost:" + port + "/ws", request); // Then: assertThat(result != null); assertThat(result.getGreeting() != null); assertThat(result.getGreeting().getContent() != null); assertThat(result.getGreeting().getContent().startsWith("Hello") && result.getGreeting().getContent().contains(NAME)); } // ... see at github }
Build
With JDK 1.8:
export JAVA_HOME=/path/to/jdk/8
For higher JDK versions up to 11, we'd have to adjust dependencies. How to shift this sample up-to-date (jdk-17-LTS)? ... "Null Problemo!"
Clean, test and bootRun:
./gradlew clean test bootRun
When first time imported into our IDE, a "priming build", respectively ./gradlew genJaxb
will be useful (/generate the jaxb classes).
Use
get the wsdl:
curl http://localhost:8080/ws/greetings.wsdl
gets us (unformatted):
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:sch="http://stackoverflow.com/greeting-soap/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://stackoverflow.com/greeting-soap/" targetNamespace="http://stackoverflow.com/greeting-soap/"> <wsdl:types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified" targetNamespace="http://stackoverflow.com/greeting-soap/"> <xs:element name="getGreetingRequest"> <xs:complexType> <xs:sequence> <xs:element name="name" type="xs:string" minOccurs="0" nillable="true" default="from XSD" /> </xs:sequence> </xs:complexType> </xs:element> <xs:element name="getGreetingResponse"> <xs:complexType> <xs:sequence> <xs:element name="greeting" type="tns:greeting"/> </xs:sequence> </xs:complexType> </xs:element> <xs:complexType name="greeting"> <xs:sequence> <xs:element name="id" type="xs:long"/> <xs:element name="content" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:schema> </wsdl:types> <wsdl:message name="getGreetingResponse"> <wsdl:part element="tns:getGreetingResponse" name="getGreetingResponse"></wsdl:part> </wsdl:message> <wsdl:message name="getGreetingRequest"> <wsdl:part element="tns:getGreetingRequest" name="getGreetingRequest"></wsdl:part> </wsdl:message> <wsdl:portType name="GreetingPort"> <wsdl:operation name="getGreeting"> <wsdl:input message="tns:getGreetingRequest" name="getGreetingRequest"></wsdl:input> <wsdl:output message="tns:getGreetingResponse" name="getGreetingResponse"></wsdl:output> </wsdl:operation> </wsdl:portType> <wsdl:binding name="GreetingPortSoap11" type="tns:GreetingPort"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/> <wsdl:operation name="getGreeting"> <soap:operation soapAction=""/> <wsdl:input name="getGreetingRequest"> <soap:body use="literal"/> </wsdl:input> <wsdl:output name="getGreetingResponse"> <soap:body use="literal"/> </wsdl:output> </wsdl:operation> </wsdl:binding> <wsdl:service name="GreetingPortService"> <wsdl:port binding="tns:GreetingPortSoap11" name="GreetingPortSoap11"> <soap:address location="http://localhost:8080/ws"/> </wsdl:port> </wsdl:service> </wsdl:definitions>
send a request:
curl --header "content-type: text/xml" -d @request.xml http://localhost:8080/ws > output.xml
with this ./request.xml:
<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:gs="http://stackoverflow.com/greeting-soap/"> <soapenv:Header/> <soapenv:Body> <gs:getGreetingRequest> <gs:name>John</gs:name> </gs:getGreetingRequest> </soapenv:Body> </soapenv:Envelope>
we get the following ./output.xml (unformatted):
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"> <SOAP-ENV:Header/> <SOAP-ENV:Body> <ns2:getGreetingResponse xmlns:ns2="http://stackoverflow.com/greeting-soap/"> <ns2:greeting> <ns2:id>1</ns2:id> <ns2:content>Hello, John!</ns2:content> </ns2:greeting> </ns2:getGreetingResponse> </SOAP-ENV:Body> </SOAP-ENV:Envelope>
Everything combined in a github repository:

- 12,237
- 5
- 33
- 64