forked from quarkusio/quarkus
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request quarkusio#35869 from mkouba/issue-35848
ArC: introduce Shutdown annotation
- Loading branch information
Showing
7 changed files
with
321 additions
and
9 deletions.
There are no files selected for viewing
58 changes: 58 additions & 0 deletions
58
core/runtime/src/main/java/io/quarkus/runtime/Shutdown.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
* @ApplicationScoped | ||
* class Bean1 { | ||
* void onShutdown(@Observes ShutdownEvent event) { | ||
* // place the logic here | ||
* } | ||
* } | ||
* | ||
* @ApplicationScoped | ||
* class Bean2 { | ||
* | ||
* @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; | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
141 changes: 141 additions & 0 deletions
141
extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/ShutdownBuildSteps.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 9 additions & 0 deletions
9
extensions/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/Messages.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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<>(); | ||
} |
37 changes: 37 additions & 0 deletions
37
...yment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationInvalidMethodTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
} | ||
|
||
} | ||
|
||
} |
52 changes: 52 additions & 0 deletions
52
...ons/arc/deployment/src/test/java/io/quarkus/arc/test/shutdown/ShutdownAnnotationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
|
||
} | ||
|
||
} |