Skip to content

Conversation

@iluwa
Copy link
Contributor

@iluwa iluwa commented Aug 1, 2025

Added the @RecurringTask annotation to simplify the creation of basic recurring tasks in a Spring environment.

The main implementation is in the RecurringTaskRegistryPostProcessor class, which scans Spring beans for methods annotated with @RecurringTask and registers a corresponding Task bean definition.

The annotated method must follow these rules:

  • It must return void.
  • It may have or may not have two parameters of type TaskInstance and ExecutionContext
  • It can also accept multiple Spring beans as parameters, which are injected from the context at runtime.

Fixes

#509

Reminders

  • Added/ran automated tests
  • Update README and/or examples
  • Ran mvn spotless:apply

cc @kagkarlsson

Copy link
Owner

@kagkarlsson kagkarlsson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! Thank you for contributing such a useful feature! A few comments and questions, but looks good 👍

Comment on lines 94 to 100
for (int i = 0; i < method.getParameterTypes().length; i++) {
Class<?> parameterType = method.getParameterTypes()[i];
if (!INPUT_ARGUMENTS_AVAILABLE_CLASSES.contains(parameterType)
&& context.getBeanNamesForType(parameterType).length == 0) {
throw new IllegalArgumentException(
"RecurringTask annotated method is required to have specific inputs: TaskInstance, ExecutionContext or a Spring bean");
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe possible to use for (Class<?> parameterType : method.getParameterTypes()) ... style loop?

Also, what do we gain by allowing inject of Spring beans via method-parameters? Isn't normal spring injection sufficient?

Copy link
Contributor Author

@iluwa iluwa Sep 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a lot of cases when a task depends on some class with the logic. If the injection is implemented on the input parameter level, then it is possible to pass dependencies to the method itself rather then injecting everything in the class.

The case with method injection:

@Configuration
public class ScheduledConfig {
  void recurringSampleTask1(Service1 service1) {...}
  void recurringSampleTask2(Service2 service2) {...}
  void recurringSampleTask3(Service3 service3) {...}
}

The case with class injections that should work out of the box:

@Configuration
public class ScheduledConfig {
  private final Service1 service1;
  private final Service2 service2;
  private final Service3 service3;

  contructor {...}

  void recurringSampleTask1() { use service1 }
  void recurringSampleTask2() { use service2 }
  void recurringSampleTask3() { use service3 }
}

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Feels a bit like re-implementing the Spring-injection, but not completely (for instance, what about Qualifiers?)

Are there examples of other projects doing this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I checked Spring Scheduled and JobRunr, both don't have this feature.

Thank you for mentioning Qualifier, in this implementation it won't work, as other things like Lazy etc.

Agree, it is easier and less error-prone to leave injection to Spring. I'll remove it and fix the documentation and examples.

Comment on lines 18 to 20
String name();

String cron() default "-";
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered supporting the other inputs to Cron: zoneId and cronStyle?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't think about this.
I see that there are 5 different CronStyles, I'll try to implement all of them.

  CRON4J,
  QUARTZ,
  UNIX,
  SPRING,
  SPRING53;

And I think ZoneId should be added as a separate field in annotation.

})
.toArray();
try {
ReflectionUtils.makeAccessible(method);
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be better to require the method to be public?

Comment on lines 404 to 407
@RecurringTask(name = "taskFromAnnotation")
public void taskFromAnnotation() {
log.info("I'm a task from annotation");
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the schedule for this one? 🤔

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I see the default:

String cron() default "-";

Should we perhaps drop the default? User have to supply it..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On my day-to-day job my team uses this value as a way to disable jobs.
Maybe here it adds more confusion than it helps. I'll remove the default and make it required.

Comment on lines +15 to +19
@Bean
public RecurringTaskRegistryPostProcessor recurringTaskRegistryPostProcessor(
GenericApplicationContext context) {
return new RecurringTaskRegistryPostProcessor(context);
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be possible to have the RecurringTaskRegistryPostProcessor resolve property-references in the cron-pattern?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I did a quick test and it's possible to make, needs some enhancements of RecurringTaskRegistryPostProcessor.
I'll add this to the PR.

private Task<?> createTaskFromMethod(
RecurringTask recurringTask, Method method, Object existingObject, String resolvedCron) {

return Tasks.recurring(recurringTask.name(), Schedules.cron(resolvedCron))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I miss the option to configure the FailureHandler.

As far as I can see, the FailureHandler behavior cannot be configured. This means that the default Failure Handler is used automatically, which is not suitable for everyone.

I would be grateful if this functionality could be taken into account in this pull request.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Default is

this.onFailure = new FailureHandler.OnFailureReschedule<>(schedule);

Do you need a completely custom implementation? (I.e. lookup a FailureHandler in the app-context based on a bean-name?)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. I think that would be useful feature. You define your FailureHandlerBean. This FailureHandler will then be used for all the task, which are created via @RecurringTask annotation.

But this will not be enough for the case, where you have multiple @RecurringTask where each of them need an different FailureHandler.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am also ok with adding customizable failure-handler in a future iteration of this.

This is a convenient short-hand for most common recurring tasks, and I don't think it needs to cover all variants

@beilCrxmarkets
Copy link
Contributor

@kagkarlsson
Just a thought for consideration —
At my company, we also explored wrapping the db-scheduler library and initializing tasks via annotations.
This approach turned out to be a perfect fit for static recurring tasks, as they usually do not have any arguments.
However, when we tried to extend the annotation concept to dynamic recurring tasks (e.g., tasks with a persistent schedule), we ran into difficulties. The main problem was losing type safety for task arguments, which could lead to issues during refactoring.
For dynamic recurring tasks, the best approach we found was to provide an interface. To keep the API consistent, we eventually decided to drop the annotation idea for static recurring tasks as well and instead also use an interface for them.
I described this in more detail here: #743
In short:
Using an annotation to initialize static recurring tasks might look great at first glance, but could result in inconsistent APIs in the long run, since it is not a good fit for all task types.

@kagkarlsson
Copy link
Owner

Yeah the @RecurringTask annotation will work for static recurring tasks (which are declarative), but not dynamic ones, which will have varying data and schedule which are mostly determined at runtime I assume(?). I think the annotations need to be scoped to the static recurring tasks. And anyone have requirements that cannot be met using the annotation, can use Tasks.recurring... or similar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants