Repository: quarkusio/quarkus
Issue: Hibernate Validator: `@ApplicationScoped` custom `ValueExtractor` bean lead to `ValueExtractorDefinitionException`

### Describe the bug

Even after we fix #20347, custom `ValueExtractor` implementations provided as CDI beans can only have the `@Singleton` scope or `@Dependent` pseudo-scope. Any scope requiring instantiation will lead to an exception on startup complaining about "parallel definitions of value extractors on a given class", even though that's not the case.

### Expected behavior

A value extractor annotated with `@ApplicationScoped` should be correctly added to the `ValidatorFactory`, and be used automatically to constraints on extracted elements (e.g. `Container<@NotBlank String>`).

### Actual behavior

Exception on startup:

```
java.lang.ExceptionInInitializerError
        at java.base/java.lang.Class.forName0(Native Method)
        at java.base/java.lang.Class.forName(Class.java:398)
        at io.quarkus.runner.bootstrap.StartupActionImpl.run(StartupActionImpl.java:215)
        at io.quarkus.test.QuarkusUnitTest.beforeAll(QuarkusUnitTest.java:509)
        at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.lambda$invokeBeforeAllCallbacks$10(ClassBasedTestDescriptor.java:381)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.invokeBeforeAllCallbacks(ClassBasedTestDescriptor.java:381)
        at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:205)
        at org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor.before(ClassBasedTestDescriptor.java:80)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:148)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
        at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
        at java.base/java.util.ArrayList.forEach(ArrayList.java:1541)
        at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)
        at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)
        at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)
        at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)
        at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)
        at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
        at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)
        at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:107)
        at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
        at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
        at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
        at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
        at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:114)
        at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:86)
        at org.junit.platform.launcher.core.DefaultLauncherSession$DelegatingLauncher.execute(DefaultLauncherSession.java:86)
        at org.junit.platform.launcher.core.SessionPerRequestLauncher.execute(SessionPerRequestLauncher.java:53)
        at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:188)
        at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:154)
        at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:128)
        at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:428)
        at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)
        at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:562)
        at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:548)
Caused by: java.lang.RuntimeException: Failed to start quarkus
        at io.quarkus.runner.ApplicationImpl.<clinit>(ApplicationImpl.zig:236)
        ... 46 more
Caused by: javax.validation.valueextraction.ValueExtractorDefinitionException: HV000218: Having parallel definitions of value extractors on a given class is not allowed: io.quarkus.hibernate.validator.test.valueextractor.ApplicationScopedCustomValueExtractorTest_ApplicationScopedContainerValueExtractor_ClientProxy.
        at org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor.getValueExtractorDefinition(ValueExtractorDescriptor.java:142)
        at org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor.<init>(ValueExtractorDescriptor.java:45)
        at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.addValueExtractor(AbstractConfigurationImpl.java:271)
        at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.addValueExtractor(AbstractConfigurationImpl.java:80)
        at io.quarkus.hibernate.validator.runtime.HibernateValidatorRecorder$2.created(HibernateValidatorRecorder.java:143)
        at io.quarkus.arc.runtime.ArcRecorder.initBeanContainer(ArcRecorder.java:70)
        at io.quarkus.deployment.steps.ArcProcessor$generateResources-1025303321.deploy_0(ArcProcessor$generateResources-1025303321.zig:128)
        at io.quarkus.deployment.steps.ArcProcessor$generateResources-1025303321.deploy(ArcProcessor$generateResources-1025303321.zig:40)
        at io.quarkus.runner.ApplicationImpl.<clinit>(ApplicationImpl.zig:194)
        ... 46 more
```

### How to Reproduce?

I'm going to add a disabled test as part of the PR for #20347. Look for `ApplicationScopedCustomValueExtractorTest` in the codebase.

### Output of `uname -a` or `ver`

_No response_

### Output of `java -version`

_No response_

### GraalVM version (if different from Java)

_No response_

### Quarkus version or git rev

