diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java b/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java
new file mode 100644
index 0000000000000..38aaf9cc3cb27
--- /dev/null
+++ b/core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java
@@ -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.
+ *
+ * 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.
+ *
+ * Furthermore, {@link #value()} can be used to specify the priority of the generated observer method and thus affects observers
+ * ordering.
+ *
+ * The contextual instance is destroyed immediately after the method is invoked for {@link Dependent} beans.
+ *
+ * The following examples are functionally equivalent.
+ *
+ *
+ * @ApplicationScoped
+ * class Bean1 {
+ * void onShutdown(@Observes ShutdownEvent event) {
+ * // place the logic here
+ * }
+ * }
+ *
+ * @ApplicationScoped
+ * class Bean2 {
+ *
+ * @Shutdown
+ * void shutdown() {
+ * // place the logic here
+ * }
+ * }
+ *
+ *
+ * @see ShutdownEvent
+ */
+@Target(METHOD)
+@Retention(RUNTIME)
+public @interface Shutdown {
+
+ /**
+ *
+ * @return the priority
+ * @see jakarta.annotation.Priority
+ */
+ int value() default ObserverMethod.DEFAULT_PRIORITY;
+
+}
diff --git a/core/runtime/src/main/java/io/quarkus/runtime/Startup.java b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java
index 58bf079f15cd7..e7f79520f62ac 100644
--- a/core/runtime/src/main/java/io/quarkus/runtime/Startup.java
+++ b/core/runtime/src/main/java/io/quarkus/runtime/Startup.java
@@ -27,7 +27,7 @@
* Furthermore, {@link #value()} can be used to specify the priority of the generated observer method and thus affects observers
* ordering.
*
- * 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.
*
* The following examples are functionally equivalent.
*
diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java
new file mode 100644
index 0000000000000..46980b7889a7c
--- /dev/null
+++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java
@@ -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() {
+ @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() {
+ @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 configurators) {
+
+ AnnotationStore annotationStore = observerRegistration.getContext().get(BuildExtension.Key.ANNOTATION_STORE);
+
+ for (BeanInfo bean : observerRegistration.getContext().beans().classBeans()) {
+ ClassInfo beanClass = bean.getTarget().get().asClass();
+ List 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 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 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();
+ }
+}
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
index b95bd0f1bf49f..477eee7c6d506 100644
--- 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
@@ -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;
@@ -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;
@@ -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
@@ -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);
+ }
}
}
}
@@ -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
diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java
new file mode 100644
index 0000000000000..83fb75c34ec56
--- /dev/null
+++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java
@@ -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 MESSAGES = new CopyOnWriteArrayList<>();
+}
\ No newline at end of file
diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java
new file mode 100644
index 0000000000000..4ea9ea9859ee5
--- /dev/null
+++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java
@@ -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) {
+ }
+
+ }
+
+}
diff --git a/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java
new file mode 100644
index 0000000000000..411796f31f42b
--- /dev/null
+++ b/extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java
@@ -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");
+ }
+
+ }
+
+}