diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java index abd6e9b6590862..b5ce33e3898814 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ArcProcessor.java @@ -149,7 +149,7 @@ AdditionalBeanBuildItem quarkusApplication(CombinedIndexBuildItem combinedIndex) List quarkusApplications = new ArrayList<>(); for (ClassInfo quarkusApplication : combinedIndex.getIndex() .getAllKnownImplementors(DotName.createSimple(QuarkusApplication.class.getName()))) { - if (quarkusApplication.classAnnotation(DotNames.DECORATOR) == null) { + if (quarkusApplication.declaredAnnotation(DotNames.DECORATOR) == null) { quarkusApplications.add(quarkusApplication.name().toString()); } } @@ -470,7 +470,7 @@ public ObserverRegistrationPhaseBuildItem registerSyntheticObservers(BeanRegistr } else { declaringClass = injectionPoint.getTarget().asMethod().declaringClass(); } - if (declaringClass.classAnnotation(DotNames.KOTLIN_METADATA_ANNOTATION) != null) { + if (declaringClass.declaredAnnotation(DotNames.KOTLIN_METADATA_ANNOTATION) != null) { validationErrors.produce(new ValidationErrorBuildItem( new DefinitionException( "kotlin.collections.List cannot be used together with the @All qualifier, please use MutableList or java.util.List instead: " diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ArcLookupProblemDetectedTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ArcLookupProblemDetectedTest.java index fe6f89ef2525a3..bdfb511d39fe7f 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ArcLookupProblemDetectedTest.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/unused/ArcLookupProblemDetectedTest.java @@ -1,13 +1,14 @@ package io.quarkus.arc.test.unused; -import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.logging.Formatter; import java.util.logging.LogRecord; import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.UnsatisfiedResolutionException; import javax.enterprise.inject.spi.CDI; import org.jboss.logmanager.formatters.PatternFormatter; @@ -31,7 +32,7 @@ public class ArcLookupProblemDetectedTest { Formatter fmt = new PatternFormatter("%m"); String message = fmt.format(warning); assertTrue(message.contains( - "Stack frame: io.quarkus.arc.test.unused.ArcLookupProblemDetectedTest.testWarning"), + "Stack frame: io.quarkus.arc.test.unused.ArcLookupProblemDetectedTest"), message); assertTrue(message.contains( "Required type: class io.quarkus.arc.test.unused.ArcLookupProblemDetectedTest$Alpha"), @@ -41,7 +42,7 @@ public class ArcLookupProblemDetectedTest { @Test public void testWarning() { // Note that the warning is only displayed once, subsequent calls use a cached result - assertFalse(CDI.current().select(Alpha.class).isResolvable()); + assertThrows(UnsatisfiedResolutionException.class, () -> CDI.current().select(Alpha.class).get()); } // unused bean, will be removed diff --git a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java index f0bac3581223e1..737a99bd19da4f 100644 --- a/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java +++ b/extensions/arc/runtime/src/main/java/io/quarkus/arc/runtime/BeanContainer.java @@ -20,6 +20,9 @@ default T instance(Class type, Annotation... qualifiers) { } /** + * Note that if there are multiple sub classes of the given type this will return the exact match. This means + * that this can be used to directly instantiate superclasses of other beans without causing problems. This behavior differs + * to standard CDI rules where an ambiguous dependency would exist. * * @param type * @param qualifiers diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java index 1ab27aef29823e..3daab08d6d1c3a 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/ResteasyReactiveRecorder.java @@ -149,8 +149,8 @@ public void accept(Closeable closeable) { shutdownContext.addShutdownTask(new ShutdownContext.CloseRunnable(closeable)); } }; - CurrentIdentityAssociation currentIdentityAssociation = Arc.container().instance(CurrentIdentityAssociation.class) - .get(); + CurrentIdentityAssociation currentIdentityAssociation = Arc.container().select(CurrentIdentityAssociation.class) + .orNull(); ClassLoader tccl = Thread.currentThread().getContextClassLoader(); if (contextFactory == null) { contextFactory = new RequestContextFactory() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java index 6f027a93944cb4..8cb3edbdeb8564 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/Beans.java @@ -916,7 +916,7 @@ public void visit(int version, int access, String name, String signature, mv.visitInsn(Opcodes.RETURN); mv.visitMaxs(1, 1); mv.visitEnd(); - LOGGER.debugf("Added a no-args constructor to bean class: ", className); + LOGGER.debugf("Added a no-args constructor to bean class: %s", className); } }; } @@ -935,7 +935,7 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str if (name.equals(Methods.INIT)) { access = access & (~Opcodes.ACC_PRIVATE); LOGGER.debugf( - "Changed visibility of a private no-args constructor to package-private: ", + "Changed visibility of a private no-args constructor to package-private: %s", className); } return super.visitMethod(access, name, descriptor, signature, exceptions); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java index a8f01b096116c5..898dc6f8656fb0 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ComponentsProviderGenerator.java @@ -3,6 +3,7 @@ import static java.util.stream.Collectors.toList; import static org.objectweb.asm.Opcodes.ACC_PRIVATE; import static org.objectweb.asm.Opcodes.ACC_PUBLIC; +import static org.objectweb.asm.Opcodes.ACC_STATIC; import io.quarkus.arc.Arc; import io.quarkus.arc.Components; @@ -15,6 +16,7 @@ import io.quarkus.gizmo.ClassCreator; import io.quarkus.gizmo.ClassOutput; import io.quarkus.gizmo.FieldDescriptor; +import io.quarkus.gizmo.FunctionCreator; import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; @@ -122,12 +124,18 @@ Collection generate(String name, BeanDeployment beanDeployment, Map generate(String name, BeanDeployment beanDeployment, Map { private final MapTypeCache typeCache; - public RemovedBeanAdder(ClassCreator componentsProvider, MethodCreator getComponentsMethod, + public RemovedBeanAdder(ClassCreator componentsProvider, BytecodeCreator targetMethod, ResultHandle removedBeansHandle, ResultHandle typeCacheHandle, ClassOutput classOutput) { - super(getComponentsMethod, componentsProvider); + super(targetMethod, componentsProvider); this.removedBeansHandle = removedBeansHandle; this.typeCacheHandle = typeCacheHandle; this.sharedQualifers = new HashMap<>(); @@ -454,10 +462,10 @@ MethodCreator newAddMethod() { // Clear the shared maps for each addRemovedBeansX() method sharedQualifers.clear(); - // private void addRemovedBeans1(List removedBeans, List typeCache) + // static void addRemovedBeans1(List removedBeans, List typeCache) MethodCreator addMethod = componentsProvider .getMethodCreator(ADD_REMOVED_BEANS + group++, void.class, List.class, Map.class) - .setModifiers(ACC_PRIVATE); + .setModifiers(ACC_STATIC); // Get the TCCL - we will use it later ResultHandle currentThread = addMethod .invokeStaticMethod(MethodDescriptors.THREAD_CURRENT_THREAD); @@ -470,10 +478,11 @@ MethodCreator newAddMethod() { @Override void invokeAddMethod() { - getComponentsMethod.invokeVirtualMethod( + // Static methods are invoked from within the generated supplier + targetMethod.invokeStaticMethod( MethodDescriptor.ofMethod(componentsProvider.getClassName(), addMethod.getMethodDescriptor().getName(), void.class, List.class, Map.class), - getComponentsMethod.getThis(), removedBeansHandle, typeCacheHandle); + removedBeansHandle, typeCacheHandle); } @Override @@ -613,10 +622,10 @@ MethodCreator newAddMethod() { @Override void invokeAddMethod() { - getComponentsMethod.invokeVirtualMethod( + targetMethod.invokeVirtualMethod( MethodDescriptor.ofMethod(componentsProvider.getClassName(), addMethod.getMethodDescriptor().getName(), void.class, Map.class), - getComponentsMethod.getThis(), beanIdToBeanHandle); + targetMethod.getThis(), beanIdToBeanHandle); } @Override @@ -708,12 +717,12 @@ static abstract class ComponentAdder implements A protected int group; private int componentsAdded; protected MethodCreator addMethod; - protected final MethodCreator getComponentsMethod; + protected final BytecodeCreator targetMethod; protected final ClassCreator componentsProvider; - public ComponentAdder(MethodCreator getComponentsMethod, ClassCreator componentsProvider) { + public ComponentAdder(BytecodeCreator getComponentsMethod, ClassCreator componentsProvider) { this.group = 1; - this.getComponentsMethod = getComponentsMethod; + this.targetMethod = getComponentsMethod; this.componentsProvider = componentsProvider; } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java index 0079555f3a4d55..8d792b40d9f87b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/ArcContainer.java @@ -88,7 +88,8 @@ public interface ArcContainer { * Returns a supplier that can be used to create new instances, or null if no matching bean can be found. * * Note that if there are multiple sub classes of the given type this will return the exact match. This means - * that this can be used to directly instantiate superclasses of other beans without causing problems. + * that this can be used to directly instantiate superclasses of other beans without causing problems. This behavior differs + * to standard CDI rules where an ambiguous dependency would exist. * * see https://github.com/quarkusio/quarkus/issues/3369 * diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java index 627d3e38f90e9e..2d782dd059cf7d 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/Components.java @@ -4,11 +4,12 @@ import java.util.Collection; import java.util.Map; import java.util.Set; +import java.util.function.Supplier; public final class Components { private final Collection> beans; - private final Collection removedBeans; + private final Supplier> removedBeans; private final Collection> observers; private final Collection contexts; private final Map, Set> transitiveInterceptorBindings; @@ -18,7 +19,8 @@ public final class Components { public Components(Collection> beans, Collection> observers, Collection contexts, Map, Set> transitiveInterceptorBindings, - Collection removedBeans, Map> qualifierNonbindingMembers, Set qualifiers) { + Supplier> removedBeans, Map> qualifierNonbindingMembers, + Set qualifiers) { this.beans = beans; this.observers = observers; this.contexts = contexts; @@ -44,7 +46,7 @@ public Map, Set> getTransitiveIntercepto return transitiveInterceptorBindings; } - public Collection getRemovedBeans() { + public Supplier> getRemovedBeans() { return removedBeans; } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java index 64cc64e4af19fa..95a7da7c5fc999 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/InjectableInstance.java @@ -49,4 +49,26 @@ public interface InjectableInstance extends Instance { @Override Iterator iterator(); + /** + * If there is exactly one bean that matches the required type and qualifiers, returns the instance, otherwise returns + * {@code other}. + * + * @param other + * @return the bean instance or the other value + */ + default T orElse(T other) { + return isResolvable() ? get() : other; + } + + /** + * If there is exactly one bean that matches the required type and qualifiers, returns the instance, otherwise returns + * {@code null}. + * + * @param other + * @return the bean instance or {@code null} + */ + default T orNull() { + return orElse(null); + } + } diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java index 48a5125bf98d07..6d95b55c40cd6b 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/ArcContainerImpl.java @@ -24,6 +24,7 @@ import java.lang.reflect.TypeVariable; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -74,7 +75,7 @@ public class ArcContainerImpl implements ArcContainer { private final AtomicBoolean running; private final List> beans; - private final List removedBeans; + private final LazyValue> removedBeans; private final List> interceptors; private final List> decorators; private final List> observers; @@ -97,7 +98,7 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { id = String.valueOf(ID_GENERATOR.incrementAndGet()); running = new AtomicBoolean(true); List> beans = new ArrayList<>(); - List removedBeans = new ArrayList<>(); + List>> removedBeans = new ArrayList<>(); List> interceptors = new ArrayList<>(); List> decorators = new ArrayList<>(); List> observers = new ArrayList<>(); @@ -122,7 +123,7 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { beans.add(bean); } } - removedBeans.addAll(components.getRemovedBeans()); + removedBeans.add(components.getRemovedBeans()); observers.addAll(components.getObservers()); // Add custom contexts for (InjectableContext context : components.getContexts()) { @@ -163,7 +164,17 @@ public ArcContainerImpl(CurrentContextFactory currentContextFactory) { this.interceptors = List.copyOf(interceptors); this.decorators = List.copyOf(decorators); this.observers = List.copyOf(observers); - this.removedBeans = List.copyOf(removedBeans); + this.removedBeans = new LazyValue<>(new Supplier>() { + @Override + public List get() { + List removed = new ArrayList<>(); + for (Supplier> supplier : removedBeans) { + removed.addAll(supplier.get()); + } + LOGGER.debugf("Loaded %s removed beans lazily", removed.size()); + return List.copyOf(removed); + } + }); this.transitiveInterceptorBindings = Map.copyOf(transitiveInterceptorBindings); this.registeredQualifiers = new Qualifiers(qualifiers, qualifierNonbindingMembers); } @@ -384,7 +395,7 @@ public List> getBeans() { } public List getRemovedBeans() { - return removedBeans; + return removedBeans.get(); } public List> getInterceptors() { @@ -449,7 +460,11 @@ private InjectableBean getBean(Type requiredType, Annotation... qualifier } else { registeredQualifiers.verify(qualifiers); } - Set> resolvedBeans = resolved.getValue(new Resolvable(requiredType, qualifiers)); + Resolvable resolvable = new Resolvable(requiredType, qualifiers); + Set> resolvedBeans = resolved.getValue(resolvable); + if (resolvedBeans.isEmpty()) { + scanRemovedBeans(resolvable); + } return resolvedBeans.size() != 1 ? null : (InjectableBean) resolvedBeans.iterator().next(); } @@ -610,43 +625,53 @@ List> getMatchingBeans(Resolvable resolvable) { matching.add(bean); } } - if (matching.isEmpty() && !removedBeans.isEmpty()) { - List removedMatching = new ArrayList<>(); - for (RemovedBean removedBean : removedBeans) { - if (matches(removedBean.getTypes(), removedBean.getQualifiers(), resolvable.requiredType, - resolvable.qualifiers)) { - removedMatching.add(removedBean); - } - } - if (!removedMatching.isEmpty()) { - String separator = "===================="; - String msg = "\n%1$s%1$s%1$s%1$s\n" - + "CDI: programmatic lookup problem detected\n" - + "-----------------------------------------\n" - + "At least one bean matched the required type and qualifiers but was marked as unused and removed during build\n\n" - + "Stack frame: %5$s\n" - + "Required type: %3$s\n" - + "Required qualifiers: %4$s\n" - + "Removed beans:\n\t- %2$s\n" - + "Solutions:\n" - + "\t- Application developers can eliminate false positives via the @Unremovable annotation\n" - + "\t- Extensions can eliminate false positives via build items, e.g. using the UnremovableBeanBuildItem\n" - + "\t- See also https://quarkus.io/guides/cdi-reference#remove_unused_beans\n" - + "\t- Enable the DEBUG log level to see the full stack trace\n" - + "%1$s%1$s%1$s%1$s\n"; - StackWalker walker = StackWalker.getInstance(); - StackFrame frame = walker.walk(this::findCaller); - LOGGER.warnf(msg, separator, - removedMatching.stream().map(Object::toString).collect(Collectors.joining("\n\t- ")), - resolvable.requiredType, Arrays.toString(resolvable.qualifiers), frame != null ? frame : "n/a"); - if (LOGGER.isDebugEnabled()) { - LOGGER.debug("\nCDI: programmatic lookup stack trace:\n" + walker.walk(this::collectStack)); - } + return matching; + } + + List getMatchingRemovedBeans(Resolvable resolvable) { + List matching = new ArrayList<>(); + for (RemovedBean removedBean : removedBeans.get()) { + if (matches(removedBean.getTypes(), removedBean.getQualifiers(), resolvable.requiredType, + resolvable.qualifiers)) { + matching.add(removedBean); } } return matching; } + void scanRemovedBeans(Type requiredType, Annotation... qualifiers) { + scanRemovedBeans(new Resolvable(requiredType, qualifiers)); + } + + void scanRemovedBeans(Resolvable resolvable) { + List removedMatching = getMatchingRemovedBeans(resolvable); + if (!removedMatching.isEmpty()) { + String separator = "===================="; + String msg = "\n%1$s%1$s%1$s%1$s\n" + + "CDI: programmatic lookup problem detected\n" + + "-----------------------------------------\n" + + "At least one bean matched the required type and qualifiers but was marked as unused and removed during build\n\n" + + "Stack frame: %5$s\n" + + "Required type: %3$s\n" + + "Required qualifiers: %4$s\n" + + "Removed beans:\n\t- %2$s\n" + + "Solutions:\n" + + "\t- Application developers can eliminate false positives via the @Unremovable annotation\n" + + "\t- Extensions can eliminate false positives via build items, e.g. using the UnremovableBeanBuildItem\n" + + "\t- See also https://quarkus.io/guides/cdi-reference#remove_unused_beans\n" + + "\t- Enable the DEBUG log level to see the full stack trace\n" + + "%1$s%1$s%1$s%1$s\n"; + StackWalker walker = StackWalker.getInstance(); + StackFrame frame = walker.walk(this::findCaller); + LOGGER.warnf(msg, separator, + removedMatching.stream().map(Object::toString).collect(Collectors.joining("\n\t- ")), + resolvable.requiredType, Arrays.toString(resolvable.qualifiers), frame != null ? frame : "n/a"); + if (LOGGER.isDebugEnabled()) { + LOGGER.debug("\nCDI: programmatic lookup stack trace:\n" + walker.walk(this::collectStack)); + } + } + } + private StackFrame findCaller(Stream stream) { return stream .filter(this::isCallerFrame) diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java index 00104c0ed26f5a..cdcbb86b07ca6a 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/impl/InstanceImpl.java @@ -185,6 +185,7 @@ public H get() { private InjectableBean bean() { List> beans = beans(); if (beans.isEmpty()) { + ArcContainerImpl.instance().scanRemovedBeans(requiredType, requiredQualifiers.toArray(new Annotation[] {})); throw new UnsatisfiedResolutionException( "No bean found for required type [" + requiredType + "] and qualifiers [" + requiredQualifiers + "]"); } else if (beans.size() > 1) {