Detta inlägg ingår i serien Spring från början och kommer att behandla det springstöd som finns för SOAP-baserade webbtjänster i modulen Spring WS.

Generellt sätt kan man säga att webbtjänster är XML/HTTP (XML över HTTP). Webbtjänster är alltså ofta tjänster som finns tillgängliga över ett nätverk. Eftersom en webbtjänst typiskt är baserad på ett XML-gränssnitt gör detta att webbtjänsten blir oberoende av programmeringsspråk. Exempelvis kan tjänsten implementeras i Java men anropas från en klient skriven i C++.

SOAP är ett standardiserat protokoll för överföring av XML-meddelanden som används för webbtjänster. Detta protokoll är standardiserat av W3C och det finns ett brett stöd för SOAP-baserade webbtjänster via Spring/Java, .NET, C++ och så vidare. Andra standarder för webbtjänster är exempelvis REST som inte kommer att behandlas i detta inlägg.

En SOAP-baserad webbtjänst kan beskrivas med hjälp av WSDL. Det finns inget krav på att WSDL måste användas men en WSDL-definition måste finnas tillgänglig för att en klient på ett automatiserat sätt ska kunna skapa proxy-objekt och liknande för att komma åt en webbtjänst.

Den typ av webbtjänster som kommer att diskuteras i detta inlägg är alltså webbtjänster som definieras via WSDL och använder protokollet SOAP.

Contract First

Om man ska generalisera lite kan man säga att det finns två vedertagna metoder för att utveckla webbtjänster, Contract First och Contract Last. Contract Last innebär helt enkelt att man först skapar ett javagränssnitt och sedan utifrån detta skapar en XML-definition via exempelvis WSDL som beskriver detta gränssnitt. Contract First innebär det motsatta det vill säga att man först skapar XML-kontraktet och sedan utifrån detta kontrakt skapar javagränssnittet.

Spring WS erbjuder endast stöd för det senare det vill säga Contract First och detta motiveras av Spring själva med följande:

  • Java/XML inkompatibilitet – Vissa javatyper är svåra att beskriva i XML och det finns inget standardiserat sätt att göra detta på. Detta innebär att beroende på vilket sätt som används för att generera XML kan resultatet se olika ut.
  • Stabilitet – Om man genererar XML-kontraktet från ett javagränssnitt så kommer kontraktet att förändras om javagränssnittet förändras. Olika verktyg kan dessutom generera olika XML baserat på hur gränssnittet ser ut.
  • Prestanda – Om man genererar XML utifrån ett javagränssnitt kan en komplex objektgraf leda till att en väldigt stor mängd XML genereras. Ett objekt kan ha ett beroende till ett objekt som har ett beroende till ett objekt och så vidare. Om ett kontrakt specificeras så är det tydligt vilken XML som skickas.
  • Återanvändbarhet – Genom att deklarera ett schema i en separat XSD (XML Schema Definition) kan denna återanvändas i andra delar av applikationen eller rent av i andra applikationer.
  • Version – Om ett XML-schema måste uppdateras kan fortfarande samma javaimplementation användas för att hantera gamla och nya versioner.

Kontraktet

Kontraktet som används för att uttrycka en webbtjänst baseras på en vanlig XSD. I exemplet nedan (som faktiskt är samma exempel som i inlägget tidigare om Distribuerade tjänster via Spring) så används följande XSD:

<xs:schema
      xmlns:xs="http://www.w3.org/2001/XMLSchema"
      xmlns:calc="http://cygni.se/exporters/schemas"
      elementFormDefault="qualified"
      targetNamespace="http://cygni.se/exporters/schemas">

    <xs:element
        name="CalculatorRequest"
        type="calc:CalculatorOperationType" />
    <xs:element
        name="CalculatorResponse"
        type="calc:CalculatorResponseType" />

    <xs:complexType name="CalculatorOperationType">
        <xs:sequence>
            <xs:element name="First" type="xs:integer" />
            <xs:element name="Second" type="xs:integer" />
            <xs:element name="Operation" type="calc:OperationType" />
        </xs:sequence>
    </xs:complexType>

    <xs:complexType name="CalculatorResponseType">
        <xs:all>
            <xs:element name="Answer" type="xs:integer" />
            <xs:element name="Error" type="xs:string" />
        </xs:all>
    </xs:complexType>

    <xs:simpleType name="OperationType">
        <xs:restriction base="xs:string">
            <xs:enumeration value="ADD" />
            <xs:enumeration value="SUBTRACT" />
            <xs:enumeration value="MULTIPLY" />
            <xs:enumeration value="DIVIDE" />
        </xs:restriction>
    </xs:simpleType>
