From 26a5640743a171d48d7e330210fe6bcf6c885bee Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 9 May 2022 09:45:08 +0200 Subject: [PATCH] Improve stereotypes support This commit adds a better API for registering custom stereotypes, similar to existing APIs: `StereotypeRegistrar` and `StereotypeRegistrarBuildItem`. The existing `AdditionalStereotypeBuildItem` is very confusing to use, so it is deprecated and all its usages are replaced with the new API. Also, this commit allows defining stereotypes for synthetic beans. Scope, name, alternative status and priority are all applied to the synthetic bean, but interceptor bindings are not. We don't apply interceptors to synthetic beans in general, this case is not an exception. --- docs/src/main/asciidoc/cdi-integration.adoc | 22 +++ .../AdditionalStereotypeBuildItem.java | 7 +- .../quarkus/arc/deployment/ArcProcessor.java | 31 ++++- .../AutoProducerMethodsProcessor.java | 14 +- .../StereotypeRegistrarBuildItem.java | 21 +++ .../deployment/SmallRyeHealthProcessor.java | 5 +- .../di/deployment/SpringDIProcessor.java | 28 ++-- .../arc/processor/BeanConfigurator.java | 26 ++++ .../arc/processor/BeanConfiguratorBase.java | 19 ++- .../quarkus/arc/processor/BeanDeployment.java | 32 +++-- .../quarkus/arc/processor/BeanProcessor.java | 25 +++- .../java/io/quarkus/arc/processor/Beans.java | 8 +- .../quarkus/arc/processor/StereotypeInfo.java | 21 ++- .../arc/processor/StereotypeRegistrar.java | 17 +++ .../io/quarkus/arc/AlternativePriority.java | 2 +- .../io/quarkus/arc/test/ArcTestContainer.java | 12 ++ .../SyntheticBeanWithStereotypeTest.java | 128 ++++++++++++++++++ .../AdditionalStereotypesTest.java | 111 +++++++++++++++ 18 files changed, 475 insertions(+), 54 deletions(-) create mode 100644 extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StereotypeRegistrarBuildItem.java create mode 100644 independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeRegistrar.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java create mode 100644 independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java diff --git a/docs/src/main/asciidoc/cdi-integration.adoc b/docs/src/main/asciidoc/cdi-integration.adoc index 12317c0764a08d..73cfe4d4fa845b 100644 --- a/docs/src/main/asciidoc/cdi-integration.adoc +++ b/docs/src/main/asciidoc/cdi-integration.adoc @@ -478,6 +478,28 @@ QualifierRegistrarBuildItem addQualifiers() { } ---- +== Use Case - Additional Stereotypes + +It is sometimes useful to register an existing annotation that is not annotated with `@javax.enterprise.inject.Stereotype` as a CDI stereotype. +This is similar to what CDI achieves through `BeforeBeanDiscovery#addStereotype()`. +We are going to use `StereotypeRegistrarBuildItem` to get it done. + +.`StereotypeRegistrarBuildItem` Example +[source,java] +---- +@BuildStep +StereotypeRegistrarBuildItem addStereotypes() { + return new StereotypeRegistrarBuildItem(new StereotypeRegistrar() { + @Override + public Set getAdditionalStereotypes() { + return Collections.singleton(DotName.createSimple(NotAStereotype.class.getName())); + } + }); +} +---- + +If the newly registered stereotype annotation doesn't have the appropriate meta-annotations, such as scope or interceptor bindings, use an <> to add them. + [[injection_point_transformation]] == Use Case - Injection Point Transformation diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalStereotypeBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalStereotypeBuildItem.java index 979825edea548e..f70fd4624f4e55 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalStereotypeBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AdditionalStereotypeBuildItem.java @@ -9,8 +9,13 @@ import io.quarkus.builder.item.MultiBuildItem; /** - * A map of additional stereotype classes to their instances that we want to process. + * A map of additional annotation types (that have the same meaning as the {@code @Stereotype} meta-annotation) + * to their occurences on other annotations (that become custom stereotypes). + * + * @deprecated use {@link StereotypeRegistrarBuildItem}; + * this class will be removed at some time after Quarkus 3.0 */ +@Deprecated public final class AdditionalStereotypeBuildItem extends MultiBuildItem { private final Map> stereotypes; 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 06b60eb867bb18..0a79235181bf1c 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 @@ -71,6 +71,7 @@ import io.quarkus.arc.processor.ReflectionRegistration; import io.quarkus.arc.processor.ResourceOutput; import io.quarkus.arc.processor.StereotypeInfo; +import io.quarkus.arc.processor.StereotypeRegistrar; import io.quarkus.arc.processor.Transformation; import io.quarkus.arc.processor.Types; import io.quarkus.arc.runtime.AdditionalBean; @@ -154,6 +155,25 @@ AdditionalBeanBuildItem quarkusApplication(CombinedIndexBuildItem combinedIndex) .build(); } + @BuildStep + StereotypeRegistrarBuildItem convertLegacyAdditionalStereotypes(List buildItems) { + return new StereotypeRegistrarBuildItem(new StereotypeRegistrar() { + @Override + public Set getAdditionalStereotypes() { + Set result = new HashSet<>(); + for (AdditionalStereotypeBuildItem buildItem : buildItems) { + result.addAll(buildItem.getStereotypes() + .values() + .stream() + .flatMap(Collection::stream) + .map(AnnotationInstance::name) + .collect(Collectors.toSet())); + } + return result; + } + }); + } + // PHASE 1 - build BeanProcessor @BuildStep public ContextRegistrationPhaseBuildItem initialize( @@ -167,7 +187,7 @@ public ContextRegistrationPhaseBuildItem initialize( List observerTransformers, List interceptorBindingRegistrars, List qualifierRegistrars, - List additionalStereotypeBuildItems, + List stereotypeRegistrars, List applicationClassPredicates, List additionalBeans, List resourceAnnotations, @@ -251,11 +271,6 @@ public void transform(TransformationContext transformationContext) { .map((s) -> new BeanDefiningAnnotation(s.getName(), s.getDefaultScope())).collect(Collectors.toList()); beanDefiningAnnotations.add(new BeanDefiningAnnotation(ADDITIONAL_BEAN, null)); builder.setAdditionalBeanDefiningAnnotations(beanDefiningAnnotations); - final Map> additionalStereotypes = new HashMap<>(); - for (final AdditionalStereotypeBuildItem item : additionalStereotypeBuildItems) { - additionalStereotypes.putAll(item.getStereotypes()); - } - builder.setAdditionalStereotypes(additionalStereotypes); builder.addResourceAnnotations( resourceAnnotations.stream().map(ResourceAnnotationBuildItem::getName).collect(Collectors.toList())); // register all annotation transformers @@ -278,6 +293,10 @@ public void transform(TransformationContext transformationContext) { for (QualifierRegistrarBuildItem registrar : qualifierRegistrars) { builder.addQualifierRegistrar(registrar.getQualifierRegistrar()); } + // register additional stereotypes + for (StereotypeRegistrarBuildItem registrar : stereotypeRegistrars) { + builder.addStereotypeRegistrar(registrar.getStereotypeRegistrar()); + } builder.setRemoveUnusedBeans(arcConfig.shouldEnableBeanRemoval()); if (arcConfig.shouldOnlyKeepAppBeans()) { builder.addRemovalExclusion(new AbstractCompositeApplicationClassesPredicate( diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoProducerMethodsProcessor.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoProducerMethodsProcessor.java index 9390adae838284..a1cf21c30d99e5 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoProducerMethodsProcessor.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoProducerMethodsProcessor.java @@ -3,7 +3,7 @@ import static io.quarkus.arc.processor.Annotations.contains; import static io.quarkus.arc.processor.Annotations.containsAny; -import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Set; @@ -32,20 +32,20 @@ public class AutoProducerMethodsProcessor { @BuildStep void annotationTransformer(ArcConfig config, BeanArchiveIndexBuildItem beanArchiveIndex, CustomScopeAnnotationsBuildItem scopes, - List additionalStereotypes, + List stereotypeRegistrars, BuildProducer annotationsTransformer) throws Exception { if (!config.autoProducerMethods) { return; } - List qualifiersAndStereotypes = new ArrayList<>(); + Set qualifiersAndStereotypes = new HashSet<>(); for (AnnotationInstance qualifier : beanArchiveIndex.getIndex().getAnnotations(DotNames.QUALIFIER)) { qualifiersAndStereotypes.add(qualifier.target().asClass().name()); } - for (AnnotationInstance qualifier : beanArchiveIndex.getIndex().getAnnotations(DotNames.STEREOTYPE)) { - qualifiersAndStereotypes.add(qualifier.target().asClass().name()); + for (AnnotationInstance stereotype : beanArchiveIndex.getIndex().getAnnotations(DotNames.STEREOTYPE)) { + qualifiersAndStereotypes.add(stereotype.target().asClass().name()); } - for (AdditionalStereotypeBuildItem additionalStereotype : additionalStereotypes) { - qualifiersAndStereotypes.addAll(additionalStereotype.getStereotypes().keySet()); + for (StereotypeRegistrarBuildItem stereotypeRegistrar : stereotypeRegistrars) { + qualifiersAndStereotypes.addAll(stereotypeRegistrar.getStereotypeRegistrar().getAdditionalStereotypes()); } LOGGER.debugf("Add missing @Produces to methods annotated with %s", qualifiersAndStereotypes); annotationsTransformer.produce(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StereotypeRegistrarBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StereotypeRegistrarBuildItem.java new file mode 100644 index 00000000000000..578850c253ffda --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StereotypeRegistrarBuildItem.java @@ -0,0 +1,21 @@ +package io.quarkus.arc.deployment; + +import io.quarkus.arc.processor.StereotypeRegistrar; +import io.quarkus.builder.item.MultiBuildItem; + +/** + * Makes it possible to register annotations that should be considered stereotypes but are not annotated with + * {@code javax.enterprise.inject.Stereotype}. + */ +public final class StereotypeRegistrarBuildItem extends MultiBuildItem { + + private final StereotypeRegistrar registrar; + + public StereotypeRegistrarBuildItem(StereotypeRegistrar registrar) { + this.registrar = registrar; + } + + public StereotypeRegistrar getStereotypeRegistrar() { + return registrar; + } +} diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java index 3ea4143b196ac5..25c2fdf837c0e3 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/SmallRyeHealthProcessor.java @@ -363,7 +363,10 @@ AnnotationsTransformerBuildItem annotationTransformer(BeanArchiveIndexBuildItem stereotypeAnnotations.add(annotationClass.name()); } } - List healthAnnotations = new ArrayList<>(5); + // TODO additional stereotypes? see AutoProducerMethodsProcessor.annotationTransformer + // (note that CustomScopeAnnotationsBuildItem ignores ArC annotation transformations) + + List healthAnnotations = new ArrayList<>(6); healthAnnotations.add(LIVENESS); healthAnnotations.add(READINESS); healthAnnotations.add(STARTUP); diff --git a/extensions/spring-di/deployment/src/main/java/io/quarkus/spring/di/deployment/SpringDIProcessor.java b/extensions/spring-di/deployment/src/main/java/io/quarkus/spring/di/deployment/SpringDIProcessor.java index 1c0ea355fa5de3..51b12c5636c104 100644 --- a/extensions/spring-di/deployment/src/main/java/io/quarkus/spring/di/deployment/SpringDIProcessor.java +++ b/extensions/spring-di/deployment/src/main/java/io/quarkus/spring/di/deployment/SpringDIProcessor.java @@ -27,21 +27,22 @@ import org.jboss.jandex.MethodParameterInfo; import org.jboss.jandex.Type; -import io.quarkus.arc.deployment.AdditionalStereotypeBuildItem; import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem; +import io.quarkus.arc.deployment.StereotypeRegistrarBuildItem; import io.quarkus.arc.processor.BuiltinScope; import io.quarkus.arc.processor.DotNames; +import io.quarkus.arc.processor.StereotypeRegistrar; import io.quarkus.arc.processor.Transformation; import io.quarkus.deployment.Feature; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; -/* - * A simple processor that maps annotations Spring DI annotation to CDI annotation +/** + * A simple processor that maps Spring DI annotations to CDI annotations. * Arc's handling of annotation mapping (by creating an extra abstraction layer on top of the Jandex index) - * suits this sort of handling perfectly + * suits this sort of handling perfectly. */ public class SpringDIProcessor { @@ -50,11 +51,10 @@ public class SpringDIProcessor { static final DotName SPRING_COMPONENT = DotName.createSimple("org.springframework.stereotype.Component"); static final DotName SPRING_SERVICE = DotName.createSimple("org.springframework.stereotype.Service"); static final DotName SPRING_REPOSITORY = DotName.createSimple("org.springframework.stereotype.Repository"); - private static final Set SPRING_STEREOTYPE_ANNOTATIONS = Arrays.stream(new DotName[] { + private static final Set SPRING_STEREOTYPE_ANNOTATIONS = Set.of( SPRING_COMPONENT, SPRING_SERVICE, - SPRING_REPOSITORY, - }).collect(Collectors.toSet()); + SPRING_REPOSITORY); private static final DotName CONFIGURATION_ANNOTATION = DotName .createSimple("org.springframework.context.annotation.Configuration"); @@ -120,18 +120,24 @@ SpringBeanNameToDotNameBuildItem createBeanNamesMap(BeanArchiveIndexBuildItem be @BuildStep AnnotationsTransformerBuildItem beanTransformer( final BeanArchiveIndexBuildItem beanArchiveIndexBuildItem, - final BuildProducer additionalStereotypeBuildItemBuildProducer) { + final BuildProducer stereotypeRegistrarProducer) { final IndexView index = beanArchiveIndexBuildItem.getIndex(); final Map> stereotypeScopes = getStereotypeScopes(index); - final Map> instances = new HashMap<>(); + final Set stereotypeAnnotations = new HashSet<>(); for (final DotName name : stereotypeScopes.keySet()) { - instances.put(name, index.getAnnotations(name) + stereotypeAnnotations.addAll(index.getAnnotations(name) .stream() .filter(it -> it.target().kind() == AnnotationTarget.Kind.CLASS && it.target().asClass().isAnnotation()) + .map(AnnotationInstance::name) .collect(Collectors.toSet())); } - additionalStereotypeBuildItemBuildProducer.produce(new AdditionalStereotypeBuildItem(instances)); + stereotypeRegistrarProducer.produce(new StereotypeRegistrarBuildItem(new StereotypeRegistrar() { + @Override + public Set getAdditionalStereotypes() { + return stereotypeAnnotations; + } + })); return new AnnotationsTransformerBuildItem(context -> { final Collection annotations = context.getAnnotations(); if (annotations.isEmpty()) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java index b9a83ed191036b..645f6951095472 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfigurator.java @@ -43,6 +43,31 @@ public void done() { if (implClass == null) { throw new IllegalStateException("Unable to find the bean class in the index: " + implClazz); } + + ScopeInfo scope = this.scope; + if (scope == null) { + scope = Beans.initStereotypeScope(stereotypes, implClass, beanDeployment); + } + if (scope == null) { + scope = BuiltinScope.DEPENDENT.getInfo(); + } + + String name = this.name; + if (name == null) { + name = Beans.initStereotypeName(stereotypes, implClass); + } + + Boolean alternative = this.alternative; + if (alternative == null) { + alternative = Beans.initStereotypeAlternative(stereotypes); + } + + Integer priority = this.priority; + if (priority == null) { + // TODO use `Beans.initAlternativePriority` instead? IMHO no for synthetic beans + priority = Beans.initStereotypeAlternativePriority(stereotypes); + } + beanConsumer.accept(new BeanInfo.Builder() .implClazz(implClass) .providerType(providerType) @@ -52,6 +77,7 @@ public void done() { .qualifiers(qualifiers) .alternative(alternative) .priority(priority) + .stereotypes(stereotypes) .name(name) .creator(creatorConsumer) .destroyer(destroyerConsumer) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java index ebdc449f3101e2..5e325dfab60aa8 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanConfiguratorBase.java @@ -9,8 +9,10 @@ import io.quarkus.gizmo.ResultHandle; import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; +import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Consumer; @@ -32,7 +34,8 @@ public abstract class BeanConfiguratorBase types; protected final Set qualifiers; protected ScopeInfo scope; - protected boolean alternative; + protected Boolean alternative; + protected final List stereotypes; protected String name; protected Consumer creatorConsumer; protected Consumer destroyerConsumer; @@ -47,7 +50,7 @@ protected BeanConfiguratorBase(DotName implClazz) { this.implClazz = implClazz; this.types = new HashSet<>(); this.qualifiers = new HashSet<>(); - this.scope = BuiltinScope.DEPENDENT.getInfo(); + this.stereotypes = new ArrayList<>(); this.removable = true; } @@ -68,6 +71,8 @@ public THIS read(BeanConfiguratorBase base) { scope(base.scope); alternative = base.alternative; priority = base.priority; + stereotypes.clear(); + stereotypes.addAll(base.stereotypes); name(base.name); creator(base.creatorConsumer); destroyer(base.destroyerConsumer); @@ -197,6 +202,16 @@ public THIS priority(int value) { return self(); } + public THIS addStereotype(StereotypeInfo stereotype) { + this.stereotypes.add(stereotype); + return self(); + } + + public THIS stereotypes(StereotypeInfo... stereotypes) { + Collections.addAll(this.stereotypes, stereotypes); + return self(); + } + /** * The provider type is the "real" type of the bean instance created via * {@link InjectableReferenceProvider#get(CreationalContext)}. 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 4f8e8911da1385..5f1b4193208e41 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 @@ -175,8 +175,13 @@ public class BeanDeployment { repeatingInterceptorBindingAnnotations = findContainerAnnotations(interceptorBindings, this.beanArchiveIndex); buildContextPut(Key.INTERCEPTOR_BINDINGS.asString(), Collections.unmodifiableMap(interceptorBindings)); + Set additionalStereotypes = new HashSet<>(); + for (StereotypeRegistrar stereotypeRegistrar : builder.stereotypeRegistrars) { + additionalStereotypes.addAll(stereotypeRegistrar.getAdditionalStereotypes()); + } + this.stereotypes = findStereotypes(this.beanArchiveIndex, interceptorBindings, beanDefiningAnnotations, customContexts, - builder.additionalStereotypes, annotationStore); + additionalStereotypes, annotationStore); buildContextPut(Key.STEREOTYPES.asString(), Collections.unmodifiableMap(stereotypes)); this.transitiveInterceptorBindings = findTransitiveInterceptorBindings(interceptorBindings.keySet(), @@ -729,19 +734,17 @@ private static Set recursiveBuild(DotName name, private Map findStereotypes(IndexView index, Map interceptorBindings, Collection additionalBeanDefiningAnnotations, Map> customContexts, - Map> additionalStereotypes, AnnotationStore annotationStore) { + Set additionalStereotypes, AnnotationStore annotationStore) { Map stereotypes = new HashMap<>(); - final List stereotypeAnnotations = new ArrayList<>(index.getAnnotations(DotNames.STEREOTYPE)); - for (final Collection annotations : additionalStereotypes.values()) { - stereotypeAnnotations.addAll(annotations); - } + Set stereotypeNames = new HashSet<>(); - for (AnnotationInstance stereotype : stereotypeAnnotations) { - stereotypeNames.add(stereotype.target().asClass().name()); + for (AnnotationInstance annotation : index.getAnnotations(DotNames.STEREOTYPE)) { + stereotypeNames.add(annotation.target().asClass().name()); } - for (AnnotationInstance stereotype : stereotypeAnnotations) { - final DotName stereotypeName = stereotype.target().asClass().name(); + stereotypeNames.addAll(additionalStereotypes); + + for (DotName stereotypeName : stereotypeNames) { ClassInfo stereotypeClass = getClassByName(index, stereotypeName); if (stereotypeClass != null && !isExcluded(stereotypeClass)) { @@ -769,6 +772,11 @@ private Map findStereotypes(IndexView index, Map findStereotypes(IndexView index, Map additionalBeanDefiningAnnotations; - Map> additionalStereotypes; ResourceOutput output; ReflectionRegistration reflectionRegistration; @@ -303,6 +302,7 @@ public static class Builder { final List contextRegistrars; final List qualifierRegistrars; final List interceptorBindingRegistrars; + final List stereotypeRegistrars; final List beanDeploymentValidators; final List>> suppressConditionGenerators; @@ -323,7 +323,6 @@ public static class Builder { public Builder() { name = DEFAULT_NAME; additionalBeanDefiningAnnotations = Collections.emptySet(); - additionalStereotypes = Collections.emptyMap(); reflectionRegistration = ReflectionRegistration.NOOP; resourceAnnotations = new ArrayList<>(); annotationTransformers = new ArrayList<>(); @@ -334,6 +333,7 @@ public Builder() { contextRegistrars = new ArrayList<>(); qualifierRegistrars = new ArrayList<>(); interceptorBindingRegistrars = new ArrayList<>(); + stereotypeRegistrars = new ArrayList<>(); beanDeploymentValidators = new ArrayList<>(); suppressConditionGenerators = new ArrayList<>(); @@ -388,9 +388,23 @@ public Builder setAdditionalBeanDefiningAnnotations( return this; } + /** + * @deprecated use {@link #addStereotypeRegistrar(StereotypeRegistrar)}; + * this method will be removed at some time after Quarkus 3.0 + */ + @Deprecated public Builder setAdditionalStereotypes(Map> additionalStereotypes) { Objects.requireNonNull(additionalStereotypes); - this.additionalStereotypes = additionalStereotypes; + this.stereotypeRegistrars.add(new StereotypeRegistrar() { + @Override + public Set getAdditionalStereotypes() { + return additionalStereotypes.values() + .stream() + .flatMap(Collection::stream) + .map(AnnotationInstance::name) + .collect(Collectors.toSet()); + } + }); return this; } @@ -404,6 +418,11 @@ public Builder addInterceptorBindingRegistrar(InterceptorBindingRegistrar bindin return this; } + public Builder addStereotypeRegistrar(StereotypeRegistrar stereotypeRegistrar) { + this.stereotypeRegistrars.add(stereotypeRegistrar); + return this; + } + public Builder setOutput(ResourceOutput output) { this.output = output; return this; 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 4a8eeaa552a7fc..bbbc77acc67662 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 @@ -287,7 +287,7 @@ private static DefinitionException multipleScopesFound(String baseMessage, List< + scopes.stream().map(s -> s.getDotName().toString()).collect(Collectors.joining(", "))); } - private static ScopeInfo initStereotypeScope(List stereotypes, AnnotationTarget target, + static ScopeInfo initStereotypeScope(List stereotypes, AnnotationTarget target, BeanDeployment beanDeployment) { if (stereotypes.isEmpty()) { return null; @@ -316,7 +316,7 @@ private static ScopeInfo initStereotypeScope(List stereotypes, A return BeanDeployment.getValidScope(stereotypeScopes.isEmpty() ? additionalBDAScopes : stereotypeScopes, target); } - private static boolean initStereotypeAlternative(List stereotypes) { + static boolean initStereotypeAlternative(List stereotypes) { if (stereotypes.isEmpty()) { return false; } @@ -328,7 +328,7 @@ private static boolean initStereotypeAlternative(List stereotype return false; } - private static Integer initStereotypeAlternativePriority(List stereotypes) { + static Integer initStereotypeAlternativePriority(List stereotypes) { if (stereotypes.isEmpty()) { return null; } @@ -340,7 +340,7 @@ private static Integer initStereotypeAlternativePriority(List st return null; } - private static String initStereotypeName(List stereotypes, AnnotationTarget target) { + static String initStereotypeName(List stereotypes, AnnotationTarget target) { if (stereotypes.isEmpty()) { return null; } 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 ad7c96239bcce1..f76e0a38e6f560 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 @@ -17,12 +17,12 @@ public class StereotypeInfo { 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 an AdditionalStereotypeBuildItem - private final boolean isAdditionalStereotypeBuildItem; + // allows to differentiate between standard stereotype and one that was added through StereotypeRegistrarBuildItem + private final boolean isAdditionalStereotype; public StereotypeInfo(ScopeInfo defaultScope, List interceptorBindings, boolean alternative, Integer alternativePriority, - boolean isNamed, boolean isAdditionalBeanDefiningAnnotation, boolean isAdditionalStereotypeBuildItem, + boolean isNamed, boolean isAdditionalBeanDefiningAnnotation, boolean isAdditionalStereotype, ClassInfo target, boolean isInherited, List parentStereotypes) { this.defaultScope = defaultScope; this.interceptorBindings = interceptorBindings; @@ -31,7 +31,7 @@ public StereotypeInfo(ScopeInfo defaultScope, List intercept this.isNamed = isNamed; this.target = target; this.isAdditionalBeanDefiningAnnotation = isAdditionalBeanDefiningAnnotation; - this.isAdditionalStereotypeBuildItem = isAdditionalStereotypeBuildItem; + this.isAdditionalStereotype = isAdditionalStereotype; this.isInherited = isInherited; this.parentStereotypes = parentStereotypes; } @@ -79,12 +79,21 @@ public boolean isAdditionalBeanDefiningAnnotation() { return isAdditionalBeanDefiningAnnotation; } + /** + * @deprecated use {@link #isAdditionalStereotype()}; + * this method will be removed at some time after Quarkus 3.0 + */ + @Deprecated public boolean isAdditionalStereotypeBuildItem() { - return isAdditionalStereotypeBuildItem; + return isAdditionalStereotype; + } + + public boolean isAdditionalStereotype() { + return isAdditionalStereotype; } public boolean isGenuine() { - return !isAdditionalBeanDefiningAnnotation && !isAdditionalStereotypeBuildItem; + return !isAdditionalBeanDefiningAnnotation && !isAdditionalStereotype; } public List getParentStereotypes() { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeRegistrar.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeRegistrar.java new file mode 100644 index 00000000000000..1f4f5e5a37c4ad --- /dev/null +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/StereotypeRegistrar.java @@ -0,0 +1,17 @@ +package io.quarkus.arc.processor; + +import java.util.Set; +import javax.enterprise.inject.Stereotype; +import org.jboss.jandex.DotName; + +/** + * Makes it possible to turn an annotation into a stereotype without adding a {@link Stereotype} annotation to it. + */ +public interface StereotypeRegistrar extends BuildExtension { + + /** + * Returns a set of annotation types (their names) that should be treated as stereotypes. + * To modify (meta-)annotations on these annotations, use {@link AnnotationsTransformer}. + */ + Set getAdditionalStereotypes(); +} diff --git a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/AlternativePriority.java b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/AlternativePriority.java index f6aa7645fa9dcf..cf3688e34ef9bb 100644 --- a/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/AlternativePriority.java +++ b/independent-projects/arc/runtime/src/main/java/io/quarkus/arc/AlternativePriority.java @@ -10,7 +10,7 @@ * If a bean is annotated with this annotation, it is considered an enabled alternative with given priority. * Effectively, this is a shortcut for {@code Alternative} plus {@code Priority} annotations. * - * This annotation can be used not only on bean classes, but also method and field producers (unlike pure {@code Priority}. + * This annotation can be used not only on bean classes, but also method and field producers (unlike pure {@code Priority}). * * @deprecated Use {@link Alternative} and {@link io.quarkus.arc.Priority}/{@link javax.annotation.Priority} instead */ diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java index a4ac9d0625cdce..a6815a9963bec2 100644 --- a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/ArcTestContainer.java @@ -17,6 +17,7 @@ import io.quarkus.arc.processor.ObserverTransformer; import io.quarkus.arc.processor.QualifierRegistrar; import io.quarkus.arc.processor.ResourceOutput; +import io.quarkus.arc.processor.StereotypeRegistrar; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; @@ -74,6 +75,7 @@ public static class Builder { private final List contextRegistrars; private final List qualifierRegistrars; private final List interceptorBindingRegistrars; + private final List stereotypeRegistrars; private final List annotationsTransformers; private final List injectionsPointsTransformers; private final List observerTransformers; @@ -93,6 +95,7 @@ public Builder() { contextRegistrars = new ArrayList<>(); qualifierRegistrars = new ArrayList<>(); interceptorBindingRegistrars = new ArrayList<>(); + stereotypeRegistrars = new ArrayList<>(); annotationsTransformers = new ArrayList<>(); injectionsPointsTransformers = new ArrayList<>(); observerTransformers = new ArrayList<>(); @@ -161,6 +164,11 @@ public Builder interceptorBindingRegistrars(InterceptorBindingRegistrar... regis return this; } + public Builder stereotypeRegistrars(StereotypeRegistrar... registrars) { + Collections.addAll(this.stereotypeRegistrars, registrars); + return this; + } + public Builder beanDeploymentValidators(BeanDeploymentValidator... validators) { Collections.addAll(this.beanDeploymentValidators, validators); return this; @@ -204,6 +212,7 @@ public ArcTestContainer build() { private final List contextRegistrars; private final List qualifierRegistrars; private final List interceptorBindingRegistrars; + private final List stereotypeRegistrars; private final List annotationsTransformers; private final List injectionPointsTransformers; private final List observerTransformers; @@ -226,6 +235,7 @@ public ArcTestContainer(Class... beanClasses) { this.observerRegistrars = Collections.emptyList(); this.contextRegistrars = Collections.emptyList(); this.interceptorBindingRegistrars = Collections.emptyList(); + this.stereotypeRegistrars = Collections.emptyList(); this.qualifierRegistrars = Collections.emptyList(); this.annotationsTransformers = Collections.emptyList(); this.injectionPointsTransformers = Collections.emptyList(); @@ -248,6 +258,7 @@ public ArcTestContainer(Builder builder) { this.contextRegistrars = builder.contextRegistrars; this.qualifierRegistrars = builder.qualifierRegistrars; this.interceptorBindingRegistrars = builder.interceptorBindingRegistrars; + this.stereotypeRegistrars = builder.stereotypeRegistrars; this.annotationsTransformers = builder.annotationsTransformers; this.injectionPointsTransformers = builder.injectionsPointsTransformers; this.observerTransformers = builder.observerTransformers; @@ -370,6 +381,7 @@ private ClassLoader init(ExtensionContext context) { contextRegistrars.forEach(builder::addContextRegistrar); qualifierRegistrars.forEach(builder::addQualifierRegistrar); interceptorBindingRegistrars.forEach(builder::addInterceptorBindingRegistrar); + stereotypeRegistrars.forEach(builder::addStereotypeRegistrar); annotationsTransformers.forEach(builder::addAnnotationTransformer); injectionPointsTransformers.forEach(builder::addInjectionPointTransformer); observerTransformers.forEach(builder::addObserverTransformer); diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java new file mode 100644 index 00000000000000..d9c02b0f9c30da --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/beans/SyntheticBeanWithStereotypeTest.java @@ -0,0 +1,128 @@ +package io.quarkus.arc.test.buildextension.beans; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.AlternativePriority; +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.BeanRegistrar; +import io.quarkus.arc.processor.StereotypeInfo; +import io.quarkus.arc.processor.StereotypeRegistrar; +import io.quarkus.arc.test.ArcTestContainer; +import io.quarkus.gizmo.MethodDescriptor; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Set; +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Named; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InterceptorBinding; +import javax.interceptor.InvocationContext; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class SyntheticBeanWithStereotypeTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(ToBeStereotype.class, SimpleBinding.class, SimpleInterceptor.class) + .additionalClasses(SomeBean.class) + .stereotypeRegistrars(new MyStereotypeRegistrar()) + .annotationsTransformers(new MyAnnotationTrasnformer()) + .beanRegistrars(new MyBeanRegistrar()) + .build(); + + @Test + public void test() { + InstanceHandle bean = Arc.container().select(SomeBean.class).getHandle(); + assertEquals(ApplicationScoped.class, bean.getBean().getScope()); + assertEquals("someBean", bean.getBean().getName()); + assertTrue(bean.getBean().isAlternative()); + assertEquals(11, bean.getBean().getPriority()); + + SomeBean instance = bean.get(); + assertNotNull(instance); + assertEquals("hello", instance.hello()); + // interceptors are _not_ applied to synthetic beans + } + + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + @interface ToBeStereotype { + } + + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + @InterceptorBinding + @interface SimpleBinding { + } + + @Interceptor + @Priority(1) + @SimpleBinding + static class SimpleInterceptor { + @AroundInvoke + public Object invoke(InvocationContext context) throws Exception { + return "intercepted: " + context.proceed(); + } + } + + @ToBeStereotype + static class SomeBean { + public String hello() { + return "hello"; + } + } + + static class MyStereotypeRegistrar implements StereotypeRegistrar { + @Override + public Set getAdditionalStereotypes() { + return Set.of(DotName.createSimple(ToBeStereotype.class.getName())); + } + } + + static class MyAnnotationTrasnformer implements AnnotationsTransformer { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext transformationContext) { + if (transformationContext.getTarget().asClass().name() + .equals(DotName.createSimple(ToBeStereotype.class.getName()))) { + transformationContext.transform() + .add(ApplicationScoped.class) + .add(SimpleBinding.class) + .add(Named.class) + .add(AlternativePriority.class, AnnotationValue.createIntegerValue("value", 11)) + .done(); + } + } + } + + static class MyBeanRegistrar implements BeanRegistrar { + @Override + public void register(RegistrationContext context) { + StereotypeInfo stereotype = context.get(Key.STEREOTYPES).get(DotName.createSimple(ToBeStereotype.class.getName())); + + context.configure(SomeBean.class) + .types(SomeBean.class) + .stereotypes(stereotype) + .creator(mc -> mc.returnValue(mc.newInstance(MethodDescriptor.ofConstructor(SomeBean.class)))) + .done(); + } + } +} diff --git a/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java new file mode 100644 index 00000000000000..104c4bb87b01d7 --- /dev/null +++ b/independent-projects/arc/tests/src/test/java/io/quarkus/arc/test/buildextension/stereotypes/AdditionalStereotypesTest.java @@ -0,0 +1,111 @@ +package io.quarkus.arc.test.buildextension.stereotypes; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +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.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.arc.processor.StereotypeRegistrar; +import io.quarkus.arc.test.ArcTestContainer; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.Set; +import javax.annotation.Priority; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Alternative; +import javax.inject.Named; +import javax.interceptor.AroundInvoke; +import javax.interceptor.Interceptor; +import javax.interceptor.InterceptorBinding; +import javax.interceptor.InvocationContext; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class AdditionalStereotypesTest { + @RegisterExtension + public ArcTestContainer container = ArcTestContainer.builder() + .beanClasses(ToBeStereotype.class, SimpleBinding.class, SimpleInterceptor.class, SomeBean.class) + .stereotypeRegistrars(new MyStereotypeRegistrar()) + .annotationsTransformers(new MyAnnotationTrasnformer()) + .build(); + + @Test + public void test() { + InstanceHandle bean = Arc.container().select(SomeBean.class).getHandle(); + assertEquals(ApplicationScoped.class, bean.getBean().getScope()); + assertEquals("someBean", bean.getBean().getName()); + assertTrue(bean.getBean().isAlternative()); + assertEquals(11, bean.getBean().getPriority()); + + SomeBean instance = bean.get(); + assertNotNull(instance); + assertEquals("intercepted: hello", instance.hello()); + } + + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + @interface ToBeStereotype { + } + + @Target({ TYPE, METHOD, FIELD, PARAMETER }) + @Retention(RUNTIME) + @InterceptorBinding + @interface SimpleBinding { + } + + @Interceptor + @Priority(1) + @SimpleBinding + static class SimpleInterceptor { + @AroundInvoke + public Object invoke(InvocationContext context) throws Exception { + return "intercepted: " + context.proceed(); + } + } + + @ToBeStereotype + static class SomeBean { + public String hello() { + return "hello"; + } + } + + static class MyStereotypeRegistrar implements StereotypeRegistrar { + @Override + public Set getAdditionalStereotypes() { + return Set.of(DotName.createSimple(ToBeStereotype.class.getName())); + } + } + + static class MyAnnotationTrasnformer implements AnnotationsTransformer { + @Override + public boolean appliesTo(AnnotationTarget.Kind kind) { + return kind == AnnotationTarget.Kind.CLASS; + } + + @Override + public void transform(TransformationContext transformationContext) { + if (transformationContext.getTarget().asClass().name() + .equals(DotName.createSimple(ToBeStereotype.class.getName()))) { + transformationContext.transform() + .add(ApplicationScoped.class) + .add(SimpleBinding.class) + .add(Named.class) + .add(Alternative.class) + .add(Priority.class, AnnotationValue.createIntegerValue("value", 11)) + .done(); + } + } + + } +}