¿Cuál es la mejor manera de manejar una ExecutionException?

Tengo un método que realiza alguna tarea con un tiempo de espera. Uso ExecutorServer.submit() para obtener un objeto Future y luego llamo a future.get() con un tiempo de espera. Esto funciona bien, pero mi pregunta es la mejor manera de manejar las excepciones verificadas que mi tarea puede generar. El siguiente código funciona y conserva las excepciones comprobadas, pero parece extremadamente torpe y propenso a romperse si cambia la lista de excepciones comprobadas en la firma del método.

Alguna sugerencia en como arreglar esto? Necesito apuntar a Java 5, pero también me gustaría saber si hay buenas soluciones en las versiones más nuevas de Java.

public static byte[] doSomethingWithTimeout( int timeout ) throws ProcessExecutionException, InterruptedException, IOException, TimeoutException {

    Callable<byte[]> callable = new Callable<byte[]>() {
        public byte[] call() throws IOException, InterruptedException, ProcessExecutionException {
            //Do some work that could throw one of these exceptions
            return null;
        }
    };

    try {
        ExecutorService service = Executors.newSingleThreadExecutor();
        try {
            Future<byte[]> future = service.submit( callable );
            return future.get( timeout, TimeUnit.MILLISECONDS );
        } finally {
            service.shutdown();
        }
    } catch( Throwable t ) { //Exception handling of nested exceptions is painfully clumsy in Java
        if( t instanceof ExecutionException ) {
            t = t.getCause();
        }
        if( t instanceof ProcessExecutionException ) {
            throw (ProcessExecutionException)t;
        } else if( t instanceof InterruptedException ) {
            throw (InterruptedException)t;
        } else if( t instanceof IOException ) {
            throw (IOException)t;
        } else if( t instanceof TimeoutException ) {
            throw (TimeoutException)t;
        } else if( t instanceof Error ) {
            throw (Error)t;
        } else if( t instanceof RuntimeException) {
            throw (RuntimeException)t;
        } else {
            throw new RuntimeException( t );
        }
    }
}

=== ACTUALIZACIÓN ===

Muchas personas publicaron respuestas que recomendaban 1) volver a lanzar como una excepción general o 2) volver a lanzar como una excepción no verificada. No quiero hacer ninguno de estos, porque estos tipos de excepción (ProcessExecutionException, InterruptedException, IOException, TimeoutException) son importantes: cada uno será manejado de manera diferente por la llamada procesada. Si no necesitara una función de tiempo de espera, me gustaría que mi método lanzara estos 4 tipos de excepción específicos (bueno, excepto TimeoutException). No creo que agregar una función de tiempo de espera deba cambiar la firma de mi método para generar un tipo de excepción genérico.

preguntado el 03 de mayo de 12 a las 20:05

Solo haz tu método throws Exception y lanzar todo esto con la misma línea de código. puedes atrapar ExecutionException y justo throw e.getCause -- y no atrape nada más, simplemente deje que se propague por sí solo. -

Hola Marko, gracias por la sugerencia, pero necesito mi API para lanzar estos 4 tipos específicos de excepción. No quiero lanzar una excepción genérica. -

11 Respuestas

He analizado este problema en profundidad, y es un desastre. No hay una respuesta fácil en Java 5, ni en 6 o 7. Además de la torpeza, verbosidad y fragilidad que señalas, tu solución en realidad tiene el problema de que el ExecutionException que te estas desnudando cuando llamas getCause() en realidad contiene la mayor parte de la información importante de seguimiento de la pila.

Es decir, toda la información de la pila del subproceso que ejecuta el método en el código que presentó está solo en ExcecutionException, y no en las causas anidadas, que solo cubren marcos que comienzan en call() en el Invocable. Ese es tu doSomethingWithTimeout ¡El método ni siquiera aparecerá en los rastros de la pila de las excepciones que está lanzando aquí! Solo obtendrás la pila incorpórea del ejecutor. Esto se debe a que el ExecutionException es el único que se creó en el subproceso de llamada (ver FutureTask.get()).

La única solución que conozco es complicada. Gran parte del problema se origina con la especificación de excepción liberal de Callable - throws Exception. Puede definir nuevas variantes de Callable que especifican exactamente qué excepciones arrojan, como:

public interface Callable1<T,X extends Exception> extends Callable<T> {

    @Override
    T call() throws X; 
}

Esto permite que los métodos que ejecutan callables tengan una precisión más precisa. throws cláusula. Si desea admitir firmas con hasta N excepciones, lamentablemente necesitará N variantes de esta interfaz.

Ahora puede escribir un contenedor alrededor del JDK Executor que toma el Callable mejorado y devuelve un mejorado Future, algo como guayaba ComprobadoFuturo. Los tipos de excepción comprobados se propagan en tiempo de compilación desde la creación y el tipo de la ExecutorService, al devuelto Futures, y terminar en el getChecked método sobre el futuro.

Así es como se enhebra el tipo de seguridad en tiempo de compilación. Esto significa que en lugar de llamar:

Future.get() throws InterruptedException, ExecutionException;

Puedes llamar:

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

Por lo tanto, se evita el problema de desenvolver: su método arroja inmediatamente las excepciones del tipo requerido y están disponibles y verificadas en el momento de la compilación.

Inside getChecked, sin embargo tu posible necesita resolver el problema de desempaquetado de la "causa faltante" descrito anteriormente. Puede hacer esto uniendo la pila actual (del hilo de llamada) a la pila de la excepción lanzada. Esto amplía el uso habitual de un seguimiento de pila en Java, ya que una sola pila se extiende a través de subprocesos, pero funciona y es fácil de entender una vez que sabe lo que está sucediendo.

Otra opción es crear otro excepción de la misma cosa que la que se echa, y poner la original como causa de la nueva. Obtendrá el seguimiento completo de la pila, y la relación de causa será la misma que funciona con ExecutionException - pero tendrá el tipo correcto de excepción. Sin embargo, deberá usar la reflexión y no se garantiza que funcione, por ejemplo, para objetos sin constructor que tenga los parámetros habituales.

Respondido 06 Oct 12, 05:10

Marcando esta como la respuesta más correcta, aunque no es la solución que esperaba... - jesse barnum

De hecho, yo mismo no encontré ninguna solución ideal, y solo vivo con el hecho de que agregar la ejecución de subprocesos fundamentalmente hace que el manejo de excepciones sea más difícil, tanto a nivel de código cuando se trata cuando se verifican las excepciones como durante el diagnóstico porque la pila tiene más marcos y un "causa" adicional que no estaba allí antes. - abejaencuerda

Esto es lo que hago en esta situación. Esto logra lo siguiente:

  • Vuelve a lanzar las excepciones marcadas sin envolverlas
  • Pega los rastros de la pila

Código:

public <V> V waitForThingToComplete(Future<V> future) {
    boolean interrupted = false;
    try {
        while (true) {
            try {
                return future.get();
            } catch (InterruptedException e) {
                interrupted = true;
            }
        }
    } catch (ExecutionException e) {
        final Throwable cause = e.getCause();
        this.prependCurrentStackTrace(cause);
        throw this.<RuntimeException>maskException(cause);
    } catch (CancellationException e) {
        throw new RuntimeException("operation was canceled", e);
    } finally {
        if (interrupted)
            Thread.currentThread().interrupt();
    }
}

// Prepend stack frames from the current thread onto exception trace
private void prependCurrentStackTrace(Throwable t) {
    final StackTraceElement[] innerFrames = t.getStackTrace();
    final StackTraceElement[] outerFrames = new Throwable().getStackTrace();
    final StackTraceElement[] frames = new StackTraceElement[innerFrames.length + outerFrames.length];
    System.arraycopy(innerFrames, 0, frames, 0, innerFrames.length);
    frames[innerFrames.length] = new StackTraceElement(this.getClass().getName(),
      "<placeholder>", "Changed Threads", -1);
    for (int i = 1; i < outerFrames.length; i++)
        frames[innerFrames.length + i] = outerFrames[i];
    t.setStackTrace(frames);
}

// Checked exception masker
@SuppressWarnings("unchecked")
private <T extends Throwable> T maskException(Throwable t) throws T {
    throw (T)t;
}

Parece funcionar.

Respondido 17 Oct 13, 21:10

