Каков наилучший способ обработки исключения 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();
    }
}
Другие вопросы по тегам