Skip to content

Commit

Permalink
Improve stereotypes support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Ladicek committed Jun 22, 2022
1 parent 52db148 commit 26a5640
Show file tree
Hide file tree
Showing 18 changed files with 475 additions and 54 deletions.
22 changes: 22 additions & 0 deletions docs/src/main/asciidoc/cdi-integration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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<DotName> 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 <<annotations_transformer_build_item,annotation transformation>> to add them.

[[injection_point_transformation]]
== Use Case - Injection Point Transformation

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<DotName, Collection<AnnotationInstance>> stereotypes;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -154,6 +155,25 @@ AdditionalBeanBuildItem quarkusApplication(CombinedIndexBuildItem combinedIndex)
.build();
}

@BuildStep
StereotypeRegistrarBuildItem convertLegacyAdditionalStereotypes(List<AdditionalStereotypeBuildItem> buildItems) {
return new StereotypeRegistrarBuildItem(new StereotypeRegistrar() {
@Override
public Set<DotName> getAdditionalStereotypes() {
Set<DotName> 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(
Expand All @@ -167,7 +187,7 @@ public ContextRegistrationPhaseBuildItem initialize(
List<ObserverTransformerBuildItem> observerTransformers,
List<InterceptorBindingRegistrarBuildItem> interceptorBindingRegistrars,
List<QualifierRegistrarBuildItem> qualifierRegistrars,
List<AdditionalStereotypeBuildItem> additionalStereotypeBuildItems,
List<StereotypeRegistrarBuildItem> stereotypeRegistrars,
List<ApplicationClassPredicateBuildItem> applicationClassPredicates,
List<AdditionalBeanBuildItem> additionalBeans,
List<ResourceAnnotationBuildItem> resourceAnnotations,
Expand Down Expand Up @@ -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<DotName, Collection<AnnotationInstance>> 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
Expand All @@ -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<BeanInfo>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -32,20 +32,20 @@ public class AutoProducerMethodsProcessor {
@BuildStep
void annotationTransformer(ArcConfig config, BeanArchiveIndexBuildItem beanArchiveIndex,
CustomScopeAnnotationsBuildItem scopes,
List<AdditionalStereotypeBuildItem> additionalStereotypes,
List<StereotypeRegistrarBuildItem> stereotypeRegistrars,
BuildProducer<AnnotationsTransformerBuildItem> annotationsTransformer) throws Exception {
if (!config.autoProducerMethods) {
return;
}
List<DotName> qualifiersAndStereotypes = new ArrayList<>();
Set<DotName> 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() {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,10 @@ AnnotationsTransformerBuildItem annotationTransformer(BeanArchiveIndexBuildItem
stereotypeAnnotations.add(annotationClass.name());
}
}
List<DotName> healthAnnotations = new ArrayList<>(5);
// TODO additional stereotypes? see AutoProducerMethodsProcessor.annotationTransformer
// (note that CustomScopeAnnotationsBuildItem ignores ArC annotation transformations)

List<DotName> healthAnnotations = new ArrayList<>(6);
healthAnnotations.add(LIVENESS);
healthAnnotations.add(READINESS);
healthAnnotations.add(STARTUP);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand All @@ -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<DotName> SPRING_STEREOTYPE_ANNOTATIONS = Arrays.stream(new DotName[] {
private static final Set<DotName> 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");
Expand Down Expand Up @@ -120,18 +120,24 @@ SpringBeanNameToDotNameBuildItem createBeanNamesMap(BeanArchiveIndexBuildItem be
@BuildStep
AnnotationsTransformerBuildItem beanTransformer(
final BeanArchiveIndexBuildItem beanArchiveIndexBuildItem,
final BuildProducer<AdditionalStereotypeBuildItem> additionalStereotypeBuildItemBuildProducer) {
final BuildProducer<StereotypeRegistrarBuildItem> stereotypeRegistrarProducer) {
final IndexView index = beanArchiveIndexBuildItem.getIndex();
final Map<DotName, Set<DotName>> stereotypeScopes = getStereotypeScopes(index);
final Map<DotName, Collection<AnnotationInstance>> instances = new HashMap<>();
final Set<DotName> 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<DotName> getAdditionalStereotypes() {
return stereotypeAnnotations;
}
}));
return new AnnotationsTransformerBuildItem(context -> {
final Collection<AnnotationInstance> annotations = context.getAnnotations();
if (annotations.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -52,6 +77,7 @@ public void done() {
.qualifiers(qualifiers)
.alternative(alternative)
.priority(priority)
.stereotypes(stereotypes)
.name(name)
.creator(creatorConsumer)
.destroyer(destroyerConsumer)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,7 +34,8 @@ public abstract class BeanConfiguratorBase<THIS extends BeanConfiguratorBase<THI
protected final Set<Type> types;
protected final Set<AnnotationInstance> qualifiers;
protected ScopeInfo scope;
protected boolean alternative;
protected Boolean alternative;
protected final List<StereotypeInfo> stereotypes;
protected String name;
protected Consumer<MethodCreator> creatorConsumer;
protected Consumer<MethodCreator> destroyerConsumer;
Expand All @@ -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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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)}.
Expand Down
Loading

0 comments on commit 26a5640

Please sign in to comment.