处理执行异常的最佳方法是什么?

我有一个方法,可以执行一些超时的任务。我使用 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 );
        }
    }
}

=== 更新 ===

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


答案 1

我已经深入研究了这个问题,这是一团糟。Java 5中没有简单的答案,6或7中也没有简单的答案。除了您指出的笨拙,冗长和脆弱性之外,您的解决方案实际上还存在一个问题,即您在调用时剥离的实际上包含大多数重要的堆栈跟踪信息!ExecutionExceptiongetCause()

也就是说,在所呈现的代码中执行方法的线程的所有堆栈信息仅在 ExcecutionException 中,而不在嵌套原因中,嵌套原因仅涵盖从 Callable 中开始的帧。也就是说,您的方法甚至不会出现在您在此处引发的异常的堆栈跟踪中!您只能从执行器获得无形堆栈。这是因为 是唯一在调用线程上创建的(请参阅)。call()doSomethingWithTimeoutExecutionExceptionFutureTask.get()

我所知道的唯一解决方案是复杂的。很多问题都起源于自由的例外规范 - 。您可以定义新的变体,其中准确指定它们引发的异常,例如:Callablethrows ExceptionCallable

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

    @Override
    T call() throws X; 
}

这允许执行可调用对象的方法具有更精确的子句。如果要支持最多具有 N 个异常的签名,则很遗憾,您需要此接口的 N 个变体。throws

现在,您可以在JDK周围编写一个包装器,该包装器采用增强的Callable,并返回增强的,类似于番石榴的CheckedFuture。选中的异常类型在编译时从 的 创建和类型传播到返回的 s,并最终在将来的方法上。ExecutorFutureExecutorServiceFuturegetChecked

这就是如何对编译时类型安全进行线程处理。这意味着不要调用:

Future.get() throws InterruptedException, ExecutionException;

您可以致电:

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

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

但是,在里面,您仍然需要解决上述“缺失原因”的解包问题。您可以通过将当前堆栈(调用线程的)拼接到引发异常的堆栈上来执行此操作。这是对Java中堆栈跟踪的通常使用的延伸,因为单个堆栈跨线程延伸,但是一旦你知道发生了什么,它就起作用并且很容易理解。getChecked

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


答案 2

在这种情况下,我该怎么做。这将完成以下任务:

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

法典:

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

似乎有效。