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.