JobRunr in Other Frameworks and JVM Languages
Learn how to use JobRunr with any JVM language or frameworks that lacks an official integration, including Grails, Play Framework, Scala, and Groovy.
On this page
If you are using a JVM language other than Java or Kotlin (e.g., Scala, Groovy, …) or a JVM framework without a dedicated JobRunr integration (e.g., Grails, Play, Helidon, Open Liberty, …), then, this page covers what you need to know. JobRunr should still work in all of these situations. The setup requires a bit more manual wiring, and in some languages the lambda-based scheduling API doesn’t work, but there is an alternative. Let’s detail the concrete steps.
Scheduling jobs in JVM languages other than Java and Kotlin using JobRunr
Throughout the getting started guides, we’ve used Java and Kotlin examples to demonstrate JobRunr’s capabilities. What about other JVM languages, such as Scala or Groovy? You can still use JobRunr with them, although there is one caveat.
Namely, scheduling jobs using a JobLambda: it is not supported because JVM languages such as Scala and Groovy generate bytecode differently. As a result, JobRunr cannot reliably parse the generated bytecode to extract the information required to execute a job, such as the class, method, and parameters. For Java and Kotlin, JobRunr is able to get all the information it needs from the lambda with the help of ASM.
Lambda based scheduling is very convenient, it allows to create jobs from virtually any public method, basically decoupling job scheduling from an application architecture. Fortunately, JobRunr offers a different approach: JobRequest based scheduling.
JobRequest and JobRequestHandler
The JobRequest / JobRequestHandler pattern avoids the need to inspect lambdas altogether. A JobRequest is simply a data object that is serialized to JSON, while a JobRequestHandler is a regular service class that JobRunr uses to execute when the job runs.
NoteA
JobRequestmust be serializable to and from JSON with your chosen library. Keep it small: it is stored in the database for every job instance.
Here is what this looks like in Scala, using the newsletter example from the quickstart guides:
import org.jobrunr.jobs.lambdas.JobRequest
import org.jobrunr.jobs.lambdas.JobRequestHandler
class NewsletterSubscriptionJobRequest(val email: String) extends JobRequest {
override def getJobRequestHandler: Class[NewsletterSubscriptionJobRequestHandler] =
classOf[NewsletterSubscriptionJobRequestHandler]
}
class NewsletterSubscriptionJobRequestHandler extends JobRequestHandler[NewsletterSubscriptionJobRequest] {
override def run(jobRequest: NewsletterSubscriptionJobRequest): Unit =
println("Confirmation email sent to " + jobRequest.email)
}
Then, to enqueue or schedule it, we can use BackgroundJobRequest or JobRequestScheduler:
BackgroundJobRequest.enqueue(NewsletterSubscriptionJobRequest("user@example.com"))
Examples
This pattern has been successfully applied to schedule jobs in both Scala and Groovy using JobRunr:
- Integrating JobRunr into Play Framework by Tanin Na Nakorn shows how to integrate JobRunr into Play Framework using Scala.
- Integrating JobRunr with Apache Grails shows how to integrate JobRunr in Apache Grails, a Groovy framework built on top of Spring Boot.
Integrating JobRunr in other JVM frameworks
NoteThis section uses APIs introduced in JobRunr 8.7.0.
JobRunr has official integrations for Micronaut, Quarkus, and Spring Boot. These integrations provide a native experience: auto-configuration using existing framework resources (e.g., a DataSource bean), property-based configuration, dependency injection for JobRunr’s core objects, etc.
A simple start using JobRunr is by configuring the library using the Fluent API which is framework agnostic. This approach is enough for most users.
Initializing JobRunr and exposing Beans
The example below shows how to wire up JobRunr using Jakarta EE CDI, a similar approach can be adapted for any framework that supports bean injection.
@ApplicationScoped
public class JobRunrConfiguration {
@Produces
@Singleton
public StorageProvider storageProvider(DataSource dataSource) {
return SqlStorageProviderFactory.using(dataSource);
}
@Produces
@Singleton
public JobRunrConfigurationResult initializeJobRunr(StorageProvider storageProvider) {
return JobRunr.configure() // Using JobRunr Pro? Replace `JobRunr` by `JobRunrPro`
.useJobActivator(this::jobInstanceProvider)
.useStorageProvider(storageProvider)
.useBackgroundJobServer(
BackgroundJobServerConfiguration.usingStandardBackgroundJobServerConfiguration(),
/* startServerOnInit*/ false
)
.useDashboard(
JobRunrDashboardWebServerConfiguration.usingStandardDashboardConfiguration(),
/* startServerOnInit*/ false
)
.initialize();
}
@Produces
@Singleton
public JobScheduler jobScheduler(JobRunrConfigurationResult configurationResult) {
return configurationResult.getJobScheduler();
}
@Produces
@Singleton
public JobRequestScheduler jobRequestScheduler(JobRunrConfigurationResult configurationResult) {
return configurationResult.getJobRequestScheduler();
}
@Produces
@Singleton
public BackgroundJobServer backgroundJobServer(JobRunrConfigurationResult configurationResult) {
return configurationResult.getBackgroundJobServer();
}
@Produces
@Singleton
public JobRunrDashboardWebServer dashboardWebServer(JobRunrConfigurationResult configurationResult) {
return configurationResult.getDashboardWebServer();
}
private <T> T jobInstanceProvider(Class<T> aClass) {
try {
return CDI.current().select(aClass).get();
} catch (IllegalStateException e) {
throw new JobActivatorShutdownException("CDI container is shutting down", e);
}
}
}
The configuration class initializes JobRunr once at startup and registers its core objects as CDI beans, making them available for injection across the application.
- The
StorageProvideris built from the framework-managedDataSource. It is mandatory dependency: JobRunr uses it to persist and retrieve job state. - JobRunr is initialized with the
StorageProviderbean and a CDI-backedJobActivator(see the note below). Both theBackgroundJobServerand the dashboard are configured, but neither is started yet (startServerOnInit: false); they are started from the lifecycle hooks (see below). The initialization result is wrapped in a holder bean so the objects can each be exposed individually. - The
JobScheduleris registered as a bean that can be injected to schedule jobs using Java 8 lambdas. - The
JobRequestScheduleris registered as a bean that can be injected to schedule jobs using aJobRequest. - The
BackgroundJobServeris registered as a bean so it can be started and stopped from lifecycle hooks (see below). - The
JobRunrDashboardWebServeris registered as a bean for the same reason: it was created withstartServerOnInit: false, so it is started and stopped from the lifecycle hooks alongside theBackgroundJobServer.
ImportantThe
JobActivatoris a functional interface that tells JobRunr how to resolve job service instances at execution time. Without it, JobRunr falls back to calling the default no-arg constructor, so job classes will not have their dependencies injected. By wiring it to CDI, JobRunr retrieves fully initialized instances from the container each time a job runs.
Tying JobRunr to the application lifecycle
With the beans in place, the last step is to tie job processing to the application lifecycle: start the BackgroundJobServer and the JobRunrDashboardWebServer once the application is ready and shut them down cleanly when the application stops. Both were created with startServerOnInit: false, so they are started here:
@ApplicationScoped
public class JobRunrStarter {
private final BackgroundJobServer backgroundJobServer;
private final JobRunrDashboardWebServer dashboardWebServer;
private final StorageProvider storageProvider;
public JobRunrStarter(BackgroundJobServer backgroundJobServer, JobRunrDashboardWebServer dashboardWebServer, StorageProvider storageProvider) {
this.backgroundJobServer = backgroundJobServer;
this.dashboardWebServer = dashboardWebServer;
this.storageProvider = storageProvider;
}
void startup(@Observes StartupEvent event) {
dashboardWebServer.start();
backgroundJobServer.start();
}
void shutdown(@Observes ShutdownEvent event) {
backgroundJobServer.stop();
dashboardWebServer.stop();
storageProvider.close();
}
}
The configuration above covers the essentials, but the Fluent API supports much more. As requirements grow, .configure() can be extended with additional options: publishing metrics via Micrometer with .useMicroMeter(...), or enabling dynamic queues load balancing (a JobRunr Pro feature).
For a reference on what a fully automated framework integration looks like, the Spring Boot Starter is a good example.
Examples
A similar strategy has been employed in the below examples:
- Integrating JobRunr into Play Framework by Tanin Na Nakorn shows how to integrate JobRunr into Play Framework.
Next steps
- Background job dependencies — how the
JobActivatorbridges JobRunr and any IoC container - JobRequest and JobRequestHandler reference
- Fluent API reference
- Serialization — configure your JSON library
- JobRunr Pro JobRunr Pro — batches, job chaining, priority queues, rate limiting, and an advanced dashboard, all available regardless of your stack
