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);
}