[Рекомендации] Как изящно повторить попытку

Java

Аромат этой статьи: Ледяная черника. Ожидаемое чтение: 20 минут.

инструкция

В последнее время компания ведет деятельность и нуждается в опоре на сторонний интерфейс.На этапе тестирования нештатной ситуации нет.Однако после выхода в интернет обнаруживается, что зависимый интерфейс иногда возвращает системные исключения из-за внутренних ошибок .Хоть вероятность и мала, но всегда тревожит из-за этого.Всегда плохо, не говоря уже о том, что сообщения в очереди недоставленных сообщений все равно нужно доставлять заново при эксплуатации и обслуживании, поэтому обязательно нужно добавить механизм повторных попыток .

Механизм повторных попыток может защитить систему от влияния колебаний сети и временной недоступности зависимых служб, чтобы система могла работать более стабильно. Система, которая делает вас устойчивым, как собака, еще более стабильна.

1

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

@Slf4j
@Component
public class HelloService {

    private static AtomicLong helloTimes = new AtomicLong();

    public String hello(){
        long times = helloTimes.incrementAndGet();
        if (times % 4 != 0){
            log.warn("发生异常,time:{}", LocalTime.now() );
            throw new HelloRetryException("发生Hello异常");
        }
        return "hello";
    }
}

Куда звонить:

@Slf4j
@Service
public class HelloRetryService implements IHelloService{

    @Autowired
    private HelloService helloService;

    public String hello(){
        return helloService.hello();
    }
}

Другими словами, этот интерфейс будет успешным только один раз из 4 раз.

Повторить попытку вручную

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

// 手动重试
public String hello(){
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloService.hello();
            log.info("helloService返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloService.hello() 调用失败,准备重试");
        }
    }
    throw new HelloRetryException("重试次数耗尽");
}

Результат выглядит следующим образом:

发生异常,time:10:17:21.079413300
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
发生异常,time:10:17:21.085861800
helloService.hello() 调用失败,准备重试
helloService返回:hello
service.helloRetry():hello

Программа сделала 4 попытки за очень короткий промежуток времени, а затем успешно вернулась.

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

Кроме того, это требует множества навязчивых модификаций кода, что, очевидно, некрасиво.

3.png

прокси-режим

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

Итак, нам нужно использовать более элегантный способ без прямого изменения бизнес-кода, так что же нам делать?

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

@Slf4j
public class HelloRetryProxyService implements IHelloService{
   
    @Autowired
    private HelloRetryService helloRetryService;
    
    @Override
    public String hello() {
        int maxRetryTimes = 4;
        String s = "";
        for (int retry = 1; retry <= maxRetryTimes; retry++) {
            try {
                s = helloRetryService.hello();
                log.info("helloRetryService 返回:{}", s);
                return s;
            } catch (HelloRetryException e) {
                log.info("helloRetryService.hello() 调用失败,准备重试");
            }
        }
        throw new HelloRetryException("重试次数耗尽");
    }
}

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

@Override
public String hello() {
    int maxRetryTimes = 4;
    String s = "";
    for (int retry = 1; retry <= maxRetryTimes; retry++) {
        try {
            s = helloRetryService.hello();
            log.info("helloRetryService 返回:{}", s);
            return s;
        } catch (HelloRetryException e) {
            log.info("helloRetryService.hello() 调用失败,准备重试");
        }
        // 延时一秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    throw new HelloRetryException("重试次数耗尽");
}

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

4.png

Динамический прокси JDK

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

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {

    private final Object subject;

    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }

            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }

}

Давайте возьмем модульный тест:

 @Test
public void helloDynamicProxy() {
    IHelloService realService = new HelloService();
    IHelloService proxyService = (IHelloService)RetryInvocationHandler.getProxy(realService);

    String hello = proxyService.hello();
    log.info("hello:{}", hello);
}

Результат выглядит следующим образом:

hello times:1
发生异常,time:11:22:20.727586700
times:1,time:11:22:20.728083
hello times:2
发生异常,time:11:22:21.728858700
times:2,time:11:22:21.729343700
hello times:3
发生异常,time:11:22:22.729706600
times:3,time:11:22:22.729706600
hello times:4
hello:hello

вывод после 4 попытокHello, в соответствии с ожиданиями.

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

Но не слишком радуйтесь, потому что прокси HelloService — это простой класс и не зависит от других классов, поэтому нет проблем с его созданием напрямую, но если прокси-класс зависит от других классов, управляемых контейнером Spring, тогда этот способ вызовет исключение, потому что в созданный экземпляр прокси не внедряется зависимый экземпляр.

