From 8322420827399e8658f7479bb3a0785072544bd7 Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 1 Sep 2022 10:32:23 +0200 Subject: [PATCH] Rework the BeanDefiningAnnotationBuildItem default scope - it was previously implemented as a "synthetic" stereotype; the downsides of this solution were (1) there was a need to distinguish "real" and "bda" stereotypes so that the bda one can be ignored if a real stereotype with a default scope is used (otherwise users might get unexpected definition errors) and (2) it was not possible to define CDI qualifier as a bean defining annotation with a default scope --- .../BeanDefiningAnnotationScopeTest.java} | 75 ++++++++- .../arc/processor/BeanDefiningAnnotation.java | 4 + .../quarkus/arc/processor/BeanDeployment.java | 55 +++---- .../java/io/quarkus/arc/processor/Beans.java | 142 +++++++++++------- .../quarkus/arc/processor/StereotypeInfo.java | 38 +++-- .../ScopeInheritanceStereotypeTest.java | 37 +++++ .../MultipleStereotypeScopesTest.java | 45 ++++++ 7 files changed, 292 insertions(+), 104 deletions(-) rename extensions/arc/deployment/src/test/java/io/quarkus/arc/test/{stereotype/BeanDefiningAnnotationStereotypeTestCase.java => bda/BeanDefiningAnnotationScopeTest.java} (56%) create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/inheritance/ScopeInheritanceStereotypeTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/stereotypes/MultipleStereotypeScopesTest.java diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/stereotype/BeanDefiningAnnotationStereotypeTestCase.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/bda/BeanDefiningAnnotationScopeTest.java similarity index 56% rename from extensions/arc/deployment/src/test/java/io/quarkus/arc/test/stereotype/BeanDefiningAnnotationStereotypeTestCase.java rename to extensions/arc/deployment/src/test/java/io/quarkus/arc/test/bda/BeanDefiningAnnotationScopeTest.java index 187050f1e4b56..069008aba8a71 100644 --- a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/stereotype/BeanDefiningAnnotationStereotypeTestCase.java +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/bda/BeanDefiningAnnotationScopeTest.java @@ -1,9 +1,10 @@ -package io.quarkus.arc.test.stereotype; +package io.quarkus.arc.test.bda; import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotEquals; import java.lang.annotation.Retention; @@ -17,6 +18,7 @@ import javax.enterprise.inject.Instance; import javax.enterprise.inject.Stereotype; import javax.inject.Inject; +import javax.inject.Qualifier; import org.jboss.jandex.DotName; import org.junit.jupiter.api.Test; @@ -28,12 +30,13 @@ import io.quarkus.builder.BuildStep; import io.quarkus.test.QuarkusUnitTest; -public class BeanDefiningAnnotationStereotypeTestCase { +public class BeanDefiningAnnotationScopeTest { @RegisterExtension static QuarkusUnitTest runner = new QuarkusUnitTest() - .withApplicationRoot((jar) -> jar - .addClasses(MyBean.class, MakeItBean.class, DependentStereotype.class)) + .withApplicationRoot(root -> root + .addClasses(MyBean.class, AnotherBean.class, YetAnotherBean.class, MakeItBean.class, + QualifierMakeItBean.class, DependentStereotype.class)) .addBuildChainCustomizer(buildCustomizer()); static Consumer buildCustomizer() { @@ -47,6 +50,9 @@ public void accept(BuildChainBuilder builder) { public void execute(BuildContext context) { context.produce(new BeanDefiningAnnotationBuildItem(DotName.createSimple(MakeItBean.class.getName()), DotName.createSimple(ApplicationScoped.class.getName()))); + context.produce( + new BeanDefiningAnnotationBuildItem(DotName.createSimple(QualifierMakeItBean.class.getName()), + DotName.createSimple(ApplicationScoped.class.getName()))); } }).produces(BeanDefiningAnnotationBuildItem.class).build(); } @@ -54,11 +60,28 @@ public void execute(BuildContext context) { } @Inject - Instance instance; + Instance my; + + @Inject + Instance another; + + @Inject + @QualifierMakeItBean + Instance yetAnother; @Test - public void test() { - assertNotEquals(instance.get().getId(), instance.get().getId()); + public void testExplicitStereotypeScopeWins() { + assertNotEquals(my.get().getId(), my.get().getId()); + } + + @Test + public void testDefaultScopeIsUsed() { + assertEquals(another.get().getId(), another.get().getId()); + } + + @Test + public void testQualifierAsBeanDefiningAnnotation() { + assertEquals(yetAnother.get().getId(), yetAnother.get().getId()); } @DependentStereotype @@ -78,11 +101,49 @@ void init() { } + @MakeItBean + static class AnotherBean { + + private String id; + + public String getId() { + return id; + } + + @PostConstruct + void init() { + this.id = UUID.randomUUID().toString(); + } + + } + + @QualifierMakeItBean + static class YetAnotherBean { + + private String id; + + public String getId() { + return id; + } + + @PostConstruct + void init() { + this.id = UUID.randomUUID().toString(); + } + + } + @Target({ TYPE, METHOD, FIELD }) @Retention(RUNTIME) public @interface MakeItBean { } + @Qualifier + @Target({ TYPE, METHOD, FIELD }) + @Retention(RUNTIME) + public @interface QualifierMakeItBean { + } + @Dependent @Stereotype @Target({ TYPE, METHOD, FIELD }) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDefiningAnnotation.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDefiningAnnotation.java index c4d01cce4c0db..fe25605c3c2a2 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDefiningAnnotation.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDefiningAnnotation.java @@ -7,6 +7,10 @@ public class BeanDefiningAnnotation { private final DotName annotation; private final DotName defaultScope; + public BeanDefiningAnnotation(DotName annotation) { + this(annotation, null); + } + public BeanDefiningAnnotation(DotName annotation, DotName defaultScope) { this.annotation = annotation; this.defaultScope = defaultScope; diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java index 6ec9288e33dfb..82fceeac70c73 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanDeployment.java @@ -99,7 +99,7 @@ public class BeanDeployment { private final Map> customContexts; - private final Collection beanDefiningAnnotations; + private final Map beanDefiningAnnotations; final boolean transformUnproxyableClasses; @@ -113,9 +113,11 @@ public class BeanDeployment { BeanDeployment(BuildContextImpl buildContext, BeanProcessor.Builder builder) { this.buildContext = buildContext; - Set beanDefiningAnnotations = new HashSet<>(); + Map beanDefiningAnnotations = new HashMap<>(); if (builder.additionalBeanDefiningAnnotations != null) { - beanDefiningAnnotations.addAll(builder.additionalBeanDefiningAnnotations); + for (BeanDefiningAnnotation bda : builder.additionalBeanDefiningAnnotations) { + beanDefiningAnnotations.put(bda.getAnnotation(), bda); + } } this.beanDefiningAnnotations = beanDefiningAnnotations; this.resourceAnnotations = new HashSet<>(builder.resourceAnnotations); @@ -180,8 +182,8 @@ public class BeanDeployment { additionalStereotypes.addAll(stereotypeRegistrar.getAdditionalStereotypes()); } - this.stereotypes = findStereotypes(this.beanArchiveIndex, interceptorBindings, beanDefiningAnnotations, customContexts, - additionalStereotypes, annotationStore); + this.stereotypes = findStereotypes(this.beanArchiveIndex, interceptorBindings, customContexts, additionalStereotypes, + annotationStore); buildContextPut(Key.STEREOTYPES.asString(), Collections.unmodifiableMap(stereotypes)); this.transitiveInterceptorBindings = findTransitiveInterceptorBindings(interceptorBindings.keySet(), @@ -221,7 +223,8 @@ public ContextConfigurator configure(Class scopeAnnotation return new ContextConfigurator(scopeAnnotation, c -> { ScopeInfo scope = new ScopeInfo(c.scopeAnnotation, c.isNormal); - beanDefiningAnnotations.add(new BeanDefiningAnnotation(scope.getDotName(), null)); + beanDefiningAnnotations.put(scope.getDotName(), + new BeanDefiningAnnotation(scope.getDotName(), null)); customContexts.put(scope, c.creator); }); } @@ -242,8 +245,9 @@ void registerScopes() { BeanRegistrar.RegistrationContext registerBeans(List beanRegistrars) { List injectionPoints = new ArrayList<>(); - this.beans.addAll(findBeans(initBeanDefiningAnnotations(beanDefiningAnnotations, stereotypes.keySet()), observers, - injectionPoints, jtaCapabilities)); + this.beans.addAll( + findBeans(initBeanDefiningAnnotations(beanDefiningAnnotations.values(), stereotypes.keySet()), observers, + injectionPoints, jtaCapabilities)); // Note that we use unmodifiable views because the underlying collections may change in the next phase // E.g. synthetic beans are added and unused interceptors removed buildContextPut(Key.BEANS.asString(), Collections.unmodifiableList(beans)); @@ -615,6 +619,10 @@ StereotypeInfo getStereotype(DotName name) { return stereotypes.get(name); } + BeanDefiningAnnotation getBeanDefiningAnnotation(DotName name) { + return beanDefiningAnnotations.get(name); + } + Set getResourceAnnotations() { return resourceAnnotations; } @@ -738,7 +746,6 @@ private static Set recursiveBuild(DotName name, } private Map findStereotypes(IndexView index, Map interceptorBindings, - Collection additionalBeanDefiningAnnotations, Map> customContexts, Set additionalStereotypes, AnnotationStore annotationStore) { @@ -756,7 +763,7 @@ private Map findStereotypes(IndexView index, Map scopes = new ArrayList<>(); + Set scopes = new HashSet<>(); List bindings = new ArrayList<>(); List parentStereotypes = new ArrayList<>(); boolean isNamed = false; @@ -794,25 +801,9 @@ private Map findStereotypes(IndexView index, Map stereotypeScopes, AnnotationTarget target) { - switch (stereotypeScopes.size()) { + static ScopeInfo getValidScope(Set scopes, AnnotationTarget target) { + switch (scopes.size()) { case 0: return null; case 1: - return stereotypeScopes.iterator().next(); + return scopes.iterator().next(); default: - throw new DefinitionException("All stereotypes must specify the same scope or the bean must declare a scope: " - + target + " declares scopes " + stereotypeScopes.stream().map(ScopeInfo::getDotName) + throw new DefinitionException( + "Different scopes defined for: " + target + "; scopes: " + scopes.stream().map(ScopeInfo::getDotName) .map(DotName::toString).collect(Collectors.joining(", "))); } } 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 b493c5d2eecff..6f027a93944cb 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 @@ -77,15 +77,17 @@ static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMet boolean isAlternative = false; boolean isDefaultBean = false; List stereotypes = new ArrayList<>(); + Set beanDefiningAnnotationScopes = new HashSet<>(); String name = null; for (AnnotationInstance annotation : beanDeployment.getAnnotations(producerMethod)) { + DotName annotationName = annotation.name(); //only check for method annotations since at this point we will get both // method and method param annotations if (annotation.target().kind() != AnnotationTarget.Kind.METHOD) { continue; } - if (DotNames.NAMED.equals(annotation.name())) { + if (DotNames.NAMED.equals(annotationName)) { AnnotationValue nameValue = annotation.value(); if (nameValue != null) { name = nameValue.asString(); @@ -94,6 +96,10 @@ static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMet annotation = normalizedNamedQualifier(name, annotation); } } + BeanDefiningAnnotation bda = beanDeployment.getBeanDefiningAnnotation(annotationName); + if (bda != null && bda.getDefaultScope() != null) { + beanDefiningAnnotationScopes.add(beanDeployment.getScope(bda.getDefaultScope())); + } Collection qualifierCollection = beanDeployment.extractQualifiers(annotation); for (AnnotationInstance qualifierAnnotation : qualifierCollection) { // Qualifiers @@ -103,34 +109,34 @@ static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMet // we needn't process it further, the annotation was a qualifier (or multiple repeating ones) continue; } - if (DotNames.ALTERNATIVE.equals(annotation.name())) { + if (DotNames.ALTERNATIVE.equals(annotationName)) { isAlternative = true; continue; } - if (DotNames.ALTERNATIVE_PRIORITY.equals(annotation.name())) { + if (DotNames.ALTERNATIVE_PRIORITY.equals(annotationName)) { isAlternative = true; priority = annotation.value().asInt(); continue; } // This is not supported ATM but should work once we upgrade to Common Annotations 2.1 - if ((!isAlternative || priority == null) && annotation.name().equals(DotNames.PRIORITY)) { + if ((!isAlternative || priority == null) && annotationName.equals(DotNames.PRIORITY)) { priority = annotation.value().asInt(); continue; } - if (priority == null && DotNames.ARC_PRIORITY.equals(annotation.name())) { + if (priority == null && DotNames.ARC_PRIORITY.equals(annotationName)) { priority = annotation.value().asInt(); continue; } - if (DotNames.DEFAULT_BEAN.equals(annotation.name())) { + if (DotNames.DEFAULT_BEAN.equals(annotationName)) { isDefaultBean = true; continue; } - ScopeInfo scopeAnnotation = beanDeployment.getScope(annotation.name()); + ScopeInfo scopeAnnotation = beanDeployment.getScope(annotationName); if (scopeAnnotation != null) { scopes.add(scopeAnnotation); continue; } - StereotypeInfo stereotype = beanDeployment.getStereotype(annotation.name()); + StereotypeInfo stereotype = beanDeployment.getStereotype(annotationName); if (stereotype != null) { stereotypes.add(stereotype); continue; @@ -140,12 +146,19 @@ static BeanInfo createProducerMethod(Set beanTypes, MethodInfo producerMet if (scopes.size() > 1) { throw multipleScopesFound("Producer method " + producerMethod, scopes); } + // 1. Explicit scope + // 2. Stereotype scope + // 3. Bean defining annotation default scope ScopeInfo scope; if (scopes.isEmpty()) { scope = initStereotypeScope(stereotypes, producerMethod, beanDeployment); + if (scope == null) { + scope = initBeanDefiningAnnotationScope(beanDefiningAnnotationScopes, producerMethod); + } } else { scope = scopes.get(0); } + if (!isAlternative) { isAlternative = initStereotypeAlternative(stereotypes); } @@ -185,10 +198,12 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB boolean isAlternative = false; boolean isDefaultBean = false; List stereotypes = new ArrayList<>(); + Set beanDefiningAnnotationScopes = new HashSet<>(); String name = null; for (AnnotationInstance annotation : beanDeployment.getAnnotations(producerField)) { - if (DotNames.NAMED.equals(annotation.name())) { + DotName annotationName = annotation.name(); + if (DotNames.NAMED.equals(annotationName)) { AnnotationValue nameValue = annotation.value(); if (nameValue != null) { name = nameValue.asString(); @@ -197,6 +212,10 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB annotation = normalizedNamedQualifier(name, annotation); } } + BeanDefiningAnnotation bda = beanDeployment.getBeanDefiningAnnotation(annotationName); + if (bda != null && bda.getDefaultScope() != null) { + beanDefiningAnnotationScopes.add(beanDeployment.getScope(bda.getDefaultScope())); + } Collection qualifierCollection = beanDeployment.extractQualifiers(annotation); for (AnnotationInstance qualifierAnnotation : qualifierCollection) { // Qualifiers @@ -206,11 +225,11 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB // we needn't process it further, the annotation was a qualifier (or multiple repeating ones) continue; } - if (DotNames.ALTERNATIVE.equals(annotation.name())) { + if (DotNames.ALTERNATIVE.equals(annotationName)) { isAlternative = true; continue; } - if (DotNames.ALTERNATIVE_PRIORITY.equals(annotation.name())) { + if (DotNames.ALTERNATIVE_PRIORITY.equals(annotationName)) { isAlternative = true; priority = annotation.value().asInt(); continue; @@ -220,21 +239,21 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB priority = annotation.value().asInt(); continue; } - if (priority == null && DotNames.ARC_PRIORITY.equals(annotation.name())) { + if (priority == null && DotNames.ARC_PRIORITY.equals(annotationName)) { priority = annotation.value().asInt(); continue; } - ScopeInfo scopeAnnotation = beanDeployment.getScope(annotation.name()); + ScopeInfo scopeAnnotation = beanDeployment.getScope(annotationName); if (scopeAnnotation != null) { scopes.add(scopeAnnotation); continue; } - StereotypeInfo stereotype = beanDeployment.getStereotype(annotation.name()); + StereotypeInfo stereotype = beanDeployment.getStereotype(annotationName); if (stereotype != null) { stereotypes.add(stereotype); continue; } - if (DotNames.DEFAULT_BEAN.equals(annotation.name())) { + if (DotNames.DEFAULT_BEAN.equals(annotationName)) { isDefaultBean = true; continue; } @@ -243,9 +262,15 @@ static BeanInfo createProducerField(FieldInfo producerField, BeanInfo declaringB if (scopes.size() > 1) { throw multipleScopesFound("Producer field " + producerField, scopes); } + // 1. Explicit scope + // 2. Stereotype scope + // 3. Bean defining annotation default scope ScopeInfo scope; if (scopes.isEmpty()) { scope = initStereotypeScope(stereotypes, producerField, beanDeployment); + if (scope == null) { + scope = initBeanDefiningAnnotationScope(beanDefiningAnnotationScopes, producerField); + } } else { scope = scopes.get(0); } @@ -293,27 +318,28 @@ static ScopeInfo initStereotypeScope(List stereotypes, Annotatio return null; } final Set stereotypeScopes = new HashSet<>(); - final Set additionalBDAScopes = new HashSet<>(); for (StereotypeInfo stereotype : stereotypes) { - if (!stereotype.isAdditionalBeanDefiningAnnotation()) { - ScopeInfo defaultScope = stereotype.getDefaultScope(); - if (defaultScope == null) { - List parentStereotypes = new ArrayList<>(stereotype.getParentStereotypes().size()); - for (AnnotationInstance annotation : stereotype.getParentStereotypes()) { - StereotypeInfo parentStereotype = beanDeployment.getStereotype(annotation.name()); - parentStereotypes.add(parentStereotype); - } - defaultScope = initStereotypeScope(parentStereotypes, target, beanDeployment); + ScopeInfo defaultScope = stereotype.getDefaultScope(); + if (defaultScope == null) { + List parentStereotypes = new ArrayList<>(stereotype.getParentStereotypes().size()); + for (AnnotationInstance annotation : stereotype.getParentStereotypes()) { + StereotypeInfo parentStereotype = beanDeployment.getStereotype(annotation.name()); + parentStereotypes.add(parentStereotype); } - if (defaultScope != null) { - stereotypeScopes.add(defaultScope); - } - } else { - additionalBDAScopes.add(stereotype.getDefaultScope()); + defaultScope = initStereotypeScope(parentStereotypes, target, beanDeployment); } + if (defaultScope != null) { + stereotypeScopes.add(defaultScope); + } + } + return BeanDeployment.getValidScope(stereotypeScopes, target); + } + + static ScopeInfo initBeanDefiningAnnotationScope(Set beanDefiningAnnotationScopes, AnnotationTarget target) { + if (beanDefiningAnnotationScopes.isEmpty()) { + return null; } - // if the stereotypeScopes set is empty, operate on additional BDA stereotypes instead - return BeanDeployment.getValidScope(stereotypeScopes.isEmpty() ? additionalBDAScopes : stereotypeScopes, target); + return BeanDeployment.getValidScope(beanDefiningAnnotationScopes, target); } static boolean initStereotypeAlternative(List stereotypes) { @@ -965,8 +991,10 @@ void processAnnotation(AnnotationInstance annotation, BeanDeployment beanDeployment, Set qualifiers, List stereotypes, - List scopes) { - if (DotNames.NAMED.equals(annotation.name())) { + List scopes, + Set beanDefiningAnnotationScopes) { + DotName annotationName = annotation.name(); + if (DotNames.NAMED.equals(annotationName)) { AnnotationValue nameValue = annotation.value(); if (nameValue != null) { name = nameValue.asString(); @@ -975,45 +1003,46 @@ void processAnnotation(AnnotationInstance annotation, annotation = normalizedNamedQualifier(name, annotation); } } + BeanDefiningAnnotation bda = beanDeployment.getBeanDefiningAnnotation(annotationName); + if (bda != null && bda.getDefaultScope() != null) { + beanDefiningAnnotationScopes.add(beanDeployment.getScope(bda.getDefaultScope())); + } // Qualifiers Collection qualifierCollection = beanDeployment.extractQualifiers(annotation); for (AnnotationInstance qualifierAnnotation : qualifierCollection) { qualifiers.add(qualifierAnnotation); } - // Treat the case when an additional bean defining annotation that is also a qualifier declares the default scope - StereotypeInfo stereotype = beanDeployment.getStereotype(annotation.name()); - if (stereotype != null) { - stereotypes.add(stereotype); - return; - } if (!qualifierCollection.isEmpty()) { // we needn't process it further, the annotation was a qualifier (or multiple repeating ones) return; } - if (annotation.name() - .equals(DotNames.ALTERNATIVE)) { + if (annotationName.equals(DotNames.ALTERNATIVE)) { isAlternative = true; return; } - if (annotation.name() - .equals(DotNames.ALTERNATIVE_PRIORITY)) { + if (annotationName.equals(DotNames.ALTERNATIVE_PRIORITY)) { isAlternative = true; priority = annotation.value().asInt(); return; } - if (DotNames.DEFAULT_BEAN.equals(annotation.name())) { + if (DotNames.DEFAULT_BEAN.equals(annotationName)) { isDefaultBean = true; return; } - if ((!isAlternative || priority == null) && annotation.name().equals(DotNames.PRIORITY)) { + if ((!isAlternative || priority == null) && annotationName.equals(DotNames.PRIORITY)) { priority = annotation.value().asInt(); return; } - if (priority == null && annotation.name().equals(DotNames.ARC_PRIORITY)) { + if (priority == null && annotationName.equals(DotNames.ARC_PRIORITY)) { priority = annotation.value().asInt(); return; } - ScopeInfo scopeAnnotation = beanDeployment.getScope(annotation.name()); + StereotypeInfo stereotype = beanDeployment.getStereotype(annotationName); + if (stereotype != null) { + stereotypes.add(stereotype); + return; + } + ScopeInfo scopeAnnotation = beanDeployment.getScope(annotationName); if (scopeAnnotation != null) { if (!scopes.contains(scopeAnnotation)) { scopes.add(scopeAnnotation); @@ -1028,22 +1057,29 @@ BeanInfo create() { List stereotypes = new ArrayList<>(); Collection annotations = beanDeployment.getAnnotations(beanClass); + Set beanDefiningAnnotationScopes = new HashSet<>(); for (AnnotationInstance annotation : annotations) { - processAnnotation(annotation, beanClass, beanDeployment, qualifiers, stereotypes, scopes); + processAnnotation(annotation, beanClass, beanDeployment, qualifiers, stereotypes, scopes, + beanDefiningAnnotationScopes); } processSuperClass(beanClass, beanDeployment, qualifiers, stereotypes); if (scopes.size() > 1) { throw multipleScopesFound("Bean class " + beanClass, scopes); } + // 1. Explicit scope (including inherited one) + // 2. Stereotype scope + // 3. Bean defining annotation default scope ScopeInfo scope; if (scopes.isEmpty()) { - // try to search stereotypes for scope - scope = initStereotypeScope(stereotypes, beanClass, beanDeployment); - // if that fails, try inheriting them + // Inheritance of type-level metadata: "A scope type explicitly declared by X and inherited by Y from X takes precedence over default scopes of stereotypes declared or inherited by Y." + scope = inheritScope(beanClass, beanDeployment); if (scope == null) { - scope = inheritScope(beanClass, beanDeployment); + scope = initStereotypeScope(stereotypes, beanClass, beanDeployment); + if (scope == null) { + scope = initBeanDefiningAnnotationScope(beanDefiningAnnotationScopes, beanClass); + } } } else { scope = scopes.get(0); diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeInfo.java index f76e0a38e6f56..2c8d5ffd6bd34 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeInfo.java @@ -15,30 +15,39 @@ public class StereotypeInfo { private final boolean isInherited; private final List parentStereotypes; private final ClassInfo target; - // allows to differentiate between standard stereotype and one that is in fact additional bean defining annotation - private final boolean isAdditionalBeanDefiningAnnotation; - // allows to differentiate between standard stereotype and one that was added through StereotypeRegistrarBuildItem + // used to differentiate between standard stereotype and one that was added through StereotypeRegistrarBuildItem private final boolean isAdditionalStereotype; + /** + * + * @deprecated This constructor will be removed at some time after Quarkus 3.0 + */ + @Deprecated public StereotypeInfo(ScopeInfo defaultScope, List interceptorBindings, boolean alternative, - Integer alternativePriority, - boolean isNamed, boolean isAdditionalBeanDefiningAnnotation, boolean isAdditionalStereotype, + Integer alternativePriority, boolean isNamed, boolean isAdditionalBeanDefiningAnnotation, + boolean isAdditionalStereotype, ClassInfo target, boolean isInherited, List parentStereotypes) { + this(defaultScope, interceptorBindings, alternative, alternativePriority, isNamed, isAdditionalStereotype, target, + isInherited, parentStereotypes); + } + + public StereotypeInfo(ScopeInfo defaultScope, List interceptorBindings, boolean alternative, + Integer alternativePriority, boolean isNamed, boolean isAdditionalStereotype, ClassInfo target, boolean isInherited, + List parentStereotypes) { this.defaultScope = defaultScope; this.interceptorBindings = interceptorBindings; this.alternative = alternative; this.alternativePriority = alternativePriority; this.isNamed = isNamed; - this.target = target; - this.isAdditionalBeanDefiningAnnotation = isAdditionalBeanDefiningAnnotation; - this.isAdditionalStereotype = isAdditionalStereotype; this.isInherited = isInherited; this.parentStereotypes = parentStereotypes; + this.target = target; + this.isAdditionalStereotype = isAdditionalStereotype; } public StereotypeInfo(ScopeInfo defaultScope, List interceptorBindings, boolean alternative, - Integer alternativePriority, - boolean isNamed, ClassInfo target, boolean isInherited, List parentStereotype) { + Integer alternativePriority, boolean isNamed, ClassInfo target, boolean isInherited, + List parentStereotype) { this(defaultScope, interceptorBindings, alternative, alternativePriority, isNamed, false, false, target, isInherited, parentStereotype); } @@ -75,8 +84,13 @@ public DotName getName() { return target.name(); } + /** + * + * @deprecated This method will be removed at some time after Quarkus 3.0 + */ + @Deprecated public boolean isAdditionalBeanDefiningAnnotation() { - return isAdditionalBeanDefiningAnnotation; + return false; } /** @@ -93,7 +107,7 @@ public boolean isAdditionalStereotype() { } public boolean isGenuine() { - return !isAdditionalBeanDefiningAnnotation && !isAdditionalStereotype; + return !isAdditionalStereotype; } public List getParentStereotypes() { diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/inheritance/ScopeInheritanceStereotypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/inheritance/ScopeInheritanceStereotypeTest.java new file mode 100644 index 0000000000000..dd5d99634dee7 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/inheritance/ScopeInheritanceStereotypeTest.java @@ -0,0 +1,37 @@ +package io.quarkus.arc.test.inheritance; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InjectableBean; +import io.quarkus.arc.test.ArcTestContainer; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Model; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class ScopeInheritanceStereotypeTest { + + @RegisterExtension + public ArcTestContainer container = new ArcTestContainer(SuperBean.class, SubBean.class); + + @Test + public void testExplicitScopeTakesPrecedence() { + // Inheritance of type-level metadata: "A scope type explicitly declared by X and inherited by Y from X takes precedence over default scopes of stereotypes declared or inherited by Y." + InjectableBean bean = Arc.container().instance(SubBean.class).getBean(); + assertEquals(ApplicationScoped.class, bean.getScope()); + } + + @ApplicationScoped + static class SuperBean { + + public void ping() { + } + } + + @Model + // should inherit @ApplicationScoped + static class SubBean extends SuperBean { + + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/stereotypes/MultipleStereotypeScopesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/stereotypes/MultipleStereotypeScopesTest.java new file mode 100644 index 0000000000000..5234190b54440 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/stereotypes/MultipleStereotypeScopesTest.java @@ -0,0 +1,45 @@ +package io.quarkus.arc.test.stereotypes; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.test.ArcTestContainer; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Model; +import javax.enterprise.inject.Stereotype; +import javax.enterprise.inject.spi.DefinitionException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class MultipleStereotypeScopesTest { + + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder().beanClasses(ModelBean.class, MyStereotype.class) + .shouldFail().build(); + + @Test + public void testFailure() { + assertNotNull(container.getFailure()); + assertTrue(container.getFailure() instanceof DefinitionException); + } + + @MyStereotype + @Model + static class ModelBean { + + } + + @ApplicationScoped + @Stereotype + @Target({ TYPE, METHOD, FIELD }) + @Retention(RUNTIME) + @interface MyStereotype { + } + +}