Skip to content

Commit

Permalink
ArC - support non-producer methods annotated with @startup
Browse files Browse the repository at this point in the history
- resolves quarkusio#11710
  • Loading branch information
mkouba committed Sep 13, 2022
1 parent 13b15b1 commit 78191c0
Show file tree
Hide file tree
Showing 3 changed files with 129 additions and 38 deletions.
35 changes: 29 additions & 6 deletions core/runtime/src/main/java/io/quarkus/runtime/Startup.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,20 @@
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.
* This annotation can be used to initialize a CDI bean at application startup:
* <ul>
* <li>If a bean class is annotated then a contextual instance is created and the {@link javax.annotation.PostConstruct}
* callbacks are invoked.</li>
* <li>If a producer method is annotated then a contextual instance is created, i.e. the producer method is invoked.</li>
* <li>If a producer field is annotated then a contextual instance is created, i.e. the producer field is read.</li>
* <li>If a non-static non-producer no-args method of a bean class is annotated then a contextual instance is created, the
* lifecycle callbacks are invoked and finally the method itself is invoked.</li>
* <p>
* The behavior is similar to a declaration of a {@link StartupEvent} observer. In fact, a synthetic observer of the
* {@link StartupEvent} is generated for each occurence of this annotation.
* <p>
* Furthermore, {@link #value()} can be used to specify the priority of the generated observer method and thus affects observers
* ordering.
* <p>
* The contextual instance is destroyed immediately afterwards for {@link Dependent} beans.
* <p>
Expand All @@ -25,14 +34,28 @@
* <pre>
* &#064;ApplicationScoped
* class Bean1 {
* void onStart(@Observes StartupEvent event) {
* void onStart(&#064;Observes StartupEvent event) {
* // place the logic here
* }
* }
*
* &#064;Startup
* &#064;ApplicationScoped
* class Bean2 {
*
* &#064;PostConstruct
* void init() {
* // place the logic here
* }
* }
*
* &#064;ApplicationScoped
* class Bean3 {
*
* &#064;Startup
* void init() {
* // place the logic here
* }
* }
* </pre>
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
package io.quarkus.arc.deployment;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.enterprise.context.spi.Contextual;
import javax.enterprise.context.spi.CreationalContext;
import javax.enterprise.inject.spi.ObserverMethod;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationTarget.Kind;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
Expand All @@ -19,6 +27,7 @@
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.ObserverConfigurator;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
Expand Down Expand Up @@ -61,58 +70,92 @@ UnremovableBeanBuildItem unremovableBeans() {
}

@BuildStep
void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistrationPhase,
void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistration,
BuildProducer<ObserverConfiguratorBuildItem> configurators) {

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

for (BeanInfo bean : observerRegistrationPhase.getContext().beans().withTarget()) {
AnnotationInstance startupAnnotation = annotationStore.getAnnotation(bean.getTarget().get(), STARTUP_NAME);
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) {
registerStartupObserver(observerRegistrationPhase, bean, startupAnnotation);
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();
}
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 (!method.isSynthetic()
&& !Modifier.isStatic(method.flags())
&& method.parametersCount() == 0
&& annotationStore.hasAnnotation(method, STARTUP_NAME)
&& !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
startupMethods.add(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);
}
}
}
}