</xs:schema>

Användning av ovanstående schema kan se ut som nedan när operanderna 3 och 2 ska adderas (det vill säga 3 + 2 = 5):

<CalculatorRequest>
    <First>3</First>
    <Second>2</Second>
    <Operation>ADD</Operation>
</CalculatorRequest>

Om allt går bra blir svaret ett CalculatorResponse enligt nedan:

<CalculatorResponse>
    <Answer>5</Answer>
</CalculatorResponse>

Observera att detta endast är ”vanlig” XML baserad på en XSD. Inga WSDL-definitioner eller liknande ännu.

Javatjänst

Det förra inlägget i denna serie som handlade om Distribuerade tjänster via Spring beskrev hur man med hjälp av en service exporter kan exponera tjänster via exempelvis RMI, Hessian och så vidare. Springstödet för webbtjänster baseras på samma princip men det krävs lite mer manuellt arbete för att få en webbtjänst att fungera. Vi utgår från samma exempel som i Distribuerade tjänster via Spring det vill säga CalculatorService som passar väl ihop med det webbtjänstkontrakt som vi definierat ovan.

Javagränssnittet för CalculatorService ser ut så här:

package se.cygni.sample.spring.exporters;

public interface CalculatorService {
    int add(int first, int second);
    int subtract(int first, int second);
    int divide(int first, int second);
    int multiply(int first, int second);
}

Tjänsten realiseras med hjälp av en POJO enligt nedan:

package se.cygni.sample.spring.exporters;

public class PojoCalculatorService implements CalculatorService {

    public int add(int first, int second) {
        return first + second;
    }

    public int subtract(int first, int second) {
        return first - second;
    }

    public int divide(int first, int second) {
        return first / second;
    }

    public int multiply(int first, int second) {
        return first * second;
    }
}

Inga konstigheter, nu gäller det bara att koppla ihop XML-kontraktet och javatjänsten via en webbtjänst.

Webbtjänst

Springs webbtjänststöd på serversidan baseras kring en komponent som kallas MessageDispatcher. Denna komponent vidarebefordrar webbtjänstanrop till så kallade Endpoints. En endpoint är alltså en mottagare av ett webbtjänstanrop och är en javaklass som kan hanteras via Spring som vilken annan komponent som helst. Observera att man programmatiskt inte behöver känna till komponenten MessageDispatcher, Spring WS hanterar detta åt dig. Som applikationsutvecklare behöver du endast skapa/konfigurera endpoints.

Eftersom en endpoint är en javaklass och kontraktet är XML måste en mappning mellan XML och Java ske. Detta kallas marshalling/unmarshalling och Spring erbjuder stöd för detta via sin OXM teknologi (OXM – Object/XML Mapping). Spring OXM är alltså ett abstraktionslager för olika typer av tekniker för Object/XML Mapping, för mer information rekommenderas Springs OXM-dukumentation.

De tekniker som följer med Spring för marshalling/unmarshalling är:

Den teknologi som används i detta exempel är baserat på XStream. XStream är ett enkelt ramverk för serialisering av javaobjekt till och från XML. XStream erbjuder bland annat stöd för serialisering via annotationer.

Marshalling och unmarshalling är definierat via två springgränssnitt, Marshaller och Unmarshaller. Gränssnittet för marshalling är enkelt och innehåller metoden marshal som används för att konvertera ett Object till XML. XML:en skrivs till ett javax.xml.transform.Result.

Marshaller:

package org.springframework.oxm;

import java.io.IOException;
import javax.xml.transform.Result;

public interface Marshaller {
    void marshal(Object graph, Result result) throws XmlMappingException, IOException;
    boolean supports(Class clazz);
}

