{{announcement.body}}
{{announcement.title}}

JobRunr and Spring Data

DZone 's Guide to

JobRunr and Spring Data

In this article, see a tutorial on how to generate salary slips using JobRunr and Spring Data.

· Java Zone ·
Free Resource

In this tutorial, we will be working for the fictional company Acme Corp and we need to generate the salary slips for all of Acme Corp's employees.

TLDR; you can find the complete project on our GitHub repository: https://github.com/jobrunr/example-salary-slip

To do so, we will be using 3 open-source components:

  • JobRunr: JobRunr allows to easily schedule and process background jobs using Java 8 lambda's. It is backed by persistent storage and can process jobs in a parallel and distributed manner. Thanks to the built-in dashboard we have an in-depth overview into all our background jobs.
  • Spring Data Jpa: If you want to easily access data in a relational database, Spring Data Jpa is here to help. You can create repositories using nothing more than a simple interface
  • Docx-Stamper: Docx-Stamper allows to easily generate Word (.docx) documents backed by templates

Architecture

During this tutorial, we will generate the weekly salary slip of all of Acme Corp's employees and email it to them. How? Well, by

  • Creating a recurring job using JobRunr that will run every week — it will get all of Acme Corp's employees using Spring Data Jpa and for each of these employees schedule a new background job to create the salary slip
  • Each of these background jobs will fetch the Employee and
    • consume a TimeClockService which gives the amount of hours an employee worked for the given week.
    • generate a salary slip document using a DocumentGenerationService which will contain the name of the employee and the amount of hour he or she worked. The salary slip document is generated from a Word template and converted to a PDF file.
    • send an email to the employee with his salary slip using an EmailService.

We will use a Word template and replace placeholders with actual values and then convert it to a PDF

Let's Get Started!

In this tutorial we omit all Java imports for brevity. To find them, just visit the example project on https://github.com/jobrunr/example-salary-slip

For building this salary slip service, we use gradle and our build.gradle file is as follows:

Gradle Build File

Java
 




x
40


 
1
plugins {
2
    id 'java'
3
    id 'idea'
4
    id 'org.springframework.boot' version '2.2.2.RELEASE'
5
    id 'io.spring.dependency-management' version '1.0.8.RELEASE'
6
}
7
 
            
8
group 'org.paycheck'
9
version '1.0-SNAPSHOT'
10
 
            
11
repositories {
12
    mavenCentral()
13
    jcenter()
14
    mavenLocal()
15
}
16
 
            
17
configurations.all {
18
    exclude group: 'org.slf4j', module: 'slf4j-log4j12'
19
}
20
 
            
21
dependencies {
22
    implementation 'org.jobrunr:jobrunr:0.9.2'
23
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
24
    implementation 'org.springframework.boot:spring-boot-starter-mail'
25
    implementation 'com.fasterxml.jackson.core:jackson-databind'
26
    implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
27
    implementation 'org.wickedsource.docx-stamper:docx-stamper:1.4.0'
28
    implementation 'org.docx4j:docx4j-core:8.1.6'
29
    implementation 'org.docx4j:docx4j-JAXB-ReferenceImpl:11.1.3'
30
 
            
31
    implementation 'com.github.javafaker:javafaker:1.0.2'
32
    implementation 'com.h2database:h2'
33
 
            
34
    testImplementation 'org.junit.jupiter:junit-jupiter:5.6.1'
35
    testImplementation 'org.awaitility:awaitility:4.0.2'
36
}
37
 
            
38
test {
39
    useJUnitPlatform()
40
}


our build.gradle file uses JobRunr, Spring Boot Data, Sprint Boot Mail and Docx-Stamper. We exclude slf4j-log4j12 as it clashes with Logback provided by Spring
 

Employee Entity

Since we need to create salary slips for all employees let us start with the Employee class — it is a simple Entity with some fields like firstName, lastName and email.

Java
 




