48

我有一种方法可以在超时的情况下执行某些任务。我使用 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 除外)。我不认为添加超时功能应该改变我的方法签名来抛出一个通用的异常类型。

4

11 回答 11

22

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

也就是说,你提供的代码中执行方法的线程的所有堆栈信息都只在ExecutionException中,而不是在嵌套原因中,它只覆盖call()Callable中开始的帧。也就是说,您的doSomethingWithTimeout方法甚至不会出现在您在这里抛出的异常的堆栈跟踪中!您只会从 executor 那里获得无实体的堆栈。这是因为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的 s,并getChecked在未来的方法上结束。

这就是你如何通过编译时类型安全。这意味着,而不是调用:

Future.get() throws InterruptedException, ExecutionException;

您可以致电:

CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException

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

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

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

于 2012-10-06T04:21:44.847 回答
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;
}

似乎工作。

于 2013-10-17T20:07:35.090 回答
1

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

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

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

将此实现更改为

if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}
于 2012-05-03T20:00:40.630 回答
1

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

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

所以你有一个阻抗不匹配:一方面是 Throwable,另一方面是已知的异常类型。您必须解决的唯一解决方案是执行您已完成的操作:检查类型,然后使用强制转换再次抛出它。它可以写成不同的,但最终看起来是一样的......

于 2012-07-03T09:33:23.677 回答
0

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

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

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

于 2012-05-03T19:30:14.230 回答
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

于 2012-06-28T00:26:40.853 回答
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");
    }
}

}

于 2018-11-02T18:34:29.300 回答
-1

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

...
抛出:

CancellationException - 如果计算被取消

ExecutionException - 如果计算抛出异常

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

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

顺便说一句,你不应该抓住Throwable,因为这也会抓住RuntimeExceptionsand Errors。捕捉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);
}
于 2012-05-04T08:55:03.610 回答
-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.....
}
于 2012-06-30T20:27:17.070 回答
-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);
        }
    }

}
于 2012-07-01T16:08:54.843 回答
-2

我找到了一种解决问题的方法。如果是 ExecutionException,您可以通过调用 exception.getCause() 来获得原始异常然后您需要将异常包装在某种运行时异常中,或者(对我来说最好的方法是)使用来自项目 lombok 的 @SneakyThrows 注释(https:// projectlombok.org/)。我给出一小段代码示例。此外,您可以在抛出异常之前添加一些 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();
    }
}
于 2016-03-15T14:18:58.550 回答