En esta quinta parte del tutorial veremos cómo enviar y recibir ficheros utilizando un servicio web SOAP, especialmente de la forma más eficiente posible gracias el protocolo MTOM.
- Servidor
- Clientes
- Securización TLS + BASIC
- Handlers
Servicio SOAP «normal»
Vamos a añadir un nuevo servicio SOAP a la parte del servidor de nuestro proyecto de ejemplo con dos operaciones para subir y descargar un fichero. No voy a explayarme pues esto ya lo vimos en la primera parte del tutorial.
package com.danielme.demo.jaxws.cxf.ws; import java.io.IOException; import javax.jws.WebService; @WebService public interface IFileTransferService { byte[] downloadFileContent() throws IOException; void uploadFile(byte[] fileContent); }
La siguiente implementación devuelve un fichero pdf.
package com.danielme.demo.jaxws.cxf.ws; import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import org.apache.log4j.Logger; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; @Service(value = "fileTransferService") public class FileTransferServiceImpl implements IFileTransferService { private static final Logger logger = Logger.getLogger(FileTransferServiceImpl.class); @Override public byte[] downloadFileContent() throws IOException { File file = new ClassPathResource("small.pdf").getFile(); Path path = Paths.get(file.getPath()); return Files.readAllBytes(path); } @Override public void uploadFile(byte[] fileContent) { logger.info("file uploaded, length " + fileContent.length); } }
Añadimos el endpoint al applicationContext.xml
<jaxws:endpoint id="fileTransferServiceWS" implementor="#fileTransferService" address="/v/1/transferFileService"> </jaxws:endpoint>
Si desplegamos la aplicación podemos ver el descriptor del nuevo servicio en la url
http://localhost:8080/spring-cxf-ws/ws/v/1/transferFileService?wsdl
<?xml version="1.0" encoding="UTF-8"?> <wsdl:definitions xmlns:wsdl="http://schemas.xmlsoap.org/wsdl/" xmlns:ns1="http://schemas.xmlsoap.org/soap/http" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="http://ws.cxf.jaxws.demo.danielme.com/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" name="FileTransferServiceImplService" targetNamespace="http://ws.cxf.jaxws.demo.danielme.com/"> <wsdl:types> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" attributeFormDefault="unqualified" elementFormDefault="unqualified" targetNamespace="http://ws.cxf.jaxws.demo.danielme.com/"> <xs:element name="downloadFileContent" type="tns:downloadFileContent" /> <xs:element name="downloadFileContentResponse" type="tns:downloadFileContentResponse" /> <xs:element name="uploadFile" type="tns:uploadFile" /> <xs:element name="uploadFileResponse" type="tns:uploadFileResponse" /> <xs:complexType name="uploadFile"> <xs:sequence> <xs:element minOccurs="0" name="arg0" type="xs:base64Binary" /> </xs:sequence> </xs:complexType> <xs:complexType name="uploadFileResponse"> <xs:sequence /> </xs:complexType> <xs:complexType name="downloadFileContent"> <xs:sequence /> </xs:complexType> <xs:complexType name="downloadFileContentResponse"> <xs:sequence> <xs:element minOccurs="0" name="return" type="xs:base64Binary" /> </xs:sequence> </xs:complexType> <xs:element name="IOException" type="tns:IOException" /> <xs:complexType name="IOException"> <xs:sequence> <xs:element minOccurs="0" name="message" type="xs:string" /> </xs:sequence> </xs:complexType> </xs:schema> </wsdl:types> <wsdl:message name="IOException"> <wsdl:part element="tns:IOException" name="IOException" /> </wsdl:message> <wsdl:message name="downloadFileContentResponse"> <wsdl:part element="tns:downloadFileContentResponse" name="parameters" /> </wsdl:message> <wsdl:message name="uploadFile"> <wsdl:part element="tns:uploadFile" name="parameters" /> </wsdl:message> <wsdl:message name="uploadFileResponse"> <wsdl:part element="tns:uploadFileResponse" name="parameters" /> </wsdl:message> <wsdl:message name="downloadFileContent"> <wsdl:part element="tns:downloadFileContent" name="parameters" /> </wsdl:message> <wsdl:portType name="IFileTransferService"> <wsdl:operation name="uploadFile"> <wsdl:input message="tns:uploadFile" name="uploadFile" /> <wsdl:output message="tns:uploadFileResponse" name="uploadFileResponse" /> </wsdl:operation> <wsdl:operation name="downloadFileContent"> <wsdl:input message="tns:downloadFileContent" name="downloadFileContent" /> <wsdl:output message="tns:downloadFileContentResponse" name="downloadFileContentResponse" /> <wsdl:fault message="tns:IOException" name="IOException" /> </wsdl:operation> </wsdl:portType> <wsdl:binding name="FileTransferServiceImplServiceSoapBinding" type="tns:IFileTransferService"> <soap:binding style="document" transport="http://schemas.xmlsoap.org/soap/http" /> <wsdl:operation name="uploadFile"> <soap:operation soapAction="" style="document" /> <wsdl:input name="uploadFile"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="uploadFileResponse"> <soap:body use="literal" /> </wsdl:output> </wsdl:operation> <wsdl:operation name="downloadFileContent"> <soap:operation soapAction="" style="document" /> <wsdl:input name="downloadFileContent"> <soap:body use="literal" /> </wsdl:input> <wsdl:output name="downloadFileContentResponse"> <soap:body use="literal" /> </wsdl:output> <wsdl:fault name="IOException"> <soap:fault name="IOException" use="literal" /> </wsdl:fault> </wsdl:operation> </wsdl:binding> <wsdl:service name="FileTransferServiceImplService"> <wsdl:port binding="tns:FileTransferServiceImplServiceSoapBinding" name="FileTransferServiceImplPort"> <soap:address location="http://localhost:8080/spring-cxf-ws/ws/v/1/transferFileService" /> </wsdl:port> </wsdl:service> </wsdl:definitions>
Probemos el servicio con SOAP UI.
Podemos ver que el mensaje SOAP de respuesta tiene la siguiente estructura.
<?xml version="1.0" encoding="UTF-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/"> <soap:Body> <ns2:downloadFileContentResponse xmlns:ns2="http://ws.cxf.jaxws.demo.danielme.com/"> <return>JVBERi0xLjMNJeLjz9MNCjcgMCBvYmo9iag1zdGFydHhyZWYNCjExNg0KJSVFT0YNCg==</return> </ns2:downloadFileContentResponse> </soap:Body> </soap:Envelope>
El fichero se ha enviado dentro del mensaje SOAP de respuesta serializado en una cadena en Base 64 y esto plantea los siguientes problemas:
- El tamaño de un fichero binario convertido en una cadena en Base 64 se incrementa considerablemente (un 30%-40% aproximadamente). Esto supone una ralentización adicional en la transmisión del mensaje.
- Serializar y deserializar el fichero en una cadena supone un coste computacional que ralentiza aún más el proceso de comunicación con el servicio web.
- Al trabajar con byte[] el fichero se carga en memoria lo que puede provocar un OutOfMemoryError si es muy grande. Es lo que podemos observar en la captura anterior tomada en SOAP UI realizando la prueba con un fichero de 100Mb.
Si vamos a transferir ficheros muy pequeños estos inconvenientes probablemente no supongan ningún problema, pero si los ficheros pueden ser grandes y/o queremos optimizar el rendimiento (y además tenemos que utilizar un servicio web SOAP en lugar de una solución más específica como por ejemplo un servicio FTP) recomiendo utilizar la alternativa que veremos en la próxima sección.
Servicio SOAP con MTOM
El protocolo MTOM (Message Transmission Optimisation Protocol) se desarrolló para proporcionar una transmisión de ficheros binarios lo más eficiente posible dentro del protocolo SOAP. La idea consiste en no enviar los ficheros binarios serializados como texto dentro del mensaje XML SOAP sino como adjuntos de la petición codificados en base64Binary.
Vamos a crear un nuevo servicio web equivalente a IFileTransferService pero utilizando MTOM.
package com.danielme.demo.jaxws.cxf.ws; import java.io.IOException; import javax.activation.DataHandler; import javax.jws.WebService; import javax.xml.ws.soap.MTOM; @WebService @MTOM public interface IFileTransferMTOMService { DataHandler downloadFileContent() throws IOException; void uploadFile(DataHandler dataHandler); }
Obsérvese las dos diferencias con respecto al servicio sin MTOM:
- Se ha usado la anotación @MTOM. Como alternativa, también se podría anotar la implementación del servicio con @BindingType(value = javax.xml.ws.soap.SOAPBinding.SOAP12HTTP_MTOM_BINDING)
- En lugar de byte[], el fichero binario se modela con la clase DataHandler para permitir hacer streaming del mismo y evitar cargarlo en memoria.
La implementación queda así
package com.danielme.demo.jaxws.cxf.ws; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import javax.activation.DataHandler; import javax.activation.FileDataSource; import org.apache.log4j.Logger; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; @Service(value = "fileTransferMTOMService") // @BindingType(value = javax.xml.ws.soap.SOAPBinding.SOAP12HTTP_MTOM_BINDING) public class FileTransferMTOMServiceImpl implements IFileTransferMTOMService { private static final Logger logger = Logger.getLogger(FileTransferMTOMServiceImpl.class); @Override public DataHandler downloadFileContent() throws IOException { File file = new ClassPathResource("small.pdf").getFile(); return new DataHandler(new FileDataSource(file)); } @Override public void uploadFile(DataHandler dataHandler) { try { File file = File.createTempFile("upload", ".pdf"); file.deleteOnExit(); FileOutputStream outputStream = new FileOutputStream(file); dataHandler.writeTo(outputStream); outputStream.close(); logger.info("file saved in = " + file.getPath()); } catch (IOException ex) { logger.error(ex.getMessage(), ex); } } }
Lo definimos en el applicationContext.xml
<jaxws:endpoint id="fileTransferMTOMServiceWS" implementor="#fileTransferMTOMService" address="/v/1/transferFileMTOMService">
El nuevo servicio lo tenemos en la url
http://localhost:8080/spring-cxf-ws/ws/v/1/transferFileMTOMService?wsdl
Si lo utilizamos para descargar el fichero ahora obtendremos una respuesta distinta.
- La respuesta es de tipo application/xop+xml
- En el XML sólo se indica el identificador del fichero dentro de los adjuntos de la respuesta.
- El fichero aparece dentro de la pestaña de Attachments y se puede abrir haciendo click.
Ahora utilicemos el servicio con MTOM en los clientes Java implementados en la segunda parte del tutorial. En ambos casos, tenemos que incluir en el proyecto una copia de la clase IFileTransferMTOMService.
Cliente Spring
Se añade la definición del servicio al applicationContext.xml
<jaxws:client id="fileTransferMTOMClient" serviceClass="com.danielme.demo.jaxws.cxf.ws.IFileTransferMTOMService" address="${endpoint-mtom}"/>
El servicio ya puede ser utilizado directamente. En este nuevo Main se descarga el fichero y vuelve a ser enviado al Servicio Web. Puesto que el DataHandler es un stream, el fichero no se habrá descargado totalmente hasta que el stream se haya leído en su totalidad, en el ejemplo se escribe en un fichero temporal utilizando FileOutputStream.
package com.danielme.demo.jaxws.cxf.client.spring; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import javax.activation.DataHandler; import javax.activation.FileDataSource; import org.apache.log4j.Logger; import org.springframework.context.support.ClassPathXmlApplicationContext; import com.danielme.demo.jaxws.cxf.ws.IFileTransferMTOMService; public class MainMTOM { private static final Logger logger = Logger.getLogger(MainMTOM.class); public static void main(String[] args) throws Exception { // Initializes Spring Container ClassPathXmlApplicationContext applicationContext = new ClassPathXmlApplicationContext( "applicationContext.xml"); IFileTransferMTOMService fileTransferService = (IFileTransferMTOMService) applicationContext .getBean("fileTransferMTOMClient"); File fileDownloaded = download(fileTransferService); upload(fileTransferService, fileDownloaded); applicationContext.close(); } private static File download(IFileTransferMTOMService fileTransferService) throws IOException { DataHandler dataHandlerResponse = fileTransferService.downloadFileContent(); File file = File.createTempFile("temp", ".pdf"); // file.deleteOnExit(); FileOutputStream outputStream = new FileOutputStream(file); dataHandlerResponse.writeTo(outputStream); outputStream.close(); logger.info("path = " + file.getPath()); return file; } private static void upload(IFileTransferMTOMService fileTransferService, File fileDownloaded) { DataHandler dataHandlerSend = new DataHandler(new FileDataSource(fileDownloaded)); fileTransferService.uploadFile(dataHandlerSend); } }
Cliente sin Spring
Similar al anterior, véase cómo se activa MTOM de forma programática aunque en nuestro ejemplo no es realmente necesario ya que la interfaz está anotada con @MTOM
package com.danielme.demo.jaxws.cxf.client; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.util.HashMap; import java.util.Map; import java.util.Properties; import javax.activation.DataHandler; import javax.activation.FileDataSource; import org.apache.cxf.interceptor.LoggingInInterceptor; import org.apache.cxf.interceptor.LoggingOutInterceptor; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; import org.apache.log4j.Logger; import com.danielme.demo.jaxws.cxf.ws.IFileTransferMTOMService; /** * * @author danielme.com * */ public class MainMTOM { public static final Logger logger = Logger.getLogger(MainMTOM.class); public static void main(String[] args) throws Exception { Properties properties = new Properties(); properties.load(Main.class.getClassLoader().getResourceAsStream("config.properties")); // client JaxWsProxyFactoryBean jaxWsProxyFactoryBean = new JaxWsProxyFactoryBean(); jaxWsProxyFactoryBean.setServiceClass(IFileTransferMTOMService.class); jaxWsProxyFactoryBean.setAddress(properties.getProperty("endpoint-mtom")); Map<String, Object> props = new HashMap<String, Object>(); props.put("mtom-enabled", Boolean.TRUE); jaxWsProxyFactoryBean.setProperties(props); // logging jaxWsProxyFactoryBean.getInInterceptors().add(new LoggingInInterceptor()); jaxWsProxyFactoryBean.getOutInterceptors().add(new LoggingOutInterceptor()); IFileTransferMTOMService fileTransferClient = (IFileTransferMTOMService) jaxWsProxyFactoryBean .create(); File fileDownloaded = download(fileTransferClient); upload(fileTransferClient, fileDownloaded); } private static File download(IFileTransferMTOMService fileTransferClient) throws IOException { DataHandler dataHandlerResponse = fileTransferClient.downloadFileContent(); File file = File.createTempFile("temp", ".pdf"); // file.deleteOnExit(); FileOutputStream outputStream = new FileOutputStream(file); dataHandlerResponse.writeTo(outputStream); outputStream.close(); logger.info("path = " + file.getPath()); return file; } private static void upload(IFileTransferMTOMService fileTransferClient, File file) { DataHandler dataHandlerSend = new DataHandler(new FileDataSource(file)); fileTransferClient.uploadFile(dataHandlerSend); } }
Código de ejemplo
El código de ejemplo del tutorial completo se encuentra en GitHub Para más información sobre cómo utilizar GitHub, consultar este artículo.