xxxxxxxxxx
1
49


 
1
package org.jobrunr.example.employee;
2
 
            
3
@Entity
4
public class Employee {
5
 
            
6
    @Id
7
    @GeneratedValue(strategy = GenerationType.AUTO)
8
    private Long id;
9
    private String firstName;
10
    private String lastName;
11
    private String email;
12
 
            
13
    protected Employee() {
14
    }
15
 
            
16
    public Employee(String firstName, String lastName, String email) {
17
        this(null, firstName, lastName, email);
18
    }
19
 
            
20
    public Employee(Long id, String firstName, String lastName, String email) {
21
        this.id = id;
22
        this.firstName = firstName;
23
        this.lastName = lastName;
24
        this.email = email;
25
    }
26
 
            
27
    @Override
28
    public String toString() {
29
        return String.format(
30
                "Employee[id=%d, firstName='%s', lastName='%s']",
31
                id, firstName, lastName);
32
    }
33
 
            
34
    public Long getId() {
35
        return id;
36
    }
37
 
            
38
    public String getFirstName() {
39
        return firstName;
40
    }
41
 
            
42
    public String getLastName() {
43
        return lastName;
44
    }
45
 
            
46
    public String getEmail() {
47
        return email;
48
    }
49
}


our Employee class is a simple javax persistence entity


EmployeeRepository

The EmployeeRepository extends the Spring Data CrudRepository and adds an extra method to fetch all the id's of the Employees.

Java
 




xxxxxxxxxx
1


 
1
package org.jobrunr.example.employee;
2
 
            
3
public interface EmployeeRepository extends CrudRepository<Employee, Long> {
4
 
            
5
    @Query("select e.id from Employee e")
6
    Stream<Long> getAllEmployeeIds();
7
 
            
8
}



WorkWeek

Since the salary slip is generated once per week, we need a class representing the amount of time an employee has worked that week — the WorkWeek class. It has some extra fields like the weekNbr and a from and to date, which we will use for our generated salary slip document.

Java
 




xxxxxxxxxx
1
65


 
1
package org.jobrunr.example.timeclock;
2
 
           
3
public class WorkWeek {
4
 
           
5
    private final int weekNbr;
6
    private final BigDecimal workHoursMonday;
7
    private final BigDecimal workHoursTuesday;
8
    private final BigDecimal workHoursWednesday;
9
    private final BigDecimal workHoursThursday;
10
    private final BigDecimal workHoursFriday;
11
    private final LocalDate from;
12
    private final LocalDate to;
13
 
           
14
    public WorkWeek(BigDecimal workHoursMonday, BigDecimal workHoursTuesday, BigDecimal workHoursWednesday, BigDecimal workHoursThursday, BigDecimal workHoursFriday) {
15
        this.workHoursMonday = workHoursMonday;
16
        this.workHoursTuesday = workHoursTuesday;
17
        this.workHoursWednesday = workHoursWednesday;
18
        this.workHoursThursday = workHoursThursday;
19
        this.workHoursFriday = workHoursFriday;
20
        WeekFields weekFields = WeekFields.of(Locale.getDefault());
21
        weekNbr = now().get(weekFields.weekOfWeekBasedYear());
22
        this.from = now().with(TemporalAdjusters.previous(DayOfWeek.MONDAY));
23
        this.to = now().with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY));
24
    }
25
 
           
26
    public BigDecimal getWorkHoursMonday() {
27
        return workHoursMonday;
28
    }
29
 
           
30
    public BigDecimal getWorkHoursTuesday() {
31
        return workHoursTuesday;
32
    }
33
 
           
34
    public BigDecimal getWorkHoursWednesday() {
35
        return workHoursWednesday;
36
    }
37
 
           
38
    public BigDecimal getWorkHoursThursday() {
39
        return workHoursThursday;
40
    }
41
 
           
42
    public BigDecimal getWorkHoursFriday() {
43
        return workHoursFriday;
44
    }
