12.3.13

Comprimir con gzip una request SOAP, en Spring Web Services

En un proyecto reciente, tuve que implementar un cliente SOAP que debía ineractuar con un servicio web de un tercero, ya implementado y definido, esto es, el descriptor del servicio y la forma de interactuar con el mismo eran un dato del problema. Uno de estos requisitos era que la request HTTP tenía que ir comprimida con gzip. Si no era así, el servidor devolvería un error (en este caso, era una exigencia razonable, pues los datos a enviar podían pesar bastante).

De entre las opciones que tenemos por ahí, me decanté por emplear Spring Web Services, que encuentro cómodo y sencillo de usar. Los que tengáis algo de experiencia con este framework, sabréis que para implementar un cliente, debemos recurrir a la clase WebServiceTemplate. Internamente, esta clase utiliza una implementación de WebServiceMessageSender para el transporte. Si no se configura, la implementación por defecto es HttpUrlConnectionMessageSender, que utiliza por debajo las clases del propio JRE para el transporte HTTP. Otra opción puede ser utilizar HttpComponentsMessageSender, que utiliza la librería de Apache HttpClient.

Sin embargo, ninguna de estas implementaciones nos ofrece la posibilidad de comprimir la request. El response puede venir comprimido sin problemas, sin más que establecer el setAcceptGzipEncoding(boolean) a true (cosa que ya viene así por defecto), pero no tenemos ayuda para la request. Buscando por la red, uno tampoco encuentra mucha información, salvo indicaciones de que debemos implementar nuestro propio WebServiceMessageSender. Y efectivamente, así es. Pero no os preocupéis, no es necesario implementar uno desde cero.

Si miramos el código fuente de HttpUrlConnectionMessageSender, veremos que la implementación del método createConnection(URI) es algo así (pongo el de la versión 2.0.5):


public WebServiceConnection createConnection(URI uri) throws IOException {
    URL url = uri.toURL();
    URLConnection connection = url.openConnection();
    if (!(connection instanceof HttpURLConnection)) {
        throw new HttpTransportException("URI [" + uri + "] is not an HTTP URL");
    }
    else {
        HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
        prepareConnection(httpURLConnection);
        return new HttpUrlConnection(httpURLConnection);
    }
}

Esto es, a partir del URI pasado, obtiene un objeto URL, que utiliza a su vez para obtener una URLConnection. Como el protocolo es HTTP, sabemos que en realidad, el objeto es una instancia de HttpURLConnection. Finalmente, envuelve este objeto con una HttpUrlConnection, que es lo que devuelve. Mucho ojito con los nombres, que es fácil liarse. HttpURLConnection es la clase del JDK que hereda de URLConnection, mientras que HttpUrlConnection es la clase de Spring que implementa WebServiceConnection.

El objeto WebServiceConnection es lo que se va a utilizar dentro del WebServiceMessageSender para tratar con la conexión HTTP, por lo que también tendremos que hacernos una implementación propia. Vemos que HttpUrlConnection (la de Spring) tiene un método getRequestOutputStream() que devuelve el OutputStream donde escribir lo que queramos enviar con la request. Lo que necesitamos es envolver este OutputStream con un GZIPOutputStream (aunque HttpUrlConnection también tiene un método getConnection que devuelve la HttpURLConnection original del JDK, no es usado en el HttpUrlConnectionMessageSender).

Así que lo que hacemos es crearnos una clase que herede de HttpUrlConnection, y sobreescribimos su método getRequestOutputStream(). Para que el método pueda ser llamado más de una vez y devuelva el mismo objeto, utilizaremos una variable de instancia donde almacenar dicho objeto:


import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.zip.GZIPOutputStream;

import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.http.HttpUrlConnection;

public class GZipHttpUrlConnection extends HttpUrlConnection {

    private OutputStream outputStream;

    GZipHttpUrlConnection(HttpURLConnection connection) {
        super(connection);
    }

    @Override
    protected OutputStream getRequestOutputStream() throws IOException {
        if (outputStream == null) {
            outputStream = new GZIPOutputStream(super.getRequestOutputStream());
        }
        return outputStream;
    }
}

Esta clase es la que devolverá nuestra implementación de WebServiceMessageSender en el método createConnection(URI). Para ello, heredaremos de HttpUrlConnectionMessageSender y sobreescribiremos el método createConnection(URI):


