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)); + } }