Если вам необходимо внедрить в ваш код надежную логику повторных попыток, проверенным способом будет использование библиотеки пружинных повторов . Моя цель здесь не в том, чтобы показать, как использовать сам проект 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=" 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 ) @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. |