Каков наилучший способ обработки 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 );
}
}
}
=== UPDATE ===
Многие люди опубликовали ответы, которые рекомендовали либо 1) повторное бросание в качестве общего исключения, либо 2) повторное бросание в качестве неконтролируемого исключения. Я не хочу делать ничего из этого, потому что эти типы исключений (ProcessExecutionException, InterruptedException, IOException, TimeoutException) важны - каждый из них будет обрабатываться по-разному обработанным вызовом. Если мне не нужна функция тайм-аута, я бы хотел, чтобы мой метод выбрал эти 4 конкретных типа исключений (ну, кроме исключения TimeoutException). Я не думаю, что добавление функции тайм-аута должно изменить мою сигнатуру метода, чтобы создать общий тип исключения.
Ответы
Ответ 1
Я подробно рассмотрел эту проблему, и это беспорядок. В Java 5 и 6 или 7 нет легкого ответа. В дополнение к неуклюжемости, многословию и хрупкости, которые вы указываете, ваше решение действительно имеет проблему, которую ExecutionException
отключает, когда вы вызываете getCause()
на самом деле содержит большую часть важной информации о трассировке стека!
То есть вся информация стека потока, выполняющего метод в представленном вами кодеке, находится только в ExcecutionException, а не во вложенных причинах, которые охватывают только рамки, начинающиеся с call()
в Callable. То есть, ваш метод 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
s и заканчиваются методом getChecked
в будущем.
То, как вы передаете безопасность типа времени компиляции. Это означает, что вместо вызова:
Future.get() throws InterruptedException, ExecutionException;
Вы можете позвонить:
CheckedFuture.getChecked() throws InterruptedException, ProcessExecutionException, IOException
Таким образом, проблему разворачивания можно избежать - ваш метод немедленно выдает исключения требуемого типа, и они доступны и проверяются во время компиляции.
Внутри getChecked
, однако вам все равно необходимо решить проблему "пропадания причины", описанную выше. Вы можете сделать это, сшив текущий стек (вызывающего потока) в стек вызванного исключения. Это растягивает обычное использование трассировки стека в Java, поскольку один стек растягивается по потокам, но он работает и легко понять, как только вы знаете, что происходит.
Другой вариант - создать другое исключение из того же самого, что и брошенное, и установить оригинал как причину нового. Вы получите полную трассировку стека, и отношение причин будет таким же, как и с ExecutionException
, но у вас будет правильный тип исключения. Однако вам нужно использовать отражение, и не гарантируется работа, например, для объектов без конструктора, имеющих обычные параметры.
Ответ 2
Вот пара интересных сведений для проверенных и против проверенных исключений. Обсуждение Брайана Гетца и аргумент против отмеченных исключений из Eckel Discussion. Но я не знал, были ли вы уже реализованы и подумали о проверенном рефакторе исключений, который обсуждается Джошуа в этой книге.
Согласно Эффективному жемчугу Java, одним из предпочтительных методов обработки проверенных исключений является превращение проверенного исключения в Un-Checked Exception. Так, например,
try{
obj.someAction()
}catch(CheckedException excep){
}
измените эту реализацию на
if(obj.canThisOperationBeperformed){
obj.someAction()
}else{
// Handle the required Exception.
}
Ответ 3
Я боюсь, что нет ответа на вашу проблему. В основном, вы запускаете задачу в другом потоке, чем тот, в котором находитесь, и хотите использовать шаблон ExecutorService, чтобы поймать все исключения, которые может выполнить задача, плюс бонус прерывания этой задачи через определенное время. Ваш подход правильный: вы не могли бы сделать это с голым Runnable.
И это исключение, что у вас нет информации о том, что вы хотите его снова добавить, с определенным типом: ProcessExecutionException, InterruptedException или IOException. Если это другой тип, вы хотите изменить его как исключение RuntimeException (это не лучшее решение, так как вы не охватываете все случаи).
Таким образом, у вас есть несоответствие импактности: сбрасывание с одной стороны и известный тип исключения - с другой. Единственное решение, которое вы должны решить, это сделать то, что вы сделали: проверить тип и бросить его снова с помощью броска. Его можно написать по-разному, но в конце он будет выглядеть одинаково...
Ответ 4
Вот что я делаю в этой ситуации. Это выполняет следующее:
- Re-throws проверяет исключения, не обертывая их
- Склеивает трассировки стека
код:
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;
}
Кажется, работает.
Ответ 5
Я не уверен, почему у вас есть блок if/else в catch и instanceof
, я думаю, что вы можете делать то, что хотите: -
catch( ProcessExecutionException ex )
{
// handle ProcessExecutionException
}
catch( InterruptException ex )
{
// handler InterruptException*
}
Одна вещь, чтобы рассмотреть, чтобы уменьшить беспорядок, заключается в том, чтобы поймать исключение внутри вашего вызываемого метода и повторно бросить как ваше собственное исключение или исключения для конкретного домена/пакета. Сколько исключений, которые необходимо создать, во многом будет зависеть от того, как ваш код вызова будет реагировать на исключение.
Ответ 6
В javadoc java.util.concurrent.Future.get()
указано следующее. Тогда почему бы просто не уловить ExecutionException (и отмену и прервать, объявленный методом 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);
}
Ответ 7
В вызывающем классе перехватите Throwable
last. Например,
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
Ответ 8
Я нашел один способ решить проблему.
Если это ExecutionException, вы можете получить оригинальное, вызвав exception.getCause()
Затем вам нужно обернуть свое исключение в какое-то исключение Runtime Exception или (что для меня лучше всего) использовать аннотацию @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();
}
}
Ответ 9
Вот еще один способ сделать это, хотя я не уверен, что это менее неуклюжий или менее склонный к перерыву, чем сделать это с проверкой экземпляра, как в вашем вопросе:
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.....
}
Ответ 10
Я бы не сказал, что рекомендую это, но здесь вы можете это сделать. Это безопасный тип и тот, кто приходит, чтобы изменить его после того, как вы, вероятно, будете ему недовольны.
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);
}
}
}