diff --git a/bom/application/pom.xml b/bom/application/pom.xml
index 3969dd42139d8b..3f71032ed71dba 100644
--- a/bom/application/pom.xml
+++ b/bom/application/pom.xml
@@ -57,7 +57,7 @@
4.0.0
3.12.0
2.10.0
- 6.4.0
+ 6.4.2-SNAPSHOT
4.6.0
2.1.2
1.0.13
diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceMethodSearch.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceMethodSearch.java
new file mode 100644
index 00000000000000..69f89e0cbe26e4
--- /dev/null
+++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceMethodSearch.java
@@ -0,0 +1,408 @@
+package io.quarkus.smallrye.faulttolerance.deployment;
+
+import java.lang.reflect.Modifier;
+import java.util.ArrayDeque;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Deque;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.ClassType;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+import org.jboss.jandex.ParameterizedType;
+import org.jboss.jandex.Type;
+import org.jboss.jandex.TypeVariable;
+import org.jboss.jandex.VoidType;
+import org.jboss.jandex.WildcardType;
+
+import io.quarkus.arc.processor.AssignabilityCheck;
+import io.quarkus.arc.processor.KotlinDotNames;
+
+// copy of `io.smallrye.faulttolerance.config.SecurityActions` and translation from reflection to Jandex
+final class FaultToleranceMethodSearch {
+ private final IndexView index;
+ private final AssignabilityCheck assignability;
+
+ FaultToleranceMethodSearch(IndexView index) {
+ this.index = index;
+ this.assignability = new AssignabilityCheck(index, null);
+ }
+
+ /**
+ * Finds a fallback method for given guarded method. If the guarded method is present on given {@code beanClass}
+ * and is actually declared by given {@code declaringClass} and has given {@code parameterTypes} and {@code returnType},
+ * then a fallback method of given {@code name}, with parameter types and return type matching the parameter types
+ * and return type of the guarded method, is searched for on the {@code beanClass} and its superclasses and
+ * superinterfaces, according to the specification rules. Returns {@code null} if no matching fallback method exists.
+ *
+ * @param beanClass the class of the bean that has the guarded method
+ * @param declaringClass the class that actually declares the guarded method (can be a supertype of bean class)
+ * @param name name of the fallback method
+ * @param parameterTypes parameter types of the guarded method
+ * @param returnType return type of the guarded method
+ * @return the fallback method or {@code null} if none exists
+ */
+ MethodInfo findFallbackMethod(ClassInfo beanClass, ClassInfo declaringClass,
+ String name, Type[] parameterTypes, Type returnType) {
+
+ Set result = findMethod(beanClass, declaringClass, name, parameterTypes, returnType, false);
+ return result.isEmpty() ? null : result.iterator().next();
+ }
+
+ /**
+ * Finds a set of fallback methods with exception parameter for given guarded method. If the guarded method
+ * is present on given {@code beanClass} and is actually declared by given {@code declaringClass} and has given
+ * {@code parameterTypes} and {@code returnType}, then fallback methods of given {@code name}, with parameter types
+ * and return type matching the parameter types and return type of the guarded method, and with one additional
+ * parameter assignable to {@code Throwable} at the end of parameter list, is searched for on the {@code beanClass}
+ * and its superclasses and superinterfaces, according to the specification rules. Returns an empty set if no
+ * matching fallback method exists.
+ *
+ * @param beanClass the class of the bean that has the guarded method
+ * @param declaringClass the class that actually declares the guarded method (can be a supertype of bean class)
+ * @param name name of the fallback method
+ * @param parameterTypes parameter types of the guarded method
+ * @param returnType return type of the guarded method
+ * @return the fallback method or an empty set if none exists
+ */
+ Set findFallbackMethodsWithExceptionParameter(ClassInfo beanClass, ClassInfo declaringClass,
+ String name, Type[] parameterTypes, Type returnType) {
+ return findMethod(beanClass, declaringClass, name, parameterTypes, returnType, true);
+ }
+
+ /**
+ * Finds a before retry method for given guarded method. If the guarded method is present on given {@code beanClass}
+ * and is actually declared by given {@code declaringClass}, then a before retry method of given {@code name},
+ * with no parameters and return type of {@code void}, is searched for on the {@code beanClass} and its superclasses and
+ * superinterfaces, according to the specification rules. Returns {@code null} if no matching before retry method exists.
+ *
+ * @param beanClass the class of the bean that has the guarded method
+ * @param declaringClass the class that actually declares the guarded method (can be a supertype of bean class)
+ * @param name name of the before retry method
+ * @return the before retry method or {@code null} if none exists
+ */
+ MethodInfo findBeforeRetryMethod(ClassInfo beanClass, ClassInfo declaringClass, String name) {
+ Set result = findMethod(beanClass, declaringClass, name, new Type[0], VoidType.VOID, false);
+ return result.isEmpty() ? null : result.iterator().next();
+ }
+
+ private Set findMethod(ClassInfo beanClass, ClassInfo declaringClass, String name,
+ Type[] expectedParameterTypes, Type expectedReturnType, boolean expectedExceptionParameter) {
+
+ Set result = new HashSet<>();
+
+ TypeMapping expectedMapping = TypeMapping.createFor(beanClass, declaringClass, index);
+
+ // if we find a matching method on the bean class or one of its superclasses or superinterfaces,
+ // then we have to check that the method is either identical to or an override of a method that:
+ // - is declared on a class which is a superclass of the declaring class, or
+ // - is declared on an interface which implemented by the declaring class
+ //
+ // this is to satisfy the specification, which says: fallback method must be on the same class, a superclass
+ // or an implemented interface of the class which declares the annotated method
+ //
+ // we fake this by checking that the matching method has the same name as one of the method declared on
+ // the declaring class or any of its superclasses or any of its implemented interfaces (this is actually
+ // quite precise, the only false positive would occur in presence of overloads)
+ Set declaredMethodNames = findDeclaredMethodNames(declaringClass);
+
+ Deque worklist = new ArrayDeque<>();
+ {
+ // add all superclasses first, so that they're preferred
+ // interfaces are added during worklist iteration
+ ClassInfo clazz = beanClass;
+ TypeMapping typeMapping = new TypeMapping();
+ worklist.add(new ClassWithTypeMapping(clazz, typeMapping));
+ while (clazz.superName() != null) {
+ ClassInfo superclass = index.getClassByName(clazz.superName());
+ if (superclass == null) {
+ throw new IllegalArgumentException("Class not in index: " + clazz.superName());
+ }
+ Type genericSuperclass = clazz.superClassType();
+ typeMapping = typeMapping.getDirectSupertypeMapping(superclass, genericSuperclass);
+ worklist.add(new ClassWithTypeMapping(superclass, typeMapping));
+
+ clazz = superclass;
+ }
+ }
+ while (!worklist.isEmpty()) {
+ ClassWithTypeMapping classWithTypeMapping = worklist.removeFirst();
+ ClassInfo clazz = classWithTypeMapping.clazz;
+ TypeMapping actualMapping = classWithTypeMapping.typeMapping;
+
+ Set methods = getMethodsFromClass(clazz, name, expectedParameterTypes, expectedReturnType,
+ expectedExceptionParameter, declaringClass, actualMapping, expectedMapping);
+ for (MethodInfo method : methods) {
+ if (declaredMethodNames.contains(method.name())) {
+ result.add(method);
+ if (!expectedExceptionParameter) {
+ return result;
+ }
+ }
+ }
+
+ List interfaces = clazz.interfaceNames();
+ for (int i = 0; i < interfaces.size(); i++) {
+ ClassInfo iface = index.getClassByName(interfaces.get(i));
+ if (iface == null) {
+ throw new IllegalArgumentException("Class not in index: " + interfaces.get(i));
+ }
+ Type genericIface = clazz.interfaceTypes().get(i);
+ worklist.add(new ClassWithTypeMapping(iface,
+ actualMapping.getDirectSupertypeMapping(iface, genericIface)));
+ }
+ }
+
+ return result;
+ }
+
+ private Set findDeclaredMethodNames(ClassInfo declaringClass) {
+ Set result = new HashSet<>();
+
+ Deque worklist = new ArrayDeque<>();
+ worklist.add(declaringClass);
+ while (!worklist.isEmpty()) {
+ ClassInfo clazz = worklist.removeFirst();
+ for (MethodInfo m : clazz.methods()) {
+ result.add(m.name());
+ }
+
+ if (clazz.superName() != null) {
+ ClassInfo superClass = index.getClassByName(clazz.superName());
+ if (superClass != null) {
+ worklist.add(superClass);
+ }
+ }
+ for (DotName interfaceName : clazz.interfaceNames()) {
+ ClassInfo iface = index.getClassByName(interfaceName);
+ if (iface != null) {
+ worklist.add(iface);
+ }
+ }
+ }
+
+ return result;
+ }
+
+ /**
+ * Returns all methods that:
+ *
+ * - are declared directly on given {@code classToSearch},
+ * - have given {@code name},
+ * - have matching {@code parameterTypes},
+ * - have matching {@code returnType},
+ * - have an additional {@code exceptionParameter} if required,
+ * - are accessible from given {@code guardedMethodDeclaringClass}.
+ *
+ */
+ private Set getMethodsFromClass(ClassInfo classToSearch, String name, Type[] parameterTypes,
+ Type returnType, boolean exceptionParameter, ClassInfo guardedMethodDeclaringClass,
+ TypeMapping actualMapping, TypeMapping expectedMapping) {
+ Set set = new HashSet<>();
+ for (MethodInfo method : classToSearch.methods()) {
+ if (method.name().equals(name)
+ && isAccessibleFrom(method, guardedMethodDeclaringClass)
+ && signaturesMatch(method, parameterTypes, returnType, exceptionParameter,
+ actualMapping, expectedMapping)) {
+ set.add(method);
+ }
+ }
+ return set;
+ }
+
+ private boolean isAccessibleFrom(MethodInfo method, ClassInfo guardedMethodDeclaringClass) {
+ if (Modifier.isPublic(method.flags()) || Modifier.isProtected(method.flags())) {
+ return true;
+ }
+ if (Modifier.isPrivate(method.flags())) {
+ return method.declaringClass() == guardedMethodDeclaringClass;
+ }
+ // not public, not protected and not private => default
+ // accessible only if in the same package
+ return method.declaringClass().name().packagePrefixName()
+ .equals(guardedMethodDeclaringClass.name().packagePrefixName());
+ }
+
+ private boolean signaturesMatch(MethodInfo method, Type[] expectedParameterTypes, Type expectedReturnType,
+ boolean expectedExceptionParameter, TypeMapping actualMapping, TypeMapping expectedMapping) {
+ int expectedParameters = expectedParameterTypes.length;
+ if (expectedExceptionParameter) {
+ // need to figure this out _before_ expanding the `expectedParameterTypes` array
+ boolean kotlinSuspendingFunction = isKotlinSuspendingFunction(expectedParameterTypes);
+ // adjust `expectedParameterTypes` so that there's one more element on the position
+ // where the exception parameter should be, and the value on that position is `null`
+ expectedParameterTypes = Arrays.copyOfRange(expectedParameterTypes, 0, expectedParameters + 1);
+ if (kotlinSuspendingFunction) {
+ expectedParameterTypes[expectedParameters] = expectedParameterTypes[expectedParameters - 1];
+ expectedParameterTypes[expectedParameters - 1] = null;
+ }
+ expectedParameters++;
+ }
+
+ List methodParams = method.parameterTypes();
+ if (expectedParameters != methodParams.size()) {
+ return false;
+ }
+
+ for (int i = 0; i < expectedParameters; i++) {
+ Type methodParam = methodParams.get(i);
+ Type expectedParamType = expectedParameterTypes[i];
+ if (expectedParamType != null) {
+ if (!typeMatches(methodParam, expectedParamType, actualMapping, expectedMapping)) {
+ return false;
+ }
+ } else { // exception parameter
+ boolean isThrowable = methodParam.kind() == Type.Kind.CLASS
+ && assignability.isAssignableFrom(ClassType.create(Throwable.class), methodParam);
+ if (!isThrowable) {
+ return false;
+ }
+ }
+ }
+
+ if (!typeMatches(method.returnType(), expectedReturnType, actualMapping, expectedMapping)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private static boolean isKotlinSuspendingFunction(Type[] parameterTypes) {
+ int params = parameterTypes.length;
+ if (params > 0) {
+ return parameterTypes[params - 1].name().equals(KotlinDotNames.CONTINUATION);
+ }
+ return false;
+ }
+
+ private boolean typeMatches(Type actualType, Type expectedType,
+ TypeMapping actualMapping, TypeMapping expectedMapping) {
+ actualType = actualMapping.map(actualType);
+ expectedType = expectedMapping.map(expectedType);
+
+ if (actualType.kind() == Type.Kind.CLASS
+ || actualType.kind() == Type.Kind.PRIMITIVE
+ || actualType.kind() == Type.Kind.VOID) {
+ return expectedType.equals(actualType);
+ } else if (actualType.kind() == Type.Kind.ARRAY && expectedType.kind() == Type.Kind.ARRAY) {
+ return typeMatches(actualType.asArrayType().componentType(), expectedType.asArrayType().componentType(),
+ actualMapping, expectedMapping);
+ } else if (actualType.kind() == Type.Kind.PARAMETERIZED_TYPE && expectedType.kind() == Type.Kind.PARAMETERIZED_TYPE) {
+ return parameterizedTypeMatches(actualType.asParameterizedType(), expectedType.asParameterizedType(),
+ actualMapping, expectedMapping);
+ } else if (actualType.kind() == Type.Kind.WILDCARD_TYPE && expectedType.kind() == Type.Kind.WILDCARD_TYPE) {
+ return wildcardTypeMatches(actualType.asWildcardType(), expectedType.asWildcardType(),
+ actualMapping, expectedMapping);
+ } else {
+ return false;
+ }
+ }
+
+ private boolean wildcardTypeMatches(WildcardType actualType, WildcardType expectedType,
+ TypeMapping actualMapping, TypeMapping expectedMapping) {
+ Type actualLowerBound = actualType.superBound();
+ Type expectedLowerBound = expectedType.superBound();
+ boolean lowerBoundsMatch = (actualLowerBound == null && expectedLowerBound == null)
+ || (actualLowerBound != null && expectedLowerBound != null
+ && typeMatches(actualLowerBound, expectedLowerBound, actualMapping, expectedMapping));
+ boolean upperBoundsMatch = typeMatches(actualType.extendsBound(), expectedType.extendsBound(),
+ actualMapping, expectedMapping);
+ return lowerBoundsMatch && upperBoundsMatch;
+ }
+
+ private boolean parameterizedTypeMatches(ParameterizedType actualType, ParameterizedType expectedType,
+ TypeMapping actualMapping, TypeMapping expectedMapping) {
+ boolean genericClassMatch = typeMatches(ClassType.create(actualType.name()), ClassType.create(expectedType.name()),
+ actualMapping, expectedMapping);
+ boolean typeArgumentsMatch = typeListMatches(actualType.arguments(), expectedType.arguments(),
+ actualMapping, expectedMapping);
+ return genericClassMatch && typeArgumentsMatch;
+ }
+
+ private boolean typeListMatches(List actualTypes, List expectedTypes,
+ TypeMapping actualMapping, TypeMapping expectedMapping) {
+ if (actualTypes.size() != expectedTypes.size()) {
+ return false;
+ }
+ for (int i = 0; i < actualTypes.size(); i++) {
+ if (!typeMatches(actualTypes.get(i), expectedTypes.get(i), actualMapping, expectedMapping)) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ private record ClassWithTypeMapping(ClassInfo clazz, TypeMapping typeMapping) {
+ }
+
+ private record TypeMapping(Map map) {
+ private TypeMapping() {
+ this(Collections.emptyMap());
+ }
+
+ /**
+ * Bean class can be a subclass of the class that declares the guarded method.
+ * This method returns a mapping of the type parameters of the method's declaring class
+ * to the type arguments provided on the bean class or any class between it and the declaring class.
+ *
+ * @param beanClass class of the bean which has the guarded method
+ * @param declaringClass class that actually declares the guarded method
+ * @param index index to use for locating superclasses
+ * @return type mapping
+ */
+ private static TypeMapping createFor(ClassInfo beanClass, ClassInfo declaringClass, IndexView index) {
+ TypeMapping result = new TypeMapping();
+ if (beanClass == declaringClass) {
+ return result;
+ }
+
+ ClassInfo current = beanClass;
+ while (current != declaringClass && current != null) {
+ if (current.superName() == null) {
+ break;
+ }
+ ClassInfo superClass = index.getClassByName(current.superName());
+ if (superClass == null) {
+ throw new IllegalArgumentException("Class not in index: " + current.superName());
+ }
+ result = result.getDirectSupertypeMapping(superClass, current.superClassType());
+ current = superClass;
+ }
+
+ return result;
+ }
+
+ private Type map(Type type) {
+ Type result = map.get(type);
+ return result != null ? result : type;
+ }
+
+ private TypeMapping getDirectSupertypeMapping(ClassInfo supertype, Type genericSupertype) {
+ List typeParameters = supertype.typeParameters();
+ List typeArguments = genericSupertype.kind() == Type.Kind.PARAMETERIZED_TYPE
+ ? genericSupertype.asParameterizedType().arguments()
+ : Collections.emptyList();
+
+ Map result = new HashMap<>();
+
+ for (int i = 0; i < typeArguments.size(); i++) {
+ Type typeArgument = typeArguments.get(i);
+ if (typeArgument.kind() == Type.Kind.CLASS) {
+ result.put(typeParameters.get(i), typeArgument);
+ } else {
+ Type type = map.get(typeArgument);
+ result.put(typeParameters.get(i), type != null ? type : typeArgument);
+ }
+ }
+
+ return new TypeMapping(result);
+ }
+ }
+}
diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java
index 96d96c90d853b8..c68cd65bce9f20 100644
--- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java
+++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceScanner.java
@@ -1,10 +1,14 @@
package io.quarkus.smallrye.faulttolerance.deployment;
import java.lang.annotation.Annotation;
+import java.util.ArrayList;
+import java.util.Collection;
import java.util.HashSet;
+import java.util.List;
import java.util.Set;
import java.util.function.Consumer;
+import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.faulttolerance.Asynchronous;
import org.eclipse.microprofile.faulttolerance.Bulkhead;
import org.eclipse.microprofile.faulttolerance.CircuitBreaker;
@@ -19,7 +23,9 @@
import org.jboss.jandex.Type;
import io.quarkus.arc.processor.AnnotationStore;
+import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.builditem.AnnotationProxyBuildItem;
+import io.quarkus.deployment.builditem.nativeimage.ReflectiveMethodBuildItem;
import io.quarkus.deployment.recording.RecorderContext;
import io.quarkus.gizmo.ClassOutput;
import io.smallrye.common.annotation.Blocking;
@@ -45,13 +51,19 @@ final class FaultToleranceScanner {
private final RecorderContext recorderContext;
+ private final BuildProducer reflectiveMethod;
+
+ private final FaultToleranceMethodSearch methodSearch;
+
FaultToleranceScanner(IndexView index, AnnotationStore annotationStore, AnnotationProxyBuildItem proxy,
- ClassOutput output, RecorderContext recorderContext) {
+ ClassOutput output, RecorderContext recorderContext, BuildProducer reflectiveMethod) {
this.index = index;
this.annotationStore = annotationStore;
this.proxy = proxy;
this.output = output;
this.recorderContext = recorderContext;
+ this.reflectiveMethod = reflectiveMethod;
+ this.methodSearch = new FaultToleranceMethodSearch(index);
}
boolean hasFTAnnotations(ClassInfo clazz) {
@@ -141,6 +153,8 @@ FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo
result.annotationsPresentDirectly = annotationsPresentDirectly;
+ searchForMethods(result, beanClass, method, annotationsPresentDirectly);
+
return result;
}
@@ -169,6 +183,92 @@ private A getAnnotation(Class annotationType, MethodIn
return getAnnotationFromClass(annotationType, beanClass);
}
+ // ---
+
+ private void searchForMethods(FaultToleranceMethod result, ClassInfo beanClass, MethodInfo method,
+ Set> annotationsPresentDirectly) {
+ if (result.fallback != null) {
+ String fallbackMethod = getMethodNameFromConfig(method, annotationsPresentDirectly,
+ Fallback.class, "fallbackMethod");
+ if (fallbackMethod == null) {
+ fallbackMethod = result.fallback.fallbackMethod();
+ }
+ if (fallbackMethod != null && !fallbackMethod.isEmpty()) {
+ ClassInfo declaringClass = method.declaringClass();
+ Type[] parameterTypes = method.parameterTypes().toArray(new Type[0]);
+ Type returnType = method.returnType();
+ MethodInfo foundMethod = methodSearch.findFallbackMethod(beanClass,
+ declaringClass, fallbackMethod, parameterTypes, returnType);
+ Set foundMethods = methodSearch.findFallbackMethodsWithExceptionParameter(beanClass,
+ declaringClass, fallbackMethod, parameterTypes, returnType);
+ result.fallbackMethod = createMethodDescriptorIfNotNull(foundMethod);
+ result.fallbackMethodsWithExceptionParameter = createMethodDescriptorsIfNotEmpty(foundMethods);
+ if (foundMethod != null) {
+ reflectiveMethod.produce(new ReflectiveMethodBuildItem("@Fallback method", foundMethod));
+ }
+ for (MethodInfo m : foundMethods) {
+ reflectiveMethod.produce(new ReflectiveMethodBuildItem("@Fallback method", m));
+ }
+ }
+ }
+
+ if (result.beforeRetry != null) {
+ String beforeRetryMethod = getMethodNameFromConfig(method, annotationsPresentDirectly,
+ BeforeRetry.class, "methodName");
+ if (beforeRetryMethod == null) {
+ beforeRetryMethod = result.beforeRetry.methodName();
+ }
+ if (beforeRetryMethod != null && !beforeRetryMethod.isEmpty()) {
+ MethodInfo foundMethod = methodSearch.findBeforeRetryMethod(beanClass,
+ method.declaringClass(), beforeRetryMethod);
+ result.beforeRetryMethod = createMethodDescriptorIfNotNull(foundMethod);
+ if (foundMethod != null) {
+ reflectiveMethod.produce(new ReflectiveMethodBuildItem("@BeforeRetry method", foundMethod));
+ }
+ }
+ }
+ }
+
+ // copy of generated code to obtain a config value and translation from reflection to Jandex
+ // no need to check whether `ftAnnotation` is enabled, this will happen at runtime
+ private String getMethodNameFromConfig(MethodInfo method, Set> annotationsPresentDirectly,
+ Class extends Annotation> ftAnnotation, String memberName) {
+ String result;
+ org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig();
+ if (annotationsPresentDirectly.contains(ftAnnotation)) {
+ // ///
+ String key = method.declaringClass().name() + "/" + method.name() + "/" + ftAnnotation.getSimpleName() + "/"
+ + memberName;
+ result = config.getOptionalValue(key, String.class).orElse(null);
+ } else {
+ // //
+ String key = method.declaringClass().name() + "/" + ftAnnotation.getSimpleName() + "/" + memberName;
+ result = config.getOptionalValue(key, String.class).orElse(null);
+ }
+ if (result == null) {
+ // /
+ result = config.getOptionalValue(ftAnnotation.getSimpleName() + "/" + memberName, String.class).orElse(null);
+ }
+ return result;
+ }
+
+ private MethodDescriptor createMethodDescriptorIfNotNull(MethodInfo method) {
+ return method == null ? null : createMethodDescriptor(method);
+ }
+
+ private List createMethodDescriptorsIfNotEmpty(Collection methods) {
+ if (methods.isEmpty()) {
+ return null;
+ }
+ List result = new ArrayList<>(methods.size());
+ for (MethodInfo method : methods) {
+ result.add(createMethodDescriptor(method));
+ }
+ return result;
+ }
+
+ // ---
+
private A getAnnotationFromClass(Class annotationType, ClassInfo clazz) {
DotName annotationName = DotName.createSimple(annotationType);
if (annotationStore.hasAnnotation(clazz, annotationName)) {
diff --git a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java
index 8ad9312182049c..fc6547ce82ac32 100644
--- a/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java
+++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/SmallRyeFaultToleranceProcessor.java
@@ -1,7 +1,6 @@
package io.quarkus.smallrye.faulttolerance.deployment;
import java.time.temporal.ChronoUnit;
-import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
@@ -9,7 +8,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.OptionalInt;
-import java.util.Queue;
import java.util.Set;
import jakarta.annotation.Priority;
@@ -18,7 +16,6 @@
import org.eclipse.microprofile.config.Config;
import org.eclipse.microprofile.config.ConfigProvider;
import org.jboss.jandex.AnnotationInstance;
-import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
@@ -88,7 +85,6 @@ public void build(BuildProducer annotationsTran
BuildProducer systemProperty,
CombinedIndexBuildItem combinedIndexBuildItem,
BuildProducer reflectiveClass,
- BuildProducer reflectiveMethod,
BuildProducer config,
BuildProducer runtimeInitializedClassBuildItems) {
@@ -104,6 +100,8 @@ public void build(BuildProducer annotationsTran
IndexView index = combinedIndexBuildItem.getIndex();
// Add reflective access to fallback handlers and before retry handlers
+ // (reflective access to fallback methods and before retry methods is added
+ // in `FaultToleranceScanner.searchForMethods`)
Set handlers = new HashSet<>();
for (ClassInfo implementor : index.getAllKnownImplementors(DotNames.FALLBACK_HANDLER)) {
handlers.add(implementor.name().toString());
@@ -120,43 +118,6 @@ public void build(BuildProducer annotationsTran
}
beans.produce(handlerBeans.build());
}
- // Add reflective access to fallback methods
- for (AnnotationInstance annotation : index.getAnnotations(DotNames.FALLBACK)) {
- AnnotationValue fallbackMethodValue = annotation.value("fallbackMethod");
- if (fallbackMethodValue == null) {
- continue;
- }
- String fallbackMethod = fallbackMethodValue.asString();
-
- Queue classesToScan = new ArrayDeque<>(); // work queue
-
- // @Fallback can only be present on methods, so this is just future-proofing
- AnnotationTarget target = annotation.target();
- if (target.kind() == Kind.METHOD) {
- classesToScan.add(target.asMethod().declaringClass().name());
- }
-
- while (!classesToScan.isEmpty()) {
- DotName name = classesToScan.poll();
- ClassInfo clazz = index.getClassByName(name);
- if (clazz == null) {
- continue;
- }
-
- // we could further restrict the set of registered methods based on matching parameter types,
- // but that's relatively complex and SmallRye Fault Tolerance has to do it anyway
- clazz.methods()
- .stream()
- .filter(it -> fallbackMethod.equals(it.name()))
- .forEach(it -> reflectiveMethod.produce(new ReflectiveMethodBuildItem(getClass().getName(), it)));
-
- DotName superClass = clazz.superName();
- if (superClass != null && !DotNames.OBJECT.equals(superClass)) {
- classesToScan.add(superClass);
- }
- classesToScan.addAll(clazz.interfaceNames());
- }
- }
// Add reflective access to custom backoff strategies
for (ClassInfo strategy : index.getAllKnownImplementors(DotNames.CUSTOM_BACKOFF_STRATEGY)) {
reflectiveClass.produce(ReflectiveClassBuildItem.builder(strategy.name().toString()).methods().build());
@@ -270,6 +231,7 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
AnnotationProxyBuildItem annotationProxy,
BuildProducer generatedClasses,
BuildProducer reflectiveClass,
+ BuildProducer reflectiveMethod,
BuildProducer errors,
BuildProducer faultToleranceInfo) {
@@ -293,7 +255,7 @@ void processFaultToleranceAnnotations(SmallRyeFaultToleranceRecorder recorder,
ClassOutput classOutput = new GeneratedClassGizmoAdaptor(generatedClasses, false);
FaultToleranceScanner scanner = new FaultToleranceScanner(index, annotationStore, annotationProxy, classOutput,
- recorderContext);
+ recorderContext, reflectiveMethod);
List ftMethods = new ArrayList<>();
List exceptions = new ArrayList<>();
diff --git a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/FaultToleranceTestResource.java b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/FaultToleranceTestResource.java
index 066599ea669bfa..02cd7864c74291 100644
--- a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/FaultToleranceTestResource.java
+++ b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/FaultToleranceTestResource.java
@@ -27,4 +27,11 @@ public String retried() {
return counter + ":" + name;
}
+ @GET
+ @Path("/fallback")
+ public String fallback() {
+ AtomicInteger counter = new AtomicInteger();
+ String name = service.fallbackMethod(counter);
+ return counter + ":" + name;
+ }
}
diff --git a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java
index abdab357c779e4..72e0facaa7e721 100644
--- a/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java
+++ b/integration-tests/main/src/main/java/io/quarkus/it/faulttolerance/Service.java
@@ -5,6 +5,7 @@
import jakarta.annotation.PostConstruct;
import jakarta.enterprise.context.ApplicationScoped;
+import org.eclipse.microprofile.faulttolerance.Fallback;
import org.eclipse.microprofile.faulttolerance.Retry;
import io.smallrye.faulttolerance.api.ApplyFaultTolerance;
@@ -36,4 +37,16 @@ public String retriedMethod(AtomicInteger counter) {
}
throw new MyFaultToleranceError();
}
+
+ @Fallback(fallbackMethod = "fallback")
+ public String fallbackMethod(AtomicInteger counter) {
+ if (counter.incrementAndGet() >= THRESHOLD) {
+ return name;
+ }
+ throw new IllegalArgumentException();
+ }
+
+ private String fallback(AtomicInteger counter) {
+ return "fallback";
+ }
}
diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/FaultToleranceTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/FaultToleranceTestCase.java
index 4e59aa209f9876..355d2ae1bc9179 100644
--- a/integration-tests/main/src/test/java/io/quarkus/it/main/FaultToleranceTestCase.java
+++ b/integration-tests/main/src/test/java/io/quarkus/it/main/FaultToleranceTestCase.java
@@ -20,7 +20,7 @@ public class FaultToleranceTestCase {
URL uri;
@Test
- public void testRetry() throws Exception {
+ public void test() throws Exception {
RestAssured
.given().baseUri(uri.toString())
.when().get()
@@ -30,5 +30,10 @@ public void testRetry() throws Exception {
.given().baseUri(uri.toString() + "/retried")
.when().get()
.then().body(is("2:Lucie"));
+
+ RestAssured
+ .given().baseUri(uri.toString() + "/fallback")
+ .when().get()
+ .then().body(is("1:fallback"));
}
}