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.

Note

A JobRequest must 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 in other JVM frameworks

Note

This 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.

  1. The StorageProvider is built from the framework-managed DataSource. It is mandatory dependency: JobRunr uses it to persist and retrieve job state.
  2. JobRunr is initialized with the StorageProvider bean and a CDI-backed JobActivator (see the note below). Both the BackgroundJobServer and 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.
  3. The JobScheduler is registered as a bean that can be injected to schedule jobs using Java 8 lambdas.
  4. The JobRequestScheduler is registered as a bean that can be injected to schedule jobs using a JobRequest.
  5. The BackgroundJobServer is registered as a bean so it can be started and stopped from lifecycle hooks (see below).
  6. The JobRunrDashboardWebServer is registered as a bean for the same reason: it was created with startServerOnInit: false, so it is started and stopped from the lifecycle hooks alongside the BackgroundJobServer.
Important

The JobActivator is 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:

Next steps