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.