¿No es esto siempre va a arrojar excepciones no verificadas? - jesse barnum

Vuelve a lanzar cualquier excepción que se haya lanzado originalmente, ya sea que esté marcada o no. Normalmente, declararía waitForThingToComplete() para lanzar cualquier excepción verificada lanzada por la devolución de llamada futura (si corresponde). La persona que llama al método "piensa" que la devolución de llamada se está invocando en el hilo actual, incluso si no es así. He usado este código para un aspecto AOP que ejecuta métodos anotados en un subproceso diferente, haciendo que el subproceso actual se bloquee hasta que se complete, sin el conocimiento de las personas que llaman al método. - Archie

Por qué "<placeholder>"? - IGR

Ahí es donde normalmente colocaría el nombre del archivo de origen. Esta entrada de seguimiento de pila se crea dinámicamente en tiempo de ejecución. - Archie

Aquí hay un par de información interesante para verificar y contra las excepciones verificadas. Discusión de Brian Goetz y un argumento en contra de las excepciones comprobadas de Discusión de Eckel. Pero no sabía si ya implementó y pensó en el refactor de excepción verificado que discute Joshua en este primer libro.

De acuerdo con las perlas de Java efectivas, uno de los métodos preferidos para manejar las excepciones verificadas es convertir una excepción verificada en una excepción no verificada. Así por ejemplo,

try{
obj.someAction()
}catch(CheckedException excep){
}

cambiar esta implementación a

if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}

contestado el 03 de mayo de 12 a las 21:05

Una sensación muy fuerte de que este método debería arrojar excepciones comprobadas. Cada uno debe ser capturado y manejado por el código de llamada. El proceso que tengo actualmente mantiene el tipo de excepción, simplemente está lleno de código repetitivo inconveniente. - jesse barnum

Me temo que no hay respuesta a su problema. Básicamente, está iniciando una tarea en un subproceso diferente al que se encuentra y desea utilizar el patrón ExecutorService para capturar todas las excepciones que puede generar la tarea, además de la ventaja de interrumpir esa tarea después de una cierta cantidad de tiempo. Su enfoque es el correcto: no podría hacer eso con un Runnable simple.

Y esta excepción, de la que no tiene información, desea lanzarla nuevamente, con un tipo determinado: ProcessExecutionException, InterruptedException o IOException. Si es de otro tipo, desea volver a lanzarlo como RuntimeException (que, por cierto, no es la mejor solución, ya que no cubre todos los casos).

Entonces, tiene un desajuste de impedancia allí: un Throwable por un lado y un tipo de excepción conocido por el otro. La única solución que tienes para resolverlo es hacer lo que has hecho: comprobar el tipo y lanzarlo de nuevo con un yeso. Se puede escribir de otra manera, pero se verá igual al final...

Respondido 03 Jul 12, 10:07

No estoy seguro de por qué tiene el bloque if/else en la captura y instanceof, creo que puedes hacer lo que quieras con:-

catch( ProcessExecutionException ex )
{
   // handle ProcessExecutionException
}
catch( InterruptException ex )
{
   // handler InterruptException*
}

Una cosa a considerar, para reducir el desorden, es capturar la excepción dentro de su método invocable y volver a lanzarla como su propia excepción o excepciones específicas de dominio/paquete. La cantidad de excepciones que necesita crear dependerá en gran medida de cómo responderá su código de llamada a la excepción.

contestado el 03 de mayo de 12 a las 20:05

Porque quiero volver a lanzar la excepción original, no manejarla en esta capa en mi API. No puedo volver a lanzar la excepción sin convertirla en el tipo específico, y la única forma en que conozco el tipo específico es con un operador instanceof. - jesse barnum

En la clase de llamada, captura el Throwable último. Por ejemplo,

try{
    doSomethingWithTimeout(i);
}
catch(InterruptedException e){
    // do something
}
catch(IOException e){
    // do something
} 
catch(TimeoutException e){
    // do something
}
catch(ExecutionException e){
    // do something
}
catch(Throwable t){
    // do something
}

Y el contenido de doSomethingWithTimeout(int timeout) debería verse así,

.
.
.
ExecutorService service = Executors.newSingleThreadExecutor();
try {
    Future<byte[]> future = service.submit( callable );
    return future.get( timeout, TimeUnit.MILLISECONDS );
} 
catch(Throwable t){
    throw t;
}
finally{
    service.shutdown();
}

Y la firma del método debería verse así,

doSomethingWithTimeout(int timeout) throws Throwable

Respondido el 28 de junio de 12 a las 01:06

Este enfoque sacrifica la seguridad de tipos y supone que quien escriba el código de llamada leerá los Javadocs para saber qué tipos de excepciones capturar. También tenderá a detectar errores, lo cual es una mala idea. Creo que es una compensación decente, si asumimos que la misma persona está escribiendo ambos lados de la API. En este caso, estoy escribiendo el método doSomethingWithTimeout() que se agregará a nuestro marco de desarrollo interno, y realmente me gustaría un enfoque que conserve los tipos de excepción para que el compilador pueda verificarlo. - jesse barnum

Aquí va mi respuesta. Supongamos que este código

public class Test {

    public static class Task implements Callable<Void>{

        @Override
        public Void call() throws Exception {
            throw new IOException();
        }

    }

    public static class TaskExecutor {

        private ExecutorService executor;

        public TaskExecutor() {
            this.executor = Executors.newSingleThreadExecutor();
        }

        public void executeTask(Task task) throws IOException, Throwable {
            try {
                this.executor.submit(task).get();
            } catch (ExecutionException e) {
                throw e.getCause();
            }

        }

    }



    public static void main(String[] args) {
        try {
            new TaskExecutor().executeTask(new Task());
        } catch (IOException e) {
            System.out.println("IOException");
        } catch (Throwable e) {
            System.out.println("Throwable");
        }
    }


}

Se imprimirá IOException. Creo que es una solución aceptable con el inconveniente de lanzar y atrapar Throwable con fuerza y ​​que la atrapada final se puede reducir a

} catch (Throwable e) { ... }

Además, otra oportunidad es hacerlo de la siguiente manera

public class Test {

public static class Task implements Callable<Void>{

    private Future<Void> myFuture;

    public void execute(ExecutorService executorService) {
        this.myFuture = executorService.submit(this);
    }

    public void get() throws IOException, InterruptedException, Throwable {
        if (this.myFuture != null) {
            try {
                this.myFuture.get();
            } catch (InterruptedException e) {
                throw e;
            } catch (ExecutionException e) {
                throw e.getCause();
            }
        } else {
            throw new IllegalStateException("The task hasn't been executed yet");
        }
    }

    @Override
    public Void call() throws Exception {
        throw new IOException();
    }

}

public static void main(String[] args) {
    try {
        Task task = new Task();
        task.execute(Executors.newSingleThreadExecutor());
        task.get();
    } catch (IOException e) {
        System.out.println("IOException");
    } catch (Throwable e) {
        System.out.println("Throwable");
    }
}

}

respondido 07 nov., 18:17

El problema con esto es que si Task lanza algún otro tipo de excepción, como SQLException, imprimirá 'lanzable', cuando el resultado deseado sería que la aplicación no puede compilar. Debe saber que el método Task podría generar una SQLException en lugar de permitir que el compilador lo verifique. Una vez que confía en la documentación en lugar del compilador, no hay ningún beneficio en las excepciones verificadas; también podría lanzar una subclase RuntimeException no verificada. - jesse barnum

@JesseBarnum sí, entiendo tu punto. En ese caso, el método executeTask debe agregar el lanzamiento de MySQLException en oeder para mantener la consistencia. Es por eso que reformulé la propuesta haciendo que la tarea en sí sea "ejecutable asincrónica". Sí, el método call() podría ejecutarse desde afuera y toda la lógica del manejo de excepciones podría evitarse, pero creo que es más elegante que hacer la prueba "instanceof". - Fernando Wasylyszyn

Aquí hay otra forma de hacerlo, aunque no estoy convencido de que esto sea menos torpe o menos propenso a romperse que hacerlo con una instancia de verificación como en su pregunta:

public static byte[] doSomethingWithTimeout(int timeout)
        throws ProcessExecutionException, InterruptedException, IOException, TimeoutException {
    ....
    try {
        ....
        return future.get(1000, TimeUnit.MILLISECONDS);
        .....
    } catch (ExecutionException e) {

        try {
            throw e.getCause();
        } catch (IOException ioe) {
            throw ioe;
        } catch (InterruptedException ie) {
            throw ie;
        } catch (ProcessExecutionException pee) {
            throw pee;
        } catch (Throwable t) {
            //Unhandled exception from Callable endups here
        }

    } catch (TimeoutException e) {
        throw e;
    } catch.....
}

Respondido el 30 de junio de 12 a las 21:06

Hola, Fredrik: el problema aquí es el catch (Throwable t): si supone que no desea que la firma de su método se defina como throws Throwable, entonces debe examinarlo con instanceof para volver a generar RuntimeExceptions y Errors y envolver cualquier otro tipo de excepción en una excepción sin marcar. - jesse barnum

No estoy seguro de a qué se refiere, pero: 1. Puede manejar RuntimeException de la misma manera agregando una captura (RuntimeException e) y volviendo a lanzarla de la misma manera. 2. Si su código arroja un error, tiene problemas importantes de todos modos, ¿cómo trataría un VirtualMachineError? 3. Su Callable actualmente lanza exactamente esas 3 Excepciones marcadas que se manejan y vuelven a lanzar mediante doSomethingWithTimeout(int timeout), que mencionó que era parte de su API, por lo que lanzar getCause() de ExecutionException actualmente solo puede lanzar esas Excepciones marcadas. ¿Qué quiere decir con "cualquier otra excepción"? - Federico LS

No quiero tratar de manejar los errores, pero ciertamente quiero volver a generar el error, manteniendo intacto el tipo original. Eso significa que necesito cláusulas catch separadas para Errores antes de poder atrapar (T Throwable). Lo mismo ocurre con RuntimeExceptions. De esa manera, lo único que llega a la cláusula catch( Throwable t ) es una excepción marcada que no estaba en mi lista específica de reenvíos, y puedo volver a empaquetarla como una RuntimeException. Una vez que incluye las comprobaciones de Throwable y RuntimeException, no son menos líneas de código, aunque me gusta que pueda evitar lanzar Throwable. - jesse barnum

No diría que recomiendo esto, pero aquí hay una manera en que puede hacerlo. Es de tipo seguro y cualquiera que venga a modificarlo después de usted probablemente no estará contento con él.

public class ConsumerClass {

    public static byte[] doSomethingWithTimeout(int timeout)
            throws ProcessExecutionException, InterruptedException, IOException, TimeoutException {
        MyCallable callable = new MyCallable();
        ExecutorService service = Executors.newSingleThreadExecutor();
        try {
            Future<byte[]> future = service.submit(callable);
            return future.get(timeout, TimeUnit.MILLISECONDS);
        } catch (ExecutionException e) {
            throw callable.rethrow(e);
        } finally {
            service.shutdown();
        }
    }

}

// Need to subclass this new callable type to provide the Exception classes.
// This is where users of your API have to pay the price for type-safety.
public class MyCallable extends CallableWithExceptions<byte[], ProcessExecutionException, IOException> {

    public MyCallable() {
        super(ProcessExecutionException.class, IOException.class);
    }

    @Override
    public byte[] call() throws ProcessExecutionException, IOException {
        //Do some work that could throw one of these exceptions
        return null;
    }

}

// This is the generic implementation. You will need to do some more work
// if you want it to support a number of exception types other than two.
public abstract class CallableWithExceptions<V, E1 extends Exception, E2 extends Exception>
        implements Callable<V> {

    private Class<E1> e1;
    private Class<E2> e2;

    public CallableWithExceptions(Class<E1> e1, Class<E2> e2) {
        this.e1 = e1;
        this.e2 = e2;
    }

    public abstract V call() throws E1, E2;

    // This method always throws, but calling code can throw the result
    // from this method to avoid compiler errors.
    public RuntimeException rethrow(ExecutionException ee) throws E1, E2 {
        Throwable t = ee.getCause();

        if (e1.isInstance(t)) {
            throw e1.cast(t);
        } else if (e2.isInstance(t)) {
            throw e2.cast(t);
        } else if (t instanceof Error ) {
            throw (Error) t;
        } else if (t instanceof RuntimeException) {
            throw (RuntimeException) t;
        } else {
            throw new RuntimeException(t);
        }
    }

}

