28.2.13

El método flush() de GZIPOutputStream, no garantiza el flush de los datos

La clase java.util.zip.GZIPOutputStream permite comprimir un conjunto de bytes en el formato gzip. Esta clase hereda de java.io.FilterOutputStream, por lo que su uso es muy sencillo: envolvemos un java.io.OutputStream cualquiera con la clase mencionada, y vamos llamando a las distintas variantes de write(). Al terminar el proceso (llamando a close()), habremos escrito en el stream envuelto, los datos comprimidos.

Pero hay un detalle que no está reflejado en la documentación del JDK, y es que GZIPOutputStream rompe el contrato implícito de sus padres (al menos, en Java 6). Si llamamos al método flush() de GZIPOutputStream, no siempre forzaremos el volcado de datos. Este comportamiento no es en realidad consecuencia de una reimplementación de flush() en GZIPOutputStream. Si observamos la documentación, comprobaremos que flush() es directamente heredado de FilterOutputStream, y lo que hace es llamar al método flush() del stream subyacente.

El problema está en realidad en que la clase padre de GZIPOutputStream, DeflaterOutputStream, sobreescribe los métodos write(). Esta clase padre es la que realiza la tarea de compresión de datos, ya que GZIPOutputStream simplemente añade una cabecera y una cola. Las llamadas a las distintas variantes de write(), no siempre se traducen en llamadas a los métodos write() del stream subyacente. DeflaterOutputStream utiliza un buffer propio donde guarda los bytes resultantes de la compresión, y sólo los pasa al stream subyacente en algunos casos. Esto es debido al algoritmo de compresión, que se basa en la búsqueda de símbolos y secuencias repetidas.

Sólo una llamada a los métodos close() o finish() nos garantiza que el stream subyacente ha recibido la totalidad de los bytes. Pero al hacerlo, cerramos el stream a nuevos datos. El método finish() es exclusivo de DeflaterOutputStream y fuerza la compresión, escribiendo en el stream subyacente. Una vez hecho esto, cualquier nueva llamada a write() lanzará una excepción. El método close(), llama al propio finish() y al close() del stream subyacente.

Supongo que algunos pensaréis: «Bueno, no pasa nada. Después de todo, al terminar de usar un stream, hay que cerrarlo». Sí, pero este comportamiento no cumple el principio de sustitución de Liskov. Esto puede causar problemas (y de hecho los causa, como veremos en el siguiente post) cuando pasamos nuestro objeto GZIPOutputStream a una librería o a un framework, y no tenemos control de lo que se hace con él.

Un síntoma de este problema es encontrarnos con la desagradable sorpresa de que tras llamar varias veces a los métodos write(), al stream subyacente sólo llegan los 10 bytes de la cabecera gzip, por mucho flush() que hagamos. Si esto ocurre, tened la seguridad de que nadie está haciendo un close() del objeto GZIPOutputStream.

1 comentario:

  1. Creo recordar que eso también pasa (al menos pasaba) con el OutputStream devuelto por UrlConnection... Para mí el principal problema es que te puedes quedar sin memoria como lo que tengas entre manos sea demasiado grande y cuentes con con el flush como aliado.

    Ya si no lo cierras, personalmente lo considero un error de programación, pero que no te funcione el flush...

    ResponderEliminar