如何处理 ExecutionException 是最好的方式?

50
我有一个执行某些任务的方法,其中包含一个超时机制。我使用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 );
        }
    }
}

=== 更新 ===

许多人建议重新抛出通用异常或未经检查的异常,但我不想这样做,因为这些异常类型(ProcessExecutionException、InterruptedException、IOException、TimeoutException)非常重要——它们将被调用进程以不同的方式处理。如果我不需要超时功能,那么我希望我的方法抛出这4种特定的异常类型(除了TimeoutException)。我认为添加超时功能不应该改变我的方法签名以抛出通用异常类型。


只需将您的方法声明为 throws Exception 并使用同一行代码抛出所有异常。您可以捕获 ExecutionException,然后只需 throw e.getCause -- 不要捕获其他任何异常,让它们自行传播。 - Marko Topolnik
嗨Marko,感谢你的建议,但我需要我的API抛出这4种特定类型的异常。我不想抛出通用异常。 - Jesse Barnum
11个回答

22
我深入研究了这个问题,情况很复杂。在Java 5、6或7中都没有简单的答案。除了您指出的笨拙、冗长和脆弱之外,您的解决方案实际上存在一个问题,即当您调用getCause()时剥离的ExecutionException实际上包含大部分重要的堆栈跟踪信息!
也就是说,在您提供的代码中执行方法的线程的所有堆栈信息都只在ExcecutionException中,而不在嵌套的原因中,后者仅覆盖从call()开始的帧。也就是说,您的doSomethingWithTimeout方法甚至不会出现在您抛出的异常的堆栈跟踪中!你只会得到来自执行程序的无形堆栈。这是因为ExecutionException是唯一在调用线程上创建的异常(请参见FutureTask.get())。
我知道的唯一解决方案是复杂的。很多问题源于Callable的宽松异常规范- throws Exception。您可以定义新的Callable变体,它们明确指定它们抛出的异常,例如:
public interface Callable1<T,X extends Exception> extends Callable<T> {

    @Override
    T call() throws X; 
}

这样可以使执行可调用方法的方法具有更精确的throws子句。如果您想支持最多N个异常的签名,则需要N个此接口的变体,不幸的是。

现在您可以编写一个包装JDK Executor的程序,该程序采用增强型Callable,并返回增强型Future,类似于guava的CheckedFuture。在ExecutorService的创建和类型中传播已检查的异常类型,以便将其编译时传播到返回的Future,并最终出现在未来的getChecked方法上。

这就是如何通过线程编译时类型安全性的方式。这意味着不再调用:

Future.get() throws InterruptedException, ExecutionException;

你可以调用以下内容:
CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

因此,避免了拆包问题-您的方法立即抛出所需类型的异常,并且可以在编译时获得和检查它们。
然而,在getChecked内部,您仍然需要解决上述“缺失原因”解包问题。您可以通过将当前调用线程的堆栈(stack)串联到抛出的异常堆栈中来解决这个问题。这是 Java 中通常使用堆栈跟踪的一种扩展用法,因为单个堆栈跨越多个线程,但一旦了解其工作原理,就很容易理解。
另一种选择是创建与被抛出的异常相同的另一个异常,并将原始异常设置为新异常的原因。您将获得完整的堆栈跟踪,并且原因关系将与ExecutionException的工作方式相同-但您将具有正确类型的异常。但是,您需要使用反射,并且不能保证对没有具有通常参数的构造函数的对象有效。

尽管这不是我所期望的解决方案,但我将其标记为最正确的答案。 - Jesse Barnum
1
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - BeeOnRope

4

在这种情况下,我会采取以下措施:

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

代码:

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

看起来可以工作。


这难道不会总是抛出未经检查的异常吗? - Jesse Barnum
它重新抛出最初抛出的任何异常,无论是已检查还是未检查的。通常,您会声明waitForThingToComplete()抛出Future回调函数抛出的任何已检查异常(如果有)。该方法的调用者“认为”回调在当前线程中被调用,即使它不是。我已经使用了这段代码作为AOP方面,在不同的线程上执行带注释的方法,使当前线程阻塞直到完成,而方法的调用者却不知情。 - Archie
为什么是 "<placeholder>" - igr
这通常是您放置源文件名称的位置。此堆栈跟踪条目是在运行时动态创建的。 - Archie

1

这里有一些关于已检异常的有趣信息和反对已检异常的论点。Brian Goetz讨论Eckel讨论。但我不知道你是否已经实施并思考了Joshua在这本中讨论的已检异常重构。

根据Effective Java pearls的说法,处理已检异常的首选方法之一是将已检异常转换为未检异常。例如:

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

将此实现更改为

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

我非常强烈地认为这个方法应该抛出已检查的异常。每一个异常都应该被调用代码捕获和处理。目前我使用的流程保留了异常类型,但是充满了不方便的样板代码。 - Jesse Barnum

1
很抱歉,对于您的问题我无法提供答案。基本上,您正在一个不同的线程中启动任务,并希望使用ExecutorService模式捕获该任务可能引发的所有异常,以及在一定时间后中断该任务的奖励。您的方法是正确的:您无法使用裸的Runnable实现这一点。
而且,您想要将此异常重新抛出为某种类型:ProcessExecutionException、InterruptedException或IOException。如果它是另一种类型,您想要将其作为RuntimeException重新抛出(顺便说一下,这并不是最好的解决方案,因为您没有涵盖所有情况)。
所以您在这里有一个阻抗不匹配:一方面是Throwable,另一方面是已知的异常类型。您唯一的解决方案是做您已经做过的事情:检查类型,并使用转换再次抛出它。它可以用不同的方式编写,但最终看起来都是一样的...