2.3.0.CR1, f0aba8594140c708c7c477d3b1efd4a0b29a3095

### Build tool (ie. output of `mvnw --version` or `gradlew --version`)

_No response_

### Additional information

I believe the cause is a bug in `org.hibernate.validator.internal.engine.valueextraction.ValueExtractorDescriptor#determineValueExtractorDefinitions`. It gets passed the proxy class, which extends the actual implementation class. And for some reason, when passed a class that extends another class that implements `ValueExtractor`, it behaves as if `ValueExtractor` was implemented multiple times, both by the superclass and the subclass.

It seems caused by the fact that `java.lang.Class#getAnnotatedInterfaces` returns annotated interfaces of the class **and its superclasses**, which means `determineValueExtractorDefinitions` should not be walking through superclasses to collect all annotated interfaces (when it does, it collects duplicates).


Changed files:
independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AsmUtilCopy.java
independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java
independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AsmUtilCopyTest.java
extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/valueextractor/ApplicationScopedCustomValueExtractorTest.java

diff --git a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/valueextractor/ApplicationScopedCustomValueExtractorTest.java b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/valueextractor/ApplicationScopedCustomValueExtractorTest.java
index 6daaa7a8b40..3065ac937e1 100644
--- a/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/valueextractor/ApplicationScopedCustomValueExtractorTest.java
+++ b/extensions/hibernate-validator/deployment/src/test/java/io/quarkus/hibernate/validator/test/valueextractor/ApplicationScopedCustomValueExtractorTest.java
@@ -11,13 +11,11 @@
 
 import org.jboss.shrinkwrap.api.ShrinkWrap;
 import org.jboss.shrinkwrap.api.spec.JavaArchive;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
 import io.quarkus.test.QuarkusUnitTest;
 