import org.springframework.ws.transport.WebServiceConnection;
import org.springframework.ws.transport.http.HttpTransportException;
import org.springframework.ws.transport.http.HttpUrlConnectionMessageSender;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URLConnection;

class GZipHttpUrlConnectionMessageSender extends HttpUrlConnectionMessageSender {

    @Override
    public WebServiceConnection createConnection(URI uri) throws IOException {
        URLConnection connection = uri.toURL().openConnection();
        if (!(connection instanceof HttpURLConnection)) {
            throw new HttpTransportException("URI [" + uri + "] is not an HTTP URL");
        }

        HttpURLConnection httpURLConnection = (HttpURLConnection) connection;
        prepareConnection(httpURLConnection);
        return new GZipHttpUrlConnection(httpURLConnection);
    }
}

Aún no hemos terminado. El código anterior, tal y como lo he puesto, no funcionará. Hay que hacer dos ajustes imprescindibles.

El primero es añadir la cabecera HTTP Content-Encoding con el valor gzip, mediante el método setRequestProperty(String, String) de URLConnection. Si no, el servidor no sabrá que la request viene comprimida. Podemos hacerlo en el constructor de GZipHttpUrlConnection:


GZipHttpUrlConnection(HttpURLConnection connection) {
    super(connection);
    connection.setRequestProperty("Content-Encoding", "gzip");
}

O podemos sobreescribir el método prepareConnection(HttpURLConnection) en GZipHttpUrlConnectionMessageSender:


@Override
protected void prepareConnection(HttpURLConnection connection) throws IOException {
    super(connection);
    connection.setRequestProperty("Content-Encoding", "gzip");
}

La elección es vuestra.

El segundo ajuste tiene que ver con el anterior post. Resulta que ninguna clase involucrada del framework Spring Web Services está cerrando el OutputStream de la request. Así, nos encontraremos con la desagradable sorpresa de que al servidor sólo le llegan los 10 bytes de la cabecera gzip. Para evitarlo, debemos cerrar nosotros mismos el OutputStream. ¿Donde? Pues un buen sitio donde hacerlo es sobreescribiendo el método onSendAfterWrite(WebServiceMessage) de HttpUrlConnection, ya que como se indica en la documentación, se invoca tras finalizar la escritura del mensaje. Por tanto, nuestra clase GZipHttpUrlConnection quedaría así (he añadido también el ajuste del Content-Encoding):


import java.io.IOException;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.zip.GZIPOutputStream;

import org.springframework.ws.WebServiceMessage;
import org.springframework.ws.transport.http.HttpUrlConnection;

class GZipHttpUrlConnection extends HttpUrlConnection {

    private OutputStream outputStream;

    GZipHttpUrlConnection(HttpURLConnection connection) {
        super(connection);
        connection.setRequestProperty("Content-Encoding", "gzip");
    }

    @Override
    protected OutputStream getRequestOutputStream() throws IOException {
        if (outputStream == null) {
            outputStream = new GZIPOutputStream(super.getRequestOutputStream());
        }
        return outputStream;
    }

    @Override
    protected void onSendAfterWrite(WebServiceMessage message) throws IOException {
        if (outputStream != null) {
            outputStream.close();
        }
        super.onSendAfterWrite(message);
    }
}

Aclararé que es necesario llamar al onSendAfterWrite(WebServiceMessage) del padre ya que, aunque la documentación diga que no, sí que tiene una implementación (hace un connection.connect()). Y además, hay que hacerlo al final, o si no, nos saltará una excepción al hacer el close().

Bien, con esto ya lo tenemos todo. Ya sólo nos queda configurar adecuadamente nuestra instancia de WebServiceTemplate en Spring, para que reciba una instancia de GZipHttpUrlConnectionMessageSender como messageSender. Si lo hacemos mediante XML, añadiríamos algo así:


<bean id="webServiceTemplate" class="org.springframework.ws.client.core.WebServiceTemplate">
    <property name="messageSender">
        <bean class="GZipHttpUrlConnectionMessageSender"/>
    </property>
</bean>

Y con esto, nuestro cliente SOAP ya tendría capacidad para enviar la request comprimida con gzip.