From ae7978ed29e1aabd56b679d56e6cceadbcca4188 Mon Sep 17 00:00:00 2001 From: Santiago Pericasgeertsen Date: Wed, 18 May 2022 16:24:22 -0400 Subject: [PATCH] New AnnotationFinder to handle transitive annotations in FT (#4216) * New AnnotationFinder to handle transitive annotations in FT used by interceptor bindings. This allows users to define new annotations that are themselves annotated with FT annotations to be used. See new tests and Issue 4171. Signed-off-by: Santiago Pericasgeertsen * Use AnnotationType and AnnotationMethod for annotation lookups instead of the corresponding Java classes. Signed-off-by: Santiago Pericasgeertsen * Additional cleanup of unused code. Signed-off-by: Santiago Pericasgeertsen * Removed BeanMethod pairing in extension. Signed-off-by: Santiago Pericasgeertsen * Check if an annotation type is an interceptor binding using CDI's bean manager when available. Signed-off-by: Santiago Pericasgeertsen --- .../faulttolerance/AnnotationFinder.java | 108 ++++++++++++ .../faulttolerance/AsynchronousAntn.java | 15 +- .../faulttolerance/BulkheadAntn.java | 14 +- .../faulttolerance/CircuitBreakerAntn.java | 15 +- .../faulttolerance/FallbackAntn.java | 14 +- .../FaultToleranceExtension.java | 109 +++++------- .../faulttolerance/MethodAntn.java | 162 +++++++++++------- .../faulttolerance/MethodIntrospector.java | 54 +++--- .../faulttolerance/RetryAntn.java | 15 +- .../faulttolerance/TimeoutAntn.java | 15 +- .../faulttolerance/AnnotationFinderTest.java | 45 +++++ .../faulttolerance/TimedRetry.java | 40 +++++ .../faulttolerance/TimedRetry2.java | 36 ++++ .../faulttolerance/TimeoutAnnotBean.java | 39 +++++ .../faulttolerance/TimeoutTest.java | 11 ++ 15 files changed, 493 insertions(+), 199 deletions(-) create mode 100644 microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AnnotationFinder.java create mode 100644 microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/AnnotationFinderTest.java create mode 100644 microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry.java create mode 100644 microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry2.java create mode 100644 microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutAnnotBean.java diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AnnotationFinder.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AnnotationFinder.java new file mode 100644 index 00000000000..4c1b6c560b2 --- /dev/null +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AnnotationFinder.java @@ -0,0 +1,108 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.microprofile.faulttolerance; + +import java.lang.annotation.Annotation; +import java.util.HashSet; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import javax.enterprise.inject.spi.BeanManager; +import javax.interceptor.InterceptorBinding; + +/** + * Searches for transitive annotations associated with interceptor bindings in + * a given Java package. Some of these operations can be expensive, so their + * results should be cached. + * + * For example, a new annotation {@code @TimedRetry} is itself annotated by + * {@code @Timeout} and {@code @Retry}, and both need to be found if a method + * uses {@code @TimedRetry} instead. + */ +public class AnnotationFinder { + + /** + * Array of package prefixes we avoid traversing while computing + * a transitive closure for an annotation. + */ + private static final String[] SKIP_PACKAGE_PREFIXES = { + "java.", + "javax.", + "jakarta.", + "org.microprofile." + }; + + private final Package pkg; + + private AnnotationFinder(Package pkg) { + this.pkg = pkg; + } + + /** + * Create a find given a Java package. + * + * @param pkg a package + * @return the finder + */ + static AnnotationFinder create(Package pkg) { + Objects.requireNonNull(pkg); + return new AnnotationFinder(pkg); + } + + Set findAnnotations(Set set, BeanManager bm) { + return findAnnotations(set, new HashSet<>(), new HashSet<>(), pkg, bm); + } + + /** + * Collects a set of transitive annotations in a package. Follows any + * annotation that has not been already seen (to avoid infinite loops) + * and is of interest. + * + * @param set set of annotations to start with + * @param result set of annotations returned + * @param seen set of annotations already processed + * @param pkg the package + * @return the result set of annotations + */ + private Set findAnnotations(Set set, Set result, + Set seen, Package pkg, BeanManager bm) { + for (Annotation a1 : set) { + Class a1Type = a1.annotationType(); + if (a1Type.getPackage().equals(pkg)) { + result.add(a1); + } else if (!seen.contains(a1) && isOfInterest(a1, bm)) { + seen.add(a1); + Set a1Set = Set.of(a1Type.getAnnotations()); + findAnnotations(a1Set, result, seen, pkg, bm); + } + } + return result; + } + + private boolean isOfInterest(Annotation a, BeanManager bm) { + if (bm != null && bm.isInterceptorBinding(a.annotationType()) + || a.annotationType().isAnnotationPresent(InterceptorBinding.class)) { + Optional matches = Stream.of(SKIP_PACKAGE_PREFIXES) + .filter(pp -> a.annotationType().getPackage().getName().startsWith(pp)) + .findAny(); + return matches.isEmpty(); + } + return false; + } +} diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AsynchronousAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AsynchronousAntn.java index b0d2f18f1ca..20445d2b578 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AsynchronousAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/AsynchronousAntn.java @@ -16,26 +16,25 @@ package io.helidon.microprofile.faulttolerance; -import java.lang.reflect.Method; import java.util.concurrent.CompletionStage; import java.util.concurrent.Future; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + import org.eclipse.microprofile.faulttolerance.Asynchronous; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class AsynchronousAntn. - */ class AsynchronousAntn extends MethodAntn implements Asynchronous { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - AsynchronousAntn(Class beanClass, Method method) { - super(beanClass, method); + AsynchronousAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/BulkheadAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/BulkheadAntn.java index 83faf08d9ff..cf3aca9d7d3 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/BulkheadAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/BulkheadAntn.java @@ -16,24 +16,22 @@ package io.helidon.microprofile.faulttolerance; -import java.lang.reflect.Method; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; import org.eclipse.microprofile.faulttolerance.Bulkhead; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class BulkheadAntn. - */ class BulkheadAntn extends MethodAntn implements Bulkhead { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - BulkheadAntn(Class beanClass, Method method) { - super(beanClass, method); + BulkheadAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/CircuitBreakerAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/CircuitBreakerAntn.java index db6c7ace928..9e9e853476c 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/CircuitBreakerAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/CircuitBreakerAntn.java @@ -16,25 +16,24 @@ package io.helidon.microprofile.faulttolerance; -import java.lang.reflect.Method; import java.time.temporal.ChronoUnit; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + import org.eclipse.microprofile.faulttolerance.CircuitBreaker; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class CircuitBreakerAntn. - */ class CircuitBreakerAntn extends MethodAntn implements CircuitBreaker { /** * Constructor. * - * @param beanClass The bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - CircuitBreakerAntn(Class beanClass, Method method) { - super(beanClass, method); + CircuitBreakerAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FallbackAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FallbackAntn.java index 0afb9fd26b3..d5e9b3f926b 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FallbackAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FallbackAntn.java @@ -18,24 +18,24 @@ import java.lang.reflect.Method; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + import org.eclipse.microprofile.faulttolerance.ExecutionContext; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.FallbackHandler; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class FallbackAntn. - */ class FallbackAntn extends MethodAntn implements Fallback { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - FallbackAntn(Class beanClass, Method method) { - super(beanClass, method); + FallbackAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FaultToleranceExtension.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FaultToleranceExtension.java index 3ee8f60103b..303684a7e7d 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FaultToleranceExtension.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/FaultToleranceExtension.java @@ -17,7 +17,6 @@ package io.helidon.microprofile.faulttolerance; import java.lang.annotation.Annotation; -import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.Arrays; import java.util.HashSet; @@ -61,7 +60,7 @@ import static javax.interceptor.Interceptor.Priority.LIBRARY_BEFORE; /** - * Class FaultToleranceExtension. + * CDI extension for Helidon's Fault Tolerance implementation. */ public class FaultToleranceExtension implements Extension { static final String MP_FT_NON_FALLBACK_ENABLED = "MP_Fault_Tolerance_NonFallback_Enabled"; @@ -72,34 +71,12 @@ public class FaultToleranceExtension implements Extension { private static boolean isFaultToleranceMetricsEnabled = true; - private Set registeredMethods; + private Set> registeredMethods; private ThreadPoolSupplier threadPoolSupplier; private ScheduledThreadPoolSupplier scheduledThreadPoolSupplier; - /** - * A bean method class that pairs a class and a method. - */ - private static class BeanMethod { - - private final Class beanClass; - private final Method method; - - BeanMethod(Class beanClass, Method method) { - this.beanClass = beanClass; - this.method = method; - } - - Class beanClass() { - return beanClass; - } - - Method method() { - return method; - } - } - /** * Class to mimic a {@link Priority} annotation for the purpose of changing * its value dynamically. @@ -207,7 +184,7 @@ void updatePriorityMaybe(@Observes final ProcessAnnotatedType event) { - registerFaultToleranceMethods(bm.createAnnotatedType(event.getBean().getBeanClass())); + registerFaultToleranceMethods(bm, bm.createAnnotatedType(event.getBean().getBeanClass())); } /** @@ -215,8 +192,8 @@ void registerFaultToleranceMethods(BeanManager bm, @Observes ProcessSyntheticBea * * @param event Event information. */ - void registerFaultToleranceMethods(@Observes ProcessManagedBean event) { - registerFaultToleranceMethods(event.getAnnotatedBeanClass()); + void registerFaultToleranceMethods(BeanManager bm, @Observes ProcessManagedBean event) { + registerFaultToleranceMethods(bm, event.getAnnotatedBeanClass()); } /** @@ -224,10 +201,10 @@ void registerFaultToleranceMethods(@Observes ProcessManagedBean event) { * * @param type Bean type. */ - private void registerFaultToleranceMethods(AnnotatedType type) { + private void registerFaultToleranceMethods(BeanManager bm, AnnotatedType type) { for (AnnotatedMethod method : type.getMethods()) { - if (isFaultToleranceMethod(type.getJavaClass(), method.getJavaMember())) { - getRegisteredMethods().add(new BeanMethod(type.getJavaClass(), method.getJavaMember())); + if (isFaultToleranceMethod(type, method, bm)) { + getRegisteredMethods().add(method); } } } @@ -239,39 +216,39 @@ private void registerFaultToleranceMethods(AnnotatedType type) { * * @param event Event information. */ - void registerMetricsAndInitExecutors(@Observes @Priority(LIBRARY_BEFORE + 10 + 5) @Initialized(ApplicationScoped.class) - Object event) { + void registerMetricsAndInitExecutors(BeanManager bm, + @Observes @Priority(LIBRARY_BEFORE + 10 + 5) + @Initialized(ApplicationScoped.class) Object event) { if (FaultToleranceMetrics.enabled()) { - getRegisteredMethods().stream().forEach(beanMethod -> { - final Method method = beanMethod.method(); - final Class beanClass = beanMethod.beanClass(); + getRegisteredMethods().forEach(annotatedMethod -> { + final AnnotatedType annotatedType = annotatedMethod.getDeclaringType(); // Counters for all methods - FaultToleranceMetrics.registerMetrics(method); + FaultToleranceMetrics.registerMetrics(annotatedMethod.getJavaMember()); // Metrics depending on the annotationSet present - if (MethodAntn.isAnnotationPresent(beanClass, method, Retry.class)) { - FaultToleranceMetrics.registerRetryMetrics(method); - new RetryAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Retry.class, bm)) { + FaultToleranceMetrics.registerRetryMetrics(annotatedMethod.getJavaMember()); + new RetryAntn(annotatedType, annotatedMethod).validate(); } - if (MethodAntn.isAnnotationPresent(beanClass, method, CircuitBreaker.class)) { - FaultToleranceMetrics.registerCircuitBreakerMetrics(method); - new CircuitBreakerAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, CircuitBreaker.class, bm)) { + FaultToleranceMetrics.registerCircuitBreakerMetrics(annotatedMethod.getJavaMember()); + new CircuitBreakerAntn(annotatedType, annotatedMethod).validate(); } - if (MethodAntn.isAnnotationPresent(beanClass, method, Timeout.class)) { - FaultToleranceMetrics.registerTimeoutMetrics(method); - new TimeoutAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Timeout.class, bm)) { + FaultToleranceMetrics.registerTimeoutMetrics(annotatedMethod.getJavaMember()); + new TimeoutAntn(annotatedType, annotatedMethod).validate(); } - if (MethodAntn.isAnnotationPresent(beanClass, method, Bulkhead.class)) { - FaultToleranceMetrics.registerBulkheadMetrics(method); - new BulkheadAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Bulkhead.class, bm)) { + FaultToleranceMetrics.registerBulkheadMetrics(annotatedMethod.getJavaMember()); + new BulkheadAntn(annotatedType, annotatedMethod).validate(); } - if (MethodAntn.isAnnotationPresent(beanClass, method, Fallback.class)) { - FaultToleranceMetrics.registerFallbackMetrics(method); - new FallbackAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Fallback.class, bm)) { + FaultToleranceMetrics.registerFallbackMetrics(annotatedMethod.getJavaMember()); + new FallbackAntn(annotatedType, annotatedMethod).validate(); } - if (MethodAntn.isAnnotationPresent(beanClass, method, Asynchronous.class)) { - new AsynchronousAntn(beanClass, method).validate(); + if (MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Asynchronous.class, bm)) { + new AsynchronousAntn(annotatedType, annotatedMethod).validate(); } }); } @@ -297,7 +274,7 @@ void registerMetricsAndInitExecutors(@Observes @Priority(LIBRARY_BEFORE + 10 + 5 * * @return The set. */ - private Set getRegisteredMethods() { + private Set> getRegisteredMethods() { if (registeredMethods == null) { registeredMethods = new CopyOnWriteArraySet<>(); } @@ -322,17 +299,19 @@ static Class getRealClass(Object object) { * Determines if a method has any fault tolerance annotationSet. Only {@code @Fallback} * is considered if fault tolerance is disabled. * - * @param beanClass The bean. - * @param method The method to check. + * @param annotatedType The annotated type. + * @param annotatedMethod The method to check. * @return Outcome of test. */ - static boolean isFaultToleranceMethod(Class beanClass, Method method) { - return MethodAntn.isAnnotationPresent(beanClass, method, Retry.class) - || MethodAntn.isAnnotationPresent(beanClass, method, CircuitBreaker.class) - || MethodAntn.isAnnotationPresent(beanClass, method, Bulkhead.class) - || MethodAntn.isAnnotationPresent(beanClass, method, Timeout.class) - || MethodAntn.isAnnotationPresent(beanClass, method, Asynchronous.class) - || MethodAntn.isAnnotationPresent(beanClass, method, Fallback.class); + static boolean isFaultToleranceMethod(AnnotatedType annotatedType, + AnnotatedMethod annotatedMethod, + BeanManager bm) { + return MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Retry.class, bm) + || MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, CircuitBreaker.class, bm) + || MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Bulkhead.class, bm) + || MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Timeout.class, bm) + || MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Asynchronous.class, bm) + || MethodAntn.isAnnotationPresent(annotatedType, annotatedMethod, Fallback.class, bm); } /** @@ -420,7 +399,7 @@ public R getAnnotation(Class annotationType) { Optional optional = annotationSet.stream() .filter(a -> annotationType.isAssignableFrom(a.annotationType())) .findFirst(); - return optional.isPresent() ? (R) optional.get() : null; + return (R) optional.orElse(null); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodAntn.java index db40b53c10c..4a27f36ad58 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodAntn.java @@ -20,21 +20,30 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.StringTokenizer; import java.util.logging.Level; import java.util.logging.Logger; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.BeanManager; + +import org.eclipse.microprofile.faulttolerance.Retry; + import static io.helidon.microprofile.faulttolerance.FaultToleranceParameter.getParameter; /** - * Class MethodAntn. + * Base class for all method annotations. */ abstract class MethodAntn { private static final Logger LOGGER = Logger.getLogger(MethodAntn.class.getName()); - private final Method method; + private static final AnnotationFinder ANNOTATION_FINDER = AnnotationFinder.create(Retry.class.getPackage()); + + private final AnnotatedType annotatedType; - private final Class beanClass; + private final AnnotatedMethod annotatedMethod; enum MatchingType { METHOD, CLASS @@ -69,81 +78,43 @@ public A getAnnotation() { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType Annotated type. + * @param annotatedMethod Annotated method. */ - MethodAntn(Class beanClass, Method method) { - this.beanClass = beanClass; - this.method = method; + MethodAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + this.annotatedType = annotatedType; + this.annotatedMethod = annotatedMethod; } Method method() { - return method; - } - - Class beanClass() { - return beanClass; + return annotatedMethod.getJavaMember(); } /** - * Look up an annotation on the method. + * Look up an annotation on the method using instance variables. * * @param annotClass Annotation class. * @param Annotation class type param. * @return A lookup result. */ public final LookupResult lookupAnnotation(Class annotClass) { - return lookupAnnotation(beanClass, method, annotClass); - } - - /** - * Returns underlying annotation and info as to how it was found. - * - * @param beanClass The bean class. - * @param method The method. - * @param annotClass The annotation class. - * @param Annotation type. - * @return The lookup result or {@code null}. - */ - static LookupResult lookupAnnotation(Class beanClass, Method method, - Class annotClass) { - A annotation = method.getAnnotation(annotClass); - if (annotation != null) { - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Found annotation '" + annotClass.getName() - + "' method '" + method.getName() + "'"); - } - return new LookupResult<>(MatchingType.METHOD, annotation); - } - annotation = beanClass.getAnnotation(annotClass); - if (annotation != null) { - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Found annotation '" + annotClass.getName() - + "' class '" + method.getDeclaringClass().getName() + "'"); - } - return new LookupResult<>(MatchingType.CLASS, annotation); - } - annotation = method.getDeclaringClass().getAnnotation(annotClass); - if (annotation != null) { - if (LOGGER.isLoggable(Level.FINE)) { - LOGGER.fine("Found annotation '" + method.getDeclaringClass().getName() - + "' class '" + method.getDeclaringClass().getName() + "'"); - } - return new LookupResult<>(MatchingType.CLASS, annotation); - } - return null; + return lookupAnnotation(annotatedType, annotatedMethod, annotClass, null); } /** * Finds if an annotation is present on a method or its class. * - * @param beanClass The bean class. - * @param method Method to check. + * @param annotatedType The annotated type. + * @param annotatedMethod Method to check. * @param annotClass Annotation class. + * @param beanManager CDI's bean manager or {@code null} if not available. * @return Outcome of test. */ - static boolean isAnnotationPresent(Class beanClass, Method method, Class annotClass) { - return lookupAnnotation(beanClass, method, annotClass) != null; + static boolean isAnnotationPresent(AnnotatedType annotatedType, + AnnotatedMethod annotatedMethod, + Class annotClass, + BeanManager beanManager) { + return lookupAnnotation(annotatedType, annotatedMethod, annotClass, beanManager) != null; } /** @@ -176,13 +147,13 @@ protected String getParamOverride(String parameter, MatchingType type) { // Check property depending on matching type if (type == MatchingType.METHOD) { - value = getParameter(method.getDeclaringClass().getName(), method.getName(), + value = getParameter(method().getDeclaringClass().getName(), method().getName(), annotationType, parameter); if (value != null) { return value; } } else if (type == MatchingType.CLASS) { - value = getParameter(method.getDeclaringClass().getName(), annotationType, parameter); + value = getParameter(method().getDeclaringClass().getName(), annotationType, parameter); if (value != null) { return value; } @@ -222,4 +193,77 @@ static Class[] parseThrowableArray(String array) { } return (Class[]) result.toArray(new Class[0]); } + + /** + * Returns underlying annotation and info as to how it was found. If more than one + * instance of this annotation exist (after computing the transitive closure), + * one will be returned in an undefined manner. + * + * @param type The annotated type. + * @param method The annotated method. + * @param annotClass The annotation class. + * @param Annotation type. + * @return The lookup result or {@code null}. + */ + @SuppressWarnings("unchecked") + static LookupResult lookupAnnotation(AnnotatedType type, + AnnotatedMethod method, + Class annotClass, + BeanManager beanManager) { + A annotation = (A) getMethodAnnotation(method, annotClass, beanManager); + if (annotation != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Found annotation '" + annotClass.getName() + + "' method '" + method.getJavaMember().getName() + "'"); + } + return new LookupResult<>(MatchingType.METHOD, annotation); + } + annotation = (A) getClassAnnotation(type, annotClass, beanManager); + if (annotation != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Found annotation '" + annotClass.getName() + + "' class '" + method.getJavaMember().getDeclaringClass().getName() + "'"); + } + return new LookupResult<>(MatchingType.CLASS, annotation); + } + annotation = (A) getClassAnnotation(method.getJavaMember().getDeclaringClass(), annotClass, beanManager); + if (annotation != null) { + if (LOGGER.isLoggable(Level.FINE)) { + LOGGER.fine("Found annotation '" + annotClass.getName() + + "' class '" + method.getJavaMember().getDeclaringClass().getName() + "'"); + } + return new LookupResult<>(MatchingType.CLASS, annotation); + } + return null; + } + + private static Annotation getMethodAnnotation(AnnotatedMethod m, + Class annotClass, + BeanManager beanManager) { + Set set = ANNOTATION_FINDER.findAnnotations(m.getAnnotations(), beanManager); + return set.stream() + .filter(a -> a.annotationType().equals(annotClass)) + .findFirst() + .orElse(null); + } + + private static Annotation getClassAnnotation(Class c, + Class annotClass, + BeanManager beanManager) { + Set set = ANNOTATION_FINDER.findAnnotations(Set.of(c.getAnnotations()), beanManager); + return set.stream() + .filter(a -> a.annotationType().equals(annotClass)) + .findFirst() + .orElse(null); + } + + private static Annotation getClassAnnotation(AnnotatedType type, + Class annotClass, + BeanManager beanManager) { + Set set = ANNOTATION_FINDER.findAnnotations(type.getAnnotations(), beanManager); + return set.stream() + .filter(a -> a.annotationType().equals(annotClass)) + .findFirst() + .orElse(null); + } } diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodIntrospector.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodIntrospector.java index 9ad99c8001a..92332ba1609 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodIntrospector.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/MethodIntrospector.java @@ -18,6 +18,12 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import java.util.Optional; + +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; +import javax.enterprise.inject.spi.BeanManager; +import javax.enterprise.inject.spi.CDI; import io.helidon.microprofile.faulttolerance.MethodAntn.LookupResult; @@ -31,14 +37,11 @@ import static io.helidon.microprofile.faulttolerance.FaultToleranceParameter.getParameter; import static io.helidon.microprofile.faulttolerance.MethodAntn.lookupAnnotation; -/** - * Class MethodIntrospector. - */ class MethodIntrospector { - private final Method method; + private final AnnotatedType annotatedType; - private final Class beanClass; + private final AnnotatedMethod annotatedMethod; private final Retry retry; @@ -55,30 +58,23 @@ class MethodIntrospector { * * @param method The method to introspect. */ + @SuppressWarnings("unchecked") MethodIntrospector(Class beanClass, Method method) { - this.beanClass = beanClass; - this.method = method; - - this.retry = isAnnotationEnabled(Retry.class) ? new RetryAntn(beanClass, method) : null; + BeanManager bm = CDI.current().getBeanManager(); + this.annotatedType = bm.createAnnotatedType(beanClass); + Optional> annotatedMethodOptional = + (Optional>) annotatedType.getMethods() + .stream() + .filter(am -> am.getJavaMember().equals(method)) + .findFirst(); + this.annotatedMethod = annotatedMethodOptional.orElseThrow(); + + this.retry = isAnnotationEnabled(Retry.class) ? new RetryAntn(annotatedType, annotatedMethod) : null; this.circuitBreaker = isAnnotationEnabled(CircuitBreaker.class) - ? new CircuitBreakerAntn(beanClass, method) : null; - this.timeout = isAnnotationEnabled(Timeout.class) ? new TimeoutAntn(beanClass, method) : null; - this.bulkhead = isAnnotationEnabled(Bulkhead.class) ? new BulkheadAntn(beanClass, method) : null; - this.fallback = isAnnotationEnabled(Fallback.class) ? new FallbackAntn(beanClass, method) : null; - } - - Method method() { - return method; - } - - /** - * Checks if {@code clazz} is assignable from the method's return type. - * - * @param clazz The class. - * @return Outcome of test. - */ - boolean isReturnType(Class clazz) { - return clazz.isAssignableFrom(method.getReturnType()); + ? new CircuitBreakerAntn(annotatedType, annotatedMethod) : null; + this.timeout = isAnnotationEnabled(Timeout.class) ? new TimeoutAntn(annotatedType, annotatedMethod) : null; + this.bulkhead = isAnnotationEnabled(Bulkhead.class) ? new BulkheadAntn(annotatedType, annotatedMethod) : null; + this.fallback = isAnnotationEnabled(Fallback.class) ? new FallbackAntn(annotatedType, annotatedMethod) : null; } /** @@ -157,7 +153,8 @@ Bulkhead getBulkhead() { * @return Outcome of test. */ private boolean isAnnotationEnabled(Class clazz) { - LookupResult lookupResult = lookupAnnotation(beanClass, method, clazz); + BeanManager bm = CDI.current().getBeanManager(); + LookupResult lookupResult = lookupAnnotation(annotatedType, annotatedMethod, clazz, bm); if (lookupResult == null) { return false; // not present } @@ -166,6 +163,7 @@ private boolean isAnnotationEnabled(Class clazz) { final String annotationType = clazz.getSimpleName(); // Check if property defined at method level + Method method = annotatedMethod.getJavaMember(); value = getParameter(method.getDeclaringClass().getName(), method.getName(), annotationType, "enabled"); if (value != null) { diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/RetryAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/RetryAntn.java index 6cc987136aa..9f95342dfc7 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/RetryAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/RetryAntn.java @@ -16,25 +16,24 @@ package io.helidon.microprofile.faulttolerance; -import java.lang.reflect.Method; import java.time.temporal.ChronoUnit; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class RetryAntn. - */ class RetryAntn extends MethodAntn implements Retry { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - RetryAntn(Class beanClass, Method method) { - super(beanClass, method); + RetryAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/TimeoutAntn.java b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/TimeoutAntn.java index 2c80ca08371..6e09d01da8b 100644 --- a/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/TimeoutAntn.java +++ b/microprofile/fault-tolerance/src/main/java/io/helidon/microprofile/faulttolerance/TimeoutAntn.java @@ -16,25 +16,24 @@ package io.helidon.microprofile.faulttolerance; -import java.lang.reflect.Method; import java.time.temporal.ChronoUnit; +import javax.enterprise.inject.spi.AnnotatedMethod; +import javax.enterprise.inject.spi.AnnotatedType; + import org.eclipse.microprofile.faulttolerance.Timeout; import org.eclipse.microprofile.faulttolerance.exceptions.FaultToleranceDefinitionException; -/** - * Class TimeoutAntn. - */ class TimeoutAntn extends MethodAntn implements Timeout { /** * Constructor. * - * @param beanClass Bean class. - * @param method The method. + * @param annotatedType The annotated type. + * @param annotatedMethod The annotated method. */ - TimeoutAntn(Class beanClass, Method method) { - super(beanClass, method); + TimeoutAntn(AnnotatedType annotatedType, AnnotatedMethod annotatedMethod) { + super(annotatedType, annotatedMethod); } @Override diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/AnnotationFinderTest.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/AnnotationFinderTest.java new file mode 100644 index 00000000000..8b20224c873 --- /dev/null +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/AnnotationFinderTest.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.microprofile.faulttolerance; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Set; +import java.util.stream.Collectors; + +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; +import org.junit.jupiter.api.Test; + +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.MatcherAssert.assertThat; + +class AnnotationFinderTest { + + @Test + void testAnnotationFinder() throws Exception { + Package pkg = Retry.class.getPackage(); + Method m = TimeoutAnnotBean.class.getMethod("timedRetry"); + AnnotationFinder finder = AnnotationFinder.create(pkg); + Set> transitive = finder.findAnnotations(Set.of(m.getAnnotations()), null) + .stream() + .map(Annotation::annotationType) + .collect(Collectors.toSet()); + assertThat(transitive, is(Set.of(CircuitBreaker.class, Retry.class, Timeout.class))); + } +} diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry.java new file mode 100644 index 00000000000..6f5ddf05102 --- /dev/null +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry.java @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.microprofile.faulttolerance; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.time.temporal.ChronoUnit; + +import org.eclipse.microprofile.faulttolerance.Retry; +import org.eclipse.microprofile.faulttolerance.Timeout; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@Retry(maxRetries = 2) +@Timeout(value = 1000, unit = ChronoUnit.MILLIS) +@TimedRetry2 // Need to follow this annotation +@InterceptorBinding +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Inherited +public @interface TimedRetry { +} diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry2.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry2.java new file mode 100644 index 00000000000..e77c1efc788 --- /dev/null +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimedRetry2.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.microprofile.faulttolerance; + +import javax.interceptor.InterceptorBinding; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import org.eclipse.microprofile.faulttolerance.CircuitBreaker; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@CircuitBreaker +@InterceptorBinding +@Target({TYPE, METHOD}) +@Retention(RUNTIME) +@Inherited +public @interface TimedRetry2 { +} diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutAnnotBean.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutAnnotBean.java new file mode 100644 index 00000000000..0693619afc4 --- /dev/null +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutAnnotBean.java @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2022 Oracle and/or its affiliates. + * + * 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.helidon.microprofile.faulttolerance; + +import java.util.concurrent.atomic.AtomicInteger; + +class TimeoutAnnotBean { + + private AtomicInteger invocations = new AtomicInteger(0); + + int getInvocations() { + return invocations.get(); + } + + void reset() { + invocations.set(0); + } + + @TimedRetry + public void timedRetry() throws InterruptedException { + invocations.getAndIncrement(); + FaultToleranceTest.printStatus("TimeoutStereotypeBean::timedRetry()", "failure"); + Thread.sleep(1500); + } +} diff --git a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutTest.java b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutTest.java index 8d288de341e..3b96a87db17 100644 --- a/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutTest.java +++ b/microprofile/fault-tolerance/src/test/java/io/helidon/microprofile/faulttolerance/TimeoutTest.java @@ -35,6 +35,7 @@ */ @AddBean(TimeoutBean.class) @AddBean(TimeoutNoRetryBean.class) +@AddBean(TimeoutAnnotBean.class) class TimeoutTest extends FaultToleranceTest { @Inject @@ -43,9 +44,13 @@ class TimeoutTest extends FaultToleranceTest { @Inject private TimeoutNoRetryBean timeoutNoRetryBean; + @Inject + private TimeoutAnnotBean timeoutAnnotBean; + @Override void reset() { timeoutBean.reset(); + timeoutAnnotBean.reset(); } @Test @@ -103,4 +108,10 @@ void testForceTimeoutLoop() { assertThat(System.currentTimeMillis() - start, is(greaterThanOrEqualTo(2000L))); } } + + @Test + void testForceTimeoutAnnot() { + assertThrows(TimeoutException.class, timeoutAnnotBean::timedRetry); + assertThat(timeoutAnnotBean.getInvocations(), is(3)); + } }