【问题标题】:Java: how to handle retries without copy-paste code?Java:如何在没有复制粘贴代码的情况下处理重试?
【发布时间】:2012-03-21 08:13:37
【问题描述】:

当我必须对数据库和网络操作进行重试时,我遇到了多种情况。无论我在哪里做,我都有以下类型的代码:

    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);
            }
        }
    }

问题在于,这个 Code Pattern 被复制粘贴到我的所有课程中。这真的很糟糕。我不知道如何摆脱这种 for-break-catch 复制粘贴模式,因为我通常会处理不同的异常,我想记录我失败的数据(通常也是不同的方式)。

在 Java 7 中是否有避免这种复制粘贴的好方法?

编辑:我确实使用 guice 进行依赖注入。我确实检查了异常。可能有多个变量,而不仅仅是一个数据,而且它们都是不同的类型。

Edit2:AOP 方法对我来说似乎是最有前途的。

【问题讨论】:

  • 如果这都是数据库访问代码,为什么异常经常不同?你能举几个例子吗?
  • 我刚刚编辑了这个问题,但原因是有数据库和网络,甚至只是数据库,取决于操作,它会因不同的异常而失败。 doSomethingUseful() 也可能会抛出我自己想以相同方式处理的异常。
  • 异常是选中还是未选中?
  • 有已检查和未检查的异常
  • 有一个类似的问题:stackoverflow.com/questions/13239972/…我接受了答案。

标签: java exception copy-paste


【解决方案1】:

副手,我可以想到两种不同的方法:

如果异常处理的差异可以以声明的方式表达,您可以使用 AOP 在您的方法周围编织异常处理代码。然后,您的业务代码可能如下所示:

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

优点是向方法添加重试行为真的很容易,缺点是编织建议的复杂性(您只需实现一次。如果您使用依赖注入库,很可能会提供方法拦截支持)。

另一种方法是使用命令模式:

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);
                }
            }
        }
    }
}

命令模式的问题在于方法参数。您仅限于单个参数,并且泛型对于调用者来说相当笨拙。此外,它不适用于已检查的异常。从好的方面来说,没有花哨的 AOP 东西 :-)

【讨论】:

【解决方案2】:

如前所述,AOP 和 Java 注释是一个不错的选择。我建议使用来自jcabi-aspects 的已读机制:

@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

【讨论】:

    【解决方案3】:

    我已经实现了下面的 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;
        }
    }
    

    以下是一个使用示例。要重试的代码在调用方法中。

    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);
            }});
    }
    

    如果您只想在发生特定类型的异常时重试(并且在所有其他类型的异常上失败),RetryLogic 类支持异常类参数。

    【讨论】:

      【解决方案4】:

      让您的doSomething 实现一个接口,例如Runable,并创建一个包含您上面代码的方法,将doSomething 替换为interface.run(data)

      【讨论】:

      • 我认为这是正确的想法,但需要扩展以处理日志记录(可能很简单)和异常(可能不太容易)
      • 我同意 DNA。该方法是正确的,但它没有考虑不同的异常和自定义日志记录。数据的类型也总是不同的。
      • 日志记录也可以委托给接口,实际的异常区分也可以。我知道魔鬼在细节中。
      • 哦,我想每次都对异常做不同的事情。
      • 然后将该特定操作也委托给您的界面。我认为那里没有问题。
      【解决方案5】:

      看看:this retry utility

      这种方法应该适用于大多数用例:

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

      您可以使用此实用程序轻松实现方面,用更少的代码完成此操作。

      【讨论】:

        【解决方案6】:

        扩展已经讨论过的方法,这样的东西怎么样(这个上网本上没有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);
                }
            }
        }
        

        【讨论】:

        • 这种方法很棒,但它可能不适用于已检查的异常。
        • 其实我可以在签名中声明基础检查异常。
        【解决方案7】:

        我想补充一点。大多数例外情况 (99.999%) 意味着您的代码或环境存在严重问题,需要管理员注意。如果您的代码无法连接到数据库,则可能是环境配置错误,重试它只是为了发现它在第 3、4 或第 5 次也不起作用。如果您因为此人没有提供有效的信用卡号而引发异常,则重试不会神奇地填写信用卡号。

        唯一值得重试的情况是系统极度紧张并且事情超时,但在这种情况下,重试逻辑可能会导致更多的压力(每笔事务重试 3 次)。但这就是系统为降低需求所做的事情(参见阿波罗着陆器任务的故事)。当系统被要求做的事情超出了它的能力时,它就会开始放弃工作,而超时是系统紧张(或写得不好)的信号。如果您只是增加系统的容量(添加更多内存、更大的服务器、更多的服务器、更好的算法、扩展它!),您的情况会好得多。

        另一种情况是,如果您使用乐观锁定并且您可以通过某种方式恢复并自动合并一个对象的两个版本。虽然在我警告这种方法之前我已经看到了这一点,但它可以用于简单的对象,可以在 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();
                   }
                }
            }
        }
        

        【讨论】:

        • 我同意你关于捕获异常的正确位置。我同意这是最重要的部分。但是,我说的是在您编写的每个框架方法的代码中复制粘贴“try-catch-catch”:)
        • 错误。不明白你的意思。在我上面展示的方法中,不需要复制任何必需的 try-catch 逻辑。在客户端代码中,我包含的 try-catch 是为了向您展示您可能会遇到一些您想要绕过重试逻辑的异常,因为这对于该调用来说是特殊的。我包含的那个 try-catch 完全是可选的。其他框架没有添加的特殊功能。
        猜你喜欢
        • 2011-06-08
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-09-03
        • 2020-04-07
        • 1970-01-01
        • 1970-01-01
        • 2023-03-03
        相关资源
        最近更新 更多