Przesyłanie plików za pomocą Web Service
01/04/2011 15 Comments
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:
- http://blogs.sun.com/alanf/entry/implementing_soap_with_attachments_using – “Implementing Soap With Attachments using the WS-I Attachment Profile 1.0 features in JAX-WS 2.0”.
- http://www.ws-i.org/profiles/attachmentsprofile-1.0.html – Specyfikacja Attachments Profile Version 1.0.
- http://geemonkb.blogspot.com/2009/01/jax-wx-streaming-soap-attachments.html – Wysyłanie i odbieranie plików za pomocą DataHandler bez użycia @HandlerChain.
- http://download.oracle.com/docs/cd/E12840_01/wls/docs103/webserv_adv/mtom.html – Optymalizacja sposobu wysyłania i odbierania plików przez WS.
- http://www.javaworld.com/javaworld/jw-02-2007/jw-02-handler.html – Dokładny opis łańcucha wywołań technologii Web Service.
Witam!
Czy próbował Pan integrować przez WSDL WordPressa z Web Servicem Oracle’a (Siebel)?
Powstał nawet plugin: http://wordpress.org/extend/plugins/wordpress-web-service/, ale zapewne trzeba byłoby go dostosować…
Pozdrawiam,
Mariusz
Witam!
Niestety nie próbowałem. Nie miałem także nigdy do czynienia z Siebel’em.
Proponowałbym utworzyć nowy wątek na forum OTN w kategorii Siebel: http://forums.oracle.com/forums/category.jspa?categoryID=151.
Pozdrawiam,
Łukasz
Witam!
Niestety strona http://geemonkb.blogspot.com/2009/01/jax-wx-streaming-soap-attachments.html. jest nie dostępna. Czy mógłby Pan nakreślenić rozwiązanie zastosowane na stronie “geemonkb.blogspot.com”
Oczywiście. Poniżej znajduje się kod strony serwerowej oraz klienta (wygenerowanego przy pomocy jaxws-maven-plugin). Kluczowe są adnotacje @StreamingAttachment i @MTOM, oraz MTOMFeature i HTTP_CLIENT_STREAMING_CHUNK_SIZE.
import com.sun.xml.ws.developer.StreamingDataHandler;
import com.sun.xml.ws.developer.StreamingAttachment;
import javax.jws.WebService;
import javax.jws.Oneway;
import javax.ejb.Stateless;
import javax.xml.ws.soap.MTOM;
import javax.xml.ws.WebServiceException;
import javax.xml.bind.annotation.XmlMimeType;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import java.io.File;
import java.io.IOException;
import org.apache.log4j.Logger;
@WebService
@Stateless
@MTOM
@StreamingAttachment(parseEagerly=true, memoryThreshold=1000000L)
public class FileService {
private static final Logger log = Logger.getLogger(FileService.class);
@XmlMimeType("application/octet-stream")
public DataHandler fileDownload(String path) {
return new DataHandler(new FileDataSource(path));
}
@Oneway
public void fileUpload(String path, @XmlMimeType("application/octet-stream") DataHandler data) {
StreamingDataHandler dh = null;
try {
dh = (StreamingDataHandler) data;
File file = new File(path);
dh.moveTo(file);
} catch (Exception e) {
throw new WebServiceException(e);
} finally {
if (dh != null) {
try {
dh.close();
} catch (IOException e) {
log.error("Could not close data handler.", e);
}
}
}
}
}
import com.sun.xml.ws.developer.JAXWSProperties;
import com.sun.xml.ws.developer.StreamingDataHandler;
import javax.activation.DataHandler;
import javax.activation.FileDataSource;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.WebServiceFeature;
import javax.xml.ws.WebServiceRef;
import javax.xml.ws.soap.MTOMFeature;
import java.io.File;
import java.io.IOException;
import java.util.Map;
/**
* Generacja kodu klienta WS za pomocą jaxws-maven-plugin.
*/
public class Client {
@WebServiceRef
private static FileServiceService fileServiceService;
public static void main(String[] args) throws IOException {
fileServiceService = new FileServiceService();
FileService fileService = fileServiceService.getFileServicePort(new WebServiceFeature[]{new MTOMFeature()});
Map ctxt = ((BindingProvider) fileService).getRequestContext();
ctxt.put(JAXWSProperties.HTTP_CLIENT_STREAMING_CHUNK_SIZE, 500);
DataHandler dh = fileService.fileDownload("E:\\Tmp\\movie.avi");
if (dh instanceof StreamingDataHandler) {
StreamingDataHandler sdh = (StreamingDataHandler) dh;
sdh.moveTo(new File("C:\\Tmp\\movie.avi"));
}
// fileService.fileUpload("E:\\Tmp\\movie.avi", new DataHandler(new FileDataSource("E:\\Movies\\Volvo Ocean Race\\Episode 10.wmv")));
}
}
W razie problemów proszę o dalsze komentarze.
Ja mam problem…
Kiedy wygeneruję klienta to zamiast parametru typu DataHandler mam parametr byte[].
Klienta generuję za pomocą soapUI z wsdl wygenerowanego podczas osadzania na jboss.
Zapomniałęm doprecyzować – chodzi mi o metodę fileUpload.
Co mogę z tym zrobić?
Typ byte[] powoduje niestety przesyłanie załącznika inline’owo w body koperty SOAP. Nie polecam takiego rozwiązania. Czy metoda usługi zainstalowana na serwerze przyjmuje parametr typu DataHandler i została oznaczona odpowiednimi adnotacjami? Skoro klient Web Service wygenerowany za pomocą soapUI nie wspiera stream’owania załączników, to skorzystałbym z jaxws-maven-plugin.
Niestety nie mogę użyć jaxws-maven-plugin ponieważ wygenerowany klient wymaga bibliotek powodujących konflikty któych nie mogę obejść. Klienta generuję z axis1.1.
Mój WS wygląda w ten sposób:
@WebService
@MTOM(enabled = true)
@StreamingAttachment(parseEagerly=true, memoryThreshold=1000000L)
public class MyWs{
private static final Logger log = Logger
.getLogger(MyWs.class);
@WebMethod
public void setFile(String fileName, @XmlMimeType(“application/octet-stream”) DataHandler cvAttach) {
ContextBuilder.sendToIntegrationModule(
“vm:integracja/refa/setJobApplication”, message);
}
}
Wygenerowany WSDL:
Nie wiem czy to nie zbyt wiele ale tak myślę – czy może ktoś komu odało się zrobić przesyłanie z użyciem MTOM mógłby spr. użyć powyższej klasy?
Jakieś podpowiedzi?
Muszę używać axis1.1.
Ponownie wygenerowany WSDL:
wsdl:definitions name=”MyWsService” targetNamespace=”http://ws.mydomain.pl/”>
</wsdl:definitions
Niestety nie mogę wkleić wsdl-a w org. postaci.
Przesyłam WSDL z wyciętymi ”
wsdl:types>
xsd:schema attributeFormDefault=”unqualified” elementFormDefault=”unqualified” targetNamespace=”http://ws.mydomain.pl/”>
xsd:element name=”setFile” type=”tns:setFile”/>
xsd:complexType name=”setFile”>
xsd:sequence>
xsd:element minOccurs=”0″ name=”arg0″ type=”xsd:string”/>
xsd:element expectedContentTypes=”application/octet-stream” minOccurs=”0″ name=”arg1″ type=”xsd:base64Binary”/>
/xsd:sequence>
/xsd:complexType>
xsd:element name=”setFileResponse” type=”tns:setFileResponse”/>
xsd:complexType name=”setFileResponse”>
xsd:sequence/>
/xsd:complexType>
/xsd:schema>
/wsdl:types>
wsdl:message name=”setFile”>
wsdl:part element=”tns:setFile” name=”parameters”>
/wsdl:part>
/wsdl:message>
wsdl:message name=”setFileResponse”>
wsdl:part element=”tns:setFileResponse” name=”parameters”>
/wsdl:part>
/wsdl:message>
wsdl:portType name=”MyWsServiceService”>
wsdl:operation name=”setFile”>
wsdl:input message=”tns:setFile” name=”setFile”>
/wsdl:input>
wsdl:output message=”tns:setFileResponse” name=”setFileResponse”>
/wsdl:output>
/wsdl:operation>
/wsdl:portType>
wsdl:binding name=”MyWsServiceSoapBinding” type=”tns:MyWsServiceService”>
soap:binding style=”document” transport=”http://schemas.xmlsoap.org/soap/http”/>
wsdl:operation name=”setFile”>
soap:operation soapAction=”” style=”document”/>
wsdl:input name=”setFile”>
soap:body use=”literal”/>
/wsdl:input>
wsdl:output name=”setFileResponse”>
soap:body use=”literal”/>
/wsdl:output>
/wsdl:operation>
/wsdl:binding>
wsdl:service name=”MyWsService”>
wsdl:port binding=”tns:MyWsServiceSoapBinding” name=”MyWsServiceServicePort”>
soap:address location=”http://localhost:8180/integracjaws/MyWsServiceService”/>
/wsdl:port>
/wsdl:service>
/wsdl:definitions>
Zastanwaiam się czy jest to już problem z wygenerowanym WSDL-em czy może z samym generowaniem klienta.
Wygenerowałęm klienta z użyciem: jaxws-maven-plugin
Widzę że w tym przypadku DataHandler został zmieniony na byte[].
Zastanwaiam się czy to nie jest problem z samym generowaniem WSDL.
Aplikację osadzam na jboss6.
W web.xml mam wpis:
servlet>
servlet-name>MyWs
servlet-class>pl.mydomain.ws.SabaDmzWebService
/servlet>
servlet-mapping>
servlet-name>MyWs
url-pattern>/MyWs
/servlet-mapping>
Przepraszam miało być:
servlet>
servlet-name>MyWs
servlet-class>pl.mydomain.ws.MyWs
/servlet>
servlet-mapping>
servlet-name>MyWs
url-pattern>/MyWs
/servlet-mapping>
Sprawdziłęm zachowanie dla zamieszczonego przez Pana serwisu: FileService
Sytuacja jest identyczna jak dla mojego serwisu.
JEdyna modyfikacja jakiej dokonałęm to usunięcie ‘Stateles’. Wpis w web.xml, osadzenie na jboss6 i wygenerowanie klienta za pomocą ‘jaxws-maven-plugin’ – również mam byte[] zamiast DataHandler.
Może należy coś zmieniać na serwerze?
Witam,
Znalazłęm przyczynę – jednak nie wiem jeszcze czy da się i jak to rozwiązać.
NA defaultowym jboss6 działa poprawnie, ja natomiast mam dostarczony przez klienta jboss6 ale w zmodyfikowanej postaci. Wygląda na to że coś jest na nim pozmieniane – jednak nie mam pojęcia co.