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.
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: 5000Service: 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.