Client SOAP rapidi con Spring
Interfacciarsi con servizi SOAP è un tema che ritorna spesso in ambito enterprise. Esistono molti strumenti per generare client a partire dal descrittore del servizio, ma di solito non sono molto agili. Di seguito descriverò una procedura rapida per generare un client con Spring e Maven.
TLDR; Creeremo un client SOAP generando le interfacce Java con Maven (via cxf-codegen-plugin
) e configurando lo stub del servizio (via JaxwsProxyFactoryBean
) nel contesto Spring. Su Github è disponibile il progetto di esempio.
Setup del progetto Link to heading
Se non ne abbiamo uno già a disposizione, creiamo un nuovo progetto con Maven:
➜ mvn archetype:generate -DarchetypeGroupId=org.apache.maven.archetypes \
-DarchetypeArtifactId=maven-archetype-quickstart
[INFO] Scanning for projects...
...
Define value for property 'groupId': : sample.wsclient
Define value for property 'artifactId': : webservice-client
Define value for property 'version': 1.0-SNAPSHOT: :
Define value for property 'package': sample.wsclient: :
Confirm properties configuration:
groupId: sample.wsclient
artifactId: webservice-client
...
[INFO] BUILD SUCCESS
...
ed importiamo il descrittore del servizio (WSDL) nel progetto, ad esempio:
➜ cd webservice-client
➜ mkdir -p src/main/resources/wsdl
➜ wget -O src/main/resources/wsdl/service.wsdl \
http://www.dataaccess.com/webservicesserver/numberconversion.wso\?WSDL
...
Length: 3689 (3.6K) [text/xml]
Saving to: src/main/resources/wsdl/service.wsdl
100%[=============================================>] 3,689 --.-K/s in 0s
Aggiungiamo quindi nel file pom.xml
, le dipendenze necessarie
<properties>
<spring.version>4.0.1.RELEASE</spring.version>
<cxf.version>2.7.7</cxf.version>
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
</dependencies>
ed il plugin per la generazione delle interfacce Java necessarie
<build>
<plugins>
<plugin>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-codegen-plugin</artifactId>
<version>${cxf.version}</version>
<executions>
<execution>
<id>generate-sources</id>
<phase>generate-sources</phase>
<configuration>
<!-- destination directory for generated code -->
<sourceRoot>${basedir}/src/main/java</sourceRoot>
<wsdlOptions>
<!-- service desctiptor location -->
<wsdlOption>
<wsdl>${basedir}/src/main/resources/wsdl/service.wsdl</wsdl>
<wsdlLocation>classpath:/wsdl/service.wsdl</wsdlLocation>
</wsdlOption>
</wsdlOptions>
</configuration>
<goals>
<goal>wsdl2java</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Possiamo quindi lanciare Maven per generare il codice Java
➜ cd webservice-client
➜ mvn generate-sources
...
[INFO] Building webservice-client 1.0-SNAPSHOT
...
[INFO] --- cxf-codegen-plugin:2.7.7:wsdl2java (generate-sources) ---
...
[INFO] BUILD SUCCESS
che viene opportunamente inserito in package
secondo il namespace del servizio (es. net.webservicex
)
➜ ls -R src/main/java/com
src/main/java/com:
dataaccess
src/main/java/com/dataaccess:
webservicesserver
src/main/java/com/dataaccess/webservicesserver:
NumberConversion.java
NumberConversionSoapType.java
NumberToDollars.java
NumberToDollarsResponse.java
NumberToWords.java
NumberToWordsResponse.java
ObjectFactory.java
package-info.java
Siamo ora pronti per utilizzare il web-service nella nostra applicazione.
Utilizzo del client Link to heading
Configuriamo il bean che rappresenta il servizio nel contesto Spring
<bean id="numberToWordsService"
class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
<!-- Provide project related configurations -->
<property name="serviceInterface"
value="com.dataaccess.webservicesserver.NumberConversionSoapType" />
<property name="wsdlDocumentUrl" value="classpath:/wsdl/service.wsdl" />
<property name="lookupServiceOnStartup" value="false" />
<!-- Configure service params from the WSDL -->
<property name="namespaceUri"
value="http://www.dataaccess.com/webservicesserver/" />
<property name="serviceName" value="NumberConversion" />
<property name="portName" value="NumberConversionSoap"/>
<property name="endpointAddress"
value="http://www.dataaccess.com/webservicesserver/numberconversion.wso"/>
<!-- Uncomment for HTTP-AUTH -->
<!--
<property name="username" value="user"/>
<property name="password" value="passwrod"/>
-->
<!-- Payload dumper setup -->
<property name="handlerResolver" ref="resolver"/>
</bean>
per poi utilizzarlo, ad esempio:
ClassPathXmlApplicationContext ctx =
new ClassPathXmlApplicationContext("context.xml");
// Get the auto-generated webservice stub
NumberConversionSoapType service =
(NumberConversionSoapType) ctx.getBean("numberToWordsService");
// Invoke the service
String wordsForNumber = service.numberToWords(new BigInteger("1324"));
System.out.println(wordsForNumber);
produce il seguente output
➜ one thousand three hundred and twenty four
Su Github è disponibile il codice di esempio.
Note Link to heading
A meno di non modificare l’endpoint a runtime, lo stub così generato è thread-safe. Quindi è possibile iniettarlo nella logica di business senza preoccuparsi dell’utilizzo in concorrenza.
Per semplicità di comprensione il contesto Spring di esempio è configurato via XML, ma sarebbe meglio usare JavaConfig.
SOAP payload dump Link to heading
Talvolta risulta utile verificare il payload XML che passa in chiamata e risposta al servizio. Solitamente questo implica un lungo lavoro di intelligence con strumenti tipo tcpdump e wireshark.
Fortunatamente esiste una via più semplice: configurare degli opportuni handler nel contesto Spring. Per fare ciò implementiamo un SOAPHandler
che intercetta il contenuto XML di request e response SOAP e lo stampa sullo stdout
public class SoapRequestResponseDumper
implements SOAPHandler<SOAPMessageContext> {
@Override
public boolean handleMessage(SOAPMessageContext context) {
dump(context);
return true;
}
//...
private void dump(SOAPMessageContext smc) {
Boolean outboundProperty =
(Boolean) smc.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
if (outboundProperty.booleanValue()) {
System.out.println("\nOutbound message:");
} else {
System.out.println("\nInbound message:");
}
SOAPMessage message = smc.getMessage();
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
message.writeTo(baos);
System.out.println(baos.toString());
} catch (Exception e) {
System.out.println("Exception in handler: " + e);
}
}
}
costruiamo poi un HandlerResolver
per agganciarci al client
public class HandlerChainResolver implements
javax.xml.ws.handler.HandlerResolver {
private List<Handler> handlerList;
public List<Handler> getHandlerChain(PortInfo portInfo) {
return handlerList;
}
public void setHandlerList(List<Handler> handlerList) {
this.handlerList = handlerList;
}
}
infine configuriamo opportunamente il contesto della nostra applicazione
<bean id="resolver" class="sample.wsclient.handler.HandlerChainResolver">
<property name="handlerList">
<list>
<bean class="sample.wsclient.handler.SoapPayloadDumper" />
</list>
</property>
</bean>
<bean id="numberToWordsService"
class="org.springframework.remoting.jaxws.JaxWsPortProxyFactoryBean">
...
<!-- Payload dumper setup -->
<property name="handlerResolver" ref="resolver"/>
</bean>
Effettuando una chiamata al servizio il payload viene mostrato in console, esempio:
Outbound message:
<S:Envelope xmlns:S="http://schemas.xmlsoap.org/soap/envelope/">
<S:Body>
<NumberToWords xmlns="http://www.dataaccess.com/webservicesserver/">
<ubiNum>1324</ubiNum>
</NumberToWords>
</S:Body>
</S:Envelope>
Inbound message:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header/>
<soap:Body>
<m:NumberToWordsResponse xmlns:m="http://www.dataaccess.com/webservicesserver/">
<m:NumberToWordsResult>one thousand three hundred and twenty four</m:NumberToWordsResult>
</m:NumberToWordsResponse>
</soap:Body>
</soap:Envelope>
Sicuramente uno strumento utile per avere qualche dettaglio in più quando qualcosa andrà male (previo s/System.out.println/logger.trace/g
).
Scarica il progetto di esempio su GitHub.