Backend Development 13 min read

Implementing Scheduled Device Upgrade with Spring Batch and Quartz in Spring Boot

This article explains how to handle a PC‑triggered device upgrade record by using Quartz for timed execution and Spring Batch for bulk processing, detailing Maven dependencies, YAML configuration, service and batch classes, custom reader/writer logic, a processor that calls an upgrade‑dispatch API, and the overall challenges encountered.

Architecture Digest
Architecture Digest
Architecture Digest
Implementing Scheduled Device Upgrade with Spring Batch and Quartz in Spring Boot

Introduction: The author was assigned an urgent requirement to record device upgrade triggers from a PC web page and perform batch updates using Quartz for scheduling and Spring Batch for processing.

Dependencies: The Maven pom.xml includes the following essential dependencies:

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-batch</artifactId>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
  </dependency>
</dependencies>

Configuration: application.yaml defines the datasource, batch job flag, server port, and the base URL for the upgrade‑dispatch service.

spring:
  datasource:
    username: thinklink
    password: thinklink
    url: jdbc:postgresql://172.16.205.54:5432/thinklink
    driver-class-name: org.postgresql.Driver
  batch:
    job:
      enabled: false
server:
  port: 8073
upgrade-dispatch-base-url: http://172.16.205.211:8080/api/noauth/rpc/dispatch/command/
batch-size: 5000

Service: BatchServiceImpl creates a JobParameters object containing taskId and a UUID, then launches the updateDeviceJob via JobLauncher .

@Service("batchService")
public class BatchServiceImpl implements BatchService {
    @Autowired private JobLauncher jobLauncher;
    @Autowired private Job updateDeviceJob;
    @Override
    public void createBatchJob(String taskId) throws Exception {
        JobParameters params = new JobParametersBuilder()
            .addString("taskId", taskId)
            .addString("uuid", UUID.randomUUID().toString().replace("-", ""))
            .toJobParameters();
        jobLauncher.run(updateDeviceJob, params);
    }
}

Batch configuration: BatchConfiguration defines the ItemReader using JdbcCursorItemReaderBuilder with a complex SQL query, an ItemWriter that updates task and device status in the database, and beans for Job and Step . A JobListener checks for failed devices after the job finishes and, if any are found, schedules a one‑time Quartz job to retry.

@Configuration
public class BatchConfiguration {
    @Value("${batch-size:5000}") private int batchSize;
    @Autowired private JobBuilderFactory jobBuilderFactory;
    @Autowired private StepBuilderFactory stepBuilderFactory;
    @Autowired private TaskItemProcessor taskItemProcessor;
    @Autowired private JdbcTemplate jdbcTemplate;

    @Bean @StepScope
    public JdbcCursorItemReader
itemReader(DataSource ds) {
        String sql = "SELECT e.ID AS taskId, e.user_id AS userId, ... FROM eiot_upgrade_task e ... WHERE e.ID = ?";
        return new JdbcCursorItemReaderBuilder
()
            .name("itemReader")
            .sql(sql)
            .dataSource(ds)
            .queryArguments(parameters.get("taskId").getValue())
            .rowMapper(new DispatchRequest.DispatchRequestRowMapper())
            .build();
    }

    @Bean @StepScope
    public ItemWriter
itemWriter() {
        return list -> { /* update task status, device_managered, etc. */ };
    }

    @Bean
    public Job updateDeviceJob(Step updateDeviceStep) {
        return jobBuilderFactory.get(UUID.randomUUID().toString().replace("-", ""))
            .listener(new JobListener())
            .flow(updateDeviceStep)
            .end()
            .build();
    }

    @Bean
    public Step updateDeviceStep(JdbcCursorItemReader
reader,
                                 ItemWriter
writer) {
        return stepBuilderFactory.get(UUID.randomUUID().toString().replace("-", ""))
            .
chunk(batchSize)
            .reader(reader)
            .processor(taskItemProcessor)
            .writer(writer)
            .build();
    }

    public class JobListener implements JobExecutionListener { /* beforeJob / afterJob logic */ }
}

Processor: TaskItemProcessor builds a JSON payload, sends it to the dispatch URL using RestTemplate , and returns a ProcessResult indicating success (1) or failure (2).

@Component("taskItemProcessor")
public class TaskItemProcessor implements ItemProcessor
{
    @Value("${upgrade-dispatch-base-url:http://localhost/api/v2/rpc/dispatch/command/}")
    private String dispatchUrl;
    @Autowired private JdbcTemplate jdbcTemplate;
    @Override
    public ProcessResult process(final DispatchRequest req) {
        String url = dispatchUrl + req.getDeviceId() + "/" + req.getUserId();
        // build JSON body
        JSONObject outer = new JSONObject();
        JSONObject inner = new JSONObject();
        inner.put("jobId", req.getTaskId());
        inner.put("name", req.getName());
        // ... other fields ...
        outer.put("method", "updateApp");
        outer.put("params", inner);
        // send request
        RestTemplate rt = new RestTemplate();
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
        HttpEntity
entity = new HttpEntity<>(outer.toString(), headers);
        int status = STATUS_DISPATCH_FAILED;
        try {
            ResponseEntity
resp = rt.postForEntity(url, entity, String.class);
            if (resp.getStatusCode() == HttpStatus.OK) status = STATUS_DISPATCH_SUCC;
        } catch (Exception e) { /* log */ }
        return new ProcessResult(req, status);
    }
}

Entity: DispatchRequest holds fields such as taskId , deviceId , userId , composeFile , etc., and includes a static inner class DispatchRequestRowMapper that maps a ResultSet to a DispatchRequest instance.

public class DispatchRequest {
    private String taskId; private String deviceId; private String userId; /* ... */
    public static class DispatchRequestRowMapper implements RowMapper
{
        @Override
        public DispatchRequest mapRow(ResultSet rs, int i) throws SQLException {
            DispatchRequest dr = new DispatchRequest();
            dr.setTaskId(rs.getString("taskId"));
            dr.setUserId(rs.getString("userId"));
            dr.setDeviceId(rs.getString("deviceId"));
            // ... other setters ...
            return dr;
        }
    }
}

Main class: The application is bootstrapped with @SpringBootApplication and @EnableBatchProcessing , launching the Spring context.

@SpringBootApplication
@EnableBatchProcessing
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Conclusion: Spring Batch reads 5,000 rows at a time but processes each item sequentially, which can become a performance bottleneck; the most troublesome parts are the custom ItemReader and ItemWriter SQL logic, while Quartz scheduling is relatively simple—just trigger the batch job at the desired interval.

JavaDatabasebatch processingSchedulerSpring BootQuartzSpring Batch
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.