Java: как обрабатывать повторы без кода копирования?

У меня есть несколько случаев, когда мне приходится заниматься повторной обработкой для БД и сетевых операций. Везде, где я это делаю, у меня есть следующий тип кода:

for (int iteration = 1; ; iteration++) {
 try {
 data = doSomethingUseful(data);
 break;
 } catch (SomeException | AndAnotherException e) {
 if (iteration == helper.getNumberOfRetries()) {
 throw e;
 } else {
 errorReporter.reportError("Got following error for data = {}. Continue trying after delay...", data, e);
 utilities.defaultDelayForIteration(iteration);
 handleSpecificCase(data);
 }
 }
 }

Проблема заключается в том, что этот шаблон кода скопирован во все классы. Что действительно плохо. Я не могу понять, как избавиться от этого шаблона копирования-вставки для-break-catch, так как я обычно получаю другое исключение для обработки, я хочу регистрировать данные, с которыми мне не удалось (обычно также разные способы).

Есть ли хороший способ избежать этой копии-вставки в Java 7?

Изменить: Я использую guice для инъекции зависимостей. Я проверил исключения. Вместо нескольких данных может быть несколько переменных, и они имеют разные типы.

Edit2: подход AOP выглядит наиболее перспективным для меня.

7 ответов

Вне руки, я могу думать о двух разных подходах:

Если различия в обработке исключений могут быть выражены декларативно, вы можете использовать AOP, чтобы сплести код обработки исключений вокруг ваших методов. Тогда ваш бизнес-код может выглядеть так:

@Retry(times = 3, loglevel = LogLevel.INFO)
List<user> getActiveUsers() throws DatabaseException {
 // talk to the database
}
</user>

Преимущество состоит в том, что очень легко добавить поведение повторов к методу, недостатком является сложность плетения совета (который вам нужно реализовать только один раз. Если вы используете библиотеку инъекций зависимостей, скорее всего, это будет предлагать поддержку перехвата метода).

Другой подход заключается в использовании шаблона команды:

abstract class Retrieable<i,o> {
 private final LogLevel logLevel;
 protected Retrieable(LogLevel loglevel) {
 this.logLevel = loglevel;
 }
 protected abstract O call(I input);
 // subclasses may override to perform custom logic.
 protected void handle(RuntimeException e) {
 // log the exception. 
 }
 public O execute(I input) {
 for (int iteration = 1; ; iteration++) {
 try {
 return call(input);
 } catch (RuntimeException e) {
 if (iteration == helper.getNumberOfRetries()) {
 throw e;
 } else {
 handle();
 utilities.defaultDelayForIteration(iteration);
 }
 }
 }
 }
}
</i,o>

Проблема с шаблоном команды - это аргументы метода. Вы ограничены одним параметром, а дженерики довольно неудобны для вызывающего. Кроме того, он не будет работать с проверенными исключениями. С положительной стороны, никаких фантазийных вещей AOP: -)


Как уже было сказано, AOP и Java-аннотации являются хорошим вариантом. Я бы рекомендовал использовать механизм чтения из jcabi-аспекты:

@RetryOnFailure(attempts = 2, delay = 10, verbose = false)
public String load(URL url) {
 return url.openConnection().getContent();
}

Читайте также эту запись в блоге: http://www.yegor256.com/2014/08/15/retry-java-method-on-exception.html


Я реализовал класс RetryLogic, который предоставляет повторную логику повтора и поддерживает параметры, потому что код, который нужно повторить, находится в переданном делегате.

/**
 * Generic retry logic. Delegate must throw the specified exception type to trigger the retry logic.
 */
public class RetryLogic<t>
{
 public static interface Delegate<t>
 {
 T call() throws Exception;
 }
 private int maxAttempts;
 private int retryWaitSeconds;
 @SuppressWarnings("rawtypes")
 private Class retryExceptionType;
 public RetryLogic(int maxAttempts, int retryWaitSeconds, @SuppressWarnings("rawtypes") Class retryExceptionType)
 {
 this.maxAttempts = maxAttempts;
 this.retryWaitSeconds = retryWaitSeconds;
 this.retryExceptionType = retryExceptionType;
 }
 public T getResult(Delegate<t> caller) throws Exception {
 T result = null;
 int remainingAttempts = maxAttempts;
 do {
 try {
 result = caller.call();
 } catch (Exception e){
 if (e.getClass().equals(retryExceptionType))
 {
 if (--remainingAttempts == 0)
 {
 throw new Exception("Retries exausted.");
 }
 else
 {
 try {
 Thread.sleep((1000*retryWaitSeconds));
 } catch (InterruptedException ie) {
 }
 }
 }
 else
 {
 throw e;
 }
 }
 } while (result == null && remainingAttempts > 0);
 return result;
 }
}
</t></t></t>