-@Disabled("Reproduces https://github.com/quarkusio/quarkus/issues/20375, not yet fixed")
 public class ApplicationScopedCustomValueExtractorTest {
 
     @Inject
diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AsmUtilCopy.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AsmUtilCopy.java
index 4602c12ea37..89513c0dc75 100644
--- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AsmUtilCopy.java
+++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/AsmUtilCopy.java
@@ -164,48 +164,35 @@ else if (erased)
 
     /**
      * Returns the Java bytecode signature of a given Jandex Class using the given type argument mappings.
-     * For example, given this class:
-     * 
+     *
+     * For example, given this superclass:
+     *
      * <pre>
      * {@code
      * public class Foo<R> extends Bar<R> implements List<String> {
      * }
      * </pre>
-     * 
-     * This will return <tt>&lt;R:Ljava/lang/Object;>LBar&lt;TR;>;Ljava/util/List&lt;Ljava/lang/String;>;</tt>.
-     * 
-     * @param klass the class you want the signature for.
-     * 
-     * @return a bytecode signature for that class.
-     */
-    public static String getSignature(ClassInfo klass) {
-        return getSignature(klass, NO_ARG_MAPPER);
-    }
-
-    /**
-     * Returns the Java bytecode signature of a given Jandex Class using the given type argument mappings.
-     * For example, given this class:
-     * 
+     *
+     * This will return <tt>&lt;R:Ljava/lang/Object;>LFoo&lt;TR;>;</tt>.
+     * {@code Bar} and {@code List} will be ignored, as they won't be part of the signature of the generated subclass.
+     *
+     * All will be as if the generated subclass was declared like this:
+     *
      * <pre>
      * {@code
-     * public class Foo<R> extends Bar<R> implements List<String> {
+     * public class MyGeneratedClass<R> extends Foo<R> {
      * }
      * </pre>
+     *
+     * @param superClass the superclass of the type you want to generate the signature for.
      * 
-     * This will return <tt>&lt;R:Ljava/lang/Object;>LBar&lt;TR;>;Ljava/util/List&lt;Ljava/lang/String;>;</tt>.
-     * 
-     * @param klass the class you want the signature for.
-     * 
-     * @param typeArgMapper a mapping between type variables and their resolved type.
+     * @param superClassAsType the superclass as a Jandex Type.
      * @return a bytecode signature for that class.
      */
-    public static String getSignature(ClassInfo klass, Function<TypeVariable, Type> typeArgMapper) {
-        StringBuilder signature = new StringBuilder("");
-        toSignature(signature, klass.typeParameters(), typeArgMapper, false);
-        toSignature(signature, klass.superClassType(), typeArgMapper, false);
-        for (Type superinterface : klass.interfaceTypes()) {
-            toSignature(signature, superinterface, typeArgMapper, false);
-        }
+    public static String getGeneratedSubClassSignature(ClassInfo superClass, Type superClassAsType) {
+        StringBuilder signature = new StringBuilder();
+        toSignature(signature, superClass.typeParameters(), NO_ARG_MAPPER, false);
+        toSignature(signature, superClassAsType, NO_ARG_MAPPER, false);
         return signature.toString();
     }
 
@@ -224,21 +211,6 @@ private static void toSignature(StringBuilder sb, List<TypeVariable> typeParamet
             sb.append(">");
     }
 
-    /**
-     * Returns true if the given class has type parameters or if its superclass or superinterfaces require a signature
-     */
-    public static boolean needsSignature(ClassInfo klass) {
-        if (!klass.typeParameters().isEmpty()
-                || needsSignature(klass.superClassType())) {
-            return true;
-        }
-        for (Type type : klass.interfaceTypes()) {
-            if (needsSignature(type))
-                return true;
-        }
-        return false;
-    }
-
     /**
      * Returns true if the given method has type parameters or if its return type or parameter types require a signature
      */
diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java
index 5720e8a9302..7eb5be95ff0 100644
--- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java
+++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ClientProxyGenerator.java
@@ -111,8 +111,16 @@ Collection<Resource> generate(BeanInfo bean, String beanClassName,
         ClassCreator clientProxy = ClassCreator.builder().classOutput(classOutput).className(generatedName)
                 .superClass(superClass)
                 .interfaces(interfaces.toArray(new String[0])).build();
-        if (AsmUtilCopy.needsSignature(providerClass)) {
-            clientProxy.setSignature(AsmUtilCopy.getSignature(providerClass));
+        // See https://docs.oracle.com/javase/specs/jvms/se14/html/jvms-4.html#jvms-4.7.9.1
+        // Essentially a signature is needed if a class has type parameters or extends/implements parameterized type.
+        // We're generating a subtype (subclass or subinterface) of "providerClass".
+        // The only way for that generated subtype to have type parameters or to extend/implement a parameterized type
+        // is if providerClass has type parameters.
+        // Whether supertypes or superinterfaces of providerClass have type parameters is irrelevant:
+        // as long as those type parameters are bound in providerClass,
+        // they won't affect the need for a signature in the generated subtype.
+        if (!providerClass.typeParameters().isEmpty()) {
+            clientProxy.setSignature(AsmUtilCopy.getGeneratedSubClassSignature(providerClass, bean.getProviderType()));
         }
         Map<ClassInfo, Map<TypeVariable, Type>> resolvedTypeVariables = Types.resolvedTypeVariables(providerClass,
                 bean.getDeployment());
diff --git a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AsmUtilCopyTest.java b/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AsmUtilCopyTest.java
deleted file mode 100644
index e11239251d1..00000000000
--- a/independent-projects/arc/processor/src/test/java/io/quarkus/arc/processor/AsmUtilCopyTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.quarkus.arc.processor;
-
-import static org.junit.jupiter.api.Assertions.assertFalse;
-
-import java.io.IOException;
-import org.junit.jupiter.api.Test;
-
-public class AsmUtilCopyTest {
-
-    @Test
-    public void testNeedsSignature() throws IOException {
-        assertFalse(AsmUtilCopy.needsSignature(Basics.index(Object.class).getClassByName(DotNames.OBJECT)));
-    }
-
-}