В данном случае все сложнее: нужно получить собранный экземпляр из контейнера Spring, который необходимо проксировать, затем создать для него экземпляр класса-прокси, и передать его в управление контейнеру Spring, чтобы вы не не нужно создавать его каждый раз заново.Создайте новый экземпляр прокси-класса.

Без лишних слов, просто засучите рукава и сделайте это.

timg.jpg

Создайте новый класс инструмента, чтобы получить экземпляр прокси:

@Component
public class RetryProxyHandler {

    @Autowired
    private ConfigurableApplicationContext context;

    public Object getProxy(Class clazz) {
        // 1. 从Bean中获取对象
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)context.getAutowireCapableBeanFactory();
        Map<String, Object> beans = beanFactory.getBeansOfType(clazz);
        Set<Map.Entry<String, Object>> entries = beans.entrySet();
        if (entries.size() <= 0){
            throw new ProxyBeanNotFoundException();
        }
        // 如果有多个候选bean, 判断其中是否有代理bean
        Object bean = null;
        if (entries.size() > 1){
            for (Map.Entry<String, Object> entry : entries) {
                if (entry.getKey().contains(PROXY_BEAN_SUFFIX)){
                    bean = entry.getValue();
                }
            };
            if (bean != null){
                return bean;
            }
            throw new ProxyBeanNotSingleException();
        }

        Object source = beans.entrySet().iterator().next().getValue();
        Object source = beans.entrySet().iterator().next().getValue();

        // 2. 判断该对象的代理对象是否存在
        String proxyBeanName = clazz.getSimpleName() + PROXY_BEAN_SUFFIX;
        Boolean exist = beanFactory.containsBean(proxyBeanName);
        if (exist) {
            bean = beanFactory.getBean(proxyBeanName);
            return bean;
        }

        // 3. 不存在则生成代理对象
        bean = RetryInvocationHandler.getProxy(source);

        // 4. 将bean注入spring容器
        beanFactory.registerSingleton(proxyBeanName, bean);
        return bean;
    }
}

Использование динамического прокси JDK:

@Slf4j
public class RetryInvocationHandler implements InvocationHandler {

    private final Object subject;

    public RetryInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(subject, args);
            } catch (Exception e) {
                times++;
                log.info("retry times:{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }

            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        return null;
    }

    /**
     * 获取动态代理
     *
     * @param realSubject 代理对象
     */
    public static Object getProxy(Object realSubject) {
        InvocationHandler handler = new RetryInvocationHandler(realSubject);
        return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
                realSubject.getClass().getInterfaces(), handler);
    }

}

На этом основной код завершен, измените класс HelloService и добавьте зависимость:

@Slf4j
@Component
public class HelloService implements IHelloService{

    private static AtomicLong helloTimes = new AtomicLong();

    @Autowired
    private NameService nameService;

    public String hello(){
        long times = helloTimes.incrementAndGet();
        log.info("hello times:{}", times);
        if (times % 4 != 0){
            log.warn("发生异常,time:{}", LocalTime.now() );
            throw new HelloRetryException("发生Hello异常");
        }
        return "hello " + nameService.getName();
    }
}

NameService на самом деле очень прост, цель создания — только проверить, может ли bean-компонент внедрения зависимостей нормально работать.

@Service
public class NameService {

    public String getName(){
        return "Frank";
    }
}

Давайте проведем тест:

@Test
public void helloJdkProxy() throws InvocationTargetException, NoSuchMethodException, InstantiationException, IllegalAccessException {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);
}
hello times:1
发生异常,time:14:40:27.540672200
retry times:1,time:14:40:27.541167400
hello times:2
发生异常,time:14:40:28.541584600
retry times:2,time:14:40:28.542033500
hello times:3
发生异常,time:14:40:29.542161500
retry times:3,time:14:40:29.542161500
hello times:4
hello:hello Frank

Отлично, поэтому вам не нужно беспокоиться о внедрении зависимостей, потому что объекты Bean, полученные из контейнера Spring, уже внедрены и настроены. Разумеется, здесь рассматривается только случай singleton bean.Его можно считать более совершенным.Определить, является ли тип bean в контейнере Singleton или Prototype.Если это Singleton, сделать то же самое, что и выше.Если это Prototype, каждый раз создавать новый прокси объект класса.

Кроме того, здесь используется динамический прокси JDK, поэтому есть естественный недостаток: если класс, который вы хотите проксировать, не реализует какой-либо интерфейс, то вы не можете создать для него прокси-объект, и этот метод не будет работать.

