From 93da343c75e6e44685d4bd08d6fcab59494f11be Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Fri, 6 Sep 2024 16:26:24 +0200 Subject: [PATCH] WIP static resolution of FT methods (@Fallback, @BeforeRetry) --- bom/application/pom.xml | 2 +- .../recording/AnnotationProxyProvider.java | 42 +- .../FaultToleranceMethodSearch.java | 413 ++++++++++++++++++ .../deployment/FaultToleranceScanner.java | 136 +++++- .../SmallRyeFaultToleranceProcessor.java | 46 +- 5 files changed, 580 insertions(+), 59 deletions(-) create mode 100644 extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceMethodSearch.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index fa6b7ebdaa510..b286dcd25c8d2 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -57,7 +57,7 @@ 4.0.0 3.10.0 2.9.2 - 6.4.0 + 6.4.1-SNAPSHOT 4.6.0 2.1.2 1.0.13 diff --git a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java index 205001029f4df..823f94fc8ade4 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/recording/AnnotationProxyProvider.java @@ -186,21 +186,33 @@ public A build(ClassOutput classOutput) { new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - switch (method.getName()) { - case "getAnnotationLiteralType": - return annotationLiteral; - case "getAnnotationClass": - return annotationClass; - case "getAnnotationInstance": - return annotationInstance; - case "getDefaultValues": - return defaultValues; - case "getValues": - return values; - default: - break; - } - throw new UnsupportedOperationException("Method " + method + " not implemented"); + String name = method.getName(); + return switch (name) { + case "getAnnotationLiteralType" -> annotationLiteral; + case "getAnnotationClass" -> annotationClass; + case "getAnnotationInstance" -> annotationInstance; + case "getDefaultValues" -> defaultValues; + case "getValues" -> values; + default -> { + MethodInfo member = annotationClass.firstMethod(name); + if (member != null) { + if (values.containsKey(name)) { + yield values.get(name); + } + if (annotationInstance.value(name) != null) { + yield annotationInstance.value(name).value(); + } + if (defaultValues.containsKey(name)) { + yield defaultValues.get(name); + } + if (member.defaultValue() != null) { + yield member.defaultValue().value(); + } + throw new UnsupportedOperationException("Unknown value of annotation member " + name); + } + throw new UnsupportedOperationException("Method " + method + " not implemented"); + } + }; } }); } 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 0000000000000..8fd22384ce68e --- /dev/null +++ b/extensions/smallrye-fault-tolerance/deployment/src/main/java/io/quarkus/smallrye/faulttolerance/deployment/FaultToleranceMethodSearch.java @@ -0,0 +1,413 @@ +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; + +// mostly a translation of `io.smallrye.faulttolerance.config.SecurityActions` 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 static class TypeMapping { + private final Map map; + + private TypeMapping() { + this.map = Collections.emptyMap(); + } + + private TypeMapping(Map map) { + this.map = map; + } + + /** + * 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 + * @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 96d96c90d853b..c84eb1499ec6d 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,15 @@ 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.Optional; 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 +24,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 +52,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 +154,8 @@ FaultToleranceMethod createFaultToleranceMethod(ClassInfo beanClass, MethodInfo result.annotationsPresentDirectly = annotationsPresentDirectly; + searchForMethods(result, beanClass, method, annotationsPresentDirectly); + return result; } @@ -169,6 +184,125 @@ 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)); + } + } + } + } + + // mostly a copy of generated code to obtain a config value + private static String getMethodNameFromConfig(MethodInfo method, + Set> annotationsPresentDirectly, + Class ftAnnotation, String memberName) { + String result = null; + if (isEnabled(ftAnnotation, method)) { + 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; + } + + // mostly a copy of `io.smallrye.faulttolerance.autoconfig.Config.isEnabled()` + private static boolean isEnabled(Class annotationType, MethodInfo method) { + // TODO converting strings to boolean here is inconsistent, + // but it's how SmallRye Fault Tolerance has always done it + + org.eclipse.microprofile.config.Config config = ConfigProvider.getConfig(); + + Optional onMethod = config.getOptionalValue(method.declaringClass().name() + + "/" + method.name() + "/" + annotationType.getSimpleName() + "/enabled", String.class); + if (onMethod.isPresent()) { + return Boolean.parseBoolean(onMethod.get()); + } + + Optional onClass = config.getOptionalValue(method.declaringClass().name() + + "/" + annotationType.getSimpleName() + "/enabled", String.class); + if (onClass.isPresent()) { + return Boolean.parseBoolean(onClass.get()); + } + + Optional onGlobal = config.getOptionalValue(annotationType.getSimpleName() + "/enabled", String.class); + if (onGlobal.isPresent()) { + return Boolean.parseBoolean(onGlobal.get()); + } + + if (Fallback.class.equals(annotationType)) { + return true; + } + + return config.getOptionalValue("MP_Fault_Tolerance_NonFallback_Enabled", Boolean.class).orElse(true); + } + + 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 8ad9312182049..fc6547ce82ac3 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<>();