From 136df145309a08a5563917d9a0fe68c66f559d9b Mon Sep 17 00:00:00 2001 From: Martin Kouba Date: Thu, 10 Mar 2022 18:13:29 +0100 Subject: [PATCH] Scheduler - support static scheduled methods - resolves #24234 --- .../main/asciidoc/scheduler-reference.adoc | 21 +-- .../arc/deployment/AutoAddScopeBuildItem.java | 21 +++ .../ScheduledStaticMethodTest.java | 36 +++++ .../ScheduledBusinessMethodItem.java | 4 + .../deployment/SchedulerProcessor.java | 131 ++++++++++++------ .../ScheduledStaticMethodTest.java | 36 +++++ .../java/io/quarkus/scheduler/Scheduled.java | 4 +- 7 files changed, 196 insertions(+), 57 deletions(-) create mode 100644 extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/staticmethod/ScheduledStaticMethodTest.java create mode 100644 extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/staticmethod/ScheduledStaticMethodTest.java diff --git a/docs/src/main/asciidoc/scheduler-reference.adoc b/docs/src/main/asciidoc/scheduler-reference.adoc index 51ddb830d2de9..b34b4a0d41cc1 100644 --- a/docs/src/main/asciidoc/scheduler-reference.adoc +++ b/docs/src/main/asciidoc/scheduler-reference.adoc @@ -22,22 +22,23 @@ NOTE: If you add the `quarkus-quartz` dependency to your project the lightweight == Scheduled Methods -If you annotate a method with `@io.quarkus.scheduler.Scheduled` it is automatically scheduled for invocation. -In fact, such a method must be a non-private non-static method of a CDI bean. -As a consequence of being a method of a CDI bean a scheduled method can be annotated with interceptor bindings, such as `@javax.transaction.Transactional` and `@org.eclipse.microprofile.metrics.annotation.Counted`. +A method annotated with `@io.quarkus.scheduler.Scheduled` is automatically scheduled for invocation. +A scheduled method must not be abstract or private. +It may be either static or non-static. +A scheduled method can be annotated with interceptor bindings, such as `@javax.transaction.Transactional` and `@org.eclipse.microprofile.metrics.annotation.Counted`. -NOTE: If there is no CDI scope defined on the declaring class then `@Singleton` is used. +NOTE: If there is a bean class that has no scope and declares at least one non-static method annotated with `@Scheduled` then `@Singleton` is used. Furthermore, the annotated method must return `void` and either declare no parameters or one parameter of type `io.quarkus.scheduler.ScheduledExecution`. TIP: The annotation is repeatable so a single method could be scheduled multiple times. -TIP: A CDI event of type `io.quarkus.scheduler.SuccessfulExecution` is fired synchronously and asynchronously when an execution of a scheduled method is successful. -TIP: A CDI event of type `io.quarkus.scheduler.FailedExecution` is fired synchronously and asynchronously when an execution of a scheduled method throw an exception. + +TIP: A CDI event of type `io.quarkus.scheduler.SuccessfulExecution` is fired synchronously and asynchronously when an execution of a scheduled method is successful. A CDI event of type `io.quarkus.scheduler.FailedExecution` is fired synchronously and asynchronously when an execution of a scheduled method throws an exception. === Triggers -A trigger is defined either by the `@Scheduled#cron()` or by the `@Scheduled#every()` attributes. +A trigger is defined either by the `@Scheduled#cron()` or by the `@Scheduled#every()` attribute. If both are specified, the cron expression takes precedence. If none is specified, the build fails with an `IllegalStateException`. @@ -124,9 +125,9 @@ void myMethod() { } === Identity -By default, a unique id is generated for each scheduled method. -This id is used in log messages and during debugging. -Sometimes a possibility to specify an explicit id may come in handy. +By default, a unique identifier is generated for each scheduled method. +This identifier is used in log messages, during debugging and as a parameter of some `io.quarkus.scheduler.Scheduler` methods. +Therefore, a possibility to specify an explicit identifier may come in handy. .Identity Example [source,java] diff --git a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java index 57156599ba92c..e83a3c110ee85 100644 --- a/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java +++ b/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/AutoAddScopeBuildItem.java @@ -2,11 +2,13 @@ import java.util.Collection; import java.util.function.BiConsumer; +import java.util.function.Predicate; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.ClassInfo; import org.jboss.jandex.DotName; import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; import io.quarkus.arc.processor.Annotations; import io.quarkus.arc.processor.BuiltinScope; @@ -180,6 +182,25 @@ public Builder containsAnnotations(DotName... annotationNames) { }); } + /** + * The class declares a method that matches the given predicate. + *