45
 
           
46
    public int getWeekNbr() {
47
        return weekNbr;
48
    }
49
 
           
50
    public LocalDate getFrom() {
51
        return from;
52
    }
53
 
           
54
    public LocalDate getTo() {
55
        return to;
56
    }
57
 
           
58
    public BigDecimal getTotal() {
59
        return workHoursMonday
60
                .add(workHoursTuesday)
61
                .add(workHoursWednesday)
62
                .add(workHoursThursday)
63
                .add(workHoursFriday);
64
    }
65
}



TimeClockService

To get a WorkWeek class for a certain employee, we create a TimeClockService which is a Spring Component. As we don't want to make this tutorial overly complex, here we use a stub which generates some random data. In a real-world application, this service would make a REST or SOAP request to another microservice.

Java
 




xxxxxxxxxx
1
28


 
1
package org.jobrunr.example.timeclock;
2
 
            
3
@Component
4
public class TimeClockService {
5
 
            
6
    public WorkWeek getWorkWeekForEmployee(Long employeeId) {
7
        try {
8
            //simulate a long-during call
9
            Thread.sleep(ThreadLocalRandom.current().nextInt(3, 5 + 1) * 1000);
10
            return new WorkWeek(
11
                    BigDecimal.valueOf(getRandomHours()),
12
                    BigDecimal.valueOf(getRandomHours()),
13
                    BigDecimal.valueOf(getRandomHours()),
14
                    BigDecimal.valueOf(getRandomHours()),
15
                    BigDecimal.valueOf(getRandomHours())
16
            );
17
        } catch (InterruptedException e) {
18
            Thread.currentThread().interrupt();
19
            throw new RuntimeException(e);
20
        }
21
    }
22
 
            
23
    private int getRandomHours() {
24
        Random r = new Random();
25
        return r.nextInt((8 - 5) + 1) + 5;
26
    }
27
 
            
28
}


To simplify this tutorial, we use a stub for the TimeClockService which generates random work hours for each employee.

We now have all the necessary data to generate our salary slip — except the SalarySlip class itself:

SalarySlip

Java
 




xxxxxxxxxx
1
34


 
1
package org.jobrunr.example.paycheck;
2
 
           
3
public class SalarySlip {
4
 
           
5
    private final Employee employee;
6
    private final WorkWeek workWeek;
7
 
           
8
    public SalarySlip(Employee employee, WorkWeek workWeek) {
9
        this.employee = employee;
10
        this.workWeek = workWeek;
11
    }
12
 
           
13
    public Employee getEmployee() {
14
        return employee;
15
    }
16
 
           
17
    public WorkWeek getWorkWeek() {
18
        return workWeek;
19
    }
20
 
           
21
    public BigDecimal getTotal() {
22
        BigDecimal totalPerHour = getTotalPerHour();
23
        BigDecimal amountOfWorkedHours = getAmountOfWorkedHours();
24
        return totalPerHour.multiply(amountOfWorkedHours).setScale(2, RoundingMode.HALF_UP);
25
    }
26
 
           
27
    private BigDecimal getAmountOfWorkedHours() {
28
        return workWeek.getTotal();
29
    }
30
 
           
31
    private BigDecimal getTotalPerHour() {
32
        return BigDecimal.valueOf(21.50);
33
    }
34
}


The SalarySlip class contains all the data necessary to generate a salary slip and will be used by the DocumentGenerationService to generate the salary slips as PDF documents.

DocumentGenerationService

Java
 




