Spring Boot JobRunr examples
This article shows how to use Spring Boot to create REST endpoints to run the background jobs managed by the JobRunr.
Tested with:
- Spring Boot 2.3.12.RELEASE
- JobRunr 3.1.2
- Maven 3
1. Directory Structure.
A standard Maven project structure.
2. Project Dependencies.
We need a single jobrunr-spring-boot-starter
to integrate Spring Boot web and JobRunr.
<!-- jobrunr -->
<dependency>
<groupId>org.jobrunr</groupId>
<artifactId>jobrunr-spring-boot-starter</artifactId>
<version>1.2.0</version>
</dependency>
<!-- spring boot web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Review the JobRunr
dependencies, it depends on ASM and SLF4J.
$ mvn dependency:tree
[INFO] +- org.jobrunr:jobrunr-spring-boot-starter:jar:3.1.2:compile
[INFO] | \- org.jobrunr:jobrunr:jar:3.1.2:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.30:compile
[INFO] | \- org.ow2.asm:asm:jar:9.1:compile
Actually, the JobRunr
also need one of the JSON libraries (Jackson, GSON or Json-B) for JSON mapping. In this example, we can ignore the JSON library because the spring-boot-starter-web
provides a default Jackson library.
Review the full project dependencies.
$ mvn dependency:tree
[INFO] Scanning for projects...
[INFO]
[INFO] -------------------< com.mkyong:spring-boot-jobrunr >-------------------
[INFO] Building spring-boot-jobrunr 1.0
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.1.2:tree (default-cli) @ spring-boot-jobrunr ---
[INFO] com.mkyong:spring-boot-jobrunr:jar:1.0
[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.3.12.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.3.12.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:2.3.12.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.3.12.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.3.12.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.13.3:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.13.3:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.30:compile
[INFO] | | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.26:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-json:jar:2.3.12.RELEASE:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-databind:jar:2.11.4:compile
[INFO] | | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.11.4:compile
[INFO] | | | \- com.fasterxml.jackson.core:jackson-core:jar:2.11.4:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.11.4:compile
[INFO] | | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.11.4:compile
[INFO] | | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.11.4:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.3.12.RELEASE:compile
[INFO] | | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.46:compile
[INFO] | | +- org.glassfish:jakarta.el:jar:3.0.3:compile
[INFO] | | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.46:compile
[INFO] | +- org.springframework:spring-web:jar:5.2.15.RELEASE:compile
[INFO] | | \- org.springframework:spring-beans:jar:5.2.15.RELEASE:compile
[INFO] | \- org.springframework:spring-webmvc:jar:5.2.15.RELEASE:compile
[INFO] | +- org.springframework:spring-aop:jar:5.2.15.RELEASE:compile
[INFO] | +- org.springframework:spring-context:jar:5.2.15.RELEASE:compile
[INFO] | \- org.springframework:spring-expression:jar:5.2.15.RELEASE:compile
[INFO] +- org.jobrunr:jobrunr-spring-boot-starter:jar:3.1.2:compile
[INFO] | \- org.jobrunr:jobrunr:jar:3.1.2:compile
[INFO] | +- org.slf4j:slf4j-api:jar:1.7.30:compile
[INFO] | \- org.ow2.asm:asm:jar:9.1:compile
[INFO] +- org.springframework.boot:spring-boot-starter-test:jar:2.3.12.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test:jar:2.3.12.RELEASE:test
[INFO] | +- org.springframework.boot:spring-boot-test-autoconfigure:jar:2.3.12.RELEASE:test
[INFO] | +- com.jayway.jsonpath:json-path:jar:2.4.0:test
[INFO] | | \- net.minidev:json-smart:jar:2.3.1:test
[INFO] | | \- net.minidev:accessors-smart:jar:2.3.1:test
[INFO] | +- jakarta.xml.bind:jakarta.xml.bind-api:jar:2.3.3:test
[INFO] | | \- jakarta.activation:jakarta.activation-api:jar:1.2.2:test
[INFO] | +- org.assertj:assertj-core:jar:3.16.1:test
[INFO] | +- org.hamcrest:hamcrest:jar:2.2:test
[INFO] | +- org.junit.jupiter:junit-jupiter:jar:5.6.3:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-api:jar:5.6.3:test
[INFO] | | | +- org.apiguardian:apiguardian-api:jar:1.1.0:test
[INFO] | | | +- org.opentest4j:opentest4j:jar:1.2.0:test
[INFO] | | | \- org.junit.platform:junit-platform-commons:jar:1.6.3:test
[INFO] | | +- org.junit.jupiter:junit-jupiter-params:jar:5.6.3:test
[INFO] | | \- org.junit.jupiter:junit-jupiter-engine:jar:5.6.3:test
[INFO] | | \- org.junit.platform:junit-platform-engine:jar:1.6.3:test
[INFO] | +- org.mockito:mockito-core:jar:3.3.3:test
[INFO] | | +- net.bytebuddy:byte-buddy:jar:1.10.22:test
[INFO] | | +- net.bytebuddy:byte-buddy-agent:jar:1.10.22:test
[INFO] | | \- org.objenesis:objenesis:jar:2.6:test
[INFO] | +- org.mockito:mockito-junit-jupiter:jar:3.3.3:test
[INFO] | +- org.skyscreamer:jsonassert:jar:1.5.0:test
[INFO] | | \- com.vaadin.external.google:android-json:jar:0.0.20131108.vaadin1:test
[INFO] | +- org.springframework:spring-core:jar:5.2.15.RELEASE:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.2.15.RELEASE:compile
[INFO] | +- org.springframework:spring-test:jar:5.2.15.RELEASE:test
[INFO] | \- org.xmlunit:xmlunit-core:jar:2.7.0:test
[INFO] \- org.awaitility:awaitility:jar:4.0.3:test
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.699 s
[INFO] Finished at: 2021-06-20T14:13:37+08:00
[INFO] ------------------------------------------------------------------------
3. JobRunr Setup
For Spring Boot application, we can configurate the JobRunr
via the application.properties
file.
org.jobrunr.background-job-server.enabled=true
org.jobrunr.dashboard.enabled=true
- The property
org.jobrunr.background-job-server.enabled=true
tellsJobRunr
to start aBackgroundJobServer
instance to process jobs. - The
org.jobrunr.dashboard.enabled=true
tellsJobRunr
to start the embedded dashboard to monitor the jobs’s status.
More documentation is available on jobrunr.io
4. JobRunr Storage
The JobRunr
need storage to store the job details and support major SQL databases and NoSQL databases.
In this example, we use an in-memory data store to store the job details.
package com.mkyong;
import org.jobrunr.jobs.mappers.JobMapper;
import org.jobrunr.storage.InMemoryStorageProvider;
import org.jobrunr.storage.StorageProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MainConfiguration {
// The`spring-boot-starter-web` provides Jackson as JobMapper
@Bean
public StorageProvider storageProvider(JobMapper jobMapper) {
InMemoryStorageProvider storageProvider = new InMemoryStorageProvider();
storageProvider.setJobMapper(jobMapper);
return storageProvider;
}
}
5. Create and Run JobRunr job
5.1 Below is a simple Spring managed @Service
bean to log the provided message. And we can use @Job
to define the job’s name (This will display in the JobRunr’s dashboard).
package com.mkyong.job;
import org.jobrunr.jobs.annotations.Job;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class SampleJobService {
private Logger logger = LoggerFactory.getLogger(getClass());
@Job(name = "The sample job without variable")
public void execute() {
execute("Hello world!");
}
@Job(name = "The sample job with variable %0")
public void execute(String input) {
logger.info("The sample job has begun. The variable you passed is {}", input);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
logger.error("Error while executing sample job", e);
} finally {
logger.info("Sample job has finished...");
}
}
}
5.2 We need a JobScheduler
instance to run the jobs.
import org.jobrunr.scheduling.JobScheduler;
@Autowired // or @Inject
private JobScheduler jobScheduler;
@Autowired // or @Inject
private SampleJobService sampleJobService;
5.3 Run a fire-and-forget job (one time job)
jobScheduler.enqueue(
() -> sampleJobService.executeSampleJob());
5.4 Run a job that supports parameters.
jobScheduler.enqueue(
() -> sampleJobService.executeSampleJob("some string"));
5.5 Scheduling a job in the future.
// Old APIs, jobs followed by time
/*jobScheduler.schedule(
() -> sampleJobService.executeSampleJob(),
LocalDateTime.now().plusHours(5));*/
// new APIs, time first, followed by jobs (to be able to support Kotlin)
jobScheduler.schedule(
LocalDateTime.now().plusHours(5),
() -> sampleJobService.executeSampleJob());
5.6 Scheduling a job recurrently, run every hourly.
jobScheduler.scheduleRecurrently(
Cron.hourly(),
() -> sampleJobService.executeSampleJob());
6. Spring REST endpoints
Below is a Spring Boot example of creating the following endpoints:
/run-job
endpoint to run a job immediately./schedule-job
endpoint to schedule a job to run in the future.
package com.mkyong.api;
import com.mkyong.job.SampleJobService;
import org.jobrunr.scheduling.JobScheduler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.time.Instant;
@RestController
public class JobController {
@Autowired
private JobScheduler jobScheduler;
@Autowired
private SampleJobService sampleJobService;
@GetMapping("/run-job")
public String runJob(
@RequestParam(value = "name", defaultValue = "Hello World") String name) {
jobScheduler.enqueue(() -> sampleJobService.execute(name));
return "Job is enqueued.";
}
@GetMapping("/schedule-job")
public String scheduleJob(
@RequestParam(value = "name", defaultValue = "Hello World") String name,
@RequestParam(value = "when", defaultValue = "PT3H") String when) {
jobScheduler.schedule(
Instant.now().plus(Duration.parse(when)),
() -> sampleJobService.execute(name)
);
return "Job is scheduled.";
}
}
P.S In Duration
, the PT3H
means 3 hours, a ISO 8601 standard
7. Spring Boot Application.
Below is the main Spring Boot application to start everything.
package com.mkyong;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;
@SpringBootApplication
@Import(MainConfiguration.class)
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
Output
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.3.12.RELEASE)
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port(s): 8080 (http)
o.apache.catalina.core.StandardService : Starting service [Tomcat]
org.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/9.0.46]
o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext
w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1364 ms
o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService 'applicationTaskExecutor'
j.s.t.ScheduledThreadPoolJobRunrExecutor : ThreadManager of type 'ScheduledThreadPool' started
o.j.dashboard.JobRunrDashboardWebServer : JobRunr Dashboard started at http://localhost:8000/dashboard
o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8080 (http) with context path ''
com.mkyong.MainApplication : Started MainApplication in 2.527 seconds (JVM running for 3.094)
org.jobrunr.server.BackgroundJobServer : JobRunr BackgroundJobServer (eddcedcf-8b56-4eaf-8236-c12bf7904b40)
and 96 BackgroundJobPerformers started successfully
org.jobrunr.server.ServerZooKeeper : Server eddcedcf-8b56-4eaf-8236-c12bf7904b40 is master (this BackgroundJobServer)
8. JobRunr Dashboard
The JobRunr comes with a built-in dashboard to monitor our jobs detail like the number of scheduled, enqueued, processing, succeeded, and failed jobs. By default, the JobRunr dashboard started at http://localhost:8000
.
Below is a sample of the default JobRunr dashboard.
Try to run a job, and the JobRunr dashboard will display the job status automatically.
$ curl http://localhost:8080/run-job?name="mkyong"
Job is enqueued.
8. Spring Test and Jobrunr
We can declare spring-boot-starter-test
and awaitility to test on the endpoints.
<!-- Unit Tests, exclude JUnit 4, we want JUnit 5-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
package com.mkyong;
import org.jobrunr.jobs.states.StateName;
import org.jobrunr.storage.StorageProvider;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import java.time.Duration;
import java.util.concurrent.TimeUnit;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
@SpringBootTest(webEnvironment = DEFINED_PORT)
public class JobEndpointTest {
@Autowired
TestRestTemplate restTemplate;
@Autowired
StorageProvider storageProvider;
@Test
@DisplayName("Test job enqueued.")
public void givenEndpoint_whenJobEnqueued_thenJobIsProcessedWithin30Seconds() {
String response = runJobViaRest("from-test");
assertEquals("Job is enqueued.", response);
await()
.atMost(30, TimeUnit.SECONDS)
.until(() -> storageProvider.countJobs(StateName.SUCCEEDED) == 1);
}
@Test
@DisplayName("Test job scheduled.")
public void givenEndpoint_whenJobScheduled_thenJobIsScheduled() {
String response = scheduleJobViaRest("from-test", Duration.ofHours(3));
assertEquals("Job is scheduled.", response);
await()
.atMost(30, TimeUnit.SECONDS)
.until(() -> storageProvider.countJobs(StateName.SCHEDULED) == 1);
}
private String runJobViaRest(String input) {
return restTemplate.getForObject(
"http://localhost:8080/run-job?name=" + input,
String.class);
}
private String scheduleJobViaRest(String input, Duration duration) {
return restTemplate.getForObject(
"http://localhost:8080/schedule-job?name=" + input
+ "&when=" + duration.toString(),
String.class);
}
}
Download Source Code
$ git clone https://github.com/mkyong/spring-boot.git
$ cd spring-boot-jobrunr
$ mvn spring-boot:run
Hi Mkyong, I’ve one doubt. In above output screen its given “org.jobrunr.server.ServerZooKeeper : Server eddcedcf-8b56-4eaf-8236-c12bf7904b40 is master (this BackgroundJobServer)”. What does this means and it’s use ? Does JobRunr require zookeeper cluster setup if not then how zookeeper here is being used here. If you know can you help me to understand the Zookeeper significance here ?