Lanzando una excepción adentro finalmente

27

Los analizadores de código estático como Fortify "se quejan" cuando se puede lanzar una excepción dentro de un finallybloque, diciendo eso Using a throw statement inside a finally block breaks the logical progression through the try-catch-finally. Normalmente estoy de acuerdo con esto. Pero recientemente me encontré con este código:

SomeFileWriter writer = null; 
try { 
     //init the writer
     //write into the file
} catch (...) {
     //exception handling
} finally {
     if (writer!= null) writer.close();  
}

Ahora, si writerno se puede cerrar correctamente, el writer.close()método arrojará una excepción. Se debe lanzar una excepción porque (muy probablemente) el archivo no se guardó después de la escritura.

Podría declarar una variable adicional, configurarla si hubo un error al cerrar writery lanzar una excepción después del bloque finalmente. Pero este código funciona bien y no estoy seguro de si cambiarlo o no.

¿Cuáles son los inconvenientes de lanzar una excepción dentro del finallybloque?

superM
fuente
44
Si esto es Java y puede usar Java 7, compruebe si los bloques ARM pueden resolver su problema.
Landei
@Landei, esto lo resuelve, pero desafortunadamente no estamos usando Java 7.
SuperM
Yo diría que el código que ha mostrado no es "Usar una declaración de lanzamiento dentro de un bloque finalmente" y, como tal, la progresión lógica está bien.
Mike
@ Mike, he usado el resumen estándar que muestra Fortify, pero directa o indirectamente hay una excepción lanzada finalmente.
SuperM
Desafortunadamente, Fortify también detecta el bloqueo de prueba con recursos como una excepción finalmente lanzada dentro ... es demasiado inteligente, maldición ... todavía no está seguro de cómo superarlo, parece que la razón para probar con recursos fue asegurar el cierre del finalmente recursos, y ahora cada uno de dichos sea comunicada por Fortify como una amenaza a la seguridad ..
mmona

Respuestas:

18

Básicamente, las finallycláusulas están ahí para garantizar la liberación adecuada de un recurso. Sin embargo, si se lanza una excepción dentro del bloque finalmente, esa garantía desaparece. Peor aún, si su bloque principal de código arroja una excepción, la excepción generada en el finallybloque lo ocultará. Parece que el error fue causado por la llamada a close, no por la razón real.

Algunas personas siguen un patrón desagradable de manejadores de excepciones anidados, tragando cualquier excepción lanzada en el finallybloque.

SomeFileWriter writer = null; 
try { 
     //init the writer
     //write into the file
} finally {
    if (writer!= null) {
        try {
            writer.close();
        } catch (...) {
        }
    }
}

En versiones anteriores de Java, puede "simplificar" este código envolviendo recursos en clases que hacen esta limpieza "segura" por usted. Un buen amigo mío crea una lista de tipos anónimos, cada uno de los cuales proporciona la lógica para limpiar sus recursos. Luego, su código simplemente recorre la lista y llama al método de eliminación dentro del finallybloque.

Travis Parks
fuente
1
+1 Aunque estoy entre los que usan ese código desagradable. Pero para mi justificación diré que no hago esto siempre, solo cuando la excepción no es crítica.
SuperM
2
Me encuentro haciéndolo también. Algunas veces, la creación de cualquier administrador de recursos completo (anónimo o no) resta valor al propósito del código.
Travis Parks
+1 de mí también; básicamente dijiste exactamente lo que iba a hacer pero mejor. Vale la pena señalar que algunas de las implementaciones de Stream en Java no pueden arrojar Excepciones en close (), pero la interfaz lo declara porque algunas de ellas lo hacen. Entonces, en algunas circunstancias, es posible que esté agregando un bloque catch que nunca será realmente necesario.
vaughandroid
1
¿Qué tiene de malo usar un bloque try / catch dentro de un bloque finalmente? Preferiría hacer eso en lugar de inflar todo mi código al tener que envolver todas mis conexiones y transmisiones, etc. solo para que puedan cerrarse en silencio, lo que, en sí mismo, presenta un nuevo problema de no saber si cerrar una conexión o transmisión ( o lo que sea) causa una excepción, que es algo que realmente le gustaría saber o hacer algo sobre ...
prohibición de geoingeniería
10