xxxxxxxxxx
1
19


 
1
package org.jobrunr.example.paycheck;
2
 
           
3
@Component
4
public class DocumentGenerationService {
5
 
           
6
    public void generateDocument(Path wordTemplatePath, Path wordOutputPath, Object context) throws IOException, Docx4JException {
7
        Files.createDirectories(wordOutputPath.getParent().toAbsolutePath());
8
 
           
9
        try(InputStream template = Files.newInputStream(wordTemplatePath); OutputStream out = Files.newOutputStream(wordOutputPath)) {
10
            final ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
11
            final DocxStamper stamper = new DocxStamperConfiguration().setFailOnUnresolvedExpression(true).build();
12
            stamper.stamp(template, context, byteArrayOutputStream);
13
 
           
14
            Docx4J.toPDF(WordprocessingMLPackage.load(new ByteArrayInputStream(byteArrayOutputStream.toByteArray())), out);
15
        }
16
 
           
17
    }
18
 
           
19
}



The DocumentGenerationService is also a Spring Component and has the responsibility to generate the actual salary slip documents based on a word template. The word template has a lot of placeholders, like ${employee.firstName}, ${employee.lastName} and ${workWeek.workHoursMonday.setScale(2)} that will be replaced by DocxStamper using the given context object — in our case, a SalarySlip object. Finally, the Word document with all fields filled in is converted to a PDF document.

EmailService

The emailservice is again a Spring Component and has the responsibility to email the final salary slip word document to the employee. It uses Spring Boot Starter Email and using a MimeMessage created by the JavaMailSender. It has a method called sendSalarySlip with two arguments - the employee class and the path to the salary slip for that employee. Using these argument, we can send both a personalized text email and attach the actual salary slip as an attachment.

The JavaMailSender is a class provided by Spring Boot Starter Mail and configured using a properties file. You can find the properties file here:

Java
 




xxxxxxxxxx
1
24


 
1
package org.jobrunr.example.email;
2
 
           
3
@Component
4
public class EmailService {
5
 
           
6
    public JavaMailSender emailSender;
7
 
           
8
    public EmailService(JavaMailSender emailSender) {
9
        this.emailSender = emailSender;
10
    }
11
 
           
12
    public void sendSalarySlip(Employee employee, Path salarySlipPath) throws MessagingException {
13
        MimeMessage message = emailSender.createMimeMessage();
14
        MimeMessageHelper helper = new MimeMessageHelper(message, true);
15
        helper.setTo(employee.getEmail());
16
        helper.setSubject("Your weekly salary slip");
17
        helper.setText(String.format("Dear %s,\n\nhere you can find your weekly salary slip. \n \nThanks again for your hard work,\nAcme corp", employee.getFirstName()));
18
 
           
19
        FileSystemResource file = new FileSystemResource(salarySlipPath);
20
        helper.addAttachment("Salary Slip", file);
21
        emailSender.send(message);
22
    }
23
 
           
24
}



And Finally, the SalarySlipService

The SalarySlipService is the last step of the puzzle and wires everything together:

  • it has the path to the salary slip word template
  • it uses the other components we already created:
    • the EmployeeRepository to get all employees from the database
    • the TimeClockService to get the number of hours an employee worked
    • the DocumentGenerationService to create a salary slip from the given Word template
    • the EmailService to send a personalized email with the salary slip as an attachment to the employee

It also two important public methods:

generateAndSendSalarySlip

The method generateAndSendSalarySlip uses the employee id to get the actual employee data, generates the salary slip Word document, and sends it via email to the employee. It will be a JobRunr background job and it is called from the method generateAndSendSalarySlipToAllEmployees. We annotate it with the optional Job annotation to have meaningful names in dashboard of JobRunr.

Java
 




xxxxxxxxxx
1


 
1
    @Job(name = "Generate and send salary slip to employee %0")
2
    public void generateAndSendSalarySlip(Long employeeId) throws Exception {
3
        final Employee employee = getEmployee(employeeId);
4
        Path salarySlipPath = generateSalarySlip(employee);
5
        emailService.sendSalarySlip(employee, salarySlipPath);
6
    }



The method generateAndSendSalarySlip , a standard method in our service, will become a background job. 

