web-dev-qa-db-fra.com

Quel est le meilleur moyen de gérer une ExecutionException?

J'ai une méthode qui effectue une tâche avec un délai d'attente. J'utilise ExecutorServer.submit () pour obtenir un objet Future, puis j'appelle future.get () avec un délai d'attente. Cela fonctionne bien, mais ma question est la meilleure façon de gérer les exceptions vérifiées que ma tâche peut générer. Le code suivant fonctionne et conserve les exceptions vérifiées, mais il semble extrêmement maladroit et susceptible de se rompre si la liste des exceptions vérifiées dans la signature de la méthode change.

Des suggestions sur la façon de résoudre ce problème? Je dois cibler Java 5, mais je serais également curieux de savoir s’il existe de bonnes solutions dans les nouvelles versions 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 );
        }
    }
}

=== UPDATE ===

De nombreuses personnes ont posté des réponses recommandant 1) le rejet de lancer comme exception générale ou 2) le rejet de lancer comme exception non contrôlée. Je ne souhaite effectuer aucune de ces opérations car ces types d'exceptions (ProcessExecutionException, InterruptedException, IOException, TimeoutException) sont importants - ils seront chacun traités différemment par l'appel traité. Si je n'avais pas besoin d'une fonctionnalité de délai d'attente, je voudrais que ma méthode lève ces 4 types d'exceptions spécifiques (enfin, à l'exception de TimeoutException). Je ne pense pas que l'ajout d'une fonctionnalité de délai d'attente devrait changer la signature de ma méthode pour générer un type d'exception générique.

33
Jesse Barnum

J'ai examiné ce problème en profondeur et c'est un gâchis. Il n’ya pas de réponse facile à Java 5, ni à 6 ni à 7. En plus de la maladresse, du verbosité et de la fragilité que vous soulignez, votre solution présente en réalité le problème que la ExecutionException que vous supprimez lorsque vous appelez getCause() contient en fait la plupart des informations de trace de pile importantes!

Autrement dit, toutes les informations de pile du thread qui exécute la méthode dans le code que vous avez présenté ne se trouvent que dans l'ExceptionExécution, et non dans les causes imbriquées, qui ne couvrent que les images commençant par call() dans Callable. Autrement dit, votre méthode doSomethingWithTimeout n'apparaîtra même pas dans les traces de pile des exceptions que vous lancez ici! Vous obtiendrez uniquement la pile désincarnée de l'exécuteur. En effet, la ExecutionException est la seule qui ait été créée sur le thread appelant (voir FutureTask.get()).

La seule solution que je connaisse est compliquée. Une grande partie du problème provient de la spécification d'exception libérale de Callable - throws Exception. Vous pouvez définir de nouvelles variantes de Callable qui spécifient exactement les exceptions qu’elles lancent, telles que:

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

    @Override
    T call() throws X; 
}

Cela permet aux méthodes qui exécutent des callables d’avoir une clause throws plus précise. Si vous souhaitez prendre en charge les signatures avec un maximum de N exceptions, vous aurez malheureusement besoin de N variantes de cette interface.

Maintenant, vous pouvez écrire un wrapper autour du JDK Executor qui prend le Callable amélioré et renvoie un Future amélioré, quelque chose comme CheckedFuture de goyave. Les types d'exception vérifiés sont propagés au moment de la compilation à partir de la création et du type de la variable ExecutorService vers la variable Futures renvoyée et se terminent sur la méthode getChecked dans le futur.

C'est ainsi que vous insérez la sécurité du type à la compilation. Cela signifie que plutôt que d'appeler:

Future.get() throws InterruptedException, ExecutionException;

Tu peux appeler: 

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

Le problème de décompression est ainsi évité - votre méthode lève immédiatement les exceptions du type requis. Elles sont disponibles et vérifiées au moment de la compilation.

Dans getChecked, cependant, vous still devez résoudre le problème de "cause manquante" décrit ci-dessus. Vous pouvez le faire en liant la pile actuelle (du thread appelant) à la pile de l'exception levée. Cela étend l'utilisation habituelle d'une trace de pile en Java, puisqu'un seul empilement s'étend sur plusieurs threads, mais cela fonctionne et est facile à comprendre une fois que vous savez ce qui se passe.

Une autre option consiste à créer une autre exception de la même chose que celle lancée et à définir l'original comme cause du nouveau. Vous obtiendrez la trace complète de la pile et la relation de cause sera la même que celle utilisée avec ExecutionException - mais vous aurez le bon type d'exception. Vous devrez toutefois utiliser la réflexion et son fonctionnement ne sera pas garanti, par exemple pour des objets sans constructeur ayant les paramètres habituels.

16
BeeOnRope

Voici quelques informations intéressantes pour les exceptions cochées et vérifiées. Brian Goetz discussion et un argument contre les exceptions vérifiées de Eckel Discussion . Mais je ne savais pas si vous aviez déjà implémenté et réfléchi le refactor des exceptions vérifiées dont Joshua a parlé dans ce book .

Selon Effective Java pearls, l’une des méthodes préférées de gestion des exceptions vérifiées consiste à transformer une exception vérifiée en une exception non vérifiée. Donc par exemple

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

changer cette implémentation en

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

Je crains qu'il n'y ait pas de réponse à votre problème. Fondamentalement, vous lancez une tâche dans un thread différent de celui dans lequel vous vous trouvez et souhaitez utiliser le modèle ExecutorService pour intercepter toutes les exceptions que cette tâche peut générer, plus le bonus d'interrompre cette tâche après un certain temps. Votre approche est la bonne: vous ne pouvez pas le faire avec un Runnable nu. 

Et cette exception, pour laquelle vous ne possédez aucune information, vous souhaitez la relancer avec un certain type: ProcessExecutionException, InterruptedException ou IOException. Si c'est un autre type, vous voulez le rediffuser comme une exception RuntimeException (ce qui n'est pas la meilleure solution, car vous ne couvrez pas tous les cas). 

Vous avez donc un décalage d’impendance: un Throwable d’une part et un type d’exception connu de l’autre. La seule solution que vous ayez à résoudre est de faire ce que vous avez fait: vérifiez le type et relancez-le avec un casting. Il peut être écrit différemment, mais aura la même apparence à la fin ...

1
José

Voici ce que je fais dans cette situation. Ceci accomplit ce qui suit:

  • Relance les exceptions vérifiées sans les emballer
  • Colle les traces de la pile

Code:

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;
}

Semble travailler.

1
Archie

Voici ma réponse. Supposons ce code

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");
        }
    }


}

IOException sera imprimé. Je pense que c'est une solution acceptable avec l'inconvénient de lancer et attraper Throwable avec force et que la capture finale peut être réduite à 

} catch (Throwable e) { ... }

En outre, une autre chance est de le faire de la manière suivante

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");
    }
}

}

0

Le javadoc de Java.util.concurrent.Future.get() indique ce qui suit. Alors pourquoi ne pas attraper simplement la méthode ExecutionException (et Annulation et interruption telle que déclarée par la fonction Java.util.concurrent.Future.get())?

...
Jette: 

CancellationException - si le calcul a été annulé

ExecutionException - si le calcul a généré une exception

InterruptedException - si le thread actuel a été interrompu pendant que attendre

Donc, fondamentalement, vous pouvez lancer n'importe quelle exception dans votre callable et attraper juste ExecutionException. Alors ExecutionException.getCause() tiendra l’exception réelle que votre appelable a lancée comme indiqué dans le javadoc . De cette façon, vous êtes protégé des modifications de la signature de la méthode liées à la déclaration des exceptions vérifiées.

En passant, vous ne devriez jamais attraper Throwable, car cela attraperait également RuntimeExceptions et Errors. Catching Exception est un peu mieux mais pas recommandé, car RuntimeExceptions.

Quelque chose comme:

               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
Svilen

Je ne sais pas pourquoi vous avez le bloc if/else dans la capture et instanceof, je pense que vous pouvez faire ce que vous voulez avec: -

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

Une chose à considérer, afin de réduire l'encombrement, est de capturer l'exception à l'intérieur de votre méthode callable et de la renvoyer en tant qu'exception ou exceptions spécifiques à votre domaine/package. Le nombre d'exceptions que vous devez créer dépendra en grande partie de la façon dont votre code d'appel répondra à l'exception.

0
Martin