ejemplo - future java
¿Cuál es la mejor manera de manejar una ExecutionException? (10)
Aquí hay otra manera 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.....
}
Tengo un método que realiza algunas tareas con un tiempo de espera. Yo uso el ExecutorServer.submit () para obtener un objeto Future, y luego invoco a future.get () con un timeout. Esto está funcionando bien, pero mi pregunta es la mejor manera de manejar las excepciones comprobadas que puede lanzar mi tarea. El siguiente código funciona y conserva las excepciones comprobadas, pero parece extremadamente torpe y propenso a romperse si cambia la lista de excepciones marcadas en la firma del método.
Alguna sugerencia en como arreglar esto? Necesito apuntarme 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 tirar como una excepción general, o 2) volver a lanzar como una excepción sin marcar. No deseo hacer ninguno de estos, porque estos tipos de excepción (ProcessExecutionException, InterruptedException, IOException, TimeoutException) son importantes: cada uno de ellos será manejado de manera diferente por la llamada procesada. Si no necesitara una función de tiempo de espera, entonces desearía que mi método arrojara estos 4 tipos de excepción específicos (bueno, excepto TimeoutException). No creo que agregar una característica de tiempo de espera exista debe cambiar la firma de mi método para arrojar un tipo de Excepción genérico.
Aquí hay un par de información interesante para comprobar y contra las excepciones controladas. Discusión de Brian Goetz y un argumento contra las excepciones controladas de Eckel Discussion . Pero no sabía si ya ha implementado y pensado sobre el refactor de excepciones comprobadas que analiza Joshua en este book .
De acuerdo con las perlas efectivas de Java, uno de los métodos preferidos para manejar las excepciones registradas es convertir una excepción marcada en una excepción no controlada. Así por ejemplo,
try{
obj.someAction()
}catch(CheckedException excep){
}
cambiar esta implementación a
if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}
El javadoc de java.util.concurrent.Future.get()
indica lo siguiente. Entonces, ¿por qué no simplemente atrapar ExecutionException (y Cancelación e Interrupción 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
InterruptedException - si el hilo actual fue interrumpido mientras esperaba
Así que, básicamente, puede lanzar cualquier excepción dentro de su llamador y simplemente atrapar ExecutionException
. A continuación, ExecutionException.getCause()
contendrá la excepción real que lanzó su invocable como se indica en el javadoc . De esta forma, estará protegido de los cambios en la firma del método relacionados con la declaración de excepción marcada.
Por cierto, nunca deberías atrapar a Throwable
, ya que esto captaría también RuntimeExceptions
and Errors
. Catching Exception
es un poco mejor, pero aún no es recomendable, ya que capturará 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);
}
En la clase de llamada, atrapa 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,
doSomethingWithTimeout(int timeout) throws Throwable
Encontré una forma de resolver el problema. Si es ExecutionException, puede obtener el original llamando a exception.getCause (). Luego debe ajustar 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 algunos controles de instancia 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();
}
}
Esto es lo que hago en esta situación. Esto logra lo siguiente:
- Re-throws marcó excepciones sin envolverlas
- Pega juntas las huellas 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.
He visto 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 usted señala, su solución en realidad tiene el problema de que la ExecutionException
que está eliminando cuando llama a getCause()
realidad contiene la mayor parte de la información de seguimiento de pila importante!
Es decir, toda la información de pila del subproceso que ejecuta el método en el código que usted presentó está solo en ExcecutionException, y no en las causas anidadas, que solo cubren los marcos que comienzan en call()
en el Callable. Es decir, su método doSomethingWithTimeout
ni siquiera aparecerá en los rastros de la pila de las excepciones que está lanzando aquí. Solo obtendrás la pila desencarnada del ejecutor. Esto se debe a que ExecutionException
es la única creada en el hilo de llamada (vea 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 especifiquen exactamente qué excepciones arrojan, como por ejemplo:
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 puede escribir un contenedor alrededor del Executor
JDK que toma el Callable mejorado y devuelve un Future
mejorado, algo así como CheckedFuture de guayaba. Los tipos de excepción comprobados se propagan en el momento de la compilación desde la creación y el tipo de ExecutorService
hasta los Future
devueltos, y terminan en el método getChecked
en el futuro.
Así es como pasa la seguridad del tipo de 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 verificados en tiempo de compilación.
Dentro de getChecked
, sin embargo, todavía debe resolver el problema de desenvoltura de "causa faltante" descrito anteriormente. Puede hacer esto al unir la pila actual (del hilo de llamada) en la pila de la excepción lanzada. Se trata de una ampliación del uso habitual de un seguimiento de pila en Java, ya que una sola pila se extiende por hilos, pero funciona y es fácil de entender una vez que se sabe lo que está sucediendo.
Otra opción es crear otra excepción de la misma cosa que la lanzada, 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 con ExecutionException
, pero tendrá el tipo correcto de excepción. Sin embargo, deberá utilizar el reflejo y no se garantiza que funcione, por ejemplo, para objetos sin un constructor que tenga los parámetros habituales.
Me temo que no hay respuesta para tu problema. Básicamente, estás iniciando una tarea en un hilo diferente al que estás, y deseas utilizar el patrón ExecutorService para capturar todas las excepciones que la tarea puede arrojar, más la ventaja de interrumpir esa tarea después de un cierto período 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 volver a lanzarla, con un tipo determinado: ProcessExecutionException, InterruptedException o IOException. Si es de otro tipo, quiere volver a lanzarlo como una RuntimeException (que por cierto no es la mejor solución, ya que no cubre todos los casos).
Entonces, usted tiene un desequilibrio de impendancia: un Throwable por un lado, y un tipo de excepción conocida por el otro. La única solución que tienes que resolver es hacer lo que has hecho: verificar el tipo y lanzarlo de nuevo con un yeso. Se puede escribir de manera diferente, pero se verá igual al final ...
No diría que lo recomiendo, pero esta es una forma de hacerlo. Es seguro para el tipo y quien venga a modificarlo después probablemente no esté 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);
}
}
}
No estoy seguro de por qué tienes el bloque if / else en el catch 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 atrapar la excepción dentro de su método invocable y volver a lanzar como su propia excepción o excepciones específicas de dominio / paquete. Cuántas excepciones necesita crear dependerá en gran medida de cómo su código de llamada responderá a la excepción.