The document generation fails because there is not enough disk space? Or the TimeClockService fails for an employee because the external microservice is down? No worries - as JobRunr is fault-tolerant (it will automatically retry failed jobs with an exponential back-off policy), these failing jobs will be retried 10 times automatically.

generateAndSendSalarySlipToAllEmployees

This is the main method that will be scheduled each week - it gets a stream of employee ids and using the BackgroundJob.enqueue method, we create a background job of the generateAndSendSalarySlip method.

Java
 




xxxxxxxxxx
1


 
1
    @Transactional(readOnly = true)
2
    @Job(name = "Generate and send salary slip to all employees")
3
    public void generateAndSendSalarySlipToAllEmployees() {
4
        final Stream<Long> allEmployees = employeeRepository.getAllEmployeeIds();
5
        BackgroundJob.<SalarySlipService, Long>enqueue(allEmployees, (salarySlipService, employeeId) -> salarySlipService.generateAndSendSalarySlip(employeeId));
6
    }



This method will be scheduled each week and will create new background jobs for the generation and sending of the salary slip for each employee.


The complete SalarySlipService is as follows:

Java
 




xxxxxxxxxx
1
54


 
1
package org.jobrunr.example.paycheck;
2
 
           
3
@Component
4
public class SalarySlipService {
5
 
           
6
    private static final Path salarySlipTemplatePath = Path.of("src/main/resources/templates/salary-slip-template.docx");
7
 
           
8
    private final EmployeeRepository employeeRepository;
9
    private final TimeClockService timeClockService;
10
    private final DocumentGenerationService documentGenerationService;
11
    private final EmailService emailService;
12
 
           
13
    public SalarySlipService(EmployeeRepository employeeRepository, TimeClockService timeClockService, DocumentGenerationService documentGenerationService, EmailService emailService) {
14
        this.employeeRepository = employeeRepository;
15
        this.timeClockService = timeClockService;
16
        this.documentGenerationService = documentGenerationService;
17
        this.emailService = emailService;
18
    }
19
 
           
20
    @Transactional(readOnly = true)
21
    @Job(name = "Generate and send salary slip to all employees")
22
    public void generateAndSendSalarySlipToAllEmployees() {
23
        final Stream<Long> allEmployees = employeeRepository.getAllEmployeeIds();
24
        BackgroundJob.<SalarySlipService, Long>enqueue(allEmployees, (salarySlipService, employeeId) -> salarySlipService.generateAndSendSalarySlip(employeeId));
25
    }
26
 
           
27
    @Job(name = "Generate and send salary slip to employee %0")
28
    public void generateAndSendSalarySlip(Long employeeId) throws Exception {
29
        final Employee employee = getEmployee(employeeId);
30
        Path salarySlipPath = generateSalarySlip(employee);
31
        emailService.sendSalarySlip(employee, salarySlipPath);
32
    }
33
 
           
34
    private Path generateSalarySlip(Employee employee) throws Exception {
35
        final WorkWeek workWeek = getWorkWeekForEmployee(employee.getId());
36
        final SalarySlip salarySlip = new SalarySlip(employee, workWeek);
37
        return generateSalarySlipDocumentUsingTemplate(salarySlip);
38
    }
39
 
           
40
    private Path generateSalarySlipDocumentUsingTemplate(SalarySlip salarySlip) throws Exception {
41
        Path salarySlipPath = Paths.get(System.getProperty("java.io.tmpdir"), String.valueOf(now().getYear()), format("workweek-%d", salarySlip.getWorkWeek().getWeekNbr()), format("salary-slip-employee-%d.docx", salarySlip.getEmployee().getId()));
42
        documentGenerationService.generateDocument(salarySlipTemplatePath, salarySlipPath, salarySlip);
43
        return salarySlipPath;
44
    }
45
 
           
46
    private WorkWeek getWorkWeekForEmployee(Long employeeId) {
47
        return timeClockService.getWorkWeekForEmployee(employeeId);
48
    }
49
 
           
50
    private Employee getEmployee(Long employeeId) {
51
        return employeeRepository.findById(employeeId).orElseThrow(() -> new IllegalArgumentException(format("Employee with id '%d' does not exist", employeeId)));
52
    }
53
 
           
54
}



Last But Not Least, Our Spring Boot Application

The Spring Boot Application bootstraps our application and has one important piece of code:

Java
 




xxxxxxxxxx
1


 
1
BackgroundJob.scheduleRecurringly(
2
    "generate-and-send-salary-slip",
3
    SalarySlipService::generateAndSendSalarySlipToAllEmployees,
4
    Cron.weekly(DayOfWeek.SUNDAY, 22)
5
);



This method call ensures that the generateAndSendSalarySlipToAllEmployees method of our SalarySlipService will be triggered each Sunday at 10pm.

In this SpringBootApplication, we create some fake employees, define a DataSource (in our case a simple H2 database), and initialize JobRunr using it's fluent-api.

Java
 




xxxxxxxxxx
1
48


 
1
package org.jobrunr.example;
2
 
           
3
@SpringBootApplication
4
public class SalarySlipMicroService {
5
 
           
6
    public static void main(String[] args) {
7
        SpringApplication.run(SalarySlipMicroService.class, args);
8
    }
9
 
           
10
    @Bean
11
    public CommandLineRunner demo(EmployeeRepository repository) {
12
        final Faker faker = new Faker();
13
        return (args) -> {
14
            for(int i = 0; i < 1000; i++) {
15
                repository.save(new Employee(faker.name().firstName(), faker.name().lastName(), faker.internet().emailAddress()));
16
            }
17
 
           
18
            BackgroundJob.scheduleRecurringly(
19
                    "generate-and-send-salary-slip",
20
                    SalarySlipService::generateAndSendSalarySlipToAllEmployees,
21
                    Cron.weekly(DayOfWeek.SUNDAY, 22)
22
            );
23
 
           
24
            Thread.currentThread().join();
25
        };
26
    }
27
 
           
28
    @Bean
29
    public DataSource dataSource() {
30
        final JdbcDataSource ds = new JdbcDataSource();
31
        ds.setURL("jdbc:h2:" + Paths.get(System.getProperty("java.io.tmpdir"), "paycheck"));
32
        ds.setUser("sa");
33
        ds.setPassword("sa");
34
        return ds;
35
    }
36
 
           
37
    @Bean
38
    public JobScheduler initJobRunr(ApplicationContext applicationContext) {
39
        return JobRunr.configure()
40
                .useStorageProvider(SqlStorageProviderFactory
41
                        .using(applicationContext.getBean(DataSource.class)))
42
                .useJobActivator(applicationContext::getBean)
43
                .useDefaultBackgroundJobServer()
44
                .useDashboard()
45
                .initialize();
46
    }
47
 
           
48
}



Time to Use Our New Application!

Once you start the SalarySlipMicroService application, you can open your browser using the URL http://localhost:8000/dashboard and navigate to the Recurring jobs tab.

Our recurring job that will be triggered each Sunday


To test it, we trigger it now manually. The job is processed and schedules a new job to create a salary slip for each employee. Within 15 seconds the processing of these jobs start and we will see the generated PDF documents in our tmp folder.

An immediate overview of all jobs that are being processed


We can inspect a Job and see the result of the processing - if it would fail for some reason, it will be automatically retried.

Conclusion

  • JobRunr and Spring Data integrate very well and both are very easy to use. Being able to schedule Java 8 lambda's and have them run in a background process is a really nice feature of JobRunr.
  • To convert the Word document to PDF, there is some nasty stuff in the word template (like white text) to have an OK-layout. Docx-Stamper is a great library and depends on Docx4J. Docx4J allows us to convert Word documents to PDF, but it still requires some work as a couple of hacks were done to get the layout right.
Topics:
batch processing, batch programming, distributed computing, java, java 1.8, scheduling

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}