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 Aug 17, 2022
1 parent 233f8b8 commit a010610
Show file tree
Hide file tree
Showing 19 changed files with 613 additions and 119 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 @@ -73,6 +73,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 @@ -156,6 +157,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 @@ -169,7 +189,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 @@ -253,11 +273,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 @@ -280,6 +295,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
@@ -1,13 +1,11 @@
package io.quarkus.smallrye.health.deployment;

import static io.quarkus.arc.processor.Annotations.containsAny;
import static io.quarkus.arc.processor.Annotations.getAnnotations;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
Expand All @@ -24,20 +22,15 @@
import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.logging.Logger;

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem;
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanDefiningAnnotationBuildItem;
import io.quarkus.arc.deployment.CustomScopeAnnotationsBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.processor.AnnotationsTransformer;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.deployment.Capabilities;
import io.quarkus.deployment.Capability;
import io.quarkus.deployment.Feature;
Expand Down Expand Up @@ -158,12 +151,12 @@ void build(SmallRyeHealthRecorder recorder,

// Discover the beans annotated with @Health, @Liveness, @Readiness, @Startup, @HealthGroup,
// @HealthGroups and @Wellness even if no scope is defined
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(LIVENESS));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(READINESS));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(STARTUP));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(HEALTH_GROUP));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(HEALTH_GROUPS));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(WELLNESS));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(LIVENESS, BuiltinScope.SINGLETON.getName()));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(READINESS, BuiltinScope.SINGLETON.getName()));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(STARTUP, BuiltinScope.SINGLETON.getName()));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(HEALTH_GROUP, BuiltinScope.SINGLETON.getName()));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(HEALTH_GROUPS, BuiltinScope.SINGLETON.getName()));
beanDefiningAnnotation.produce(new BeanDefiningAnnotationBuildItem(WELLNESS, BuiltinScope.SINGLETON.getName()));

// Add additional beans
additionalBean.produce(new AdditionalBeanBuildItem(QuarkusAsyncHealthCheckFactory.class));
Expand Down Expand Up @@ -350,59 +343,6 @@ ShutdownListenerBuildItem shutdownListener() {
return new ShutdownListenerBuildItem(new ShutdownReadinessListener());
}

@BuildStep
AnnotationsTransformerBuildItem annotationTransformer(BeanArchiveIndexBuildItem beanArchiveIndex,
CustomScopeAnnotationsBuildItem scopes) {

// Transform health checks that are not annotated with a scope or a stereotype with a default scope
Set<DotName> stereotypeAnnotations = new HashSet<>();
for (AnnotationInstance annotation : beanArchiveIndex.getIndex().getAnnotations(DotNames.STEREOTYPE)) {
ClassInfo annotationClass = beanArchiveIndex.getIndex().getClassByName(annotation.name());
if (annotationClass != null && scopes.isScopeIn(annotationClass.classAnnotations())) {
// Stereotype annotation with a default scope
stereotypeAnnotations.add(annotationClass.name());
}
}
List<DotName> healthAnnotations = new ArrayList<>(5);
healthAnnotations.add(LIVENESS);
healthAnnotations.add(READINESS);
healthAnnotations.add(STARTUP);
healthAnnotations.add(HEALTH_GROUP);
healthAnnotations.add(HEALTH_GROUPS);
healthAnnotations.add(WELLNESS);

return new AnnotationsTransformerBuildItem(new AnnotationsTransformer() {

@Override
public boolean appliesTo(Kind kind) {
return kind == Kind.CLASS || kind == Kind.METHOD;
}

@Override
public void transform(TransformationContext ctx) {
if (ctx.getAnnotations().isEmpty()) {
return;
}
Collection<AnnotationInstance> annotations;
if (ctx.isClass()) {
annotations = ctx.getAnnotations();
if (containsAny(annotations, stereotypeAnnotations)) {
return;
}
} else {
annotations = getAnnotations(Kind.METHOD, ctx.getAnnotations());
}
if (scopes.isScopeIn(annotations)) {
return;
}
if (containsAny(annotations, healthAnnotations)) {
ctx.transform().add(BuiltinScope.SINGLETON.getName()).done();
}
}

});
}

// UI
@BuildStep
void registerUiExtension(
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,30 @@ 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) {
priority = Beans.initStereotypeAlternativePriority(stereotypes);
}

beanConsumer.accept(new BeanInfo.Builder()
.implClazz(implClass)
.providerType(providerType)
Expand All @@ -52,6 +76,7 @@ public void done() {
.qualifiers(qualifiers)
.alternative(alternative)
.priority(priority)
.stereotypes(stereotypes)
.name(name)
.creator(creatorConsumer)
.destroyer(destroyerConsumer)
Expand Down
Loading

0 comments on commit a010610

Please sign in to comment.