Gränssnittet för unmarshalling innehåller en motsvarande unmarshal-metod som konverterar en javax.xml.transform.Source till ett Object.

package org.springframework.oxm;

import java.io.IOException;
import javax.xml.transform.Source;

public interface Unmarshaller {
    Object unmarshal(Source source) throws XmlMappingException, IOException;
    boolean supports(Class clazz);
}

En endpoint kan skapas på flera sätt men i detta exempel kommer annotationer att användas. Till skillnad från de ”automagiska” service exporters vi tidigare träffat på måste vi nu mappa upp XML-requestet till en specifik metod i en javaklass (en endpoint).

package se.cygni.sample.spring.exporters.ws;

import org.springframework.ws.server.endpoint.annotation.Endpoint;
import org.springframework.ws.server.endpoint.annotation.PayloadRoot;

import se.cygni.sample.spring.exporters.CalculatorService;

@Endpoint
public class CalculatorServiceEndpoint {
    private CalculatorService calculatorService;

    public CalculatorServiceEndpoint(CalculatorService calculatorService) {
        this.calculatorService = calculatorService;
    }

    @PayloadRoot(
            localPart = "CalculatorRequest",
            namespace = "http://cygni.se/exporters/schemas")
    public CalculatorResponse executeOperation(CalculatorRequest request) {
        try {
            int result;
            switch (request.getOperation()) {
                case ADD:
                    result = calculatorService.add(
                            request.getFirst(),
                            request.getSecond());
                    break;
                case SUBTRACT:
                    result = calculatorService.subtract(
                            request.getFirst(),
                            request.getSecond());
                    break;
                case DIVIDE:
                    result = calculatorService.divide(
                            request.getFirst(),
                            request.getSecond());
                    break;
                case MULTIPLY:
                    result = calculatorService.multiply(
                            request.getFirst(),
                            request.getSecond());
                    break;
                default:
                    return new CalculatorResponse("No valid operation");
            }
            return new CalculatorResponse(result);
        } catch (Throwable thr) {
            return new CalculatorResponse(thr.getMessage());
        }
    }
}

Exemplet ovan visar implementation av en endpoint som delegerar vidare till en implementation av CalculatorService. Metoden executeOperation mappas via annotationen @PayloadRoot mot XML-taggen CalculatorRequest.