Ниже приведен пример использования. Код, который нужно повторить, находится в пределах метода вызова.

private MyResultType getDataWithRetry(final String parameter) throws Exception {
 return new RetryLogic<myresulttype>(5, 15, Exception.class).getResult(new RetryLogic.Delegate<myresulttype> () {
 public MyResultType call() throws Exception {
 return dataLayer.getData(parameter);
 }});
}
</myresulttype></myresulttype>

Если вы хотите повторить попытку только при возникновении определенного типа исключения (и сбоя во всех других типах исключений), класс RetryLogic поддерживает параметр класса исключения.


Сделайте свой doSomething реализован интерфейс, например Runable, и создайте метод, содержащий ваш код выше, с заменой doSomething на interface.run(data)


взгляните на: эту утилиту повтора

этот метод должен работать в большинстве случаев:

public static <t> T executeWithRetry(final Callable<t> what, final int nrImmediateRetries,
 final int nrTotalRetries, final int retryWaitMillis, final int timeoutMillis)
</t></t>

вы можете с легкостью реализовать аспект, используя эту утилиту, чтобы сделать это еще меньше кода.


Расширение подхода, обсуждаемого уже, как насчет чего-то вроде этого (нет IDE на этом нетбуке, поэтому расценивайте это как псевдокод...)

// generics left as an exercise for the reader...
public Object doWithRetry(Retryable r){
for (int iteration = 1; ; iteration++) {
 try {
 return r.doSomethingUseful(data);
 } catch (Exception e) {
 if (r.isRetryException(e)) {
 if(r.tooManyRetries(i){
 throw e;
 }
 } else {
 r.handleOtherException(e);
 }
 }
}


Одна вещь, которую я хотел бы добавить. Большинство исключений (99,999%) означают, что что-то очень не так с вашим кодом или средой, которая требует внимания админов. Если ваш код не может подключиться к базе данных, это, вероятно, неправильно сконфигурированная среда, мало смысла повторять ее, чтобы узнать, что она не работает в 3-й, 4-й или 5-й раз. Если вы выбрасываете исключение, потому что человек не дал действительный номер кредитной карты, повторная попытка не будет волшебным образом заполнять номер кредитной карты.

Единственные ситуации, которые требуют повторной попытки удаленной работы, - это когда система сильно напряжена, и время от времени заканчивается, но в этой ситуации логика повторения, вероятно, будет вызывать больше напряжения, чем меньше (3 раза для 3 попыток при каждой транзакции). Но это то, что системы делают для снижения спроса (см. Рассказ миссии посадочного модуля apollo). Когда системе предлагается делать больше, чем может, она начинает отбрасывать задания, а таймауты - это сигнал, который система напряжена (или плохо написана). Вы бы оказались в гораздо лучшей ситуации, если бы просто увеличили емкость своей системы (добавьте больше бара, большие серверы, больше серверов, улучшите алгоритмы, масштабируйте его!).

Другая ситуация была бы, если бы вы использовали оптимистичную блокировку, и вы можете как-то восстановить и автоматически объединить две версии объекта. Хотя я видел это прежде, чем я предостерег этот подход, но это можно сделать для простых объектов, которые могут быть объединены без конфликтов в 100% случаев.

Логика большинства исключений должна быть улавливаться на соответствующем уровне (очень важно), убедитесь, что ваша система находится в хорошем согласованном состоянии (например, транзакции отката, закрытые файлы и т.д.), запишите ее, сообщите пользователю, что она не работает.

Но я буду смеяться над этой идеей и попытаться дать хорошую фреймворк (ну, потому что это весело, как удовольствие от кроссворда).

// client code - what you write a lot
public class SomeDao {
 public SomeReturn saveObject( final SomeObject obj ) throws RetryException {
 Retry<somereturn> retry = new Retry<somereturn>() {
 public SomeReturn execute() throws Exception {
 try {
 // doSomething
 return someReturn;
 } catch( SomeExpectedBadExceptionNotWorthRetrying ex ) {
 throw new NoRetryException( ex ); // optional exception block
 }
 }
 }
 return retry.run();
 }
}
// framework - what you write once
public abstract class Retry<t> {
 public static final int MAX_RETRIES = 3;
 private int tries = 0;
 public T execute() throws Exception;
 public T run() throws RetryException {
 try {
 return execute();
 } catch( NoRetryException ex ) {
 throw ex;
 } catch( Exception ex ) {
 tries++;
 if( MAX_RETRIES == tries ) {
 throw new RetryException("Maximum retries exceeded", ex );
 } else {
 return run();
 }
 }
 }
}
</t></somereturn></somereturn>

licensed under cc by-sa 3.0 with attribution.