0

我不太确定你为什么在 catch 语句中使用了 if/else 块和 instanceof,我认为你可以用以下方式实现你的需求:

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

考虑减少混乱的一件事是,在可调用方法内捕获异常并重新抛出为自己特定领域/包的异常或多个异常。需要创建多少个异常主要取决于调用代码对异常的响应方式。

2
因为我想重新抛出原始异常,而不是在我的 API 层处理它。如果没有将其转换为特定类型,我无法重新抛出异常,而我所知道的唯一找到特定类型的方法是使用 instanceof 运算符。 - Jesse Barnum

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


这种方法牺牲了类型安全,并假设编写调用代码的人将阅读Javadocs以了解要捕获哪些异常类型。它还倾向于捕获错误,这是一个坏主意。如果我们假设同一个人编写API的两个方面,我认为这是一个不错的权衡。在这种情况下,我正在编写doSomethingWithTimeout()方法,该方法将添加到我们的内部开发框架中,我真的希望采用一种保留异常类型以便编译器可以检查的方法。 - Jesse Barnum

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可以简化为

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

}


问题在于,如果Task抛出其他异常类型(例如SQLException),它将打印'throwable',而期望的结果是应用程序无法编译。您需要知道Task方法可能会抛出SQLException,而不是让编译器检查它。一旦您依赖文档而不是编译器,就没有受检异常的好处 - 最好只是抛出未经检查的RuntimeException子类。 - Jesse Barnum
@JesseBarnum 是的,我理解你的观点。在这种情况下,executeTask方法应该添加MySQLException的抛出以保持一致性。这就是为什么我重新制定了提案,使任务本身“异步可执行”。是的,call()方法可以从外部执行,并且可以避免所有异常处理逻辑,但我认为这比进行“instanceof”测试更优雅。 - Fernando Wasylyszyn

-1

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

...
抛出:

CancellationException - 如果计算被取消

ExecutionException - 如果计算引发异常

InterruptedException - 如果当前线程在等待时被中断

因此,基本上您可以在可调用对象内抛出任何异常,然后只需捕获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);
}

3
Svilen,我知道我可以调用ExecutionException.getCause()(这在我的现有代码示例中)。 我想重新抛出异常,保持它们原来的类型 - 这就是我遇到麻烦的地方。 - Jesse Barnum
1
我捕获Throwable,因为ExecutionException.getCause()返回的是Throwable而不是Exception。这样,我可以重复使用我正在捕获的同一个变量,而不是定义一个新的变量。我猜添加一个临时的Throwable变量并没有什么大不了的,但那似乎不是很好的改进。如果你看一下,你会发现我通过重新抛出RuntimeExceptions和Errors来处理它们的情况,而不进行修改。 - Jesse Barnum
1
我明白了,我一开始理解错了你的问题。你的 === 更新 === 让问题更加清晰了。 - Svilen

-1

这里还有另一种方法可以做到,但我并不认为这比您在问题中使用 instanceof 检查更不笨拙或更不容易出错:

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

嗨,Fredrik - 这里的问题在于 catch(Throwable t) - 如果您假定不想让您的方法签名被定义为 throws Throwable,那么您需要使用 instanceof 检查它以重新抛出 RuntimeExceptions 和 Errors,并将其他任何异常类型包装成未检查的异常。 - Jesse Barnum
我不确定你的意思,但是:1. 你可以通过添加catch (RuntimeException e)并以相同的方式重新抛出来处理RuntimeException。2. 如果你的代码抛出Error,你已经有了严重的问题,你会如何处理VirtualMachineError?3. 你的Callable当前正好抛出那些由doSomethingWithTimeout(int timeout)处理和重新抛出的3个已检查异常,你提到它是你的API的一部分,所以抛出ExecutionException的getCause()目前只能抛出那些已检查的异常。你说的“任何其他异常”是什么意思? - Fredrik LS
我不想尝试处理错误,但我肯定想重新抛出错误,保持原始类型不变。这意味着在进入catch(Throwable t)之前,我需要为错误设置单独的catch子句。RuntimeExceptions也是如此。这样,只有未包含在我的特定重新抛出列表中的已检查异常才会到达catch(Throwable t)子句,我可以将其重新打包为RuntimeException。一旦包括了Throwable和RuntimeException的检查,代码行数并没有减少,尽管我确实喜欢避免对Throwable进行强制转换。 - Jesse Barnum

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

}

这基本上只是将 instanceof 运算符移到子程序中,对吧? - Jesse Barnum
这是它所做的一件事。您还会注意到,它永远不必捕获Throwable。它永远不必重新抛出InterruptedException或TimeoutException。通用部分被干净地分解成一个可重复使用的类,以便下次您需要解决相同的问题。最后,它是类型安全的,这似乎是您对原始解决方案和其他提议的主要抱怨之一。尝试向MyCallable.call的throws子句添加异常。您被迫更新future.get块以处理它。您可以实现3个和4个异常版本来自动处理它。 - John Watts

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接