Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support non-producer methods annotated with @Startup #27897

Merged
merged 1 commit into from
Sep 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>
Ladicek marked this conversation as resolved.
Show resolved Hide resolved
* <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");
}

}

}