Java Bugfixing: InputStream & OutputStream

 

Cuando nos encontramos una base de código legacy, uno de los errores latentes que más aparece en el código de desarrolladores menos experimentados es dejarse streams (input/output streams) abiertos, o a falta de realizar flush.

Veamos un ejemplo clásico de lectura de un fichero en el que el desarrollador olvida cerrar un stream y las consecuencias de dicho error:

import java.io.FileOutputStream;

public class UnclosedStream {
	public static final void main(String[] args) throws Exception{
		for(int i=0;i!=100000;i++) {
			FileOutputStream fos=new FileOutputStream("/tmp/file_"+i+".txt");
			// ... operaciones con el stream
			// Aqui olvidamos cerrar el stream
		}
	}
}

Resultado de esta ejecución:

Exception in thread "main" java.io.FileNotFoundException: /tmp/file_10230.txt (Too many open files)
	at java.io.FileOutputStream.open0(Native Method)
	at java.io.FileOutputStream.open(FileOutputStream.java:270)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:213)
	at java.io.FileOutputStream.<init>(FileOutputStream.java:101)
	at com.chocodev.blog.io.streams.UnclosedStream.main(UnclosedStream.java:8)

El error que vemos es “Too many open files”, indicando que la aplicación ha conseguido llegar al número máximo de ficheros que el sistema operativo está dispuesto a permitir (búsquese ulimit). Éste error desprende un olor espantoso cuando un proyecto es de cierto tamaño, lo trágico es que en nuestro ejemplo está claro dónde se produce el error y dónde solucionarlo, pero en el caso de un proyecto de algunas decenas/centenas de miles de líneas de código donde desarrollan varias personas, este error se presenta normalmente en algún lugar inverosímil no relacionado con el bloque de código en el que realmente se comete el error. Es fácil ver los síntomas pero puede ser difícil encontrar el culpable.

Ante este tipo de errores, cabe plantearse cualquiera de las siguientes soluciones:

– Java < 1.7

– Opción 1: Correcto cierre de stream, usando clases estándar

 

public class ClosedStream {
	private static final Logger LOG=Logger.getLogger(ClosedStream.class.getName());
	public static final void main(String[] args) throws Exception {
		for (int i = 0; i != 100000; i++) {
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream("/tmp/file_" + i + ".txt");
				// ... operar con el stream				
			} catch (IOException ex) {
				LOG.log(Level.SEVERE, ex.getMessage(), ex);
			} finally {
				if (fos != null) {
					try {
						fos.close();
					} catch (Exception ex) {
						LOG.log(Level.SEVERE, ex.getMessage(), ex);
					}
				}
			}
		}
	}
}

Esta forma de operar con stream posteriormente cerrándolo siempre en un bloque finally, libera el recurso pase lo que pase y no requiere de otras librerías, aunque hay que reconocer que no es un código excesivamente conciso (en parte por el propio funcionamiento del lenguaje) ya que nos obliga a hacer otro try dentro del finally para capturar alguna posible excepción derivada de la llamada a close.

– Opcion 2: Correcto cierre de stream, usando librerías

Hay librerías bastante prácticas para el cierre de streams, por ejemplo, si utilizamos commons-io:

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.apache.commons.io.IOUtils;

public class ClosedStreamCommonsIO {
	private static final Logger LOG=Logger.getLogger(ClosedStreamCommonsIO.class.getName());
	public static final void main(String[] args) throws Exception {
		for (int i = 0; i != 100000; i++) {
			FileOutputStream fos = null;
			try {
				fos = new FileOutputStream("/tmp/file_" + i + ".txt");
				// ... operar con el stream				
			} catch (IOException ex) {
				LOG.log(Level.SEVERE, ex.getMessage(), ex);
			} finally {
				IOUtils.closeQuietly(fos);				
			}
		}
	}
}

En este caso, IOUtils.closeQuietly se encarga del cierre del stream ignorando cualquier posible error producido en el close

– Java >= 1.7

– Uso de try-with-resources

A partir de Java 1.7, apareció el bloque try-with-resources, que simplifica de forma estándar el cierre de streams en un amplio porcentaje de los casos de la siguiente forma:

import java.io.FileOutputStream;

public class ClosedStreamJava7 {
	public static final void main(String[] args) throws Exception {
		for (int i = 0; i != 100000; i++) {
			try(FileOutputStream fos =  
					new FileOutputStream("/tmp/file_" + i + ".txt"))
			{
				// ... operar con el stream				
			}
		}
	}
}

En este caso, los streams declarados dentro del try(…) son cerrados una vez terminado el bloque, incluso es posible declarar varios dentro del mismo try:

try(FileOutputStream f1=new FileOutputStream("f1");
	FileOutputStream f2=new FileOutputStream("f2")){
	// ... operar con los streams
}

Con el bloque try-with-resources se puede utilizar cualquier clase de la API que implemente el interfaz java.io.Closeable

En este post os he mostrado posiblemente el culpable de la mayoría de los problemas que terminan con un “esta aplicación necesita reiniciarse cada X días / cada noche para mantener el rendimiento”. Esta frase es la peor que nosotros como desarrolladores podemos darle a un administrador de sistemas, y la peor que un administrador de sistemas puede darle a un cliente.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *