¿Cuál es la mejor manera de manejar una excepción de ejecución?


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

Cualquier sugerencia sobre cómo ¿arreglar esto? Necesito apuntar a Java 5, pero también me gustaría saber si hay buenas soluciones en las versiones más recientes 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 sin marcar. No quiero hacer ninguno de estos, porque estos tipos de excepción (ProcessExecutionException, InterrumpedException, IOException, TimeoutException) son importantes-cada uno será manejado de otro modo por la llamada procesada. Si no necesitara una función de tiempo de espera, entonces querría que mi método lanzara estos 4 tipos de excepciones específicas (bueno, excepto TimeoutException). No creo que agregar una función de tiempo de espera deba cambiar la firma de mi método para lanzar un tipo de excepción genérico.

Author: assylias, 2012-05-03

10 answers

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, la verbosidad y la fragilidad que señala, su solución en realidad tiene el problema de que el ExecutionException que se está despojando cuando se llama getCause() en realidad contiene la mayor parte de la información de seguimiento de la pila importante!

Es decir, toda la información de la pila del hilo que ejecuta el método en el código que presentó está solo en la ExcecutionException, y no en las causas anidadas, que solo cubren fotogramas que comienzan en call() en el Invocable. Es decir, su método doSomethingWithTimeout ni siquiera aparecerá en los rastros de pila de las excepciones que está lanzando aquí! Solo obtendrás la pila desencarnada del ejecutor. Esto se debe a que el ExecutionException es el único que se creó en el hilo invocador (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 lanzan, 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 cláusula throws más precisa. Si desea admitir firmas con hasta N excepciones, necesitará N variantes de esta interfaz, desafortunadamente.

Ahora puedes escribir un wrapper alrededor del JDK Executor que toma el Callable mejorado, y devuelve un Future mejorado, algo así como el CheckedFuture de guayava. Los tipos de excepción comprobados se propagan en tiempo de compilación desde la creación y el tipo de ExecutorService, a los Future devueltos, y terminan en el método getChecked en el futuro.

Así es como conectas la seguridad de tipo 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 desempaquetado: su método lanza inmediatamente las excepciones del tipo requerido y están disponibles y verificadas en compile tiempo.

Dentro de getChecked, sin embargo, todavía necesita resolver el problema de "causa faltante" descrito anteriormente. Puede hacer esto uniendo la pila actual (del hilo invocador) en la pila de la excepción lanzada. Este es un estiramiento el uso habitual de un seguimiento de pila en Java, ya que una sola pila se extiende a través de hilos, pero funciona y es fácil de entender una vez que sabes lo que está pasando.

Otra opción es crear otra excepción de lo mismo que el que está siendo lanzado, y establecer el original como la causa de la nueva. Obtendrá el seguimiento completo de la pila, y la relación de causa será la misma que la forma en que funciona con ExecutionException, pero tendrá el tipo correcto de excepción. Sin embargo, necesitará usar reflexión y no se garantiza que funcione, por ejemplo, para objetos sin un constructor que tenga los parámetros habituales.

 15
Author: BeeOnRope,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-06 04:21:44

Aquí hay un par de información interesante para las Excepciones verificadas y contra las verificadas. Discusión de Brian Goetz y un argumento contra excepciones verificadas de Discusión de Eckel . Pero yo no sabía si usted ya ha implementado y dado un pensamiento sobre el refactor de excepción comprobado que se discute por Josué en este libro.

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

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

Cambie esta implementación a

if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}
 1
Author: sathish_at_madison,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 20:00:40

Me temo que no hay respuesta a su problema. Básicamente, está lanzando una tarea en un hilo diferente al que está en, y desea usar el patrón ExecutorService para capturar todas las excepciones que la tarea puede lanzar, más 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 desnudo.

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

Así que tienes un impendance mismatch allí : un Lanzable por un lado, y un tipo de excepción conocido por el otro. La única solución que tienes que resolver es hacer lo que has hecho: comprobar el tipo, y lanzarlo de nuevo con un molde. Se puede escribir de manera diferente, pero se verá igual en final...

 1
Author: José,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-03 09:33:23

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

  • Vuelve a lanzar excepciones marcadas sin envolverlas
  • Pega las trazas 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.

 1
Author: Archie,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2013-10-17 20:07:35

No estoy seguro de por qué tienes 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 llamable y volver a lanzar 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 su código de llamada responderá a la excepción.

 0
Author: Martin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-03 19:37:52

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

...
Lanza:

CancellationException - si el cálculo fue cancelado

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

InterrumpedException-si el hilo actual se interrumpió mientras esperando

Así que básicamente puedes lanzar cualquier excepción dentro de tu callable y simplemente atrapar ExecutionException. Entonces ExecutionException.getCause()mantendrá la excepción real de su lanzamiento 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 marcada.

Por cierto, nunca debes atrapar Throwable, ya que esto también atraparía RuntimeExceptions y Errors. Catching 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);
                }
 0
Author: Svilen,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-05-04 08:55:03

En la clase que llama, 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 su firma de método debería verse como,

doSomethingWithTimeout(int timeout) throws Throwable

 0
Author: mre,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-28 00:46:18

Aquí hay otra manera de hacerlo, aunque no estoy convencido de que esto sea menos torpe o menos propenso a romper 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.....
}
 -1
Author: Fredrik LS,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-06-30 20:27:17

No diría que recomiendo esto, pero aquí hay una manera de hacerlo. Es seguro para el tipo y quienquiera que venga a modificarlo después de usted probablemente no estará satisfecho 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);
        }
    }

}
 -1
Author: John Watts,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-07-01 16:08:54

He encontrado una manera de resolver el problema. Si se trata de ExecutionException, puede obtener uno original llamando a exception.getCause() Entonces necesita 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 comprobaciones instanceof antes de lanzar una excepción para asegurarse de que este sea el 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();
    }
}
 -2
Author: vsabinin,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2016-03-15 14:18:58