skip to content

How does SystemCommandTasklet work and what must you configure to use it safely?

level: seniorimportance: should knowfreq 30%

answer

  1. Runs an external OS command as a step
  2. timeout is mandatory (ms)
  3. Runs async on a TaskExecutor to enforce timeout
  4. exitCodeMapper: 0 -> COMPLETED, non-zero -> FAILED
  5. Beware command injection; set workingDirectory/env

basics

~20 s

SystemCommandTasklet runs an external OS command (like a shell script) as a batch step. You set the command, a timeout, and usually a working directory. It runs the command in a separate thread and checks the exit code.

solid answer

~50 s

SystemCommandTasklet (package org.springframework.batch.core.step.tasklet) executes an external operating-system command from within a step — e.g. invoking a shell script, a native tool, or a data-load utility. You configure the command (in Spring Batch 5 as a tokenized String array), a mandatory timeout in milliseconds, optionally a workingDirectory, environmentParams, and a systemProcessExitCodeMapper that maps the process exit code to an ExitStatus. It runs the command asynchronously via a TaskExecutor so it can enforce the timeout and honor interruption; if the process exceeds the timeout it's killed and the step fails. afterPropertiesSet() (called because it's an InitializingBean) validates the command and timeout. Safety-wise: never build the command from untrusted input (command injection), set a sane timeout, and map non-zero exit codes to FAILED so failures surface. It's ideal for wrapping legacy scripts you can't reimplement in the JVM.

code

java · 16 lines
java
@Bean
@StepScope
SystemCommandTasklet gzipTasklet(@Value("#{jobParameters['file']}") String file) {
    SystemCommandTasklet t = new SystemCommandTasklet();
    t.setCommand("gzip", file);        // tokenized form, not one big string
    t.setTimeout(30_000);               // required
    t.setWorkingDirectory("/data/exports");
    t.setInterruptOnCancel(true);
    // default SimpleSystemProcessExitCodeMapper: 0 -> COMPLETED, else FAILED
    return t;
}

@Bean
Step gzipStep(JobRepository repo, PlatformTransactionManager tx, SystemCommandTasklet gzipTasklet) {
    return new StepBuilder("gzip", repo).tasklet(gzipTasklet, tx).build();
}

go deeper

for a junior

Enough to know it runs an external command as a step.

for a middle

Should set command, timeout, and workingDirectory and know default exit-code mapping.

for a senior

Should explain async execution/timeout enforcement, exit-code mapper, and command-injection safety.

for a principal

Should reason about portability, custom exit-code mappers, interruption/cancellation semantics, and why shelling out is a last resort vs JVM-native implementations.

## Purpose `SystemCommandTasklet` is a Spring Batch `Tasklet` that **runs an external OS process** as a step. Use it to wrap tools you can't or won't reimplement in Java: a shell/Python script, `gzip`, an ETL CLI, `pg_dump`, etc. It implements `StepExecutionListener` and `InitializingBean`. ## Required / common configuration - **`command`** — the command to run. In Spring Batch 5 the preferred form is a **tokenized String array** (e.g. `setCommand("/bin/sh", "-c", "./load.sh")`) rather than a single string, which avoids fragile whitespace splitting. - **`timeout`** (required) — max milliseconds the process may run; on expiry the tasklet interrupts/kills it and the step fails. Must be set or `afterPropertiesSet()` throws. - **`workingDirectory`** (optional) — directory to run the command in. - **`environmentParams`** (optional) — environment variables for the child process. - **`interruptOnCancel`** (optional) — whether to kill the process if the job is stopped. - **`terminationCheckInterval`** — how often to poll for completion. - **`taskExecutor`** — the command runs on a separate thread so the tasklet can enforce timeout and respond to interruption; by default a `SimpleAsyncTaskExecutor`. - **`systemProcessExitCodeMapper`** — maps the numeric OS exit code to an `ExitStatus`. The default `SimpleSystemProcessExitCodeMapper` maps `0 -> COMPLETED` and anything non-zero -> `FAILED`. ## Execution flow 1. On step start, the tasklet launches the command asynchronously on the `TaskExecutor`. 2. `execute()` polls (every `terminationCheckInterval`) for the process to finish, checking against the `timeout` and for step interruption. 3. When the process exits, the exit code is run through the `systemProcessExitCodeMapper` to produce the step's `ExitStatus`. 4. If it times out or is interrupted, the process is destroyed and the step fails. ```java @Bean @StepScope SystemCommandTasklet backupTasklet() { SystemCommandTasklet t = new SystemCommandTasklet(); t.setCommand("/bin/sh", "-c", "pg_dump mydb > /backups/db.sql"); t.setTimeout(60_000); // 60s t.setWorkingDirectory("/backups"); t.setInterruptOnCancel(true); return t; } ``` ## Safety and gotchas - **Command injection**: never interpolate untrusted job parameters into the command. Prefer the tokenized array form and validate inputs; using `sh -c "...user input..."` is dangerous. - **Timeout is mandatory** — without it `afterPropertiesSet()` fails; also a runaway process would hang the job. - **Exit-code mapping**: the default treats non-zero as FAILED. If your tool uses other conventions, provide a custom `SystemProcessExitCodeMapper` so success/failure is judged correctly. - **Portability**: the command is OS-specific; a step that works on Linux CI may fail on a Windows dev box. - **Working directory / environment**: don't assume the JVM's cwd or PATH; set them explicitly. - **Separate thread / TaskExecutor**: because it runs async, a synchronous single-thread executor could defeat the timeout — keep the default async executor. - **stdout/stderr**: the tasklet judges by exit code, not output; capture logs via redirection if you need them.

  • Why does SystemCommandTasklet run the command on a separate thread?
    So the tasklet can enforce the configured timeout and respond to job interruption/cancellation — it polls for completion and can destroy the process if it overruns or the step is stopped.
  • How is process success or failure determined?
    Via the SystemProcessExitCodeMapper. The default SimpleSystemProcessExitCodeMapper maps exit code 0 to ExitStatus.COMPLETED and any non-zero code to FAILED; supply a custom mapper for tools with different conventions.
  • What is the biggest security risk and how do you mitigate it?
    Command injection from interpolating untrusted input into the command. Mitigate by using the tokenized array form, validating/whitelisting inputs, and avoiding sh -c with user-controlled strings.

saying these in an interview costs you the question

  • Thinking timeout is optional
  • Building the command by concatenating untrusted job parameters
  • Assuming success is judged by stdout rather than exit code
  • Believing it runs synchronously on the batch thread

context