Introduce Startup annotation

- resolves #5955
This commit is contained in:
Martin Kouba
2020-02-03 15:17:28 +01:00
parent a63bba571d
commit 622f3e7a50
8 changed files with 409 additions and 5 deletions

View File

@@ -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.
* <p>
* The contextual instance is destroyed immediately afterwards for {@link Dependent} beans.
* <p>
* The following examples are functionally equivalent.
*
* <pre>
* &#064;ApplicationScoped
* class Bean1 {
* void onStart(@Observes StartupEvent event) {
* // place the logic here
* }
* }
*
* &#064;Startup
* &#064;ApplicationScoped
* class Bean2 {
* }
* </pre>
*
* @see StartupEvent
*/
@Target({ TYPE, METHOD, FIELD })
@Retention(RUNTIME)
public @interface Startup {
/**
*
* @return the priority
* @see javax.annotation.Priority
*/
int value() default ObserverMethod.DEFAULT_PRIORITY;
}

View File

@@ -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.

View File

@@ -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<BeanInfo>() {
@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<ObserverConfiguratorBuildItem> 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<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(
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<Foo> 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();
}
}

View File

@@ -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<String> LOG = new CopyOnWriteArrayList<String>();
@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<BuildChainBuilder> buildCustomizer() {
return new Consumer<BuildChainBuilder>() {
@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");
}
}
}

View File

@@ -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

View File

@@ -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<AnnotationTarget> getTarget() {
return target;
}

View File

@@ -91,6 +91,16 @@ public final class BeanStream implements Iterable<BeanInfo> {
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

View File

@@ -20,7 +20,7 @@ public final class ObserverConfigurator implements Consumer<AnnotationInstance>
final Consumer<ObserverConfigurator> consumer;
final DotName beanClass;
DotName beanClass;
Type observedType;
@@ -34,8 +34,7 @@ public final class ObserverConfigurator implements Consumer<AnnotationInstance>
Consumer<MethodCreator> notifyConsumer;
public ObserverConfigurator(DotName beanClass, Consumer<ObserverConfigurator> consumer) {
this.beanClass = beanClass;
public ObserverConfigurator(Consumer<ObserverConfigurator> consumer) {
this.consumer = consumer;
this.observedQualifiers = new HashSet<>();
this.priority = ObserverMethod.DEFAULT_PRIORITY;
@@ -43,6 +42,11 @@ public final class ObserverConfigurator implements Consumer<AnnotationInstance>
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<AnnotationInstance>
}
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);
}