Main Tutorials

Spring Boot JobRunr examples

JobRunr Logo

This article shows how to use Spring Boot to create REST endpoints to run the background jobs managed by the JobRunr.

Tested with:

  1. Spring Boot 2.3.12.RELEASE
  2. JobRunr 3.1.2
  3. Maven 3

1. Directory Structure.

A standard Maven project structure.

project structure

2. Project Dependencies.

We need a single jobrunr-spring-boot-starter to integrate Spring Boot web and JobRunr.

pom.xml

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

Terminal

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

application.properties

org.jobrunr.background-job-server.enabled=true
org.jobrunr.dashboard.enabled=true
  • The property org.jobrunr.background-job-server.enabled=true tells JobRunr to start a BackgroundJobServer instance to process jobs.
  • The org.jobrunr.dashboard.enabled=true tells JobRunr 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).

SampleJobService.java

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:

  1. /run-job endpoint to run a job immediately.
  2. /schedule-job endpoint to schedule a job to run in the future.
JobController.java

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.

MainApplication.java

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

Terminal

.   ____          _            __ _ _
/\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/  ___)| |_)| | | | | || (_| |  ) ) ) )
'  |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: 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.

JobRunr dashboard

Try to run a job, and the JobRunr dashboard will display the job status automatically.

Terminal

$ curl http://localhost:8080/run-job?name="mkyong"
Job is enqueued.

JobRunr dashboard

8. Spring Test and Jobrunr

We can declare spring-boot-starter-test and awaitility to test on the endpoints.

pom.xml

	<!-- 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>
JobEndpointTest.java

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

References

About Author

author image
Founder of Mkyong.com, love Java and open source stuff. Follow him on Twitter. If you like my tutorials, consider make a donation to these charities.

Comments

Subscribe
Notify of
1 Comment
Most Voted
Newest Oldest
Inline Feedbacks
View all comments
Abhishek
2 years ago

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 ?