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

=== 更新 ===

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

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

也就是说,在您提供的代码中执行方法的线程的所有堆栈信息仅在 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; 
}

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

现在你可以围绕JDK Executor编写一个包装器,它接受增强的Callable,并返回一个增强的Future,类似于番石榴的CheckedFuture。 选中的异常类型在编译时从ExecutorService的创建和类型传播到返回的Future,并在将来的getChecked方法上结束。

这就是编译时类型安全线程化的方式。 这意味着与其调用:

Future.get() throws InterruptedException, ExecutionException;

您可以致电:

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

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

getChecked内部,但是您仍然需要解决上述"丢失原因"解包问题。 您可以通过将当前堆栈(调用线程的(拼接到抛出的异常的堆栈上来执行此操作。 这是对 Java 中堆栈跟踪的通常用法的一种延伸,因为单个堆栈跨线程延伸,但是一旦您知道发生了什么,它就可以工作并且很容易理解。

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

这是我在这种情况下所做的。这将完成以下操作:

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

法典:

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

似乎有效。

以下是一些

有关已检查和针对已检查异常的有趣信息。Brian Goetz讨论和反对Eckel Discussion检查异常的论点。但我不知道你是否已经实现并考虑了约书亚在本书中讨论的检查异常重构。

根据有效的 Java 珍珠,处理已检查异常的首选方法之一是将已检查的异常转换为未检查的异常。举个例子,

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

将此实现更改为

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

怕你的问题没有答案。基本上,您在与您所在线程不同的线程中启动任务,并希望使用 ExecutorService 模式来捕获任务可能引发的所有异常,以及在一定时间后中断该任务的好处。你的方法是正确的:你不能用一个裸露的 Runnable 做到这一点。

而这个异常,你没有关于它的信息,你想再次抛出它,具有某种类型:ProcessExecutionException,InterruptedException或IOException。如果是另一种类型,您希望将其重新抛出为 RuntimeException(顺便说一句,这不是最好的解决方案,因为您没有涵盖所有情况(。

所以你有一个阻抗不匹配:一方面是可抛出的,另一方面是已知的异常类型。你必须解决它的唯一解决方案是做你已经做过的事情:检查类型,然后用石膏再次扔掉它。它可以写得不同,但最终看起来是一样的......

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

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

为了减少混乱,需要考虑的一件事是捕获可调用方法中的异常,并重新引发为您自己的域/包特定异常或异常。需要创建多少个异常在很大程度上取决于调用代码对异常的响应方式。

在调用类中,最后捕获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

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

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。我认为这是一个可以接受的解决方案,缺点是用力投掷和接住可投掷物,最终的接球可以减少到

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

}

这是另一种方法,尽管我不相信这比像您的问题中那样使用检查实例更笨拙或更不容易中断:

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

我不会说我推荐这个,但这里有一种方法可以做到这一点。它是类型安全的,任何在你之后来修改它的人都可能会对它不满意。

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

java.util.concurrent.Future.get()的javadoc声明如下。那么为什么不直接捕获 ExecutionException(以及 java.util.concurrent.Future.get() 声明的取消和中断(方法呢?


抛出:

取消异常 - 如果计算被取消

执行异常 - 如果计算引发异常

中断异常 - 如果当前线程在 等待

所以基本上你可以在你的可调用对象中抛出任何异常,然后只捕获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);
}

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

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

相关内容

  • 没有找到相关文章

最新更新