Java tutorial
/* * Copyright 2019 Mahmoud Romeh * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package io.github.resilience4j.retry.configure; import java.lang.reflect.Method; import java.util.List; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; 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.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.Ordered; import org.springframework.util.StringUtils; import io.github.resilience4j.core.lang.Nullable; import io.github.resilience4j.fallback.FallbackDecorators; import io.github.resilience4j.fallback.FallbackMethod; import io.github.resilience4j.retry.RetryRegistry; import io.github.resilience4j.retry.annotation.Retry; import io.github.resilience4j.utils.AnnotationExtractor; /** * This Spring AOP aspect intercepts all methods which are annotated with a {@link Retry} annotation. * The aspect will handle methods that return a RxJava2 reactive type, Spring Reactor reactive type, CompletionStage type, or value type. * * The RetryRegistry is used to retrieve an instance of a Retry for a specific name. * * Given a method like this: * <pre><code> * {@literal @}Retry(name = "myService") * public String fancyName(String name) { * return "Sir Captain " + name; * } * </code></pre> * each time the {@code #fancyName(String)} method is invoked, the method's execution will pass through a * a {@link io.github.resilience4j.retry.Retry} according to the given config. * * The fallbackMethod parameter signature must match either: * * 1) The method parameter signature on the annotated method or * 2) The method parameter signature with a matching exception type as the last parameter on the annotated method */ @Aspect public class RetryAspect implements Ordered { private static final Logger logger = LoggerFactory.getLogger(RetryAspect.class); private final static ScheduledExecutorService retryExecutorService = Executors .newScheduledThreadPool(Runtime.getRuntime().availableProcessors()); private final RetryConfigurationProperties retryConfigurationProperties; private final RetryRegistry retryRegistry; private final @Nullable List<RetryAspectExt> retryAspectExtList; private final FallbackDecorators fallbackDecorators; /** * @param retryConfigurationProperties spring retry config properties * @param retryRegistry retry definition registry * @param retryAspectExtList a list of retry aspect extensions * @param fallbackDecorators the fallback decorators */ public RetryAspect(RetryConfigurationProperties retryConfigurationProperties, RetryRegistry retryRegistry, @Autowired(required = false) List<RetryAspectExt> retryAspectExtList, FallbackDecorators fallbackDecorators) { this.retryConfigurationProperties = retryConfigurationProperties; this.retryRegistry = retryRegistry; this.retryAspectExtList = retryAspectExtList; this.fallbackDecorators = fallbackDecorators; cleanup(); } @Pointcut(value = "@within(retry) || @annotation(retry)", argNames = "retry") public void matchAnnotatedClassOrMethod(Retry retry) { } @Around(value = "matchAnnotatedClassOrMethod(retryAnnotation)", argNames = "proceedingJoinPoint, retryAnnotation") public Object retryAroundAdvice(ProceedingJoinPoint proceedingJoinPoint, @Nullable Retry retryAnnotation) throws Throwable { Method method = ((MethodSignature) proceedingJoinPoint.getSignature()).getMethod(); String methodName = method.getDeclaringClass().getName() + "#" + method.getName(); if (retryAnnotation == null) { retryAnnotation = getRetryAnnotation(proceedingJoinPoint); } if (retryAnnotation == null) { //because annotations wasn't found return proceedingJoinPoint.proceed(); } String backend = retryAnnotation.name(); io.github.resilience4j.retry.Retry retry = getOrCreateRetry(methodName, backend); Class<?> returnType = method.getReturnType(); if (StringUtils.isEmpty(retryAnnotation.fallbackMethod())) { return proceed(proceedingJoinPoint, methodName, retry, returnType); } FallbackMethod fallbackMethod = FallbackMethod.create(retryAnnotation.fallbackMethod(), method, proceedingJoinPoint.getArgs(), proceedingJoinPoint.getTarget()); return fallbackDecorators .decorate(fallbackMethod, () -> proceed(proceedingJoinPoint, methodName, retry, returnType)) .apply(); } private Object proceed(ProceedingJoinPoint proceedingJoinPoint, String methodName, io.github.resilience4j.retry.Retry retry, Class<?> returnType) throws Throwable { if (CompletionStage.class.isAssignableFrom(returnType)) { return handleJoinPointCompletableFuture(proceedingJoinPoint, retry); } if (retryAspectExtList != null && !retryAspectExtList.isEmpty()) { for (RetryAspectExt retryAspectExt : retryAspectExtList) { if (retryAspectExt.canHandleReturnType(returnType)) { return retryAspectExt.handle(proceedingJoinPoint, retry, methodName); } } } return handleDefaultJoinPoint(proceedingJoinPoint, retry); } /** * @param methodName the retry method name * @param backend the retry backend name * @return the configured retry */ private io.github.resilience4j.retry.Retry getOrCreateRetry(String methodName, String backend) { io.github.resilience4j.retry.Retry retry = retryRegistry.retry(backend); if (logger.isDebugEnabled()) { logger.debug("Created or retrieved retry '{}' with max attempts rate '{}' for method: '{}'", backend, retry.getRetryConfig().getResultPredicate(), methodName); } return retry; } /** * @param proceedingJoinPoint the aspect joint point * @return the retry annotation */ @Nullable private Retry getRetryAnnotation(ProceedingJoinPoint proceedingJoinPoint) { return AnnotationExtractor.extract(proceedingJoinPoint.getTarget().getClass(), Retry.class); } /** * @param proceedingJoinPoint the AOP logic joint point * @param retry the configured sync retry * @return the result object if any * @throws Throwable */ private Object handleDefaultJoinPoint(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.retry.Retry retry) throws Throwable { return retry.executeCheckedSupplier(proceedingJoinPoint::proceed); } /** * @param proceedingJoinPoint the AOP logic joint point * @param retry the configured async retry * @return the result object if any */ @SuppressWarnings("unchecked") private Object handleJoinPointCompletableFuture(ProceedingJoinPoint proceedingJoinPoint, io.github.resilience4j.retry.Retry retry) { return retry.executeCompletionStage(retryExecutorService, () -> { try { return (CompletionStage<Object>) proceedingJoinPoint.proceed(); } catch (Throwable throwable) { throw new CompletionException(throwable); } }); } @Override public int getOrder() { return retryConfigurationProperties.getRetryAspectOrder(); } private void cleanup() { Runtime.getRuntime().addShutdownHook(new Thread(() -> { retryExecutorService.shutdown(); try { if (!retryExecutorService.awaitTermination(5, TimeUnit.SECONDS)) { retryExecutorService.shutdownNow(); } } catch (InterruptedException e) { if (!retryExecutorService.isTerminated()) { retryExecutorService.shutdownNow(); } Thread.currentThread().interrupt(); } })); } }