diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java index 53923c2136643a..962e8e432ea844 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -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; @@ -113,58 +113,56 @@ public boolean test(BeanInfo bean) { @BuildStep void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistration, + List syntheticBeans, BuildProducer 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 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 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); + } + } } } } diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/SyntheticBeanStartupTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/SyntheticBeanStartupTest.java new file mode 100644 index 00000000000000..d2319af965cced --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/SyntheticBeanStartupTest.java @@ -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 buildCustomizer() { + return new Consumer() { + + @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; + + @Test + public void testStartup() { + assertTrue(SynthBeanCreator.CREATED.get()); + assertEquals("foo", synthBean.get().getValue()); + } + + public static class SynthBeanCreator implements BeanCreator { + + static final AtomicBoolean CREATED = new AtomicBoolean(); + + @Override + public SynthBean create(SyntheticCreationalContext 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; + } + } +} 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 21f3afb3c7ac6e..c402acd10822f4 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 @@ -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))); 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 87cfaaca00bfdf..4d7f2069daa42b 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 @@ -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; @@ -54,6 +55,7 @@ public abstract class BeanConfiguratorBase injectionPoints; + protected Integer startupPriority; protected BeanConfiguratorBase(DotName implClazz) { this.implClazz = implClazz; @@ -94,6 +96,7 @@ public THIS read(BeanConfiguratorBase base) { priority = base.priority; injectionPoints.clear(); injectionPoints.addAll(base.injectionPoints); + startupPriority = base.startupPriority; return self(); } @@ -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 THIS creator(Class> creatorClazz) { return creator(mc -> { // return new FooBeanCreator().create(syntheticCreationalContext) diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java index 04a9175f2f6073..d4b95f0790de37 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanInfo.java @@ -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; @@ -81,6 +82,8 @@ public class BeanInfo implements InjectionTargetInfo { private final boolean defaultBean; + private final List aroundInvokes; + // Following fields are only used by synthetic beans private final boolean removable; @@ -95,7 +98,7 @@ public class BeanInfo implements InjectionTargetInfo { private final String targetPackageName; - private final List aroundInvokes; + private final Integer startupPriority; BeanInfo(AnnotationTarget target, BeanDeployment beanDeployment, ScopeInfo scope, Set types, Set qualifiers, List injections, BeanInfo declaringBean, DisposerInfo disposer, @@ -103,7 +106,7 @@ public class BeanInfo implements InjectionTargetInfo { Integer priority, Set 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, @@ -112,7 +115,7 @@ public class BeanInfo implements InjectionTargetInfo { List stereotypes, String name, boolean isDefaultBean, Consumer creatorConsumer, Consumer destroyerConsumer, Map params, boolean isRemovable, boolean forceApplicationClass, String targetPackageName, Integer priority, String identifier, - Set unrestrictedTypes) { + Set unrestrictedTypes, Integer startupPriority) { this.target = Optional.ofNullable(target); if (implClazz == null && target != null) { @@ -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); } @@ -551,6 +555,10 @@ public boolean isDefaultBean() { return defaultBean; } + public OptionalInt getStartupPriority() { + return startupPriority != null ? OptionalInt.of(startupPriority) : OptionalInt.empty(); + } + /** * @param requiredType * @param requiredQualifiers @@ -1076,6 +1084,8 @@ static class Builder { private Integer priority; + private Integer startupPriority; + Builder() { injections = Collections.emptyList(); stereotypes = Collections.emptyList(); @@ -1170,6 +1180,11 @@ Builder defaultBean(boolean isDefaultBean) { return this; } + Builder startupPriority(Integer value) { + this.startupPriority = value; + return this; + } + Builder creator(Consumer creatorConsumer) { this.creatorConsumer = creatorConsumer; return this; @@ -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) { diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java index 91a468bdeba58d..ff209cc42794d1 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/InterceptorInfo.java @@ -66,7 +66,7 @@ public class InterceptorInfo extends BeanInfo implements Comparable