diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Startup.java b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java new file mode 100644 index 000000000..24d828b0f --- /dev/null +++ b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java @@ -0,0 +1,52 @@ +package io.quarkus.runtime; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.spi.ObserverMethod; + +/** + * This annotation can be used to initialize a CDI bean at application startup. The behavior is similar to a declaration of an + * observer of the {@link StartupEvent} - a contextual instance is created and lifecycle callbacks (such as + * {@link javax.annotation.PostConstruct}) are invoked. In fact, a synthetic observer of the {@link StartupEvent} is generated + * for each bean annotated with this annotation. Furthermore, {@link #value()} can be used to specify the priority of the + * generated observer method and thus affect observers ordering. + *

+ * The contextual instance is destroyed immediately afterwards for {@link Dependent} beans. + *

+ * The following examples are functionally equivalent. + * + *

+ * @ApplicationScoped
+ * class Bean1 {
+ *     void onStart(@Observes StartupEvent event) {
+ *         // place the logic here 
+ *     }
+ * }
+ * 
+ * @Startup
+ * @ApplicationScoped
+ * class Bean2 {
+ * }
+ * 
+ * + * @see StartupEvent + */ +@Target({ TYPE, METHOD, FIELD }) +@Retention(RUNTIME) +public @interface Startup { + + /** + * + * @return the priority + * @see javax.annotation.Priority + */ + int value() default ObserverMethod.DEFAULT_PRIORITY; + +} diff --git a/docs/src/main/asciidoc/lifecycle.adoc b/docs/src/main/asciidoc/lifecycle.adoc index 50f06c0f0..3ed16e150 100644 --- a/docs/src/main/asciidoc/lifecycle.adoc +++ b/docs/src/main/asciidoc/lifecycle.adoc @@ -102,6 +102,32 @@ See link:writing-extensions#bootstrap-three-phases[Three Phases of Bootstrap and NOTE: In CDI applications, an event with qualifier `@Initialized(ApplicationScoped.class)` is fired when the application context is initialized. See https://docs.jboss.org/cdi/spec/2.0/cdi-spec.html#application_context[the spec, window="_blank"] for more info. +=== Using `@Startup` to initialize a CDI bean at application startup + +A bean represented by a class, producer method or field annotated with `@Startup` is initialized at application startup: + +[source,java] +---- +package org.acme.events; + +import javax.enterprise.context.ApplicationScoped; + +@Startup // <1> +@ApplicationScoped +public class EagerAppBean { + + private final String name; + + EagerAppBean(NameGenerator generator) { // <2> + this.name = generator.createName(); + } +} +---- +1. For each bean annotated with `@Startup` a synthetic observer of `StartupEvent` is generated. The default priority is used. +2. The bean constructor is called when the application starts and the resulting contextual instance is stored in the application context. + +NOTE: `@Dependent` beans are destroyed immediately afterwards to follow the behavior of observers declared on `@Dependent` beans. + == Package and run the application Run the application with: `./mvnw compile quarkus:dev`, the logged message is printed. 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 new file mode 100644 index 000000000..dd41f918a --- /dev/null +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/StartupBuildSteps.java @@ -0,0 +1,121 @@ +package io.quarkus.arc.deployment; + +import java.util.function.Predicate; + +import javax.enterprise.context.spi.Contextual; +import javax.enterprise.context.spi.CreationalContext; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationValue; +import org.jboss.jandex.DotName; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.ClientProxy; +import io.quarkus.arc.InjectableBean; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.arc.deployment.ObserverRegistrationPhaseBuildItem.ObserverConfiguratorBuildItem; +import io.quarkus.arc.impl.CreationalContextImpl; +import io.quarkus.arc.processor.AnnotationStore; +import io.quarkus.arc.processor.BeanInfo; +import io.quarkus.arc.processor.BuildExtension; +import io.quarkus.arc.processor.BuiltinScope; +import io.quarkus.arc.processor.ObserverConfigurator; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.runtime.Startup; +import io.quarkus.runtime.StartupEvent; + +public class StartupBuildSteps { + + static final DotName STARTUP_NAME = DotName.createSimple(Startup.class.getName()); + + static final MethodDescriptor ARC_CONTAINER = MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class); + static final MethodDescriptor ARC_CONTAINER_BEAN = MethodDescriptor.ofMethod(ArcContainer.class, "bean", + InjectableBean.class, String.class); + static final MethodDescriptor ARC_CONTAINER_INSTANCE = MethodDescriptor.ofMethod(ArcContainer.class, "instance", + InstanceHandle.class, InjectableBean.class); + static final MethodDescriptor INSTANCE_HANDLE_GET = MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class); + static final MethodDescriptor CLIENT_PROXY_CONTEXTUAL_INSTANCE = MethodDescriptor.ofMethod(ClientProxy.class, + "arc_contextualInstance", Object.class); + static final MethodDescriptor CONTEXTUAL_CREATE = MethodDescriptor.ofMethod(Contextual.class, + "create", Object.class, CreationalContext.class); + static final MethodDescriptor CONTEXTUAL_DESTROY = MethodDescriptor.ofMethod(Contextual.class, + "destroy", void.class, Object.class, CreationalContext.class); + + @BuildStep + UnremovableBeanBuildItem unremovableBeans() { + // Make all classes annotated with @Startup unremovable + return new UnremovableBeanBuildItem(new Predicate() { + @Override + public boolean test(BeanInfo bean) { + if (bean.isClassBean()) { + return bean.getTarget().get().asClass().annotations().containsKey(STARTUP_NAME); + } else if (bean.isProducerMethod()) { + return bean.getTarget().get().asMethod().hasAnnotation(STARTUP_NAME); + } else if (bean.isProducerField()) { + return bean.getTarget().get().asField().hasAnnotation(STARTUP_NAME); + } + return false; + } + }); + } + + @BuildStep + void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistrationPhase, + BuildProducer configurators) { + + AnnotationStore annotationStore = observerRegistrationPhase.getContext().get(BuildExtension.Key.ANNOTATION_STORE); + + for (BeanInfo bean : observerRegistrationPhase.getContext().beans().withTarget()) { + AnnotationInstance startupAnnotation = annotationStore.getAnnotation(bean.getTarget().get(), STARTUP_NAME); + if (startupAnnotation != null) { + registerStartupObserver(observerRegistrationPhase, bean, startupAnnotation); + } + } + } + + private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observerRegistrationPhase, BeanInfo bean, + AnnotationInstance startup) { + ObserverConfigurator configurator = observerRegistrationPhase.getContext().configure() + .beanClass(bean.getBeanClass()) + .observedType(StartupEvent.class); + AnnotationValue priority = startup.value(); + if (priority != null) { + configurator.priority(priority.asInt()); + } + configurator.notify(mc -> { + // InjectableBean bean = Arc.container().bean("bflmpsvz"); + ResultHandle containerHandle = mc.invokeStaticMethod(ARC_CONTAINER); + ResultHandle beanHandle = mc.invokeInterfaceMethod(ARC_CONTAINER_BEAN, containerHandle, + mc.load(bean.getIdentifier())); + if (BuiltinScope.DEPENDENT.is(bean.getScope())) { + // It does not make a lot of sense to support @Startup dependent beans but it's still a valid use case + ResultHandle contextHandle = mc.newInstance( + MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class), + beanHandle); + // Create a dependent instance + ResultHandle instanceHandle = mc.invokeInterfaceMethod(CONTEXTUAL_CREATE, beanHandle, + contextHandle); + // But destroy the instance immediately + mc.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instanceHandle, contextHandle); + } else { + // Obtains the instance from the context + // InstanceHandle handle = Arc.container().instance(bean); + ResultHandle instanceHandle = mc.invokeInterfaceMethod(ARC_CONTAINER_INSTANCE, containerHandle, + beanHandle); + if (bean.getScope().isNormal()) { + // We need to unwrap the client proxy + // ((ClientProxy) handle.get()).arc_contextualInstance(); + ResultHandle proxyHandle = mc.checkCast( + mc.invokeInterfaceMethod(INSTANCE_HANDLE_GET, instanceHandle), ClientProxy.class); + mc.invokeInterfaceMethod(CLIENT_PROXY_CONTEXTUAL_INSTANCE, proxyHandle); + } + } + mc.returnValue(null); + }); + configurator.done(); + } +} diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/StartupAnnotationTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/StartupAnnotationTest.java new file mode 100644 index 000000000..c1a9003da --- /dev/null +++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/startup/StartupAnnotationTest.java @@ -0,0 +1,174 @@ +package io.quarkus.arc.test.startup; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.function.Consumer; + +import javax.annotation.PostConstruct; +import javax.annotation.PreDestroy; +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.context.Dependent; +import javax.enterprise.inject.Produces; +import javax.enterprise.inject.spi.ObserverMethod; +import javax.inject.Singleton; + +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.AnnotationTarget.Kind; +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.arc.Unremovable; +import io.quarkus.arc.deployment.AnnotationsTransformerBuildItem; +import io.quarkus.arc.processor.AnnotationsTransformer; +import io.quarkus.builder.BuildChainBuilder; +import io.quarkus.builder.BuildContext; +import io.quarkus.builder.BuildStep; +import io.quarkus.runtime.Startup; +import io.quarkus.test.QuarkusUnitTest; + +public class StartupAnnotationTest { + + static final List LOG = new CopyOnWriteArrayList(); + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class) + .addClasses(StartMe.class, SingletonStartMe.class, DependentStartMe.class, ProducerStartMe.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(new AnnotationsTransformerBuildItem(new AnnotationsTransformer() { + + @Override + public boolean appliesTo(Kind kind) { + return AnnotationTarget.Kind.CLASS.equals(kind); + } + + @Override + public void transform(TransformationContext context) { + if (context.getTarget().asClass().name().toString().endsWith("SingletonStartMe")) { + context.transform().add(Startup.class).done(); + } + } + + })); + } + }).produces(AnnotationsTransformerBuildItem.class).build(); + } + }; + } + + @Test + public void testStartup() { + // StartMe, SingletonStartMe, ProducerStartMe, DependentStartMe + assertEquals(11, LOG.size(), "Unexpected number of log messages: " + LOG); + assertEquals("startMe_c", LOG.get(0)); + assertEquals("startMe_c", LOG.get(1)); + assertEquals("startMe_pc", LOG.get(2)); + assertEquals("singleton_c", LOG.get(3)); + assertEquals("singleton_pc", LOG.get(4)); + assertEquals("producer_pc", LOG.get(5)); + assertEquals("producer", LOG.get(6)); + assertEquals("producer_pd", LOG.get(7)); + assertEquals("dependent_c", LOG.get(8)); + assertEquals("dependent_pc", LOG.get(9)); + assertEquals("dependent_pd", LOG.get(10)); + } + + // This component should be started first + @Startup(ObserverMethod.DEFAULT_PRIORITY - 1) + @ApplicationScoped + static class StartMe { + + public StartMe() { + // This constructor will be invoked 2x - for proxy and contextual instance + LOG.add("startMe_c"); + } + + @PostConstruct + void init() { + LOG.add("startMe_pc"); + } + + @PreDestroy + void destroy() { + LOG.add("startMe_pd"); + } + + } + + // @Startup is added by an annotation transformer + @Unremovable // only classes annotated with @Startup are made unremovable + @Singleton + static class SingletonStartMe { + + public SingletonStartMe() { + LOG.add("singleton_c"); + } + + @PostConstruct + void init() { + LOG.add("singleton_pc"); + } + + @PreDestroy + void destroy() { + LOG.add("singleton_pd"); + } + + } + + @Dependent + @Startup(Integer.MAX_VALUE) + static class DependentStartMe { + + public DependentStartMe() { + LOG.add("dependent_c"); + } + + @PostConstruct + void init() { + LOG.add("dependent_pc"); + } + + @PreDestroy + void destroy() { + LOG.add("dependent_pd"); + } + + } + + static class ProducerStartMe { + + @Startup(Integer.MAX_VALUE - 1) + @Produces + String produceString() { + LOG.add("producer"); + return "ok"; + } + + @PostConstruct + void init() { + LOG.add("producer_pc"); + } + + @PreDestroy + void destroy() { + LOG.add("producer_pd"); + } + + } + +} 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 15ce526dc..4f7adbc84 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 @@ -1018,8 +1018,12 @@ public class BeanDeployment { @Override public ObserverConfigurator configure() { - return new ObserverConfigurator(DotName.createSimple(extension.getClass().getName()), - beanDeployment::addSyntheticObserver); + ObserverConfigurator configurator = new ObserverConfigurator(beanDeployment::addSyntheticObserver); + if (extension != null) { + // Extension may be null if called directly from the ObserverRegistrationPhaseBuildItem + configurator.beanClass(DotName.createSimple(extension.getClass().getName())); + } + return configurator; } @Override 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 90f7263aa..a697e6bef 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 @@ -153,6 +153,10 @@ public class BeanInfo implements InjectionTargetInfo { return identifier; } + /** + * + * @return the annotation target or an empty optional in case of synthetic beans + */ public Optional getTarget() { return target; } diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java index ce82fd43d..b1ce81048 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/BeanStream.java @@ -91,6 +91,16 @@ public final class BeanStream implements Iterable { return withBeanClass(DotName.createSimple(beanClass.getName())); } + /** + * + * @return the new stream of beans + * @see BeanInfo#getTarget() + */ + public BeanStream withTarget() { + stream = stream.filter(bean -> bean.getTarget().isPresent()); + return this; + } + /** * * @param beanClass diff --git a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java index c2335b6f6..425f5acb3 100644 --- a/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java +++ b/independent-projects/arc/processor/src/main/java/io/quarkus/arc/processor/ObserverConfigurator.java @@ -20,7 +20,7 @@ public final class ObserverConfigurator implements Consumer final Consumer consumer; - final DotName beanClass; + DotName beanClass; Type observedType; @@ -34,8 +34,7 @@ public final class ObserverConfigurator implements Consumer Consumer notifyConsumer; - public ObserverConfigurator(DotName beanClass, Consumer consumer) { - this.beanClass = beanClass; + public ObserverConfigurator(Consumer consumer) { this.consumer = consumer; this.observedQualifiers = new HashSet<>(); this.priority = ObserverMethod.DEFAULT_PRIORITY; @@ -43,6 +42,11 @@ public final class ObserverConfigurator implements Consumer this.transactionPhase = TransactionPhase.IN_PROGRESS; } + public ObserverConfigurator beanClass(DotName beanClass) { + this.beanClass = beanClass; + return this; + } + public ObserverConfigurator observedType(Class observedType) { this.observedType = Type.create(DotName.createSimple(observedType.getName()), Kind.CLASS); return this; @@ -91,6 +95,15 @@ public final class ObserverConfigurator implements Consumer } public void done() { + if (beanClass == null) { + throw new IllegalStateException("Observer bean class must be set!"); + } + if (observedType == null) { + throw new IllegalStateException("Observed type must be set!"); + } + if (notifyConsumer == null) { + throw new IllegalStateException("Bytecode generator for notify() method must be set!"); + } consumer.accept(this); }