Respondido 01 Jul 12, 17:07

Bueno, esto es esencialmente tomar la instancia de los operadores y moverlos a una subrutina, ¿verdad? - jesse barnum

Eso es una cosa que hace. También notará que nunca tiene que atrapar a Throwable. Nunca tiene que volver a lanzar InterruptedException o TimeoutException. La parte genérica se factoriza limpiamente en una clase reutilizable para la próxima vez que tenga que resolver el mismo problema. Finalmente, es de tipo seguro, lo que parecía ser una de sus principales quejas con su solución original y las demás propuestas. Intente agregar una excepción a la cláusula throws de MyCallable.call. Está obligado a actualizar el bloque future.get para manejarlo. Puede implementar 3 y 4 versiones de excepción para manejar eso automáticamente. - John Watts

El javadoc de java.util.concurrent.Future.get() afirma lo siguiente. Entonces, ¿por qué no simplemente atrapar ExecutionException (y Cancelación e Interrupción según lo declarado por el java.util.concurrent.Future.get()) método?

...
Lanza

CancellationException: si se canceló el cálculo

ExecutionException: si el cálculo arrojó una excepción

InterruptedException: si el subproceso actual se interrumpió mientras esperaba

Entonces, básicamente, puede lanzar cualquier excepción dentro de su invocable y simplemente atrapar ExecutionException. entonces ExecutionException.getCause() mantendrá la excepción real que lanzó su invocable como se indica en el javadoc. De esta manera, está protegido de los cambios de firma de método relacionados con la declaración de excepción verificada.

Por cierto, nunca debes atrapar Throwable, ya que esto atraparía también RuntimeExceptions y Errors. Atrapando Exception es un poco mejor pero aún no se recomienda, ya que atrapará RuntimeExceptions.

Algo como:

try {  
    MyResult result = myFutureTask.get();
} catch (ExecutionException e) {
    if (errorHandler != null) {
        errorHandler.handleExecutionException(e);
    }
    logger.error(e);
} catch (CancellationException e) {
    if (errorHandler != null) {
        errorHandler.handleCancelationException(e);
    }
    logger.error(e);                
} catch (InterruptedException e) {
    if (errorHandler != null) {
        errorHandler.handleInterruptedException(e);
    }
    logger.error(e);
}

contestado el 05 de mayo de 20 a las 14:05

Svilen, sé que puedo llamar a ExecutionException.getCause() (que está en mi ejemplo de código existente). Quiero volver a lanzar las excepciones, manteniendo su tipo original; ahí es donde me encuentro con problemas. - jesse barnum

Atrapo Throwable porque ExecutionException.getCause() devuelve un Throwable, no una excepción. De esa forma, puedo reutilizar la misma variable que estoy capturando en lugar de definir una nueva. Supongo que no es un gran problema agregar una variable Throwable temporal, pero eso no parecía una gran mejora. Si observa, verá que estoy manejando el caso de RuntimeExceptions and Errors al volver a lanzarlos sin modificarlos. - jesse barnum

Ya veo, supongo que entendí mal tu pregunta al principio. Su === ACTUALIZACIÓN === lo deja mucho más claro. - Svilen

He encontrado una manera de resolver el problema. Si es ExecutionException, puede obtener una original llamando aException.getCause() Luego, debe envolver su excepción en algún tipo de Excepción de tiempo de ejecución o (cuál es la mejor manera para mí) usar la anotación @SneakyThrows del proyecto lombok (https://projectlombok.org/). Doy un pequeño ejemplo de código. Además, puede agregar algunas instancias de verificación antes de lanzar una excepción para asegurarse de que esta sea la que está esperando.

@SneakyThrows
public <T> T submitAndGet(Callable<T> task) {
    try {
        return executor.submit(task).get(5, TimeUnit.SECONDS);
    } catch (InterruptedException | ExecutionException | TimeoutException e) {
        throw e.getCause();
    }
}

respondido 15 mar '16, 14:03

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.