Värt att notera är inargument och returvärde. Inargumentet är av typen CalculatorRequest och returvärdet är av typen CalculatorResponse. Dessa typer mappas alltså mot XML-taggar via unmarshalling/marshalling som diskuterats tidigare. För detta exempel är klasserna CalculatorRequest och CalculatorResponse annoterade med XStream-annotationer (XStreamAlias och en marshaller/unmarshaller för XStream är deklarerad i applikationskontextet. För mer information om XStream och annotationer rekommenderas XStream-dokumentationen.

Requestobjektet:

package se.cygni.sample.spring.exporters.ws;

import com.thoughtworks.xstream.annotations.XStreamAlias;

@XStreamAlias("CalculatorRequest")
public class CalculatorRequest {
    @XStreamAlias("First")
    private int first;

    @XStreamAlias("Second")
    private int second;

    @XStreamAlias("Operation")
    private OperationType operation;

    public CalculatorRequest() {
        // Ingen initiering nödvändig
    }

    public CalculatorRequest(
            int first,
            int second,
            OperationType operation) {

        this.first = first;
        this.second = second;
        this.operation = operation;
    }

    public int getFirst() {
        return first;
    }

    public int getSecond() {
        return second;
    }

    public OperationType getOperation() {
        return operation;
    }
}

Enumeration för vilken typ av operation som ska utföras (add, subtract och så vidare):

package se.cygni.sample.spring.exporters.ws;

public enum OperationType {
    ADD, SUBTRACT, MULTIPLY, DIVIDE;
}

Responseobjektet:

package se.cygni.sample.spring.exporters.ws;

import com.thoughtworks.xstream.annotations.XStreamAlias;

@XStreamAlias("CalculatorResponse")
public class CalculatorResponse {
    @XStreamAlias("Answer")
    private Integer answer;

    @XStreamAlias("Error")
    private String error;

    public CalculatorResponse(Integer answer) {
        this.answer = answer;
    }

    public CalculatorResponse(String error) {
        this.error = error;
    }

    public Integer getAnswer() {
        return answer;
    }

    public String getError() {
        return error;
    }
}

Själva marshaller/unmarshaller-bönan är en del av Spring OXM och deklareras enligt följande:

<bean id="xstreamMarshaller"
      class="org.springframework.oxm.xstream.AnnotationXStreamMarshaller">
    <property
        name="annotatedClasses"
        value="se.cygni.sample.spring.exporters.ws.CalculatorRequest,
               se.cygni.sample.spring.exporters.ws.CalculatorResponse" />
</bean>

Alla de marshallers/unmarshallers som tillhandahålls av Spring implementerar både Marshaller– och Unmarshaller-gränssnitten. XStream-exemplet ovan använder propertyn annotatedClasses för att deklarera vilka klasser som är annoterade med XStream-annotationer.

Eftersom webbtjänster (som namnet antyder) är webbaserade så krävs det att tjänsterna exponeras via HTTP. Det sker enkelt via en deklaration i web.xml enligt nedan.

<servlet>
    <servlet-name>spring-ws</servlet-name>
    <servlet-class>org.springframework.ws.transport.http.MessageDispatcherServlet</servlet-class>
</servlet>

<servlet-mapping>
    <servlet-name>spring-ws</servlet-name>
    <url-pattern>/spring-ws/*</url-pattern>
</servlet-mapping>

Klassen MessageDispatcherServlet är en utökning av den vanliga DispatcherServlet från Spring MVC som kommer att diskuteras i ett senare inlägg. MessageDispatcherServlet vidarebefordrar anrop till MessageDispatchern som vidare delegerar till de olika endpoints som finns deklarerade.

Den konvention som används är att applikationskontextet måste heta ``-servlet.xml, precis som för Spring MVC. Om servletnamnet är spring-ws som i exemplet ovan så måste kontextfilen heta spring-ws-servlet.xml och den måste vara placerad i WEB-INF katalogen. Nedan visas ett exempel på hur spring-ws-servlet.xml kan se ut:

<beans
      xmlns="http://www.springframework.org/schema/beans"
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
      xsi:schemaLocation=
        "http://www.springframework.org/schema/beans
            http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">

    <bean id="pojoCalculatorService"
          class="se.cygni.sample.spring.exporters.PojoCalculatorService" />

    <bean id="calculatorServiceEndpoint"
          class="se.cygni.sample.spring.exporters.ws.CalculatorServiceEndpoint">

        <constructor-arg ref="pojoCalculatorService" />
    </bean>

    <bean id="endpointAdapter"
          class=
           "org.springframework.ws.server.endpoint.adapter.GenericMarshallingMethodEndpointAdapter">
        <constructor-arg ref="xstreamMarshaller" />
    </bean>

    <bean id="xstreamMarshaller"
          class="org.springframework.oxm.xstream.AnnotationXStreamMarshaller">
        <property name="annotatedClasses"
            value="se.cygni.sample.spring.exporters.ws.CalculatorRequest,
                   se.cygni.sample.spring.exporters.ws.CalculatorResponse" />
    </bean>

    <bean id="annotationMapper"
          class=
      "org.springframework.ws.server.endpoint.mapping.PayloadRootAnnotationMethodEndpointMapping" />

    <bean id="calculatorWsdl" class="org.springframework.ws.wsdl.wsdl11.DefaultWsdl11Definition">
        <property name="schema" ref="calculatorSchema" />
        <property name="portTypeName" value="Calculator" />
        <property name="locationUri" value="/spring-ws" />
    </bean>

    <bean id="calculatorSchema" class="org.springframework.xml.xsd.SimpleXsdSchema">
        <property name="xsd" value="classpath:schemas/calculator.xsd" />
    </bean>
</beans>

De bönor som deklarerats är:

  • pojoCalculatorService – Den faktiska POJO det vill säga den konkreta implementationen av CalculatorService.
  • calculatorServiceEndpoint – Endpoint är den klass som tar emot det faktiska webbtjänstanropet. Komponenten MessageDispatcher kommer att vidarebefordra anrop till denna endpoint som sedan typiskt vidarebefordrar till en POJO.
  • endpointAdapter – Generell springklass för att hantera marshalling/unmarshalling. Den unmarshaller som används är den konfigurerade xstreamMarshaller.
  • xstreamMarshaller – Den unmarshaller/marshaller som används för XStream-hanteringen.
  • annotationMapper – Hanterar mappningen mellan XML-taggar och javametoder via @PayloadRoot-annoteringar som visats ovan.
  • calculatorWsdl – Komponent som genererar en WSDL-fil baserat på ett vanligt schema.
  • calculatorSchema – Komponent som håller en referens till ett XML-schema och kan konfigureras till WSDL-komponenten.

För att exponera en webbtjänst krävs alltså följande:

  • Ett XML-schema.
  • En endpoint som typiskt delegerar vidare till en POJO (i ovanstående fall delegeras alla anrop till POJO:n för CalculatorService). Endpointen kan skapas genom att använda annoteringarna @Endpoint och @PayloadRoot.
  • OXM – Mappning mellan XML och Java via Springs OXM-stöd. Observera att genom att implementera gränssnitten Marshaller/Unmarshaller kan egna mappningsklasser skapas.
  • Deklaration i web.xml för MessageDispatcherServlet.
  • Applikationskontext för hantering av exempelvis annoteringsmappers, WSDL och liknande.

Anropa webbtjänster

Ett enkelt sätt att anropa eller testa en webbtjänst är att använda gratisprogrammet soapUI. Detta program kan automatiskt utifrån en WSDL-fil skapa olika requests och sedan anropa den exponerade tjänsten. Dessutom kan programmet användas till att bygga upp testfall som liknar enhetstester.

Om man vill anropa en webbtjänst rent programmatiskt erbjuder Spring stöd för detta genom klassen WebServiceTemplate. Denna klass bygger som så många andra klasser i Spring på templatingmönstret som tidigare nämnts i inlägget om Spring JDBC. För att anropa en webbtjänst används typiskt också OXM det vill säga mappning mellan XML och Java. WebServiceTemplate kan konfigureras med Marshaller/Unmarshaller-objekt men erbjuder även stöd att anropa webbtjänster direkt med XML via användning av XML-objekt av typen javax.xml.transform.Source. Några metoder värda att nämna är:

  • sendSourceAndReceiveToResult – Metod för att sända ett objekt av typen javax.xml.transform.Source och spara returvärde i ett objekt av typen javax.xml.transform.Result. Detta innebär att du som applikationsutvecklare själv måste producera den XML som ska skickas via Source-objektet.
  • marshalSendAndReceive – Metod för att hantera objektserialisering via Marshaller/Unmarshallers. Detta är det sätt som rekommenderas för att XML-hanteringen helt enkelt kan abstraheras bort via Spring OXM.

Springklassen WebServiceGatewaySupport är en stödklass som kan användas som basklass för de klasser som behöver använda webbtjänster. Den innehåller metoder för att konfigurera in en WebServiceTemplate och flera stödmetoder för att sända meddelanden via WebServiceTemplate.

Sammanfattning

Webbtjänster som baseras på SOAP är i grund och botten XML/HTTP. XML är ett relativt ”pratigt” format så denna typ webbtjänster kanske inte alltid är det bästa alternativet. Det kanske är smidigare att använda exempelvis Hessian eller liknande som diskuterats tidigare i inlägget om Distribuerade tjänster via Spring.

För att exportera en tjänst som en webbtjänst via Spring krävs en rad åtgärder. Först och främst måste ett XML-schema tas fram eftersom Spring endast stöder metoden Contract First. Sedan måste en så kallad endpoint skapas och konfigureras med hjälp av de mekanismer som Spring WS erbjuder. XML-innehållet måste konverteras till javaobjekt via så kallad unmarshalling och stöd för detta finns via Spring OXM.

För att anropa en tjänst krävs det betydligt färre handgrepp. Klassen WebServiceTemplate erbjuder så kallade templatingmetoder för att på olika sätt anropa webbtjänster. Det finns framför allt två sätt att använda denna template, genom att använda Source/Result från paketet javax.xml.transform eller genom att konfigurera Marshaller/Unmarshaller från Spring OXM.