+ * The final predicate is a short-circuiting logical AND of the previous predicate (if any) and this condition. + * + * @param predicate + * @return self + */ + public Builder anyMethodMatches(Predicate predicate) { + return and((clazz, annotations, index) -> { + for (MethodInfo method : clazz.methods()) { + if (predicate.test(method)) { + return true; + } + } + return false; + }); + } + /** * The class must directly or indirectly implement the given interface. *

diff --git a/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/staticmethod/ScheduledStaticMethodTest.java b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/staticmethod/ScheduledStaticMethodTest.java new file mode 100644 index 0000000000000..f4042a7b3d6e9 --- /dev/null +++ b/extensions/quartz/deployment/src/test/java/io/quarkus/quartz/test/staticmethod/ScheduledStaticMethodTest.java @@ -0,0 +1,36 @@ +package io.quarkus.quartz.test.staticmethod; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class ScheduledStaticMethodTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Jobs.class)); + + @Test + public void testSimpleScheduledJobs() throws InterruptedException { + assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS)); + } + + static class Jobs { + + static final CountDownLatch LATCH = new CountDownLatch(1); + + @Scheduled(every = "1s") + static void everySecond() { + LATCH.countDown(); + } + } + +} diff --git a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/ScheduledBusinessMethodItem.java b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/ScheduledBusinessMethodItem.java index 8474e468d80fc..0f6c35cfad73b 100644 --- a/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/ScheduledBusinessMethodItem.java +++ b/extensions/scheduler/deployment/src/main/java/io/quarkus/scheduler/deployment/ScheduledBusinessMethodItem.java @@ -22,6 +22,10 @@ public ScheduledBusinessMethodItem(BeanInfo bean, MethodInfo method, List add @BuildStep AutoAddScopeBuildItem autoAddScope() { - return AutoAddScopeBuildItem.builder().containsAnnotations(SCHEDULED_NAME, SCHEDULES_NAME) + // We add @Singleton to any bean class that has no scope annotation and declares at least one non-static method annotated with @Scheduled + return AutoAddScopeBuildItem.builder() + .anyMethodMatches(m -> !Modifier.isStatic(m.flags()) + && (m.hasAnnotation(SCHEDULED_NAME) || m.hasAnnotation(SCHEDULES_NAME))) .defaultScope(BuiltinScope.SINGLETON) - .reason("Found scheduled business methods").build(); + .reason("Found non-static scheduled business methods").build(); } @BuildStep @@ -124,7 +122,27 @@ void collectScheduledMethods(BeanArchiveIndexBuildItem beanArchives, BeanDiscove TransformedAnnotationsBuildItem transformedAnnotations, BuildProducer scheduledBusinessMethods) { - // We need to collect all business methods annotated with @Scheduled first + // First collect static scheduled methods + List schedules = new ArrayList<>(beanArchives.getIndex().getAnnotations(SCHEDULED_NAME)); + for (AnnotationInstance annotationInstance : beanArchives.getIndex().getAnnotations(SCHEDULES_NAME)) { + for (AnnotationInstance scheduledInstance : annotationInstance.value().asNestedArray()) { + // We need to set the target of the containing instance + schedules.add(AnnotationInstance.create(scheduledInstance.name(), annotationInstance.target(), + scheduledInstance.values())); + } + } + for (AnnotationInstance annotationInstance : schedules) { + if (annotationInstance.target().kind() != METHOD) { + continue; + } + MethodInfo method = annotationInstance.target().asMethod(); + if (Modifier.isStatic(method.flags())) { + scheduledBusinessMethods.produce(new ScheduledBusinessMethodItem(null, method, schedules)); + LOGGER.debugf("Found scheduled static method %s declared on %s", method, method.declaringClass().name()); + } + } + + // Then collect all business methods annotated with @Scheduled for (BeanInfo bean : beanDiscovery.beanStream().classBeans()) { collectScheduledMethods(beanArchives.getIndex(), transformedAnnotations, bean, bean.getTarget().get().asClass(), @@ -136,10 +154,14 @@ private void collectScheduledMethods(IndexView index, TransformedAnnotationsBuil ClassInfo beanClass, BuildProducer scheduledBusinessMethods) { for (MethodInfo method : beanClass.methods()) { + if (Modifier.isStatic(method.flags())) { + // Ignore static methods + continue; + } List schedules = null; AnnotationInstance scheduledAnnotation = transformedAnnotations.getAnnotation(method, SCHEDULED_NAME); if (scheduledAnnotation != null) { - schedules = Collections.singletonList(scheduledAnnotation); + schedules = List.of(scheduledAnnotation); } else { AnnotationInstance schedulesAnnotation = transformedAnnotations.getAnnotation(method, SCHEDULES_NAME); if (schedulesAnnotation != null) { @@ -174,13 +196,16 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List params = method.parameters(); if (params.size() > 1 @@ -212,7 +237,7 @@ void validateScheduledBusinessMethods(SchedulerConfig config, List unremovableBeans() { // Beans annotated with @Scheduled should never be removed - return Arrays.asList(new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULED_NAME)), + return List.of(new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULED_NAME)), new UnremovableBeanBuildItem(new BeanClassAnnotationExclusion(SCHEDULES_NAME))); } @@ -320,20 +345,22 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas BeanInfo bean = scheduledMethod.getBean(); MethodInfo method = scheduledMethod.getMethod(); + boolean isStatic = Modifier.isStatic(method.flags()); + ClassInfo implClazz = isStatic ? method.declaringClass() : bean.getImplClazz(); String baseName; - if (bean.getImplClazz().enclosingClass() != null) { - baseName = DotNames.simpleName(bean.getImplClazz().enclosingClass()) + NESTED_SEPARATOR - + DotNames.simpleName(bean.getImplClazz()); + if (implClazz.enclosingClass() != null) { + baseName = DotNames.simpleName(implClazz.enclosingClass()) + NESTED_SEPARATOR + + DotNames.simpleName(implClazz); } else { - baseName = DotNames.simpleName(bean.getImplClazz().name()); + baseName = DotNames.simpleName(implClazz.name()); } StringBuilder sigBuilder = new StringBuilder(); sigBuilder.append(method.name()).append("_").append(method.returnType().name().toString()); for (Type i : method.parameters()) { sigBuilder.append(i.name().toString()); } - String generatedName = DotNames.internalPackageNameWithTrailingSlash(bean.getImplClazz().name()) + baseName + String generatedName = DotNames.internalPackageNameWithTrailingSlash(implClazz.name()) + baseName + INVOKER_SUFFIX + "_" + method.name() + "_" + HashUtil.sha1(sigBuilder.toString()); @@ -344,33 +371,47 @@ private String generateInvoker(ScheduledBusinessMethodItem scheduledMethod, Clas // The descriptor is: void invokeBean(Object execution) MethodCreator invoke = invokerCreator.getMethodCreator("invokeBean", void.class, Object.class) .addException(Exception.class); - // InjectableBean handle = Arc.container().instance(bean); - // handle.get().ping(); - ResultHandle containerHandle = invoke - .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); - ResultHandle beanHandle = invoke.invokeInterfaceMethod( - MethodDescriptor.ofMethod(ArcContainer.class, "bean", InjectableBean.class, String.class), - containerHandle, invoke.load(bean.getIdentifier())); - ResultHandle instanceHandle = invoke.invokeInterfaceMethod( - MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, InjectableBean.class), - containerHandle, beanHandle); - ResultHandle beanInstanceHandle = invoke - .invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); - if (method.parameters().isEmpty()) { - invoke.invokeVirtualMethod( - MethodDescriptor.ofMethod(bean.getImplClazz().name().toString(), method.name(), void.class), - beanInstanceHandle); + + if (isStatic) { + if (method.parameters().isEmpty()) { + invoke.invokeStaticMethod( + MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class)); + } else { + invoke.invokeStaticMethod( + MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class, + ScheduledExecution.class), + invoke.getMethodParam(0)); + } } else { - invoke.invokeVirtualMethod( - MethodDescriptor.ofMethod(bean.getImplClazz().name().toString(), method.name(), void.class, - ScheduledExecution.class), - beanInstanceHandle, invoke.getMethodParam(0)); - } - // handle.destroy() - destroy dependent instance afterwards - if (BuiltinScope.DEPENDENT.is(bean.getScope())) { - invoke.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "destroy", void.class), - instanceHandle); + // InjectableBean handle = Arc.container().instance(bean); + // handle.get().ping(); + ResultHandle containerHandle = invoke + .invokeStaticMethod(MethodDescriptor.ofMethod(Arc.class, "container", ArcContainer.class)); + ResultHandle beanHandle = invoke.invokeInterfaceMethod( + MethodDescriptor.ofMethod(ArcContainer.class, "bean", InjectableBean.class, String.class), + containerHandle, invoke.load(bean.getIdentifier())); + ResultHandle instanceHandle = invoke.invokeInterfaceMethod( + MethodDescriptor.ofMethod(ArcContainer.class, "instance", InstanceHandle.class, InjectableBean.class), + containerHandle, beanHandle); + ResultHandle beanInstanceHandle = invoke + .invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "get", Object.class), + instanceHandle); + if (method.parameters().isEmpty()) { + invoke.invokeVirtualMethod( + MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class), + beanInstanceHandle); + } else { + invoke.invokeVirtualMethod( + MethodDescriptor.ofMethod(implClazz.name().toString(), method.name(), void.class, + ScheduledExecution.class), + beanInstanceHandle, invoke.getMethodParam(0)); + } + // handle.destroy() - destroy dependent instance afterwards + if (BuiltinScope.DEPENDENT.is(bean.getScope())) { + invoke.invokeInterfaceMethod(MethodDescriptor.ofMethod(InstanceHandle.class, "destroy", void.class), + instanceHandle); + } } invoke.returnValue(null); diff --git a/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/staticmethod/ScheduledStaticMethodTest.java b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/staticmethod/ScheduledStaticMethodTest.java new file mode 100644 index 0000000000000..ac0777024fd5b --- /dev/null +++ b/extensions/scheduler/deployment/src/test/java/io/quarkus/scheduler/test/staticmethod/ScheduledStaticMethodTest.java @@ -0,0 +1,36 @@ +package io.quarkus.scheduler.test.staticmethod; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.scheduler.Scheduled; +import io.quarkus.test.QuarkusUnitTest; + +public class ScheduledStaticMethodTest { + + @RegisterExtension + static final QuarkusUnitTest test = new QuarkusUnitTest() + .withApplicationRoot((jar) -> jar + .addClasses(Jobs.class)); + + @Test + public void testSimpleScheduledJobs() throws InterruptedException { + assertTrue(Jobs.LATCH.await(5, TimeUnit.SECONDS)); + } + + static class Jobs { + + static final CountDownLatch LATCH = new CountDownLatch(1); + + @Scheduled(every = "1s") + static void everySecond() { + LATCH.countDown(); + } + } + +} diff --git a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java index 85e93e4337e74..d2f0aaa4de691 100644 --- a/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java +++ b/extensions/scheduler/runtime/src/main/java/io/quarkus/scheduler/Scheduled.java @@ -15,9 +15,9 @@ import io.quarkus.scheduler.Scheduled.Schedules; /** - * Marks a business method to be automatically scheduled and invoked by the container. + * Identifies a method of a bean class that is automatically scheduled and invoked by the container. *

- * The target business method must be non-private and non-static. + * A scheduled method is a non-abstract non-private method of a bean class. It may be either static or non-static. *

* The schedule is defined either by {@link #cron()} or by {@link #every()} attribute. If both are specified, the cron * expression takes precedence.