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 avCalculatorService
.calculatorServiceEndpoint
– Endpoint är den klass som tar emot det faktiska webbtjänstanropet. KomponentenMessageDispatcher
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örMessageDispatcherServlet
. - 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 typenjavax.xml.transform.Source
och spara returvärde i ett objekt av typenjavax.xml.transform.Result
. Detta innebär att du som applikationsutvecklare själv måste producera den XML som ska skickas viaSource
-objektet.marshalSendAndReceive
– Metod för att hantera objektserialisering viaMarshaller
/Unmarshaller
s. 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.