EDaBTlbkyvbhmng.jpg

Динамический прокси CGLib

Теперь, когда мы поговорили о динамическом прокси-сервере JDK, мы должны упомянуть динамический прокси-сервер CGLib. Использование динамического прокси JDK требует проксирования класса, не все классы могут быть проксированы, и динамический прокси CGLib просто решает эту проблему.

Создайте класс динамического прокси CGLib:

@Slf4j
public class CGLibRetryProxyHandler implements MethodInterceptor {
    private Object target;//需要代理的目标对象

    //重写拦截方法
    @Override
    public Object intercept(Object obj, Method method, Object[] arr, MethodProxy proxy) throws Throwable {
        int times = 0;

        while (times < RetryConstant.MAX_TIMES) {
            try {
                return method.invoke(target, arr);
            } catch (Exception e) {
                times++;
                log.info("cglib retry :{},time:{}", times, LocalTime.now());
                if (times >= RetryConstant.MAX_TIMES) {
                    throw new RuntimeException(e);
                }
            }

            // 延时一秒
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
    //定义获取代理对象方法
    public Object getCglibProxy(Object objectTarget){
        this.target = objectTarget;
        Enhancer enhancer = new Enhancer();
        enhancer.setSuperclass(objectTarget.getClass());
        enhancer.setCallback(this);
        Object result = enhancer.create();
        return result;
    }
}

Чтобы переключиться на динамический прокси CGLib, просто замените эти две строки кода:

// 3. 不存在则生成代理对象
//        bean = RetryInvocationHandler.getProxy(source);
CGLibRetryProxyHandler proxyHandler = new CGLibRetryProxyHandler();
bean = proxyHandler.getCglibProxy(source);

начать тестирование:

@Test
public void helloCGLibProxy() {
    IHelloService proxy = (IHelloService) retryProxyHandler.getProxy(HelloService.class);
    String hello = proxy.hello();
    log.info("hello:{}", hello);

    hello = proxy.hello();
    log.info("hello:{}", hello);
}
hello times:1
发生异常,time:15:06:00.799679100
cglib retry :1,time:15:06:00.800175400
hello times:2
发生异常,time:15:06:01.800848600
cglib retry :2,time:15:06:01.801343100
hello times:3
发生异常,time:15:06:02.802180
cglib retry :3,time:15:06:02.802180
hello times:4
hello:hello Frank
hello times:5
发生异常,time:15:06:03.803933800
cglib retry :1,time:15:06:03.803933800
hello times:6
发生异常,time:15:06:04.804945400
cglib retry :2,time:15:06:04.805442
hello times:7
发生异常,time:15:06:05.806886500
cglib retry :3,time:15:06:05.807881300
hello times:8
hello:hello Frank

Это здорово, и это прекрасно устраняет дефекты, вызванные динамическим прокси JDK. Индекс элегантности немного вырос.

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

fuuTyTbkyvbhmsa.jpg

Spring AOP

Хотите изменить исходную логику без навязчивости? Хотите повторить попытку с одной аннотацией? Разве это не может быть идеально реализовано с помощью Spring AOP? Используя АОП для фасетирования целевого вызова, вы можете добавить некоторую дополнительную логику до и после вызова целевого метода.

Сначала создайте аннотацию:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    int retryTimes() default 3;
    int retryInterval() default 1;
}

Есть два параметра: retryTimes представляет максимальное количество повторных попыток, а retryInterval представляет интервал между повторными попытками.

Затем добавьте аннотацию к методу, который необходимо повторить:

@Retryable(retryTimes = 4, retryInterval = 2)
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

Затем, для последнего шага, напишите АОП-аспект:

@Slf4j
@Aspect
@Component
public class RetryAspect {

    @Pointcut("@annotation(com.mfrank.springboot.retry.demo.annotation.Retryable)")
    private void retryMethodCall(){}

