Статьи

Spring Retry — способы интеграции с вашим проектом

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

Рассмотрим сервис для вызова внешней системы:

1
2
3
4
5
package retry.service;
 
public interface RemoteCallService {
    String call() throws Exception;
}

Предположим, что этот вызов может завершиться неудачно, и вы хотите, чтобы вызов повторялся трижды с задержкой в ​​2 секунды при каждом сбое вызова, поэтому для имитации этого поведения я определил фиктивную службу, используя Mockito таким образом, обратите внимание, что она возвращается как высмеянный боб весны

1
2
3
4
5
6
7
8
9
@Bean
public RemoteCallService remoteCallService() throws Exception {
    RemoteCallService remoteService = mock(RemoteCallService.class);
    when(remoteService.call())
            .thenThrow(new RuntimeException("Remote Exception 1"))
            .thenThrow(new RuntimeException("Remote Exception 2"))
            .thenReturn("Completed");
    return remoteService;
}

Таким образом, по сути, эта фиктивная служба дает сбой 2 раза и завершается третьим вызовом.

И это тест для логики повторов:

01
02
03
04
05
06
07
08
09
10
11
12
public class SpringRetryTests {
 
    @Autowired
    private RemoteCallService remoteCallService;
 
    @Test
    public void testRetry() throws Exception {
        String message = this.remoteCallService.call();
        verify(remoteCallService, times(3)).call();
        assertThat(message, is("Completed"));
    }
}

Мы гарантируем, что сервис вызывается 3 раза для учета первых двух неудачных вызовов и третьего успешного вызова.

Если бы мы напрямую включили Spring-Retry в момент вызова этого сервиса, то код выглядел бы так:

1
2
3
4
5
6
@Test
public void testRetry() throws Exception {
    String message = this.retryTemplate.execute(context -> this.remoteCallService.call());
    verify(remoteCallService, times(3)).call();
    assertThat(message, is("Completed"));
}

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

Учитывая это, ниже приведены подходы для включения логики Spring-retry.

Подход 1: Пользовательский аспект для включения Spring-Retry

Этот подход должен быть достаточно интуитивным, поскольку логику повторных попыток можно рассматривать как сквозную задачу, а хорошим способом реализации сквозной задачи является использование Аспектов. Аспект, который включает в себя Spring-retry, будет выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package retry.aspect;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.support.RetryTemplate;
 
@Aspect
public class RetryAspect {
 
    private static Logger logger = LoggerFactory.getLogger(RetryAspect.class);
 
    @Autowired
    private RetryTemplate retryTemplate;
 
    @Pointcut("execution(* retry.service..*(..))")
    public void serviceMethods() {
        //
    }
 
    @Around("serviceMethods()")
    public Object aroundServiceMethods(ProceedingJoinPoint joinPoint) {
        try {
            return retryTemplate.execute(retryContext -> joinPoint.proceed());
        } catch (Throwable e) {
            throw new RuntimeException(e);
        }
    }
}

Этот аспект перехватывает вызов удаленной службы и делегирует вызов retryTemplate. Полный рабочий тест здесь .

Подход 2: Использование Spring-Retry предоставил совет

В рамках проекта Spring-retry предоставляется совет, который позаботится о том, чтобы целевые сервисы могли быть повторены. Конфигурация AOP для создания рекомендаций по сервису требует работы с необработанным xml, в отличие от предыдущего подхода, где аспект может быть сплетен с использованием конфигурации Spring Java. Конфигурация xml выглядит следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
    xsi:schemaLocation="
 
 <aop:config>
  <aop:pointcut id="transactional"
       expression="execution(* retry.service..*(..))" />
  <aop:advisor pointcut-ref="transactional"
      advice-ref="retryAdvice" order="-1"/>
 </aop:config>
 
</beans>

Полный рабочий тест здесь .

Подход 3: декларативная логика повторов

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

1
2
3
4
5
6
7
8
9
package retry.service;
 
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
 
public interface RemoteCallService {
    @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 2000))
    String call() throws Exception;
}

и полный тест, который использует эту декларативную логику повторных попыток, также доступную здесь :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package retry;
 
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.annotation.EnableRetry;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import retry.service.RemoteCallService;
 
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.Mockito.*;
 
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class SpringRetryDeclarativeTests {
 
    @Autowired
    private RemoteCallService remoteCallService;
 
    @Test
    public void testRetry() throws Exception {
        String message = this.remoteCallService.call();
        verify(remoteCallService, times(3)).call();
        assertThat(message, is("Completed"));
    }
 
    @Configuration
    @EnableRetry
    public static class SpringConfig {
 
        @Bean
        public RemoteCallService remoteCallService() throws Exception {
            RemoteCallService remoteService = mock(RemoteCallService.class);
            when(remoteService.call())
                    .thenThrow(new RuntimeException("Remote Exception 1"))
                    .thenThrow(new RuntimeException("Remote Exception 2"))
                    .thenReturn("Completed");
            return remoteService;
        }
    }
}

Аннотация @EnableRetry активирует обработку аннотированных методов @Retryable и внутренне использует логику в соответствии с подходом 2, при этом конечный пользователь не должен быть явным об этом.

Я надеюсь, что это даст вам немного лучший вкус для включения Spring-retry в ваш проект. Весь код, который я продемонстрировал здесь, также доступен в моем проекте на github: https://github.com/bijukunjummen/test-spring-retry

Ссылка: Spring retry — способы интеграции с вашим проектом от нашего партнера JCG Биджу Кунджуммена в блоге all and sundry.