Каков наилучший способ обработки исключения 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). Я не думаю, что добавление функции тайм-аута должно изменить сигнатуру моего метода, чтобы генерировать универсальный тип исключения.
11 ответов
Я посмотрел на эту проблему подробно, и это беспорядок. Нет простого ответа ни в Java 5, ни в 6 или 7. Помимо неуклюжести, многословия и хрупкости, на которые вы указываете, ваше решение на самом деле имеет проблему, заключающуюся в том, что ExecutionException
что вы снимаете, когда звоните getCause()
на самом деле содержит большую часть важной информации трассировки стека!
То есть вся информация стека потока, выполняющего метод в представленном вами коде, содержится только в исключении 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
что-то вроде гуавы 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;
}
Кажется, работает.
Боюсь, нет ответа на вашу проблему. По сути, вы запускаете задачу в потоке, отличном от того, в котором вы находитесь, и хотите использовать шаблон ExecutorService для перехвата всех исключений, которые может выдать задача, плюс бонус за прерывание этой задачи через определенное время. Ваш подход правильный: вы не могли бы сделать это с голым Runnable.
И это исключение, о котором у вас нет информации, вы хотите выбросить его снова, с определенным типом: ProcessExecutionException, InterruptedException или IOException. Если это другой тип, вы хотите перебросить его как RuntimeException (что, кстати, не лучшее решение, так как вы не покрываете все случаи).
Таким образом, у вас есть несоответствие импеданса: Throwable с одной стороны и известный тип исключения с другой. Единственное решение, которое вам нужно решить - это сделать то, что вы сделали: проверить тип и снова бросить его с помощью приведения. Это может быть написано по-другому, но в итоге будет выглядеть одинаково...
Вот несколько интересных сведений для проверенных и проверенных исключений. Обсуждение Брайана Гетца и аргумент против проверенных исключений из Eckel Discussion. Но я не знал, реализовали ли вы уже и думали ли вы о проверенном рефакторе исключений, о котором рассказывает Иисус Навин в этой книге.
Согласно жемчужинам Effective Java, одним из предпочтительных методов обработки проверенных исключений является превращение проверенного исключения в не проверенное исключение. Так, например,
try{
obj.someAction()
}catch(CheckedException excep){
}
изменить эту реализацию на
if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}
Я не уверен, почему у вас есть блок if/else в подвохе и instanceof
Я думаю, вы можете делать то, что вы хотите с:-
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 будет напечатан. Я думаю, что это приемлемое решение с обратной стороной броска и ловли 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");
}
}
}
Вот еще один способ сделать это, хотя я не уверен, что это менее неуклюже или менее подвержено взлому, чем делать это с проверкой экземпляра, как в вашем вопросе:
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.....
}
Javadoc of java.util.concurrent.Future.get()
утверждает следующее. Тогда почему бы просто не перехватить ExecutionException (и Cancellation и Interrupted, как объявлено java.util.concurrent.Future.get()
) метод?
...
Броски:CancellationException - если вычисление было отменено
ExecutionException - если вычисление бросило исключение
InterruptedException - если текущий поток был прерван во время ожидания
Таким образом, в основном вы можете бросить любое исключение в ваш вызываемый и просто поймать ExecutionException
, затем ExecutionException.getCause()
будет удерживать фактическое исключение, которое вы бросили, как указано в Javadoc. Таким образом, вы защищены от изменений сигнатур методов, связанных с объявлением проверенных исключений.
Кстати, вы никогда не должны ловить Throwable
, как это поймает также RuntimeExceptions
а также 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);
}
Я бы не сказал, что рекомендую это, но вот способ, которым вы можете это сделать. Это безопасно для типов, и тот, кто придет, чтобы изменить его после того, как вы, вероятно, будет недоволен этим.
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);
}
}
}
Я нашел один способ решить проблему. Если это ExecutionException, вы можете получить оригинал, вызвав исключение.getCause(). Затем вам нужно обернуть ваше исключение в какое-то исключение времени выполнения или (что для меня лучше) использовать аннотацию @SneakyThrows из проекта lombok ( 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();
}
}