    @Around("retryMethodCall()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        // 获取重试次数和重试间隔
        Retryable retry = ((MethodSignature)joinPoint.getSignature()).getMethod().getAnnotation(Retryable.class);
        int maxRetryTimes = retry.retryTimes();
        int retryInterval = retry.retryInterval();

        Throwable error = new RuntimeException();
        for (int retryTimes = 1; retryTimes <= maxRetryTimes; retryTimes++){
            try {
                Object result = joinPoint.proceed();
                return result;
            } catch (Throwable throwable) {
                error = throwable;
                log.warn("调用发生异常,开始重试,retryTimes:{}", retryTimes);
            }
            Thread.sleep(retryInterval * 1000);
        }
        throw new RetryExhaustedException("重试次数耗尽", error);
    }
}

начать тестирование:

@Autowired
private HelloService helloService;

@Test
public void helloAOP(){
    String hello = helloService.hello();
    log.info("hello:{}", hello);
}

Результат выглядит следующим образом:

hello times:1
发生异常,time:16:49:30.224649800
调用发生异常,开始重试,retryTimes:1
hello times:2
发生异常,time:16:49:32.225230800
调用发生异常,开始重试,retryTimes:2
hello times:3
发生异常,time:16:49:34.225968900
调用发生异常,开始重试,retryTimes:3
hello times:4
hello:hello Frank

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

IStGDBbkyvbhmow.jpg

Аннотация повторной попытки Spring

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

Итак, давайте сначала посмотрим, насколько хорошо работает это колесо.

Сначала введите пакет jar, необходимый для повторной попытки:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>
<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

Затем добавьте аннотацию @EnableRetry к классу запуска или классу конфигурации, а затем добавьте аннотацию @Retryable к методу, который необходимо повторить (хм? Кажется, это то же самое, что и моя пользовательская аннотация? Она скопировала мою аннотацию! [Руководство смешной])

@Retryable
public String hello(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello " + nameService.getName();
}

По умолчанию попытка будет повторена три раза с интервалом в 1 секунду между попытками. Конечно, мы также можем настроить количество повторных попыток и интервалов. Это точно такая же функция, которую я реализовал ранее.

Однако механизм повторных попыток в Spring также поддерживает множество полезных функций.Например, вы можете указать, что повторяются только определенные типы исключений, так что если будут выброшены другие типы исключений, они не будут повторяться. контроль. По умолчанию он пуст и повторяет попытки для всех исключений.

@Retryable{value = {HelloRetryException.class}}
public String hello(){2
    ...
}

Вы также можете использовать include и exclude, чтобы указать, какие исключения включить или исключить для повторной попытки.

Максимальное количество попыток можно указать с помощью maxAttemps, которое по умолчанию равно 3.

Имя компонента перехватчика повторных попыток может быть установлено с помощью interceptor.

Уникальный флаг этой повторной попытки может быть установлен по метке для статистического вывода.

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

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

Например:

@Retryable(value = {HelloRetryException.class}, maxAttempts = 5,
           backoff = @Backoff(delay = 1000, multiplier = 2))
public String hello(){
    ...
}

Вызов метода будет повторен после создания исключения HelloRetryException. Максимальное количество попыток — 5. Первый интервал повтора — 1 с, затем он увеличивается в 2 раза. Второй интервал повтора — 2 с, третий интервал повтора — 2 с. 2s, второй 4s и четвертый 8s.

Механизм повторных попыток также поддерживает использование аннотации @Recover для последующей работы.Когда повторная попытка достигает заданного количества раз, метод будет вызываться, и в этом методе могут выполняться такие операции, как ведение журнала.

Здесь стоит отметить, что желающие @RecoverЕсли аннотация вступает в силу, она должна находиться в том же классе, что и метод, отмеченный @Retryable, а метод, отмеченный @Retryable, не может иметь возвращаемого значения, иначе он не вступит в силу.

И если используется аннотация @Recover, после максимального количества повторных попыток, если в методе, помеченном @Recover, не будет выброшено исключение, исходное исключение не будет выброшено.

@Recover
public boolean recover(Exception e) {
    log.error("达到最大重试次数",e);
    return false;
}

Помимо использования аннотаций, Spring Retry также поддерживает повторную попытку с кодом непосредственно при вызове:

@Test
public void normalSpringRetry() {
    // 表示哪些异常需要重试,key表示异常的字节码,value为true表示需要重试
    Map<Class<? extends Throwable>, Boolean> exceptionMap = new HashMap<>();
    exceptionMap.put(HelloRetryException.class, true);

    // 构建重试模板实例
    RetryTemplate retryTemplate = new RetryTemplate();

    // 设置重试回退操作策略,主要设置重试间隔时间
    FixedBackOffPolicy backOffPolicy = new FixedBackOffPolicy();
    long fixedPeriodTime = 1000L;
    backOffPolicy.setBackOffPeriod(fixedPeriodTime);

    // 设置重试策略,主要设置重试次数
    int maxRetryTimes = 3;
    SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(maxRetryTimes, exceptionMap);

    retryTemplate.setRetryPolicy(retryPolicy);
    retryTemplate.setBackOffPolicy(backOffPolicy);

    Boolean execute = retryTemplate.execute(
        //RetryCallback
        retryContext -> {
            String hello = helloService.hello();
            log.info("调用的结果:{}", hello);
            return true;
        },
        // RecoverCallBack
        retryContext -> {
            //RecoveryCallback
            log.info("已达到最大重试次数");
            return false;
        }
    );
}

Единственным преимуществом на данном этапе является то, что можно установить несколько стратегий повтора:

NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试

AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环

SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,
悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行

Видно, что механизм повтора в Spring вполне совершенен и мощнее, чем АОП-аспект, написанный мной.

Еще один момент, который следует напомнить здесь, заключается в том, что, поскольку Spring Retry использует расширение Aspect, будет неизбежная яма при использовании внутреннего вызова метода Aspect, если вызывающий метод, аннотированный @Retryable, и вызываемый объект находятся в одном и том же классе, повторная попытка не удастся.

Но все же есть некоторые недостатки: механизм повторных попыток Spring поддерживает только захват исключений, но не может проверять возвращаемое значение.

dtFxiMbkyvbhlzo.jpg

Guava Retry

Наконец, представлен еще один инструмент повторных попыток — Guava Retry.

По сравнению с Spring Retry, Guava Retry обладает большей гибкостью и может определять необходимость повторной попытки в соответствии с проверкой возвращаемого значения.

Давайте посмотрим на маленький каштан:

Сначала импортируйте пакет jar:

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

Затем используйте небольшую демонстрацию, чтобы почувствовать это:

@Test
public void guavaRetry() {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
        .retryIfExceptionOfType(HelloRetryException.class)
        .retryIfResult(StringUtils::isEmpty)
        .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
        .withStopStrategy(StopStrategies.stopAfterAttempt(3))
        .build();

    try {
        retryer.call(() -> helloService.hello());
    } catch (Exception e){
        e.printStackTrace();
    }
}

Сначала создайте экземпляр Retryer, а затем используйте этот экземпляр для вызова метода, который необходимо повторить.Вы можете установить механизм повтора с помощью многих методов, таких как использование retryIfException для повторения всех исключений и использование метода retryIfExceptionOfType для установки указанного исключения. Попробуйте, используйте retryIfResult, чтобы повторить возвращаемый результат, который не соответствует ожиданиям, и используйте метод retryIfRuntimeException, чтобы повторить все RuntimeExceptions.

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

В сочетании с Spring AOP можно получить более мощную функцию повтора, чем Spring Retry.

Для более близкого сравнения функции, которые может предоставить Guava Retry:

  1. Вы можете установить ограничение по времени для одного выполнения задачи и создать исключение, если время ожидания истекло.
  2. Можно настроить прослушиватель повторных попыток для выполнения дополнительной обработки.
  3. Вы можете установить политику блокировки задачи, то есть вы можете установить текущее завершение повторной попытки и что делать перед началом следующей повторной попытки.
  4. Можно настроить более гибкие стратегии, комбинируя стратегию повторных попыток остановки со стратегией ожидания, например, экспоненциальное время ожидания и до 10 вызовов, случайное время ожидания и никогда не останавливаться и т. д.

GBvgTpbkyvbhlEB.jpg

Суммировать

В этой статье представлено всестороннее нетупиковое обучение различным позициям повтора от простого до глубокого, от простейшего ручного повтора до использования статического прокси-сервера, динамического прокси-сервера JDK и динамического прокси-сервера CGLib до Spring AOP. Процесс изготовления колес вручную и, наконец, представлены два типа колес, которые в настоящее время проще в использовании, один из них — Spring Retry, который прост и груб в использовании и, естественно, соответствует структуре Spring.Одна аннотация делает все, и другой — Guava Retry, который не зависит от среды Spring, является автономным, более гибким и мощным в использовании.

Лично в большинстве сценариев механизм повторных попыток, предоставляемый Spring Retry, достаточно мощен.Если вам не нужна дополнительная гибкость, предоставляемая Guava Retry, использование Spring Retry отлично подходит. Конечно, детально разбирается конкретная ситуация, но если в этом нет необходимости, повторять создание колес не рекомендуется.Сначала хорошо изучите чужие колеса, а потом подумайте, нужно ли вам это делать самому.

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

Если у вас есть лучшие идеи для повторной попытки, вы можете предложить обмены и обсуждения, и вы также можете подписаться на мой официальный аккаунт для обмена сообщениями.

1565529015677.png