private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observerRegistrationPhase, BeanInfo bean,
AnnotationInstance startup) {
ObserverConfigurator configurator = observerRegistrationPhase.getContext().configure()
private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observerRegistration, BeanInfo bean, String id,
int priority, MethodInfo startupMethod) {
ObserverConfigurator configurator = observerRegistration.getContext().configure()
.beanClass(bean.getBeanClass())
.observedType(StartupEvent.class);
if (startup.target().kind() == Kind.METHOD) {
configurator.id(startup.target().asMethod().toString());
} else if (startup.target().kind() == Kind.FIELD) {
configurator.id(startup.target().asField().name());
}
AnnotationValue priority = startup.value();
if (priority != null) {
configurator.priority(priority.asInt());
}
configurator.id(id);
configurator.priority(priority);
configurator.notify(mc -> {
// InjectableBean<Foo> 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(
ResultHandle creationalContext = mc.newInstance(
MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class),
beanHandle);
// Create a dependent instance
ResultHandle instanceHandle = mc.invokeInterfaceMethod(CONTEXTUAL_CREATE, beanHandle,
contextHandle);
ResultHandle instance = mc.invokeInterfaceMethod(CONTEXTUAL_CREATE, beanHandle,
creationalContext);
if (startupMethod != null) {
mc.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance);
}
// But destroy the instance immediately
mc.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instanceHandle, contextHandle);
mc.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext);
} else {
// Obtains the instance from the context
// InstanceHandle<Foo> handle = Arc.container().instance(bean);
ResultHandle instanceHandle = mc.invokeInterfaceMethod(ARC_CONTAINER_INSTANCE, containerHandle,
beanHandle);
if (bean.getScope().isNormal()) {
ResultHandle instance = mc.invokeInterfaceMethod(INSTANCE_HANDLE_GET, instanceHandle);
if (startupMethod != null) {
mc.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance);
} else 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);
ResultHandle proxyHandle = mc.checkCast(instance, ClientProxy.class);
mc.invokeInterfaceMethod(CLIENT_PROXY_CONTEXTUAL_INSTANCE, proxyHandle);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,8 @@ public void transform(TransformationContext context) {

@Test
public void testStartup() {
// StartMe, SingletonStartMe, ProducerStartMe, DependentStartMe
assertEquals(14, LOG.size(), "Unexpected number of log messages: " + LOG);
// StartMe, SingletonStartMe, ProducerStartMe, StartupMethods, DependentStartMe
assertEquals(17, 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));
Expand All @@ -82,9 +82,12 @@ public void testStartup() {
assertEquals("producer_pc", LOG.get(8));
assertEquals("produce_string", LOG.get(9));
assertEquals("producer_pd", LOG.get(10));
assertEquals("dependent_c", LOG.get(11));
assertEquals("dependent_pc", LOG.get(12));
assertEquals("dependent_pd", LOG.get(13));
assertEquals("startup_pc", LOG.get(11));
assertEquals("startup_first", LOG.get(12));
assertEquals("startup_second", LOG.get(13));
assertEquals("dependent_c", LOG.get(14));
assertEquals("dependent_pc", LOG.get(15));
assertEquals("dependent_pd", LOG.get(16));
}

// This component should be started first
Expand All @@ -109,7 +112,7 @@ void destroy() {

}

// @Startup is added by an annotation transformer
// @Startup is added by an annotation transformer, the priority is ObserverMethod.DEFAULT_PRIORITY
@Unremovable // only classes annotated with @Startup are made unremovable
@Singleton
static class SingletonStartMe {
Expand Down Expand Up @@ -152,14 +155,14 @@ void destroy() {

static class ProducerStartMe {

@Startup(Integer.MAX_VALUE - 1)
@Startup(Integer.MAX_VALUE - 10)
@Produces
String produceString() {
LOG.add("produce_string");
return "ok";
}

@Startup(Integer.MAX_VALUE - 2)
@Startup(Integer.MAX_VALUE - 20)
@Produces
Long produceLong() {
LOG.add("produce_long");
Expand All @@ -178,4 +181,26 @@ void destroy() {

}

@Singleton
@Unremovable // only classes annotated with @Startup are made unremovable
static class StartupMethods {

@Startup(Integer.MAX_VALUE - 2)
String first() {
LOG.add("startup_first");
return "ok";
}

@Startup(Integer.MAX_VALUE - 1)
void second() {
LOG.add("startup_second");
}

@PostConstruct
void init() {
LOG.add("startup_pc");
}

}

}

0 comments on commit 78191c0

Please sign in to comment.