Статьи

Повторное выполнение метода с использованием Spring AOP

Один из последователей моего блога отправляет электронное письмо с просьбой показать пример использования RealWorld в Spring AOP . Он упомянул, что в большинстве примеров использование Spring AOP демонстрируется для регистрации входа / выхода метода, управления транзакциями или проверки безопасности .

Он хотел узнать, как Spring AOP используется в «Реальном проекте для реальных проблем» . Поэтому я хотел бы показать, как я использовал Spring AOP для одного из моих проектов для решения реальной проблемы.

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

Например:

  • Сбои удаленного вызова WebService из-за проблем с задержкой в ​​сети
  • Сбои запросов к базе данных из-за исключений блокировки и т. Д.

В большинстве случаев достаточно просто повторить одну и ту же операцию, чтобы устранить подобные ошибки.

Давайте посмотрим, как мы можем использовать Spring AOP для автоматической повторной попытки выполнения метода в случае возникновения какого-либо исключения. Мы можем использовать Spring AOP @Around advice для создания прокси для тех объектов, методы которых необходимо повторить, и реализовать логику повторения в Aspect .

Прежде чем приступить к реализации этих Spring Advice и Aspect, сначала давайте напишем простую утилиту для выполнения «Задачи», которая автоматически повторяется N раз, игнорируя данный набор Исключений.

1
2
3
public interface Task<T> {
 T execute();
}
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import java.util.HashSet;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
 
public class TaskExecutionUtil
{
  
 private static Logger logger = LoggerFactory.getLogger(TaskExecutionUtil.class);
 
 @SafeVarargs
 public static <T> T execute(Task<T> task,
        int noOfRetryAttempts,
        long sleepInterval,
        Class<? extends Throwable>... ignoreExceptions)
 {
   
  if (noOfRetryAttempts < 1) {
   noOfRetryAttempts = 1;
  }
  Set<Class<? extends Throwable>> ignoreExceptionsSet = new HashSet<Class<? extends Throwable>>();
  if (ignoreExceptions != null && ignoreExceptions.length > 0) {
   for (Class<? extends Throwable> ignoreException : ignoreExceptions) {
    ignoreExceptionsSet.add(ignoreException);
   }
  }
   
  logger.debug("noOfRetryAttempts = "+noOfRetryAttempts);
  logger.debug("ignoreExceptionsSet = "+ignoreExceptionsSet);
   
  T result = null;
  for (int retryCount = 1; retryCount <= noOfRetryAttempts; retryCount++) {
   logger.debug("Executing the task. Attemp#"+retryCount);
   try {
    result = task.execute();
    break;
   } catch (RuntimeException t) {
    Throwable e = t.getCause();
    logger.error(" Caught Exception class"+e.getClass());
    for (Class<? extends Throwable> ignoreExceptionClazz : ignoreExceptionsSet) {
     logger.error(" Comparing with Ignorable Exception : "+ignoreExceptionClazz.getName());
      
     if (!ignoreExceptionClazz.isAssignableFrom(e.getClass())) {
      logger.error("Encountered exception which is not ignorable: "+e.getClass());
      logger.error("Throwing exception to the caller");
       
      throw t;
     }
    }
    logger.error("Failed at Retry attempt :" + retryCount + " of : " + noOfRetryAttempts);
    if (retryCount >= noOfRetryAttempts) {
     logger.error("Maximum retrial attempts exceeded.");
     logger.error("Throwing exception to the caller");
     throw t;
    }
    try {
     Thread.sleep(sleepInterval);
    } catch (InterruptedException e1) {
     //Intentionally left blank
    }
   }
  }
  return result;
 }
 
}

Я надеюсь, что этот метод говорит сам за себя. Он принимает задачу и повторяет noOfRetryAttempts раз в случае, если метод task.execute () генерирует любое исключение, а ignoreExceptions указывает, какой тип исключений следует игнорировать при повторной попытке.

Теперь давайте создадим аннотацию Retry следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
 
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public  @interface Retry {
  
 public int retryAttempts() default 3;
  
 public long sleepInterval() default 1000L; //milliseconds
  
 Class<? extends Throwable>[] ignoreExceptions() default { RuntimeException.class };
  
}

Мы будем использовать эту аннотацию @Retry, чтобы определить, какие методы необходимо повторить.

Теперь давайте реализуем Аспект, который применяется к методу с аннотацией @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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import java.lang.reflect.Method;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
 
