Skip to content

Commit

Permalink
ArC: introduce Shutdown annotation
Browse files Browse the repository at this point in the history
- to mark a business method that should be executed during application
shutdown
- resolves quarkusio#35848
  • Loading branch information
mkouba committed Sep 12, 2023
1 parent 7508b5c commit 12e8e9e
Show file tree
Hide file tree
Showing 7 changed files with 321 additions and 9 deletions.
58 changes: 58 additions & 0 deletions core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.quarkus.runtime;

import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.inject.spi.ObserverMethod;

/**
* This annotation is used to mark a business method of a CDI bean that should be executed during application shutdown. The
* annotated method must be non-private and non-static and declare no arguments.
* <p>
* The behavior is similar to a declaration of a {@link ShutdownEvent} observer. In fact, a synthetic observer of the
* {@link ShutdownEvent} is generated for each occurence of this annotation. Within the observer, the contextual instance of a
* bean is obtained first, and then the method is invoked.
* <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 after the method is invoked for {@link Dependent} beans.
* <p>
* The following examples are functionally equivalent.
*
* <pre>
* &#064;ApplicationScoped
* class Bean1 {
* void onShutdown(&#064;Observes ShutdownEvent event) {
* // place the logic here
* }
* }
*
* &#064;ApplicationScoped
* class Bean2 {
*
* &#064;Shutdown
* void shutdown() {
* // place the logic here
* }
* }
* </pre>
*
* @see ShutdownEvent
*/
@Target(METHOD)
@Retention(RUNTIME)
public @interface Shutdown {

/**
*
* @return the priority
* @see jakarta.annotation.Priority
*/
int value() default ObserverMethod.DEFAULT_PRIORITY;

}
2 changes: 1 addition & 1 deletion core/runtime/src/main/java/io/quarkus/runtime/Startup.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* 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.
* The contextual instance is destroyed immediately after the method is invoked for {@link Dependent} beans.
* <p>
* The following examples are functionally equivalent.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package io.quarkus.arc.deployment;

import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Predicate;

import jakarta.enterprise.context.spi.Contextual;
import jakarta.enterprise.inject.spi.ObserverMethod;

import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;

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.CatchBlockCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.TryBlock;
import io.quarkus.runtime.Shutdown;
import io.quarkus.runtime.ShutdownEvent;

public class ShutdownBuildSteps {

static final DotName SHUTDOWN_NAME = DotName.createSimple(Shutdown.class.getName());

private static final Logger LOG = Logger.getLogger(ShutdownBuildSteps.class);

@BuildStep
AutoAddScopeBuildItem addScope(CustomScopeAnnotationsBuildItem customScopes) {
// Class with no built-in scope annotation but with @Shutdown annotation
return AutoAddScopeBuildItem.builder()
.defaultScope(BuiltinScope.APPLICATION)
.anyMethodMatches(new Predicate<MethodInfo>() {
@Override
public boolean test(MethodInfo m) {
return m.hasAnnotation(SHUTDOWN_NAME);
}
})
.reason("Found classes containing @Shutdown annotation.")
.build();
}

@BuildStep
UnremovableBeanBuildItem unremovableBeans() {
return new UnremovableBeanBuildItem(new Predicate<BeanInfo>() {
@Override
public boolean test(BeanInfo bean) {
if (bean.isClassBean()) {
return bean.getTarget().get().asClass().annotationsMap().containsKey(SHUTDOWN_NAME);
}
return false;
}
});
}

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

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

for (BeanInfo bean : observerRegistration.getContext().beans().classBeans()) {
ClassInfo beanClass = bean.getTarget().get().asClass();
List<MethodInfo> shutdownMethods = new ArrayList<>();
// Collect all non-static no-args methods annotated with @Shutdown
for (MethodInfo method : beanClass.methods()) {
if (annotationStore.hasAnnotation(method, SHUTDOWN_NAME)) {
if (!method.isSynthetic()
&& !Modifier.isPrivate(method.flags())
&& !Modifier.isStatic(method.flags())
&& method.parametersCount() == 0) {
shutdownMethods.add(method);
} else {
LOG.warnf("Ignored an invalid @Shutdown method declared on %s: %s", method.declaringClass().name(),
method);
}
}
}
if (!shutdownMethods.isEmpty()) {
for (MethodInfo method : shutdownMethods) {
AnnotationValue priority = annotationStore.getAnnotation(method, SHUTDOWN_NAME).value();
registerShutdownObserver(observerRegistration, bean,
method.declaringClass().name() + "#" + method.toString(),
priority != null ? priority.asInt() : ObserverMethod.DEFAULT_PRIORITY, method);
}
}
}
}

