Przesyłanie plików za pomocą Web Service


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.

Źródło: http://www.javaworld.com/javaworld/jw-02-2007/jw-02-handler.html

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:

15 Responses to Przesyłanie plików za pomocą Web Service

  1. Mariusz says:

    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

  2. andrzej says:

    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.

  3. zerdo says:

    Ja mam problem…
    Kiedy wygeneruję klienta to zamiast parametru typu DataHandler mam parametr byte[].

  4. zerdo says:

    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.

  5. zerdo says:

    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.

  6. zerdo says:

    Ponownie wygenerowany WSDL:

  7. zerdo says:

    wsdl:definitions name=”MyWsService” targetNamespace=”http://ws.mydomain.pl/”>

    </wsdl:definitions

  8. zerdo says:

    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.

  9. zerdo says:

    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>

  10. zerdo says:

    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>

  11. zerdo says:

    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?

  12. zerdo says:

    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.

Leave a reply to zerdo Cancel reply