Lo que dijo Travis Parks es cierto que las excepciones en el finallybloque consumirán cualquier valor de retorno o excepciones de los try...catchbloques.

Sin embargo, si está utilizando Java 7, el problema se puede resolver utilizando un bloque de prueba con recursos . Según los documentos, siempre que su recurso se implemente java.lang.AutoCloseable(la mayoría de los escritores / lectores de bibliotecas lo hacen ahora), el bloque de prueba con recursos lo cerrará por usted. El beneficio adicional aquí es que cualquier excepción que ocurra mientras se cierra se suprimirá, permitiendo que el valor de retorno original o la excepción pase.

Desde

FileWriter writer = null;
try {
  writer = new FileWriter("myFile.txt");
  writer.write("hello");
} catch(...) {
  // return/throw new exception
} finally {
  writer.close(); // an exception would consume the catch block's return/exception
}

A

try (FileWriter writer = new FileWriter("myFile.txt")) {
  writer.write("hello");
} catch(...) {
  // return/throw new exception, always gets returned even if writer fails to close
}

http://docs.oracle.com/javase/tutorial/essential/exceptions/tryResourceClose.html

Clintonmonk
fuente
1
Esto es útil a veces. Pero en caso de que realmente necesite lanzar una excepción cuando el archivo no se puede cerrar, este enfoque oculta los problemas.
SuperM
1
@superM En realidad, probar con recursos no oculta una excepción close(). "se suprimirá cualquier excepción que ocurra mientras se cierra, permitiendo que el valor de retorno original o excepción pase", esto simplemente no es cierto . Una excepción del close()método se suprimirá solo cuando se arroje otra excepción desde el bloque try / catch. Entonces, si el trybloque no arroja ninguna excepción, se lanzará la excepción del close()método (y no se devolverá el valor de retorno). Incluso se puede atrapar desde el catchbloque actual .
Ruslan Stelmachenko
0

Creo que esto es algo que tendrá que abordar caso por caso. En algunos casos, lo que dice el analizador es correcto, ya que el código que tiene no es tan bueno y necesita un replanteamiento. Pero puede haber otros casos en los que lanzar o incluso volver a lanzar sea lo mejor. No es algo que puedas ordenar.

drekka
fuente
0

Esta será una respuesta más conceptual a por qué estas advertencias pueden existir incluso con enfoques de prueba con recursos. Desafortunadamente, tampoco es el tipo de solución fácil que esperas lograr.

La recuperación de errores no puede fallar

finally modela un flujo de control posterior a la transacción que se ejecuta independientemente de si una transacción tiene éxito o falla.

En el caso de falla, finallycaptura la lógica que se ejecuta en medio de la recuperación de un error, antes de que se haya recuperado completamente (antes de llegar a nuestro catchdestino).

Imagine el problema conceptual que presenta al encontrarse con un error en medio de la recuperación de un error.

Imagine un servidor de base de datos donde estamos tratando de confirmar una transacción, y falla a la mitad (digamos que el servidor se quedó sin memoria en el medio). Ahora el servidor quiere revertir la transacción a un punto como si nada hubiera pasado. Sin embargo, imagine que encuentra otro error en el proceso de retroceso. Ahora terminamos teniendo una transacción medio comprometida con la base de datos: la atomicidad y la naturaleza indivisible de la transacción ahora se rompe, y la integridad de la base de datos ahora se verá comprometida.

Este problema conceptual existe en cualquier lenguaje que trate con errores, ya sea C con propagación de código de error manual, o C ++ con excepciones y destructores, o Java con excepciones y finally.

finally no puede fallar en lenguajes que lo proporcionan de la misma manera que los destructores no pueden fallar en C ++ en el proceso de encontrar excepciones.