private void registerShutdownObserver(ObserverRegistrationPhaseBuildItem observerRegistration, BeanInfo bean, String id,
int priority, MethodInfo shutdownMethod) {
ObserverConfigurator configurator = observerRegistration.getContext().configure()
.beanClass(bean.getBeanClass())
.observedType(ShutdownEvent.class);
configurator.id(id);
configurator.priority(priority);
configurator.notify(mc -> {
// InjectableBean<Foo> bean = Arc.container().bean("bflmpsvz");
ResultHandle containerHandle = mc.invokeStaticMethod(StartupBuildSteps.ARC_CONTAINER);
ResultHandle beanHandle = mc.invokeInterfaceMethod(StartupBuildSteps.ARC_CONTAINER_BEAN, containerHandle,
mc.load(bean.getIdentifier()));
if (BuiltinScope.DEPENDENT.is(bean.getScope())) {
ResultHandle creationalContext = mc.newInstance(
MethodDescriptor.ofConstructor(CreationalContextImpl.class, Contextual.class),
beanHandle);
// Create a dependent instance
ResultHandle instance = mc.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_CREATE, beanHandle,
creationalContext);
TryBlock tryBlock = mc.tryBlock();
tryBlock.invokeVirtualMethod(MethodDescriptor.of(shutdownMethod), instance);
CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class);
catchBlock.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext);
catchBlock.throwException(RuntimeException.class, "Error destroying bean with @Shutdown method",
catchBlock.getCaughtException());
// Destroy the instance immediately
mc.invokeInterfaceMethod(StartupBuildSteps.CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext);
} else {
// Obtains the instance from the context
// InstanceHandle<Foo> handle = Arc.container().instance(bean);
ResultHandle instanceHandle = mc.invokeInterfaceMethod(StartupBuildSteps.ARC_CONTAINER_INSTANCE,
containerHandle,
beanHandle);
ResultHandle instance = mc.invokeInterfaceMethod(StartupBuildSteps.INSTANCE_HANDLE_GET, instanceHandle);
mc.invokeVirtualMethod(MethodDescriptor.of(shutdownMethod), instance);
}
mc.returnValue(null);
});
configurator.done();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.jboss.jandex.DotName;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;
import org.jboss.logging.Logger;

import io.quarkus.arc.Arc;
import io.quarkus.arc.ArcContainer;
Expand All @@ -39,8 +40,10 @@
import io.quarkus.arc.processor.ObserverConfigurator;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.gizmo.CatchBlockCreator;
import io.quarkus.gizmo.MethodDescriptor;
import io.quarkus.gizmo.ResultHandle;
import io.quarkus.gizmo.TryBlock;
import io.quarkus.runtime.Startup;
import io.quarkus.runtime.StartupEvent;

Expand All @@ -61,6 +64,8 @@ public class StartupBuildSteps {
static final MethodDescriptor CONTEXTUAL_DESTROY = MethodDescriptor.ofMethod(Contextual.class,
"destroy", void.class, Object.class, CreationalContext.class);

private static final Logger LOG = Logger.getLogger(StartupBuildSteps.class);

@BuildStep
AutoAddScopeBuildItem addScope(CustomScopeAnnotationsBuildItem customScopes) {
// Class with no built-in scope annotation but with @Startup annotation
Expand Down Expand Up @@ -136,12 +141,17 @@ void registerStartupObservers(ObserverRegistrationPhaseBuildItem observerRegistr
// 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 (annotationStore.hasAnnotation(method, STARTUP_NAME)) {
if (!method.isSynthetic()
&& !Modifier.isPrivate(method.flags())
&& !Modifier.isStatic(method.flags())
&& method.parametersCount() == 0
&& !annotationStore.hasAnnotation(method, DotNames.PRODUCES)) {
startupMethods.add(method);
} else {
LOG.warnf("Ignored an invalid @Startup method declared on %s: %s", method.declaringClass().name(),
method);
}
}
}
}
Expand Down Expand Up @@ -177,9 +187,14 @@ private void registerStartupObserver(ObserverRegistrationPhaseBuildItem observer
ResultHandle instance = mc.invokeInterfaceMethod(CONTEXTUAL_CREATE, beanHandle,
creationalContext);
if (startupMethod != null) {
mc.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance);
TryBlock tryBlock = mc.tryBlock();
tryBlock.invokeVirtualMethod(MethodDescriptor.of(startupMethod), instance);
CatchBlockCreator catchBlock = tryBlock.addCatch(Exception.class);
catchBlock.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext);
catchBlock.throwException(RuntimeException.class, "Error destroying bean with @Startup method",
catchBlock.getCaughtException());
}
// But destroy the instance immediately
// Destroy the instance immediately
mc.invokeInterfaceMethod(CONTEXTUAL_DESTROY, beanHandle, instance, creationalContext);
} else {
// Obtains the instance from the context
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.quarkus.arc.test.shutdown;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;

public class Messages {

public static final List<String> MESSAGES = new CopyOnWriteArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.quarkus.arc.test.shutdown;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.runtime.Shutdown;
import io.quarkus.test.QuarkusUnitTest;

public class ShutdownAnnotationInvalidMethodTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(ShutdownMethods.class))
.setLogRecordPredicate(r -> r.getLoggerName().contains("ShutdownBuildSteps"))
.assertLogRecords(list -> {
assertEquals(1, list.size());
assertTrue(list.get(0).getMessage().startsWith("Ignored an invalid @Shutdown method declared on"));
});

@Test
public void test() {
}

// @ApplicationScoped is added automatically
static class ShutdownMethods {

@Shutdown
void invalid(String name) {
}

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.arc.test.shutdown;

import static org.junit.jupiter.api.Assertions.assertEquals;

import jakarta.annotation.PostConstruct;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.runtime.Shutdown;
import io.quarkus.test.QuarkusUnitTest;

public class ShutdownAnnotationTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot(root -> root
.addClasses(ShutdownMethods.class))
.setAllowTestClassOutsideDeployment(true)
.setAfterUndeployListener(() -> {
assertEquals(3, Messages.MESSAGES.size());
assertEquals("shutdown_pc", Messages.MESSAGES.get(0));
assertEquals("shutdown_first", Messages.MESSAGES.get(1));
assertEquals("shutdown_second", Messages.MESSAGES.get(2));
});

@Test
public void test() {
}

// @ApplicationScoped is added automatically
static class ShutdownMethods {

@Shutdown
String first() {
Messages.MESSAGES.add("shutdown_first");
return "ok";
}

@Shutdown(Integer.MAX_VALUE)
void second() {
Messages.MESSAGES.add("shutdown_second");
}

@PostConstruct
void init() {
Messages.MESSAGES.add("shutdown_pc");
}

}

}

0 comments on commit 12e8e9e

Please sign in to comment.