@Component
@Aspect
public class MethodRetryHandlerAspect {
  
 private static Logger logger = LoggerFactory.getLogger(MethodRetryHandlerAspect.class);
  
 @Around("@annotation(com.sivalabs.springretrydemo.Retry)")
 public Object audit(ProceedingJoinPoint pjp)
 {
  Object result = null;
  result = retryableExecute(pjp);
     return result;
 }
  
 protected Object retryableExecute(final ProceedingJoinPoint pjp)
 {
  MethodSignature signature = (MethodSignature) pjp.getSignature();
  Method method = signature.getMethod();
  logger.debug("-----Retry Aspect---------");
  logger.debug("Method: "+signature.toString());
 
  Retry retry = method.getDeclaredAnnotation(Retry.class);
  int retryAttempts = retry.retryAttempts();
  long sleepInterval = retry.sleepInterval();
  Class<? extends Throwable>[] ignoreExceptions = retry.ignoreExceptions();
   
  Task<Object> task = new Task<Object>() {
   @Override
   public Object execute() {
    try {
     return pjp.proceed();
    } catch (Throwable e) {
     throw new RuntimeException(e);
    }
   }
  };
  return TaskExecutionUtil.execute(task, retryAttempts, sleepInterval, ignoreExceptions);
 }
}

Вот и все. Нам просто нужно несколько тестов для его проверки.

Сначала создайте класс конфигурации AppConfig.java следующим образом:

01
02
03
04
05
06
07
08
09
10
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
 
@Configuration
@ComponentScan
@EnableAspectJAutoProxy
public class AppConfig {
 
}

И пару пустышек.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
import org.springframework.stereotype.Service;
 
@Service
public class ServiceA {
  
 private int counter = 1;
  
 public void method1() {
  System.err.println("----method1----");
 }
  
 @Retry(retryAttempts=5, ignoreExceptions={NullPointerException.class})
 public void method2() {
  System.err.println("----method2 begin----");
  if(counter != 3){
   counter++;
   throw new NullPointerException();
  }
  System.err.println("----method2 end----"); 
 }
}
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
import java.io.IOException;
import org.springframework.stereotype.Service;
 
@Service
public class ServiceB {
  
 @Retry(retryAttempts = 2, ignoreExceptions={IOException.class})
 public void method3() {
  System.err.println("----method3----");
  if(1 == 1){
   throw new ArrayIndexOutOfBoundsException();
  }
 }
  
 @Retry
 public void method4() {
  System.err.println("----method4----");
 }
}

Наконец, напишите простой тест Junit для вызова этих методов.

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
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes=AppConfig.class)
public class RetryTest
{
 @Autowired ServiceA svcA;
 @Autowired ServiceB svcB;
  
 @Test
 public void testA()
 {
  svcA.method1();
 }
  
 @Test
 public void testB()
 {
  svcA.method2();
 }
  
 @Test(expected=RuntimeException.class)
 public void testC()
 {
  svcB.method3();
 }
  
 @Test
 public void testD()
 {
  svcB.method4();
 }
}

Да, я знаю, что мог бы написать эти методы тестирования немного лучше, но я надеюсь, что вы поняли идею.

Запустите тесты JUnit и просмотрите инструкцию log, чтобы проверить, происходит ли повторная попытка метода в случае исключения или нет.

  • Случай № 1: при вызове ServiceA.method1 () метод MethodRetryHandlerAspect вообще не будет применяться.
  • Случай № 2: при вызове ServiceA.method2 () мы поддерживаем счетчик и генерируем исключение NullPointerException 2 раза. Но мы пометили этот метод, чтобы игнорировать исключения NullPointerException. Таким образом, он будет продолжать попытки 5 раз. Но 3-й раз метод будет выполнен нормально и выйдет из метода нормально.
  • Случай № 3: Когда вызывается ServiceB.method3 (), мы генерируем исключение ArrayIndexOutOfBoundsException, но этот метод помечается, чтобы игнорировать только только IOException. Таким образом, выполнение этого метода не будет повторено и немедленно вызывает исключение.
  • Случай № 4: Когда вызывается ServiceB.method4 (), все в порядке, поэтому он должен завершиться с первой попытки как обычно.

Я надеюсь, что этот пример продемонстрирует достаточно хорошее использование Spring AOP в реальном мире 🙂