Zleco no mi ostatnio wykonanie Web Service’u, którego zadanie polegało na przesyłaniu niewielkich plików binarnych. Początkowo (w pośpiechu) payload wiadomości transmitowałem jako tablicę bajtów w klasie POJO reprezentującej obiekt odpowiedzi. Niestety, podczas testów przeprowadzanych na serwerze aplikacyjnym WLS, owy mechanizm doprowadził do całkowitego padu serwera podczas przetwarzania pliku o wielkości około 10 MB. 10 MB to dziesięć razy więcej niż maksymalna rzeczywista przesyłana wielkość paczki. Szybki wgląd w szkolenie odnośnie BEA Web Services zaowocował niemal natychmiastową odpowiedzią dotyczącą sposobu przesyłania plików w technologi Web Service. Wszelkie obiekty pokaźnych rozmiarów należy bowiem stream’ować jako załączniki komunikatu SOAP.
Implementacja
Podczas implementacji WS skorzystałem z adnotacji @HandlerChain wskazującej na deskryptor zawierający definicję łańcucha wywołań kolejnych “interceptorów” metody serwisowej (patrz poniższe listingi). Plik ten znajdować się powinien w tym samym katalogu co skompilowane klasy serwisu. Każde ogniwo łańcucha wywołań spowodować może zaprzestanie dalszego przetwarzania wiadomości. Dzięki temu adnotację @HandlerChain stosuje się często podczas realizacji niestandardowego procesu autentykacji, autoryzacji, czy też pomiaru wydajności. Zawartość pliku definiującego handler chain przedstawia następujący listing:
<?xml version="1.0" encoding="UTF-8"?>
<handler-chains xmlns="http://java.sun.com/xml/ns/javaee">
<handler-chain name="SampleAttachmentHandler">
<handler>
<handler-class>edu.lantoniak.service.handler.AttachmentHandler</handler-class>
</handler>
</handler-chain>
</handler-chains>
Kod handler’a dodającego załącznik gdy ustawiono określoną zmienną kontekstową:
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.xml.namespace.QName;
import javax.xml.soap.AttachmentPart;
import javax.xml.soap.SOAPMessage;
import javax.xml.ws.handler.MessageContext;
import javax.xml.ws.handler.soap.SOAPHandler;
import javax.xml.ws.handler.soap.SOAPMessageContext;
import java.io.File;
import java.util.Collections;
import java.util.Set;
public class AttachmentHandler implements SOAPHandler<SOAPMessageContext> {
private static final Logger log = Logger.getLogger(AttachmentHandler.class);
public Set<QName> getHeaders() {
return Collections.emptySet();
}
public boolean handleMessage(SOAPMessageContext context) {
if (shouldAddAttachment(context)) {
SOAPMessage soapMessage = context.getMessage();
String path = (String) context.get(SampleServiceImpl.MY_KEY);
FileDataSource fileDataSource = new FileDataSource(path);
DataHandler dataHandler = new DataHandler(fileDataSource);
AttachmentPart attachmentPart = soapMessage.createAttachmentPart(dataHandler);
attachmentPart.setContentId(fileDataSource.getFile().getName());
soapMessage.addAttachmentPart(attachmentPart);
context.put(SampleServiceImpl.MY_KEY, null); // Cleanup
}
return true;
}
public boolean handleFault(SOAPMessageContext context) {
return true;
}
public void close(MessageContext context) {
}
/*
* Add attachment only if SampleServiceImpl.MY_KEY is set.
*/
private boolean shouldAddAttachment(MessageContext context) {
String patchKey = (String) context.get(SampleServiceImpl.MY_KEY);
return patchKey != null;
}
}
W przypadku zwrócenie false przez metodę handleMessage, któregoś z ogniw łańcucha, dalsze przetwarzanie pozostaje przerwane (czytaj dokumentację JAX-WS). Sama klasa serwisu powinna wyglądać następująco:
@Stateless
@WebService(serviceName="SampleService",
targetNamespace="http://edu.lantoniak",
wsdlLocation = "META-INF/wsdl/SampleService.wsdl",
portName="SampleServicePort",
endpointInterface="edu.lantoniak.service.SampleService")
@HandlerChain(file="handler-chain.xml")
public class SampleServiceImpl implements SampleService {
public static final String MY_KEY = "edu.lantoniak.service.MY_KEY";
@Resource
WebServiceContext webServiceContext;
@TransactionAttribute(TransactionAttributeType.NOT_SUPPORTED)
public GetFileRequestElement getSampleFile(GetFileResponseElement request) {
...
webServiceContext.getMessageContext().put(MY_KEY, request.getAbsoluteFilePath());
...
return response;
}
}
Zwracam uwagę na współdzielenie kontekstu wiadomości MessageContext. Plik reprezentowany przez ścieżkę request.getAbsoluteFilePath() dodawany jest jako zacznik podczas generacji komunikatu odpowiedzi SOAP (po ukończeniu przetwarzania instrukcji zawartych w metodzie getSampleFile). Handler wykonuje się zawsze przed i po wywołaniu docelowej procedury (patrz poniższy diagram, źródło: http://www.javaworld.com/javaworld/jw-02-2007/jw-02-handler.html). Etap przetwarzania komunikatu poznać można za pomocą np. atrybutu MessageContext.MESSAGE_OUTBOUND_PROPERTY znajdującego się w obiekcie typu MessageContext.
Piszę tutaj zapewne o rzeczach dla większości znanych. Teraz przejdę jednak do sedna. W adnotacji @WebService wskazałem na własnoręcznie stworzony plik WSDL. Według specyfikacji Attachments Profile Version 1.0 (http://www.ws-i.org/profiles/attachmentsprofile-1.0.html) chęć przesyłania załączników powinna zostać specjalnie oznaczona we wspomnianym deskryptorze. Relewantne fragmenty WSDL zamieszczam poniżej:
<xs:element name="AttachmentElement" type="xs:base64Binary"/>
<wsdl:message name="GetFileRequestMessage">
<wsdl:part name="request" element="tns:GetFileRequestElement"/>
</wsdl:message>
<wsdl:message name="GetFileResponseMessage">
<wsdl:part name="response" element="tns:GetFileResponseElement"/>
<wsdl:part name="fileAttachment" element="tns:AttachmentElement"/>
</wsdl:message>
<wsdl:portType name="SampleServicePort">
<wsdl:operation name="getSampleFile">
<wsdl:input message="tns:GetFileRequestMessage"/>
<wsdl:output message="tns:GetFileResponseMessage"/>
</wsdl:operation>
</wsdl:portType>
<wsdl:binding name="SampleServiceBinding" type="tns:SampleServicePort">
<soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http"/>
<wsdl:operation name="getSampleFile">
<soap:operation soapAction=""/>
<wsdl:input>
<soap:body use="literal"/>
</wsdl:input>
<wsdl:output>
<mime:multipartRelated>
<mime:part>
<soap:body parts="response" use="literal"/>
</mime:part>
<mime:part>
<mime:content part="fileAttachment" type="application/octet-stream"/>
</mime:part>
</mime:multipartRelated>
</wsdl:output>
</wsdl:operation>
</wsdl:binding>
Oczywiście w przypadku pominięcia wstawek określających załączniki (linie 8 i 26 – 33), pliki są w dalszym ciągu wysyłane oraz prawidłowo odbierane przez klienta! Specyfikacja jest jednak specyfikacją i nie powinna być lekceważona. Alternatywną metodę wysyłania i odbierania plików przez Web Service (bez użycia @HandlerChain) opisał Geemon Kadambalil: http://geemonkb.blogspot.com/2009/01/jax-wx-streaming-soap-attachments.html. Spoglądając z obecnej perspektywy na efekt mojej pracy, dochodzę do wnioski, iż rozwiązanie Geemon’a wydaje się bardziej eleganckie i w przyszłości będę je stosował. Własnoręczne pisanie WSDL spowodowane było koniecznością trzymania się wewnętrznych standardów nazewniczych. Może któryś z czytelników zna adnotację JAXB pozwalającą na zmianę atrybutu name tag’a wsdl:binding? Co prawda po zapoznaniu się z dokumentem JSR224 v2.2 mrel3, wątpie w istnienie owej funkcjonalności: The value of the name attribute of the wsdl:binding is not significant, by convention it contains the qualified name of the corresponding wsdl:portType suffixed with “Binding”.
Przydatne linki: