处理ExecutionException的最佳方法是什么?

时间:2012-05-03 19:14:25

标签: java executorservice future executionexception

我有一个方法可以执行超时的任务。我使用ExecutorServer.submit()来获取Future对象,然后使用超时调用future.get()。这工作正常,但我的问题是处理我的任务可以抛出的已检查异常的最佳方法。以下代码有效,并保留已检查的异常,但如果方法签名中的已检查异常列表发生更改,则它似乎非常笨拙并且容易中断。

有关如何解决此问题的任何建议?我需要针对Java 5,但我也很想知道在新版本的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 ===

许多人发布的回复建议:1)重新投掷作为一般例外,或2)作为未经检查的例外重新投掷。我不想做其中任何一个,因为这些异常类型(ProcessExecutionException,InterruptedException,IOException,TimeoutException)很重要 - 它们将由处理的调用各自处理。如果我不需要超时功能,那么我希望我的方法抛出这4种特定的异常类型(好吧,除了TimeoutException)。我不认为添加超时功能应该改变我的方法签名以抛出一般的异常类型。

11 个答案:

答案 0 :(得分:18)

我深入研究了这个问题,这是一团糟。 Java 5中没有简单的答案,也没有6或7中的答案。除了你指出的笨拙,冗长和脆弱之外,你的解决方案实际上还有一个问题,即当你打电话时你正在剥离ExecutionException getCause()实际上包含了大部分重要的堆栈跟踪信息!

也就是说,在您呈现的代码中执行方法的线程的所有堆栈信息仅在ExcecutionException中,而不在嵌套原因中,而这些原因仅覆盖Callable中从call()开始的帧。也就是说,您的doSomethingWithTimeout方法甚至不会出现在您抛出的异常的堆栈跟踪中!你只能从执行者那里得到无实体的堆栈。这是因为ExecutionException是在调用线程上创建的唯一一个(参见FutureTask.get())。

我所知道的唯一解决方案很复杂。很多问题都源于Callable - throws Exception的自由范例规范。您可以定义Callable的新变体,它们确切地指定了它们抛出的异常,例如:

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

    @Override
    T call() throws X; 
}

这允许执行callables的方法具有更精确的throws子句。如果您想支持最多N个例外的签名,不幸的是,您将需要此接口的N个变体。

现在你可以围绕JDK Executor编写一个包装器,它接受增强的Callable,并返回一个增强的Future,就像guava的CheckedFuture。已检查的异常类型在编译时从ExecutorService的创建和类型传播到返回的Future,并在未来的getChecked方法上结束。

这就是你通过编译时类型安全的方式。这意味着而不是呼叫:

Future.get() throws InterruptedException, ExecutionException;

您可以致电:

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

因此避免了解包问题 - 您的方法会立即抛出所需类型的异常,并且它们在编译时可用并进行检查。

getChecked内,但需要解决上述“缺失原因”展开问题。您可以通过将当前堆栈(调用线程)拼接到抛出异常的堆栈上来完成此操作。这延伸了Java中堆栈跟踪的常规用法,因为单个堆栈跨越线程,但是一旦你知道发生了什么,它就可以工作并且很容易理解。

另一种选择是创建与被抛出的异常相同的另一个异常,并将原始异常设置为新原因的原因。您将获得完整的堆栈跟踪,并且原因关系与它与ExecutionException的工作方式相同 - 但您将拥有正确的异常类型。但是,您需要使用反射,并且不能保证工作,例如,对于没有构造函数具有常用参数的对象。

答案 1 :(得分:3)

这是我在这种情况下所做的。这实现了以下目的:

  • 重新抛出已检查的异常而不包装它们
  • 将堆栈跟踪粘合在一起

代码:

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

似乎工作。

答案 2 :(得分:1)

以下是检查和反对Checked Exceptions的几个有趣信息。 Brian Goetz discussion以及针对来自Eckel Discussion的已检查异常的参数。但是我不知道你是否已经实现并考虑了Joshua在book中讨论的已检查异常重构。

根据Effective Java珍珠,处理Checked异常的首选方法之一是将已检查的异常转换为Un-Checked Exception。例如,

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

将此实施更改为

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

答案 3 :(得分:1)

我担心你的问题没有答案。基本上,您在与您所在的线程不同的线程中启动任务,并希望使用ExecutorService模式捕获任务可以抛出的所有异常,以及在一定时间后中断该任务的奖励。你的方法是正确的:你无法用裸露的Runnable做到这一点。

这个例外,你没有任何信息,你想再次抛出它,具有某种类型:ProcessExecutionException,InterruptedException或IOException。如果它是另一种类型,你想重新抛出它作为RuntimeException(这不是最好的解决方案,因为你没有涵盖所有的情况)。

因此,你有一个阻碍不匹配:一方面是Throwable,另一方面是一个已知的异常类型。你需要解决的唯一解决方案就是做你已经完成的事情:检查类型,然后再使用强制转换再次抛出它。它可以用不同的方式编写,但最终看起来会一样......

答案 4 :(得分:0)

我不确定你为什么在catch和instanceof中有if / else块,我认为你可以做你想做的事情: -

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

要减少混乱,要考虑的一件事是捕获可调用方法中的异常,并将其作为自己的域/包特定异常重新抛出。您需要创建多少例外很大程度上取决于您的调用代码将如何响应异常。

答案 5 :(得分:0)

java.util.concurrent.Future.get()的javadoc声明如下。那么为什么不捕获ExecutionException(以及java.util.concurrent.Future.get())方法声明的Cancellation和Interrupted?

  
    

...
    抛出:

         

CancellationException - 如果计算被取消

         

ExecutionException - 如果计算引发异常

         

InterruptedException - 如果当前线程被中断的话     等待

  

所以基本上你可以在你的callable中抛出任何异常,然后抓住ExecutionException。然后ExecutionException.getCause()将按照javadoc中的说明保留您的可调用对象的实际异常。这样,您就可以屏蔽与已检查的异常声明相关的方法签名更改。

顺便说一句,你永远不应该抓住Throwable,因为这也会抓住RuntimeExceptionsErrors。抓取Exception稍微好一点,但仍然不推荐,因为它会抓住RuntimeExceptions

类似的东西:

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

答案 6 :(得分:0)

在调用类中,最后捕获Throwable。例如,

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
}

doSomethingWithTimeout(int timeout)的内容应该如下所示,

.
.
.
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();
}

它的方法签名应该是,

doSomethingWithTimeout(int timeout) throws Throwable

答案 7 :(得分:0)

这是我的答案。让我们假设这段代码

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将被打印。我认为这是一个可以接受的解决方案,因为它有力地抛出和捕获Throwable的缺点,并且最终的捕获可以减少到

} catch (Throwable e) { ... }

另外,另一个机会是通过以下方式进行

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

}

答案 8 :(得分:-1)

这是另一种方法,虽然我不相信这不像你的问题中那样用笨拙或不太容易破坏而不是像你的问题那样用一个检查实例:

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

答案 9 :(得分:-1)

我不会说我推荐这个,但这是你可以做到的一种方式。它是类型安全的,无论谁来修改它都可能会对它不满意。

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

}

答案 10 :(得分:-2)

我找到了解决问题的方法之一。 如果它是ExecutionException,你可以通过调用exception.getCause()获得原始的ExecutionException 然后你需要在某种运行时异常中包装你的异常或(对我来说最好的方法)使用项目lombok(https://projectlombok.org/)中的@SneakyThrows注释。我给出了一小段代码示例。此外,您可以在抛出异常之前添加一些instanceof检查,以确保这是您期望的那个。

@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();
    }
}