Skip to content

Commit

Permalink
ArC: make it possible to initialize synthetic beans eagerly
Browse files Browse the repository at this point in the history
- using a configurator method
- resolves quarkusio#41159
  • Loading branch information
mkouba committed Jun 12, 2024
1 parent 3c6e8f9 commit f08fd91
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.OptionalInt;
import java.util.function.Predicate;

import jakarta.enterprise.context.spi.Contextual;
Expand Down Expand Up @@ -113,58 +113,56 @@ public boolean test(BeanInfo bean) {

@BuildStep
void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistration,
List<SyntheticBeanBuildItem> syntheticBeans,
BuildProducer<ObserverConfiguratorBuildItem> configurators) {

AnnotationStore annotationStore = observerRegistration.getContext().get(BuildExtension.Key.ANNOTATION_STORE);

for (BeanInfo bean : observerRegistration.getContext().beans().withTarget()) {
// First check if the target is annotated with @Startup
// Class for class-based bean, method for producer method, etc.
AnnotationTarget target = bean.getTarget().get();
AnnotationInstance startupAnnotation = annotationStore.getAnnotation(target, STARTUP_NAME);
if (startupAnnotation != null) {
String id;
if (target.kind() == Kind.METHOD) {
id = target.asMethod().declaringClass().name() + "#" + target.asMethod().toString();
} else if (target.kind() == Kind.FIELD) {
id = target.asField().declaringClass().name() + "#" + target.asField().toString();
} else {
id = target.asClass().name().toString();
for (BeanInfo bean : observerRegistration.getContext().beans()) {
if (bean.isSynthetic()) {
OptionalInt startupPriority = bean.getStartupPriority();
if (startupPriority.isPresent()) {
registerStartupObserver(observerRegistration, bean, bean.getIdentifier(),
startupPriority.getAsInt(), null);
}
AnnotationValue priority = startupAnnotation.value();
registerStartupObserver(observerRegistration, bean, id,
priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, null);
}

List<MethodInfo> startupMethods = Collections.emptyList();
if (target.kind() == Kind.CLASS) {
// If the target is a class then collect all non-static non-producer no-args methods annotated with @Startup
startupMethods = new ArrayList<>();
for (MethodInfo method : target.asClass().methods()) {
if (annotationStore.hasAnnotation(method, STARTUP_NAME)) {
if (!method.isSynthetic()
&& !Modifier.isPrivate(method.flags())
&& !Modifier.isStatic(method.flags())
&& method.parametersCount() == 0
&& !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
startupMethods.add(method);
} else {
if (!annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
// Producer methods annotated with @Startup are valid and processed above
LOG.warnf("Ignored an invalid @Startup method declared on %s: %s",
method.declaringClass().name(),
method);
} else {
// First check if the target is annotated with @Startup
// Class for class-based bean, method for producer method, etc.
AnnotationTarget target = bean.getTarget().get();
AnnotationInstance startupAnnotation = annotationStore.getAnnotation(target, STARTUP_NAME);
if (startupAnnotation != null) {
AnnotationValue priority = startupAnnotation.value();
registerStartupObserver(observerRegistration, bean, bean.getIdentifier(),
priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, null);
}
if (target.kind() == Kind.CLASS) {
// If the target is a class then collect all non-static non-producer no-args methods annotated with @Startup
List<MethodInfo> startupMethods = new ArrayList<>();
for (MethodInfo method : target.asClass().methods()) {
if (annotationStore.hasAnnotation(method, STARTUP_NAME)) {
if (!method.isSynthetic()
&& !Modifier.isPrivate(method.flags())
&& !Modifier.isStatic(method.flags())
&& method.parametersCount() == 0
&& !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
startupMethods.add(method);
} else {
if (!annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
// Producer methods annotated with @Startup are valid and processed above
LOG.warnf("Ignored an invalid @Startup method declared on %s: %s",
method.declaringClass().name(),
method);
}
}
}
}
}
}
if (!startupMethods.isEmpty()) {
for (MethodInfo method : startupMethods) {
AnnotationValue priority = annotationStore.getAnnotation(method, STARTUP_NAME).value();
registerStartupObserver(observerRegistration, bean,
method.declaringClass().name() + "#" + method.toString(),
priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, method);
if (!startupMethods.isEmpty()) {
for (MethodInfo method : startupMethods) {
AnnotationValue priority = annotationStore.getAnnotation(method, STARTUP_NAME).value();
registerStartupObserver(observerRegistration, bean, bean.getIdentifier() + method.toString(),
priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, method);
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package io.quarkus.arc.test.startup;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Vetoed;
import jakarta.inject.Inject;
import jakarta.inject.Provider;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.arc.BeanCreator;
import io.quarkus.arc.SyntheticCreationalContext;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.builder.BuildChainBuilder;
import io.quarkus.builder.BuildContext;
import io.quarkus.builder.BuildStep;
import io.quarkus.test.QuarkusUnitTest;

public class SyntheticBeanStartupTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(
root -> root.addClasses(SyntheticBeanStartupTest.class, SynthBean.class, SynthBeanCreator.class))
.addBuildChainCustomizer(buildCustomizer());

static Consumer<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {

@Override
public void accept(BuildChainBuilder builder) {
builder.addBuildStep(new BuildStep() {
@Override
public void execute(BuildContext context) {
context.produce(SyntheticBeanBuildItem.configure(SynthBean.class)
.scope(ApplicationScoped.class)
.identifier("ok")
.startup()
.creator(SynthBeanCreator.class)
.done());
}
}).produces(SyntheticBeanBuildItem.class).build();
}
};
}

@Inject
Provider<SynthBean> synthBean;

@Test
public void testStartup() {
assertTrue(SynthBeanCreator.CREATED.get());
assertEquals("foo", synthBean.get().getValue());
}

public static class SynthBeanCreator implements BeanCreator<SynthBean> {

static final AtomicBoolean CREATED = new AtomicBoolean();

@Override
public SynthBean create(SyntheticCreationalContext<SynthBean> context) {
CREATED.set(true);
return new SynthBean("foo");
}
}

@Vetoed
public static class SynthBean {

private final String value;

public SynthBean(String value) {
this.value = value;
}

public String getValue() {
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,8 @@ public void done() {
.defaultBean(defaultBean)
.removable(removable)
.forceApplicationClass(forceApplicationClass)
.targetPackageName(targetPackageName);
.targetPackageName(targetPackageName)
.startupPriority(startupPriority);

if (!injectionPoints.isEmpty()) {
builder.injections(Collections.singletonList(Injection.forSyntheticBean(injectionPoints)));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import jakarta.enterprise.context.NormalScope;
import jakarta.enterprise.context.spi.CreationalContext;
import jakarta.enterprise.inject.Default;
import jakarta.enterprise.inject.spi.ObserverMethod;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationValue;
Expand Down Expand Up @@ -54,6 +55,7 @@ public abstract class BeanConfiguratorBase<THIS extends BeanConfiguratorBase<THI
protected String targetPackageName;
protected Integer priority;
protected final Set<TypeAndQualifiers> injectionPoints;
protected Integer startupPriority;

protected BeanConfiguratorBase(DotName implClazz) {
this.implClazz = implClazz;
Expand Down Expand Up @@ -94,6 +96,7 @@ public THIS read(BeanConfiguratorBase<?, ?> base) {
priority = base.priority;
injectionPoints.clear();
injectionPoints.addAll(base.injectionPoints);
startupPriority = base.startupPriority;
return self();
}

Expand Down Expand Up @@ -255,6 +258,26 @@ public THIS addInjectionPoint(Type requiredType, AnnotationInstance... requiredQ
return self();
}

/**
* Initialize the bean eagerly at application startup.
*
* @param priority
* @return self
*/
public THIS startup(int priority) {
this.startupPriority = priority;
return self();
}

/**
* Initialize the bean eagerly at application startup.
*
* @return self
*/
public THIS startup() {
return startup(ObserverMethod.DEFAULT_PRIORITY);
}

public <U extends T> THIS creator(Class<? extends BeanCreator<U>> creatorClazz) {
return creator(mc -> {
// return new FooBeanCreator().create(syntheticCreationalContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.OptionalInt;
import java.util.Set;
import java.util.function.Consumer;
import java.util.stream.Collectors;
Expand Down Expand Up @@ -81,6 +82,8 @@ public class BeanInfo implements InjectionTargetInfo {

private final boolean defaultBean;

private final List<MethodInfo> aroundInvokes;

// Following fields are only used by synthetic beans

private final boolean removable;
Expand All @@ -95,15 +98,15 @@ public class BeanInfo implements InjectionTargetInfo {

private final String targetPackageName;

private final List<MethodInfo> aroundInvokes;
private final Integer startupPriority;

BeanInfo(AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, Set<Type> types,
Set<AnnotationInstance> qualifiers, List<Injection> injections, BeanInfo declaringBean, DisposerInfo disposer,
boolean alternative, List<StereotypeInfo> stereotypes, String name, boolean isDefaultBean, String targetPackageName,
Integer priority, Set<Type> unrestrictedTypes) {
this(null, null, target, beanDeployment, scope, types, qualifiers, injections, declaringBean, disposer,
alternative, stereotypes, name, isDefaultBean, null, null, Collections.emptyMap(), true, false,
targetPackageName, priority, null, unrestrictedTypes);
targetPackageName, priority, null, unrestrictedTypes, null);
}

BeanInfo(ClassInfo implClazz, Type providerType, AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope,
Expand All @@ -112,7 +115,7 @@ public class BeanInfo implements InjectionTargetInfo {
List<StereotypeInfo> stereotypes, String name, boolean isDefaultBean, Consumer<MethodCreator> creatorConsumer,
Consumer<MethodCreator> destroyerConsumer, Map<String, Object> params, boolean isRemovable,
boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier,
Set<Type> unrestrictedTypes) {
Set<Type> unrestrictedTypes, Integer startupPriority) {

this.target = Optional.ofNullable(target);
if (implClazz == null && target != null) {
Expand Down Expand Up @@ -152,6 +155,7 @@ public class BeanInfo implements InjectionTargetInfo {
this.lifecycleInterceptors = Collections.emptyMap();
this.forceApplicationClass = forceApplicationClass;
this.targetPackageName = targetPackageName;
this.startupPriority = startupPriority;
this.aroundInvokes = isInterceptor() || isDecorator() ? List.of() : Beans.getAroundInvokes(implClazz, beanDeployment);
}

Expand Down Expand Up @@ -551,6 +555,10 @@ public boolean isDefaultBean() {
return defaultBean;
}

public OptionalInt getStartupPriority() {
return startupPriority != null ? OptionalInt.of(startupPriority) : OptionalInt.empty();
}

/**
* @param requiredType
* @param requiredQualifiers
Expand Down Expand Up @@ -1076,6 +1084,8 @@ static class Builder {

private Integer priority;

private Integer startupPriority;

Builder() {
injections = Collections.emptyList();
stereotypes = Collections.emptyList();
Expand Down Expand Up @@ -1170,6 +1180,11 @@ Builder defaultBean(boolean isDefaultBean) {
return this;
}

Builder startupPriority(Integer value) {
this.startupPriority = value;
return this;
}

Builder creator(Consumer<MethodCreator> creatorConsumer) {
this.creatorConsumer = creatorConsumer;
return this;
Expand Down Expand Up @@ -1199,7 +1214,7 @@ BeanInfo build() {
return new BeanInfo(implClazz, providerType, target, beanDeployment, scope, types, qualifiers, injections,
declaringBean, disposer, alternative, stereotypes, name, isDefaultBean, creatorConsumer,
destroyerConsumer, params, removable, forceApplicationClass, targetPackageName, priority,
identifier, null);
identifier, null, startupPriority);
}

public Builder forceApplicationClass(boolean forceApplicationClass) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ public class InterceptorInfo extends BeanInfo implements Comparable<InterceptorI
mc.returnValue(ret);
},
null, params, true, false, null, priority, creatorClass.getName() + (identifier != null ? identifier : ""),
null);
null, null);
this.bindings = bindings;
this.interceptionType = interceptionType;
this.creatorClass = creatorClass;
Expand Down

0 comments on commit f08fd91

Please sign in to comment.