Bug description
ExitStatus class Javadoc declares the class as immutable and thread-safe:
▎ ExitStatus is immutable and, therefore, thread-safe.
However, setExitException(Throwable) (introduced in 6.0.3 for #5336) mutates the object in place, breaking this contract. Since ExitStatus.FAILED, ExitStatus.COMPLETED, etc. are shared static constants, calling
setExitException() on them permanently corrupts the global state for the entire application.
Environment
- Spring Batch: 6.0.4-SNAPSHOT (also affects 6.0.3)
- Java: 17+
Steps to reproduce
@test
void globalConstantCorruption() {
// precondition: FAILED has no exception
assertNull(ExitStatus.FAILED.getExitException());
// mutate the shared constant
ExitStatus.FAILED.setExitException(new RuntimeException("oops"));
// the global constant is now permanently corrupted
assertNotNull(ExitStatus.FAILED.getExitException()); // passes — should not be possible
}
Expected behavior
ExitStatus should be truly immutable. setExitException() should return a new instance, consistent with addExitDescription() and replaceExitCode() which already follow this pattern. The exitException field should be final.
Minimal Complete Reproducible example
The test above is self-contained — no database or Spring context needed. The root cause is in ExitStatus.java:
// line 76: non-final mutable field
@nullable private Throwable exitException;
// line 294-296: mutates this instead of returning a new instance
public ExitStatus setExitException(Throwable exitException) {
this.exitException = exitException;
return this;
}
A possible fix would be:
private final @nullable Throwable exitException;
public ExitStatus withExitException(Throwable exitException) {
return new ExitStatus(this.exitCode, this.exitDescription, exitException);
}
Note on backward compatibility: setExitException was introduced in 6.0.3.
If 6.0.3 is already released, deprecating it in favor of a new withExitException
method would preserve backward compatibility. If not yet released, a direct
replacement would be possible without breaking changes.
Bug description
ExitStatus class Javadoc declares the class as immutable and thread-safe:
▎ ExitStatus is immutable and, therefore, thread-safe.
However, setExitException(Throwable) (introduced in 6.0.3 for #5336) mutates the object in place, breaking this contract. Since ExitStatus.FAILED, ExitStatus.COMPLETED, etc. are shared static constants, calling
setExitException() on them permanently corrupts the global state for the entire application.
Environment
Steps to reproduce
@test
void globalConstantCorruption() {
// precondition: FAILED has no exception
assertNull(ExitStatus.FAILED.getExitException());
}
Expected behavior
ExitStatus should be truly immutable. setExitException() should return a new instance, consistent with addExitDescription() and replaceExitCode() which already follow this pattern. The exitException field should be final.
Minimal Complete Reproducible example
The test above is self-contained — no database or Spring context needed. The root cause is in ExitStatus.java:
// line 76: non-final mutable field
@nullable private Throwable exitException;
// line 294-296: mutates this instead of returning a new instance
public ExitStatus setExitException(Throwable exitException) {
this.exitException = exitException;
return this;
}
A possible fix would be:
private final @nullable Throwable exitException;
public ExitStatus withExitException(Throwable exitException) {
return new ExitStatus(this.exitCode, this.exitDescription, exitException);
}
Note on backward compatibility:
setExitExceptionwas introduced in 6.0.3.If 6.0.3 is already released, deprecating it in favor of a new
withExitExceptionmethod would preserve backward compatibility. If not yet released, a direct
replacement would be possible without breaking changes.