La única forma de evitar este problema conceptual y difícil es asegurarse de que el proceso de deshacer las transacciones y liberar recursos en el medio no pueda encontrar una excepción / error recursivo.

Entonces, el único diseño seguro aquí es un diseño donde writer.close()posiblemente no puede fallar. Por lo general, hay formas en el diseño para evitar escenarios en los que tales cosas pueden fallar en medio de la recuperación, lo que lo hace imposible.

Desafortunadamente, es la única forma: la recuperación de errores no puede fallar. La forma más fácil de garantizar esto es hacer que esos tipos de funciones de "liberación de recursos" y "efectos secundarios inversos" no puedan fallar. No es fácil: la recuperación adecuada de errores es difícil y desafortunadamente difícil de probar. Pero la forma de lograrlo es asegurarse de que las funciones que "destruyen", "cierren", "apaguen", "retrocedan", etc., no pueden encontrar un error externo en el proceso, ya que tales funciones a menudo necesitarán ser llamado en medio de la recuperación de un error existente.

Ejemplo: registro

Digamos que quieres registrar cosas dentro de un finallybloque. Esto a menudo será un gran problema a menos que el registro no pueda fallar . El registro casi seguro puede fallar, ya que es posible que desee agregar más datos al archivo, y eso puede encontrar fácilmente muchas razones para fallar.

Entonces, la solución aquí es hacerlo para que cualquier función de registro utilizada en los finallybloques no pueda arrojarse a la persona que llama (podría fallar, pero no arrojará). ¿Cómo podríamos hacer eso? Si su idioma permite el lanzamiento dentro del contexto de que finalmente haya un bloque anidado de prueba / captura, esa sería una forma de evitar lanzar al interlocutor tragándose excepciones y convirtiéndolas en códigos de error, por ejemplo, tal vez el registro se puede hacer en un sitio separado proceso o subproceso que puede fallar por separado y fuera de una pila de recuperación de errores existente desenrollar Siempre que pueda comunicarse con ese proceso sin la posibilidad de encontrarse con un error, eso también sería seguro para las excepciones, ya que el problema de seguridad solo existe en este escenario si estamos lanzando recursivamente desde el mismo hilo.

En este caso, podemos salir con la falla de registro siempre y cuando no se lance, ya que no iniciar sesión y no hacer nada no es el fin del mundo (no está filtrando ningún recurso o no revierte los efectos secundarios, por ejemplo).

De todos modos, estoy seguro de que ya puedes comenzar a imaginar lo increíblemente difícil que es realmente hacer que un software sea seguro para las excepciones. Es posible que no sea necesario buscar esto en el mayor grado posible, excepto en el software más crítico. Pero vale la pena tomar nota de cómo lograr realmente la seguridad de las excepciones, ya que incluso los autores de bibliotecas de propósito muy general a menudo pierden el control y destruyen toda la seguridad de las excepciones de su aplicación usando la biblioteca.

SomeFileWriter

Si SomeFileWriterpuede arrojar dentro close, entonces diría que generalmente es incompatible con el manejo de excepciones, a menos que nunca intente cerrarlo en un contexto que implique recuperarse de una excepción existente. Si el código está fuera de su control, podríamos ser SOL, pero valdría la pena notificar a los autores de este flagrante problema de seguridad de excepción. Si está bajo su control, mi recomendación principal es asegurarse de que cerrarlo no pueda fallar por los medios necesarios.

Imagínese si un sistema operativo realmente no puede cerrar un archivo. Ahora, cualquier programa que intente cerrar un archivo al apagarse no podrá cerrarse . ¿Qué se supone que debemos hacer ahora, simplemente mantener la aplicación abierta y en el limbo (probablemente no), simplemente filtrar el recurso del archivo e ignorar el problema (tal vez está bien si no es tan crítico)? El diseño más seguro: haga que sea imposible no cerrar un archivo.


fuente