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"); + } + + } + +}