Если вам необходимо внедрить в ваш код надежную логику повторных попыток, проверенным способом будет использование библиотеки пружинных повторов . Моя цель здесь не в том, чтобы показать, как использовать сам проект 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
|
@Beanpublic 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
|
@Testpublic 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;@Aspectpublic 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=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <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)@ContextConfigurationpublic 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. |