diff --git a/.github/workflows/artifactory-milestone-release.yml b/.github/workflows/artifactory-milestone-release.yml
deleted file mode 100644
index 4c368a39a4..0000000000
--- a/.github/workflows/artifactory-milestone-release.yml
+++ /dev/null
@@ -1,38 +0,0 @@
-name: Artifactory Milestone Release
-
-on:
- workflow_dispatch:
- inputs:
- releaseVersion:
- description: "Milestone release version"
- required: true
-
-jobs:
- build:
- name: Release milestone to Artifactory
- runs-on: ubuntu-latest
- steps:
- - name: Checkout source code
- uses: actions/checkout@v3
-
- - name: Set up JDK 17
- uses: actions/setup-java@v3
- with:
- java-version: '17'
- distribution: 'temurin'
- cache: 'maven'
-
- - name: Capture release version
- run: echo RELEASE_VERSION=${{ github.event.inputs.releaseVersion }} >> $GITHUB_ENV
-
- - name: Update release version
- run: mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$RELEASE_VERSION
-
- - name: Enforce release rules
- run: mvn org.apache.maven.plugins:maven-enforcer-plugin:enforce -Drules=requireReleaseDeps
-
- - name: Build with Maven and deploy to Artifactory's milestone repository
- env:
- ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
- ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- run: mvn -P artifactory-milestone -s settings.xml --batch-mode -Dmaven.test.skip=true deploy
diff --git a/.github/workflows/artifactory-staging.yml b/.github/workflows/artifactory-staging.yml
index ae8a04859c..f538d8d66b 100644
--- a/.github/workflows/artifactory-staging.yml
+++ b/.github/workflows/artifactory-staging.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
releaseVersion:
- description: "Release version"
+ description: "Release version (5.2.x)"
required: true
jobs:
@@ -13,26 +13,34 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout source code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4.2.2
+ with:
+ ref: '5.2.x'
- name: Set up JDK 17
- uses: actions/setup-java@v3
+ uses: actions/setup-java@v4.7.1
with:
java-version: '17'
distribution: 'temurin'
cache: 'maven'
- - name: Capture release version
- run: echo RELEASE_VERSION=${{ github.event.inputs.releaseVersion }} >> $GITHUB_ENV
-
- name: Update release version
- run: mvn versions:set -DgenerateBackupPoms=false -DnewVersion=$RELEASE_VERSION
+ run: mvn versions:set -DgenerateBackupPoms=false -DnewVersion=${{ github.event.inputs.releaseVersion }}
- name: Enforce release rules
run: mvn org.apache.maven.plugins:maven-enforcer-plugin:enforce -Drules=requireReleaseDeps
- - name: Build with Maven and deploy to Artifactory staging repository
- env:
- ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
- ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- run: mvn -P artifactory-staging -s settings.xml --batch-mode -Dmaven.test.skip=true deploy
+ - name: Build with Maven
+ run: mvn -DaltDeploymentRepository=local::file:deployment-repository --no-transfer-progress --batch-mode -Dmaven.test.skip=true deploy
+
+ - name: Deploy to Artifactory
+ uses: spring-io/artifactory-deploy-action@v0.0.2
+ with:
+ uri: 'https://repo.spring.io'
+ username: ${{ secrets.ARTIFACTORY_USERNAME }}
+ password: ${{ secrets.ARTIFACTORY_PASSWORD }}
+ build-name: 'spring-batch-${{ github.event.inputs.releaseVersion }}'
+ repository: 'libs-staging-local'
+ folder: 'deployment-repository'
+ signing-key: ${{ secrets.GPG_PRIVATE_KEY }}
+ signing-passphrase: ${{ secrets.GPG_PASSPHRASE }}
diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration-52x.yml
similarity index 88%
rename from .github/workflows/continuous-integration.yml
rename to .github/workflows/continuous-integration-52x.yml
index 9171cc9021..1f5744f64d 100644
--- a/.github/workflows/continuous-integration.yml
+++ b/.github/workflows/continuous-integration-52x.yml
@@ -1,14 +1,18 @@
-name: CI/CD build
+name: CI/CD build for 5.2.x
-on: [push, pull_request, workflow_dispatch]
+on:
+ push:
+ branches: [ "5.2.x" ]
jobs:
build:
- name: Build branch
+ name: Build 5.2.x branch
runs-on: ubuntu-latest
steps:
- name: Checkout source code
uses: actions/checkout@v3
+ with:
+ ref: '5.2.x'
- name: Set up JDK 17
uses: actions/setup-java@v3
@@ -18,11 +22,11 @@ jobs:
cache: 'maven'
- name: Build with Maven
- if: ${{ github.repository != 'spring-projects/spring-batch' || github.ref_name != 'main' }}
+ if: ${{ github.repository != 'spring-projects/spring-batch' || github.ref_name != '5.2.x' }}
run: mvn -s settings.xml --batch-mode --update-snapshots verify
- name: Build with Maven and deploy to Artifactory
- if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == 'main' }}
+ if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == '5.2.x' }}
env:
ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
@@ -39,7 +43,7 @@ jobs:
run: echo PROJECT_VERSION=$(mvn help:evaluate -Dexpression=project.version --quiet -DforceStdout) >> $GITHUB_ENV
- name: Setup SSH key
- if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == 'main' }}
+ if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == '5.2.x' }}
env:
DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }}
DOCS_SSH_HOST_KEY: ${{ secrets.DOCS_SSH_HOST_KEY }}
@@ -50,7 +54,7 @@ jobs:
echo "$DOCS_SSH_HOST_KEY" > "$HOME/.ssh/known_hosts"
- name: Deploy Java docs
- if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == 'main' }}
+ if: ${{ github.repository == 'spring-projects/spring-batch' && github.ref_name == '5.2.x' }}
env:
DOCS_HOST: ${{ secrets.DOCS_HOST }}
DOCS_PATH: ${{ secrets.DOCS_PATH }}
diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml
deleted file mode 100644
index 4af2314b75..0000000000
--- a/.github/workflows/deploy-docs.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-name: Deploy Docs
-on:
- push:
- branches-ignore: [ gh-pages ]
- tags: '**'
- repository_dispatch:
- types: request-build-reference # legacy
- workflow_dispatch:
-permissions:
- actions: write
-jobs:
- build:
- runs-on: ubuntu-latest
- if: github.repository_owner == 'spring-projects'
- steps:
- - name: Checkout
- uses: actions/checkout@v3
- with:
- ref: docs-build
- fetch-depth: 1
- - name: Dispatch (partial build)
- if: github.ref_type == 'branch'
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD) -f build-refname=${{ github.ref_name }}
- - name: Dispatch (full build)
- if: github.ref_type == 'tag'
- env:
- GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: gh workflow run deploy-docs.yml -r $(git rev-parse --abbrev-ref HEAD)
diff --git a/.github/workflows/documentation-upload.yml b/.github/workflows/documentation-upload.yml
index bf3f725cd7..2c25469f1b 100644
--- a/.github/workflows/documentation-upload.yml
+++ b/.github/workflows/documentation-upload.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
releaseVersion:
- description: "Release version"
+ description: "Release version (5.2.x)"
required: true
jobs:
@@ -17,6 +17,8 @@ jobs:
- name: Checkout source code
uses: actions/checkout@v3
+ with:
+ ref: '5.2.x'
- name: Set up JDK 17
uses: actions/setup-java@v3
diff --git a/.github/workflows/extension-build.yml b/.github/workflows/extension-build.yml
deleted file mode 100644
index 6b6b033894..0000000000
--- a/.github/workflows/extension-build.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-name: Spring Batch Extension Build
-
-on:
- workflow_dispatch:
- inputs:
- extension:
- description: "Extension name"
- required: true
- type: choice
- options:
- - spring-batch-bigquery
- - spring-batch-excel
- - spring-batch-elasticsearch
- - spring-batch-geode
- - spring-batch-neo4j
-
-jobs:
- build:
- name: Build an extension
- runs-on: ubuntu-latest
- steps:
- - name: Checkout source code
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- with:
- repository: 'spring-projects/spring-batch-extensions'
- ref: 'main'
-
- - name: Set up JDK 17
- uses: actions/setup-java@387ac29b308b003ca37ba93a6cab5eb57c8f5f93 # v4.0.0
- with:
- java-version: '17'
- distribution: 'temurin'
-
- - name: Build extension with Maven
- run: mvn -B package --file pom.xml
- working-directory: ${{ github.event.inputs.extension }}
diff --git a/.github/workflows/maven-central-release.yml b/.github/workflows/maven-central-release.yml
deleted file mode 100644
index bce60ac9ea..0000000000
--- a/.github/workflows/maven-central-release.yml
+++ /dev/null
@@ -1,83 +0,0 @@
-name: Maven Central Release
-
-on:
- workflow_dispatch:
- inputs:
- releaseVersion:
- description: "Release version"
- required: true
-
-jobs:
- build:
- runs-on: ubuntu-latest
- steps:
-
- - name: Capture release version
- run: echo RELEASE_VERSION=${{ github.event.inputs.releaseVersion }} >> $GITHUB_ENV
-
- - name: Prepare directory structure
- run: |
- mkdir -p nexus/org/springframework/batch/spring-batch-bom/$RELEASE_VERSION
- mkdir -p nexus/org/springframework/batch/spring-batch-infrastructure/$RELEASE_VERSION
- mkdir -p nexus/org/springframework/batch/spring-batch-core/$RELEASE_VERSION
- mkdir -p nexus/org/springframework/batch/spring-batch-test/$RELEASE_VERSION
- mkdir -p nexus/org/springframework/batch/spring-batch-integration/$RELEASE_VERSION
-
- - name: Download release files from Artifactory
- env:
- ARTIFACTORY_URL: "https://repo.spring.io/libs-staging-local/org/springframework/batch"
- ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }}
- ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }}
- run: |
- echo "Downloading BOM artifacts"
- cd nexus/org/springframework/batch/spring-batch-bom/$RELEASE_VERSION
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-bom/$RELEASE_VERSION/spring-batch-bom-$RELEASE_VERSION.pom
-
- echo "Downloading infrastructure artifacts"
- cd ../../../../../..
- cd nexus/org/springframework/batch/spring-batch-infrastructure/$RELEASE_VERSION
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-infrastructure/$RELEASE_VERSION/spring-batch-infrastructure-$RELEASE_VERSION.pom
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-infrastructure/$RELEASE_VERSION/spring-batch-infrastructure-$RELEASE_VERSION.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-infrastructure/$RELEASE_VERSION/spring-batch-infrastructure-$RELEASE_VERSION-javadoc.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-infrastructure/$RELEASE_VERSION/spring-batch-infrastructure-$RELEASE_VERSION-sources.jar
-
- echo "Downloading core artifacts"
- cd ../../../../../..
- cd nexus/org/springframework/batch/spring-batch-core/$RELEASE_VERSION
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-core/$RELEASE_VERSION/spring-batch-core-$RELEASE_VERSION.pom
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-core/$RELEASE_VERSION/spring-batch-core-$RELEASE_VERSION.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-core/$RELEASE_VERSION/spring-batch-core-$RELEASE_VERSION-javadoc.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-core/$RELEASE_VERSION/spring-batch-core-$RELEASE_VERSION-sources.jar
-
- echo "Downloading test artifacts"
- cd ../../../../../..
- cd nexus/org/springframework/batch/spring-batch-test/$RELEASE_VERSION
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-test/$RELEASE_VERSION/spring-batch-test-$RELEASE_VERSION.pom
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-test/$RELEASE_VERSION/spring-batch-test-$RELEASE_VERSION.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-test/$RELEASE_VERSION/spring-batch-test-$RELEASE_VERSION-javadoc.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-test/$RELEASE_VERSION/spring-batch-test-$RELEASE_VERSION-sources.jar
-
- echo "Downloading integration artifacts"
- cd ../../../../../..
- cd nexus/org/springframework/batch/spring-batch-integration/$RELEASE_VERSION
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-integration/$RELEASE_VERSION/spring-batch-integration-$RELEASE_VERSION.pom
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-integration/$RELEASE_VERSION/spring-batch-integration-$RELEASE_VERSION.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-integration/$RELEASE_VERSION/spring-batch-integration-$RELEASE_VERSION-javadoc.jar
- wget --user="$ARTIFACTORY_USERNAME" --password="$ARTIFACTORY_PASSWORD" $ARTIFACTORY_URL/spring-batch-integration/$RELEASE_VERSION/spring-batch-integration-$RELEASE_VERSION-sources.jar
-
- - name: Sign artifacts and release them to Maven Central
- uses: jvalkeal/nexus-sync@v0
- id: nexus
- with:
- url: ${{ secrets.OSSRH_URL }}
- username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }}
- password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }}
- staging-profile-name: ${{ secrets.OSSRH_STAGING_PROFILE_NAME }}
- create: true
- upload: true
- close: true
- release: true
- generate-checksums: true
- pgp-sign: true
- pgp-sign-passphrase: ${{ secrets.GPG_PASSPHRASE }}
- pgp-sign-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
diff --git a/.github/workflows/release-notes-generation.yml b/.github/workflows/release-notes-generation.yml
deleted file mode 100644
index fa601a05fa..0000000000
--- a/.github/workflows/release-notes-generation.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-name: Generate Release notes
-
-on:
- workflow_dispatch:
- inputs:
- milestoneNumber:
- description: "Milestone title"
- required: true
- generatorVersion:
- description: "Changelog Generator version"
- required: true
-
-jobs:
- build:
- name: Generate release notes
- runs-on: ubuntu-latest
- steps:
- - name: Capture milestone number and generator version
- run: |
- echo MILESTONE_NUMBER=${{ github.event.inputs.milestoneNumber }} >> $GITHUB_ENV
- echo GENERATOR_VERSION=${{ github.event.inputs.generatorVersion }} >> $GITHUB_ENV
-
- - name: Download changelog generator
- run: wget https://github.com/spring-io/github-changelog-generator/releases/download/v$GENERATOR_VERSION/github-changelog-generator.jar
-
- - name: Set up JDK 17
- uses: actions/setup-java@v3
- with:
- java-version: '17'
- distribution: 'temurin'
-
- - name: Prepare configuration file
- run: |
- cat << EOF > application.yml
- changelog:
- repository: spring-projects/spring-batch
- sections:
- - title: ":star: New features"
- labels: [ "type: feature" ]
- - title: ":rocket: Enhancements"
- labels: [ "type: enhancement" ]
- - title: ":lady_beetle: Bug fixes"
- labels: [ "type: bug" ]
- - title: ":notebook_with_decorative_cover: Documentation"
- labels: [ "in: documentation" ]
- - title: ":hammer: Tasks"
- labels: [ "type: task" ]
- EOF
-
- - name: Generate release notes
- run: java -jar github-changelog-generator.jar $MILESTONE_NUMBER release-notes.md
-
- - name: Print release notes
- run: cat release-notes.md
diff --git a/README.md b/README.md
index 5cc775db30..81533825dd 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,6 @@
# Latest news
+* March 19, 2025: [Spring Batch 5.2.2 available now](https://spring.io/blog/2025/03/19/spring-batch-5-2-2-available-now)
* December 18, 2024: [Spring Batch 5.1.3 and 5.2.1 available now](https://spring.io/blog/2024/12/18/spring-batch-5-1-3-and-5-2-1-available-now)
* November 24, 2024: [Bootiful Spring Boot 3.4: Spring Batch](https://spring.io/blog/2024/11/24/bootiful-34-batch)
* November 20, 2024: [Spring Batch 5.2.0 goes GA!](https://spring.io/blog/2024/11/20/spring-batch-5-2-0-goes-ga)
diff --git a/pom.xml b/pom.xml
index 9ad73b8107..6055c7037d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -8,7 +8,7 @@
designed to enable the development of robust batch applications vital
for the daily operations of enterprise systems. Spring Batch is part of
the Spring Portfolio.
- 5.2.2
+ 5.2.3
pom
https://projects.spring.io/spring-batch
@@ -61,30 +61,30 @@
17
- 6.2.4
- 2.0.11
- 6.4.3
- 1.14.5
+ 6.2.11
+ 2.0.12
+ 6.4.7
+ 1.14.11
- 3.4.4
- 3.4.4
- 3.4.4
- 4.4.4
- 3.3.4
- 3.2.4
- 3.2.11
+ 3.4.10
+ 3.4.10
+ 3.4.10
+ 4.4.10
+ 3.3.10
+ 3.2.7
+ 3.2.14
- 2.18.3
+ 2.18.4
1.12.0
2.12.1
- 6.6.11.Final
+ 6.6.29.Final
3.0.0
2.1.3
3.1.0
3.1.1
3.1.0
- 4.0.16
+ 4.0.20
5.3.1
5.11.4
@@ -92,13 +92,13 @@
3.0.2
- 1.4.4
+ 1.4.10
1.4.21
4.13.2
${junit-jupiter.version}
3.0
- 3.27.3
+ 3.27.4
5.16.1
2.10.0
2.18.0
@@ -133,6 +133,9 @@
15.6
2.0b6
9.4.12.0
+ 6.7.1.RELEASE
+ 6.0.0
+ 2.2.4
${spring-amqp.version}
@@ -145,11 +148,11 @@
3.14.0
- 3.5.2
- 3.5.2
- 3.11.2
+ 3.5.4
+ 3.5.4
+ 3.11.3
3.3.1
- 1.7.0
+ 1.7.2
3.1.4
3.7.1
3.4.2
diff --git a/spring-batch-bom/pom.xml b/spring-batch-bom/pom.xml
index 35c27a533f..3e1573dbe5 100644
--- a/spring-batch-bom/pom.xml
+++ b/spring-batch-bom/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-bom
pom
diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml
index 783b995348..b0292f861c 100644
--- a/spring-batch-core/pom.xml
+++ b/spring-batch-core/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-core
jar
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/ExitStatus.java b/spring-batch-core/src/main/java/org/springframework/batch/core/ExitStatus.java
index edaa4986eb..0a247b433a 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/ExitStatus.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/ExitStatus.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -15,6 +15,7 @@
*/
package org.springframework.batch.core;
+import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import java.io.PrintWriter;
@@ -28,6 +29,7 @@
*
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author JiWon Seo
*
*/
public class ExitStatus implements Serializable, Comparable {
@@ -198,7 +200,7 @@ public String toString() {
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
- public boolean equals(Object obj) {
+ public boolean equals(@Nullable Object obj) {
if (obj == null) {
return false;
}
@@ -230,7 +232,7 @@ public ExitStatus replaceExitCode(String code) {
* @return {@code true} if the exit code is {@code EXECUTING} or {@code UNKNOWN}.
*/
public boolean isRunning() {
- return "EXECUTING".equals(this.exitCode) || "UNKNOWN".equals(this.exitCode);
+ return EXECUTING.exitCode.equals(this.exitCode) || UNKNOWN.exitCode.equals(this.exitCode);
}
/**
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultJobLoader.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultJobLoader.java
index 4dde8ea152..e9e7ffeac9 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultJobLoader.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultJobLoader.java
@@ -173,7 +173,7 @@ private Collection doLoad(ApplicationContextFactory factory, boolean unregi
if (!autoRegistrationDetected) {
- Job job = (Job) context.getBean(name);
+ Job job = context.getBean(name, Job.class);
String jobName = job.getName();
// On reload try to unregister first
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespacePostProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespacePostProcessor.java
index 7f250d389c..d1b742810e 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespacePostProcessor.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/xml/CoreNamespacePostProcessor.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -119,19 +119,19 @@ private Object injectDefaults(Object bean) {
JobParserJobFactoryBean fb = (JobParserJobFactoryBean) bean;
JobRepository jobRepository = fb.getJobRepository();
if (jobRepository == null) {
- fb.setJobRepository((JobRepository) applicationContext.getBean(DEFAULT_JOB_REPOSITORY_NAME));
+ fb.setJobRepository(applicationContext.getBean(DEFAULT_JOB_REPOSITORY_NAME, JobRepository.class));
}
}
else if (bean instanceof StepParserStepFactoryBean) {
StepParserStepFactoryBean, ?> fb = (StepParserStepFactoryBean, ?>) bean;
JobRepository jobRepository = fb.getJobRepository();
if (jobRepository == null) {
- fb.setJobRepository((JobRepository) applicationContext.getBean(DEFAULT_JOB_REPOSITORY_NAME));
+ fb.setJobRepository(applicationContext.getBean(DEFAULT_JOB_REPOSITORY_NAME, JobRepository.class));
}
PlatformTransactionManager transactionManager = fb.getTransactionManager();
if (transactionManager == null && fb.requiresTransactionManager()) {
fb.setTransactionManager(
- (PlatformTransactionManager) applicationContext.getBean(DEFAULT_TRANSACTION_MANAGER_NAME));
+ applicationContext.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class));
}
}
return bean;
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
index 34bdf928b0..d5344c4797 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/CommandLineJobRunner.java
@@ -356,7 +356,7 @@ int start(String jobPath, String jobIdentifier, String[] parameters, Set
}
}
if (job == null) {
- job = (Job) context.getBean(jobName);
+ job = context.getBean(jobName, Job.class);
}
if (opts.contains("-next")) {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
index 07915965c0..169bb91d01 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcExecutionContextDao.java
@@ -27,11 +27,11 @@
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
-import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
import org.springframework.batch.core.JobExecution;
import org.springframework.batch.core.StepExecution;
@@ -57,6 +57,7 @@
* @author Michael Minella
* @author David Turanski
* @author Mahmoud Ben Hassine
+ * @author Yanming Zhou
*/
public class JdbcExecutionContextDao extends AbstractJdbcBatchMetadataDao implements ExecutionContextDao {
@@ -154,13 +155,9 @@ public ExecutionContext getExecutionContext(JobExecution jobExecution) {
Long executionId = jobExecution.getId();
Assert.notNull(executionId, "ExecutionId must not be null.");
- List results = getJdbcTemplate().query(getQuery(FIND_JOB_EXECUTION_CONTEXT),
- new ExecutionContextRowMapper(), executionId);
- if (!results.isEmpty()) {
- return results.get(0);
- }
- else {
- return new ExecutionContext();
+ try (Stream stream = getJdbcTemplate().queryForStream(getQuery(FIND_JOB_EXECUTION_CONTEXT),
+ new ExecutionContextRowMapper(), executionId)) {
+ return stream.findFirst().orElseGet(ExecutionContext::new);
}
}
@@ -169,13 +166,9 @@ public ExecutionContext getExecutionContext(StepExecution stepExecution) {
Long executionId = stepExecution.getId();
Assert.notNull(executionId, "ExecutionId must not be null.");
- List results = getJdbcTemplate().query(getQuery(FIND_STEP_EXECUTION_CONTEXT),
- new ExecutionContextRowMapper(), executionId);
- if (results.size() > 0) {
- return results.get(0);
- }
- else {
- return new ExecutionContext();
+ try (Stream stream = getJdbcTemplate().queryForStream(getQuery(FIND_STEP_EXECUTION_CONTEXT),
+ new ExecutionContextRowMapper(), executionId)) {
+ return stream.findFirst().orElseGet(ExecutionContext::new);
}
}
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobExecutionDao.java
index 9ec0a9e2d8..6fbbdb9310 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobExecutionDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobExecutionDao.java
@@ -28,6 +28,7 @@
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -74,6 +75,7 @@
* @author Dimitrios Liapis
* @author Philippe Marschall
* @author Jinwoo Bae
+ * @author Yanming Zhou
*/
public class JdbcJobExecutionDao extends AbstractJdbcBatchMetadataDao implements JobExecutionDao, InitializingBean {
@@ -98,28 +100,22 @@ SELECT COUNT(*)
private static final String UPDATE_JOB_EXECUTION = """
UPDATE %PREFIX%JOB_EXECUTION
- SET START_TIME = ?, END_TIME = ?, STATUS = ?, EXIT_CODE = ?, EXIT_MESSAGE = ?, VERSION = ?, CREATE_TIME = ?, LAST_UPDATED = ?
+ SET START_TIME = ?, END_TIME = ?, STATUS = ?, EXIT_CODE = ?, EXIT_MESSAGE = ?, VERSION = VERSION + 1, CREATE_TIME = ?, LAST_UPDATED = ?
WHERE JOB_EXECUTION_ID = ? AND VERSION = ?
""";
- private static final String FIND_JOB_EXECUTIONS = """
+ private static final String GET_JOB_EXECUTIONS = """
SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE, EXIT_MESSAGE, CREATE_TIME, LAST_UPDATED, VERSION
FROM %PREFIX%JOB_EXECUTION
- WHERE JOB_INSTANCE_ID = ?
- ORDER BY JOB_EXECUTION_ID DESC
""";
- private static final String GET_LAST_EXECUTION = """
- SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE, EXIT_MESSAGE, CREATE_TIME, LAST_UPDATED, VERSION
- FROM %PREFIX%JOB_EXECUTION E
- WHERE JOB_INSTANCE_ID = ? AND JOB_EXECUTION_ID IN (SELECT MAX(JOB_EXECUTION_ID) FROM %PREFIX%JOB_EXECUTION E2 WHERE E2.JOB_INSTANCE_ID = ?)
- """;
+ private static final String FIND_JOB_EXECUTIONS = GET_JOB_EXECUTIONS
+ + " WHERE JOB_INSTANCE_ID = ? ORDER BY JOB_EXECUTION_ID DESC";
- private static final String GET_EXECUTION_BY_ID = """
- SELECT JOB_EXECUTION_ID, START_TIME, END_TIME, STATUS, EXIT_CODE, EXIT_MESSAGE, CREATE_TIME, LAST_UPDATED, VERSION
- FROM %PREFIX%JOB_EXECUTION
- WHERE JOB_EXECUTION_ID = ?
- """;
+ private static final String GET_LAST_EXECUTION = GET_JOB_EXECUTIONS
+ + " WHERE JOB_INSTANCE_ID = ? AND JOB_EXECUTION_ID IN (SELECT MAX(JOB_EXECUTION_ID) FROM %PREFIX%JOB_EXECUTION E2 WHERE E2.JOB_INSTANCE_ID = ?)";
+
+ private static final String GET_EXECUTION_BY_ID = GET_JOB_EXECUTIONS + " WHERE JOB_EXECUTION_ID = ?";
private static final String GET_RUNNING_EXECUTIONS = """
SELECT E.JOB_EXECUTION_ID, E.START_TIME, E.END_TIME, E.STATUS, E.EXIT_CODE, E.EXIT_MESSAGE, E.CREATE_TIME, E.LAST_UPDATED, E.VERSION, E.JOB_INSTANCE_ID
@@ -146,7 +142,7 @@ SELECT COUNT(*)
private static final String DELETE_JOB_EXECUTION = """
DELETE FROM %PREFIX%JOB_EXECUTION
- WHERE JOB_EXECUTION_ID = ?
+ WHERE JOB_EXECUTION_ID = ? AND VERSION = ?
""";
private static final String DELETE_JOB_EXECUTION_PARAMETERS = """
@@ -284,7 +280,6 @@ public void updateJobExecution(JobExecution jobExecution) {
this.lock.lock();
try {
- Integer version = jobExecution.getVersion() + 1;
String exitDescription = jobExecution.getExitStatus().getExitDescription();
if (exitDescription != null && exitDescription.length() > exitMessageLength) {
@@ -301,7 +296,7 @@ public void updateJobExecution(JobExecution jobExecution) {
Timestamp lastUpdated = jobExecution.getLastUpdated() == null ? null
: Timestamp.valueOf(jobExecution.getLastUpdated());
Object[] parameters = new Object[] { startTime, endTime, jobExecution.getStatus().toString(),
- jobExecution.getExitStatus().getExitCode(), exitDescription, version, createTime, lastUpdated,
+ jobExecution.getExitStatus().getExitCode(), exitDescription, createTime, lastUpdated,
jobExecution.getId(), jobExecution.getVersion() };
// Check if given JobExecution's Id already exists, if none is found
@@ -315,7 +310,7 @@ public void updateJobExecution(JobExecution jobExecution) {
int count = getJdbcTemplate().update(getQuery(UPDATE_JOB_EXECUTION), parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.VARCHAR, Types.VARCHAR,
- Types.INTEGER, Types.TIMESTAMP, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER });
+ Types.TIMESTAMP, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER });
// Avoid concurrent modifications...
if (count == 0) {
@@ -339,16 +334,9 @@ public JobExecution getLastJobExecution(JobInstance jobInstance) {
Long id = jobInstance.getId();
- List executions = getJdbcTemplate().query(getQuery(GET_LAST_EXECUTION),
- new JobExecutionRowMapper(jobInstance), id, id);
-
- Assert.state(executions.size() <= 1, "There must be at most one latest job execution");
-
- if (executions.isEmpty()) {
- return null;
- }
- else {
- return executions.get(0);
+ try (Stream stream = getJdbcTemplate().queryForStream(getQuery(GET_LAST_EXECUTION),
+ new JobExecutionRowMapper(jobInstance), id, id)) {
+ return stream.findFirst().orElse(null);
}
}
@@ -395,7 +383,13 @@ public void synchronizeStatus(JobExecution jobExecution) {
*/
@Override
public void deleteJobExecution(JobExecution jobExecution) {
- getJdbcTemplate().update(getQuery(DELETE_JOB_EXECUTION), jobExecution.getId());
+ int count = getJdbcTemplate().update(getQuery(DELETE_JOB_EXECUTION), jobExecution.getId(),
+ jobExecution.getVersion());
+
+ if (count == 0) {
+ throw new OptimisticLockingFailureException("Attempt to delete job execution id=" + jobExecution.getId()
+ + " with wrong version (" + jobExecution.getVersion() + ")");
+ }
}
/**
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobInstanceDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobInstanceDao.java
index 27dcc8b7a2..748125922c 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobInstanceDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcJobInstanceDao.java
@@ -21,6 +21,7 @@
import java.sql.Types;
import java.util.ArrayList;
import java.util.List;
+import java.util.stream.Stream;
import org.springframework.batch.core.DefaultJobKeyGenerator;
import org.springframework.batch.core.JobExecution;
@@ -31,6 +32,7 @@
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer;
@@ -53,6 +55,7 @@
* @author Will Schipp
* @author Mahmoud Ben Hassine
* @author Parikshit Dutta
+ * @author Yanming Zhou
*/
public class JdbcJobInstanceDao extends AbstractJdbcBatchMetadataDao implements JobInstanceDao, InitializingBean {
@@ -71,7 +74,7 @@ public class JdbcJobInstanceDao extends AbstractJdbcBatchMetadataDao implements
WHERE JOB_NAME = ?
""";
- private static final String FIND_JOBS_WITH_KEY = FIND_JOBS_WITH_NAME + " and JOB_KEY = ?";
+ private static final String FIND_JOBS_WITH_KEY = FIND_JOBS_WITH_NAME + " AND JOB_KEY = ?";
private static final String COUNT_JOBS_WITH_NAME = """
SELECT COUNT(*)
@@ -79,11 +82,8 @@ SELECT COUNT(*)
WHERE JOB_NAME = ?
""";
- private static final String FIND_JOBS_WITH_EMPTY_KEY = """
- SELECT JOB_INSTANCE_ID, JOB_NAME
- FROM %PREFIX%JOB_INSTANCE
- WHERE JOB_NAME = ? AND (JOB_KEY = ? OR JOB_KEY IS NULL)
- """;
+ private static final String FIND_JOBS_WITH_EMPTY_KEY = FIND_JOBS_WITH_NAME
+ + " AND (JOB_KEY = ? OR JOB_KEY IS NULL)";
private static final String GET_JOB_FROM_ID = """
SELECT JOB_INSTANCE_ID, JOB_NAME, JOB_KEY, VERSION
@@ -124,7 +124,7 @@ SELECT COUNT(*)
private static final String DELETE_JOB_INSTANCE = """
DELETE FROM %PREFIX%JOB_INSTANCE
- WHERE JOB_INSTANCE_ID = ?
+ WHERE JOB_INSTANCE_ID = ? AND VERSION = ?
""";
private DataFieldMaxValueIncrementer jobInstanceIncrementer;
@@ -178,21 +178,12 @@ public JobInstance getJobInstance(final String jobName, final JobParameters jobP
RowMapper rowMapper = new JobInstanceRowMapper();
- List instances;
- if (StringUtils.hasLength(jobKey)) {
- instances = getJdbcTemplate().query(getQuery(FIND_JOBS_WITH_KEY), rowMapper, jobName, jobKey);
- }
- else {
- instances = getJdbcTemplate().query(getQuery(FIND_JOBS_WITH_EMPTY_KEY), rowMapper, jobName, jobKey);
+ try (Stream stream = getJdbcTemplate().queryForStream(
+ getQuery(StringUtils.hasLength(jobKey) ? FIND_JOBS_WITH_KEY : FIND_JOBS_WITH_EMPTY_KEY), rowMapper,
+ jobName, jobKey)) {
+ return stream.findFirst().orElse(null);
}
- if (instances.isEmpty()) {
- return null;
- }
- else {
- Assert.state(instances.size() == 1, "instance count must be 1 but was " + instances.size());
- return instances.get(0);
- }
}
@Override
@@ -281,7 +272,13 @@ public long getJobInstanceCount(@Nullable String jobName) throws NoSuchJobExcept
*/
@Override
public void deleteJobInstance(JobInstance jobInstance) {
- getJdbcTemplate().update(getQuery(DELETE_JOB_INSTANCE), jobInstance.getId());
+ int count = getJdbcTemplate().update(getQuery(DELETE_JOB_INSTANCE), jobInstance.getId(),
+ jobInstance.getVersion());
+
+ if (count == 0) {
+ throw new OptimisticLockingFailureException("Attempt to delete job instance id=" + jobInstance.getId()
+ + " with wrong version (" + jobInstance.getVersion() + ")");
+ }
}
/**
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
index b1e46e0c23..67a825c6cd 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDao.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
+import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
@@ -66,6 +67,7 @@
* @author Mahmoud Ben Hassine
* @author Baris Cubukcuoglu
* @author Minsoo Kim
+ * @author Yanming Zhou
* @see StepExecutionDao
*/
public class JdbcStepExecutionDao extends AbstractJdbcBatchMetadataDao implements StepExecutionDao, InitializingBean {
@@ -79,19 +81,19 @@ public class JdbcStepExecutionDao extends AbstractJdbcBatchMetadataDao implement
private static final String UPDATE_STEP_EXECUTION = """
UPDATE %PREFIX%STEP_EXECUTION
- SET START_TIME = ?, END_TIME = ?, STATUS = ?, COMMIT_COUNT = ?, READ_COUNT = ?, FILTER_COUNT = ?, WRITE_COUNT = ?, EXIT_CODE = ?, EXIT_MESSAGE = ?, VERSION = ?, READ_SKIP_COUNT = ?, PROCESS_SKIP_COUNT = ?, WRITE_SKIP_COUNT = ?, ROLLBACK_COUNT = ?, LAST_UPDATED = ?
+ SET START_TIME = ?, END_TIME = ?, STATUS = ?, COMMIT_COUNT = ?, READ_COUNT = ?, FILTER_COUNT = ?, WRITE_COUNT = ?, EXIT_CODE = ?, EXIT_MESSAGE = ?, VERSION = VERSION + 1, READ_SKIP_COUNT = ?, PROCESS_SKIP_COUNT = ?, WRITE_SKIP_COUNT = ?, ROLLBACK_COUNT = ?, LAST_UPDATED = ?
WHERE STEP_EXECUTION_ID = ? AND VERSION = ?
""";
private static final String GET_RAW_STEP_EXECUTIONS = """
SELECT STEP_EXECUTION_ID, STEP_NAME, START_TIME, END_TIME, STATUS, COMMIT_COUNT, READ_COUNT, FILTER_COUNT, WRITE_COUNT, EXIT_CODE, EXIT_MESSAGE, READ_SKIP_COUNT, WRITE_SKIP_COUNT, PROCESS_SKIP_COUNT, ROLLBACK_COUNT, LAST_UPDATED, VERSION, CREATE_TIME
FROM %PREFIX%STEP_EXECUTION
- WHERE JOB_EXECUTION_ID = ?
""";
- private static final String GET_STEP_EXECUTIONS = GET_RAW_STEP_EXECUTIONS + " ORDER BY STEP_EXECUTION_ID";
+ private static final String GET_STEP_EXECUTIONS = GET_RAW_STEP_EXECUTIONS
+ + " WHERE JOB_EXECUTION_ID = ? ORDER BY STEP_EXECUTION_ID";
- private static final String GET_STEP_EXECUTION = GET_RAW_STEP_EXECUTIONS + " AND STEP_EXECUTION_ID = ?";
+ private static final String GET_STEP_EXECUTION = GET_RAW_STEP_EXECUTIONS + " WHERE STEP_EXECUTION_ID = ?";
private static final String GET_LAST_STEP_EXECUTION = """
SELECT SE.STEP_EXECUTION_ID, SE.STEP_NAME, SE.START_TIME, SE.END_TIME, SE.STATUS, SE.COMMIT_COUNT, SE.READ_COUNT, SE.FILTER_COUNT, SE.WRITE_COUNT, SE.EXIT_CODE, SE.EXIT_MESSAGE, SE.READ_SKIP_COUNT, SE.WRITE_SKIP_COUNT, SE.PROCESS_SKIP_COUNT, SE.ROLLBACK_COUNT, SE.LAST_UPDATED, SE.VERSION, SE.CREATE_TIME, JE.JOB_EXECUTION_ID, JE.START_TIME, JE.END_TIME, JE.STATUS, JE.EXIT_CODE, JE.EXIT_MESSAGE, JE.CREATE_TIME, JE.LAST_UPDATED, JE.VERSION
@@ -114,7 +116,7 @@ SELECT COUNT(*)
private static final String DELETE_STEP_EXECUTION = """
DELETE FROM %PREFIX%STEP_EXECUTION
- WHERE STEP_EXECUTION_ID = ?
+ WHERE STEP_EXECUTION_ID = ? and VERSION = ?
""";
private static final Comparator BY_CREATE_TIME_DESC_ID_DESC = Comparator
@@ -267,7 +269,6 @@ public void updateStepExecution(StepExecution stepExecution) {
this.lock.lock();
try {
- Integer version = stepExecution.getVersion() + 1;
Timestamp startTime = stepExecution.getStartTime() == null ? null
: Timestamp.valueOf(stepExecution.getStartTime());
Timestamp endTime = stepExecution.getEndTime() == null ? null
@@ -277,13 +278,13 @@ public void updateStepExecution(StepExecution stepExecution) {
Object[] parameters = new Object[] { startTime, endTime, stepExecution.getStatus().toString(),
stepExecution.getCommitCount(), stepExecution.getReadCount(), stepExecution.getFilterCount(),
stepExecution.getWriteCount(), stepExecution.getExitStatus().getExitCode(), exitDescription,
- version, stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(),
+ stepExecution.getReadSkipCount(), stepExecution.getProcessSkipCount(),
stepExecution.getWriteSkipCount(), stepExecution.getRollbackCount(), lastUpdated,
stepExecution.getId(), stepExecution.getVersion() };
int count = getJdbcTemplate().update(getQuery(UPDATE_STEP_EXECUTION), parameters,
new int[] { Types.TIMESTAMP, Types.TIMESTAMP, Types.VARCHAR, Types.BIGINT, Types.BIGINT,
- Types.BIGINT, Types.BIGINT, Types.VARCHAR, Types.VARCHAR, Types.INTEGER, Types.BIGINT,
- Types.BIGINT, Types.BIGINT, Types.BIGINT, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER });
+ Types.BIGINT, Types.BIGINT, Types.VARCHAR, Types.VARCHAR, Types.BIGINT, Types.BIGINT,
+ Types.BIGINT, Types.BIGINT, Types.TIMESTAMP, Types.BIGINT, Types.INTEGER });
// Avoid concurrent modifications...
if (count == 0) {
@@ -324,16 +325,9 @@ private String truncateExitDescription(String description) {
@Override
@Nullable
public StepExecution getStepExecution(JobExecution jobExecution, Long stepExecutionId) {
- List executions = getJdbcTemplate().query(getQuery(GET_STEP_EXECUTION),
- new StepExecutionRowMapper(jobExecution), jobExecution.getId(), stepExecutionId);
-
- Assert.state(executions.size() <= 1,
- "There can be at most one step execution with given name for single job execution");
- if (executions.isEmpty()) {
- return null;
- }
- else {
- return executions.get(0);
+ try (Stream stream = getJdbcTemplate().queryForStream(getQuery(GET_STEP_EXECUTION),
+ new StepExecutionRowMapper(jobExecution), stepExecutionId)) {
+ return stream.findFirst().orElse(null);
}
}
@@ -378,7 +372,13 @@ public long countStepExecutions(JobInstance jobInstance, String stepName) {
*/
@Override
public void deleteStepExecution(StepExecution stepExecution) {
- getJdbcTemplate().update(getQuery(DELETE_STEP_EXECUTION), stepExecution.getId());
+ int count = getJdbcTemplate().update(getQuery(DELETE_STEP_EXECUTION), stepExecution.getId(),
+ stepExecution.getVersion());
+
+ if (count == 0) {
+ throw new OptimisticLockingFailureException("Attempt to delete step execution id=" + stepExecution.getId()
+ + " with wrong version (" + stepExecution.getVersion() + ")");
+ }
}
private static class StepExecutionRowMapper implements RowMapper {
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java
index 9fb6b33dd8..b41974c8b7 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2024 the original author or authors.
+ * Copyright 2024-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -49,6 +49,8 @@ public class ResourcelessJobRepository implements JobRepository {
private JobExecution jobExecution;
+ private long stepExecutionIdIncrementer = 0L;
+
@Override
public boolean isJobInstanceExists(String jobName, JobParameters jobParameters) {
return false;
@@ -77,12 +79,14 @@ public void update(JobExecution jobExecution) {
@Override
public void add(StepExecution stepExecution) {
- this.addAll(Collections.singletonList(stepExecution));
+ stepExecution.setId(this.stepExecutionIdIncrementer++);
}
@Override
public void addAll(Collection stepExecutions) {
- this.jobExecution.addStepExecutions(new ArrayList<>(stepExecutions));
+ for (StepExecution stepExecution : stepExecutions) {
+ this.add(stepExecution);
+ }
}
@Override
@@ -105,6 +109,9 @@ public void updateExecutionContext(JobExecution jobExecution) {
@Override
public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) {
+ if (this.jobExecution == null || !this.jobExecution.getJobInstance().getId().equals(jobInstance.getId())) {
+ return null;
+ }
return this.jobExecution.getStepExecutions()
.stream()
.filter(stepExecution -> stepExecution.getStepName().equals(stepName))
diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java
index 1891f55883..f76a48b55a 100644
--- a/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java
+++ b/spring-batch-core/src/main/java/org/springframework/batch/core/scope/context/SynchronizationManagerSupport.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2013-2021 the original author or authors.
+ * Copyright 2013-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -28,6 +28,7 @@
* @author Dave Syer
* @author Jimmy Praet
* @author Mahmoud Ben Hassine
+ * @author Yanming Zhou
* @since 3.0
*/
public abstract class SynchronizationManagerSupport {
@@ -87,11 +88,7 @@ public C register(@Nullable E execution) {
getCurrent().push(execution);
C context;
synchronized (contexts) {
- context = contexts.get(execution);
- if (context == null) {
- context = createNewContext(execution);
- contexts.put(execution, context);
- }
+ context = contexts.computeIfAbsent(execution, this::createNewContext);
}
increment();
return context;
@@ -131,11 +128,7 @@ public void increment() {
if (current != null) {
AtomicInteger count;
synchronized (counts) {
- count = counts.get(current);
- if (count == null) {
- count = new AtomicInteger();
- counts.put(current, count);
- }
+ count = counts.computeIfAbsent(current, k -> new AtomicInteger());
}
count.incrementAndGet();
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
index 907ea62ff8..17a736d1db 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/ExitStatusTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2024 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -33,6 +33,7 @@
/**
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author JiWon Seo
*
*/
class ExitStatusTests {
@@ -153,7 +154,7 @@ void testAddExitDescription() {
}
@Test
- void testAddExitDescriptionWIthStacktrace() {
+ void testAddExitDescriptionWithStacktrace() {
ExitStatus status = ExitStatus.EXECUTING.addExitDescription(new RuntimeException("Foo"));
assertNotSame(ExitStatus.EXECUTING, status);
String description = status.getExitDescription();
@@ -182,8 +183,15 @@ void testAddExitCodeWithDescription() {
}
@Test
- void testUnknownIsRunning() {
+ void testIsRunning() {
+ // running statuses
+ assertTrue(ExitStatus.EXECUTING.isRunning());
assertTrue(ExitStatus.UNKNOWN.isRunning());
+ // non running statuses
+ assertFalse(ExitStatus.COMPLETED.isRunning());
+ assertFalse(ExitStatus.FAILED.isRunning());
+ assertFalse(ExitStatus.STOPPED.isRunning());
+ assertFalse(ExitStatus.NOOP.isRunning());
}
@Test
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBeanJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBeanJobTests.java
index c47fd72c0c..a4a38c2c2a 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBeanJobTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBeanJobTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -50,7 +50,7 @@ void testBeanNameWithBeanDefinition() {
context.registerBeanDefinition("bean", new RootBeanDefinition(JobSupport.class, args, null));
context.refresh();
- JobSupport configuration = (JobSupport) context.getBean("bean");
+ JobSupport configuration = context.getBean("bean", JobSupport.class);
assertNotNull(configuration.getName());
assertEquals("foo", configuration.getName());
configuration.setBeanName("bar");
@@ -66,7 +66,7 @@ void testBeanNameWithParentBeanDefinition() {
context.registerBeanDefinition("parent", new RootBeanDefinition(JobSupport.class, args, null));
context.registerBeanDefinition("bean", new ChildBeanDefinition("parent"));
context.refresh();
- JobSupport configuration = (JobSupport) context.getBean("bean");
+ JobSupport configuration = context.getBean("bean", JobSupport.class);
assertNotNull(configuration.getName());
assertEquals("bar", configuration.getName());
configuration.setBeanName("foo");
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobScopeConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobScopeConfigurationTests.java
index af0b5fc1a6..b2820ad838 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobScopeConfigurationTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobScopeConfigurationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -82,7 +82,7 @@ void testXmlJobScopeWithInheritance() throws Exception {
context = new ClassPathXmlApplicationContext(
"org/springframework/batch/core/configuration/annotation/JobScopeConfigurationTestsInheritance-context.xml");
JobSynchronizationManager.register(jobExecution);
- SimpleHolder value = (SimpleHolder) context.getBean("child");
+ SimpleHolder value = context.getBean("child", SimpleHolder.class);
assertEquals("JOB", value.call());
}
@@ -97,9 +97,9 @@ void testJobScopeWithProxyTargetClass() throws Exception {
void testStepScopeXmlImportUsingNamespace() throws Exception {
init(JobScopeConfigurationXmlImportUsingNamespace.class);
- SimpleHolder value = (SimpleHolder) context.getBean("xmlValue");
+ SimpleHolder value = context.getBean("xmlValue", SimpleHolder.class);
assertEquals("JOB", value.call());
- value = (SimpleHolder) context.getBean("javaValue");
+ value = context.getBean("javaValue", SimpleHolder.class);
assertEquals("JOB", value.call());
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/StepScopeConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/StepScopeConfigurationTests.java
index 5774e00314..e3b7cd1cfc 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/StepScopeConfigurationTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/StepScopeConfigurationTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -81,7 +81,7 @@ void testXmlStepScopeWithInheritance() throws Exception {
context = new ClassPathXmlApplicationContext(
"org/springframework/batch/core/configuration/annotation/StepScopeConfigurationTestsInheritance-context.xml");
StepSynchronizationManager.register(stepExecution);
- SimpleHolder value = (SimpleHolder) context.getBean("child");
+ SimpleHolder value = context.getBean("child", SimpleHolder.class);
assertEquals("STEP", value.call());
}
@@ -96,9 +96,9 @@ void testStepScopeWithProxyTargetClass() throws Exception {
void testStepScopeXmlImportUsingNamespace() throws Exception {
init(StepScopeConfigurationXmlImportUsingNamespace.class);
- SimpleHolder value = (SimpleHolder) context.getBean("xmlValue");
+ SimpleHolder value = context.getBean("xmlValue", SimpleHolder.class);
assertEquals("STEP", value.call());
- value = (SimpleHolder) context.getBean("javaValue");
+ value = context.getBean("javaValue", SimpleHolder.class);
assertEquals("STEP", value.call());
}
@@ -109,9 +109,9 @@ void testStepScopeXmlImportUsingNamespace() throws Exception {
public void testStepScopeUsingNamespaceAutoregisterBeans() throws Exception {
init(StepScopeConfigurationTestsUsingNamespaceAutoregisterBeans.class);
- ISimpleHolder value = (ISimpleHolder) context.getBean("xmlValue");
+ ISimpleHolder value = context.getBean("xmlValue", ISimpleHolder.class);
assertEquals("STEP", value.call());
- value = (ISimpleHolder) context.getBean("javaValue");
+ value = context.getBean("javaValue", ISimpleHolder.class);
assertEquals("STEP", value.call());
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/PartitionStepParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/PartitionStepParserTests.java
index 263a59b701..34adb784e7 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/PartitionStepParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/PartitionStepParserTests.java
@@ -145,7 +145,7 @@ void testNestedPartitionStepStepReference() throws Throwable {
String stepExecutionName = se.getStepName();
// the partitioned step
if (stepExecutionName.equalsIgnoreCase("j3s1")) {
- PartitionStep partitionStep = (PartitionStep) this.applicationContext.getBean(stepExecutionName);
+ PartitionStep partitionStep = this.applicationContext.getBean(stepExecutionName, PartitionStep.class);
// prove that the reference in the {@link
// TaskExecutorPartitionHandler} is the step configured inline
TaskExecutorPartitionHandler taskExecutorPartitionHandler = accessPrivateField(partitionStep,
@@ -184,7 +184,7 @@ void testNestedPartitionStep() throws Throwable {
String stepExecutionName = se.getStepName();
if (stepExecutionName.equalsIgnoreCase("j4s1")) { // the partitioned
// step
- PartitionStep partitionStep = (PartitionStep) this.applicationContext.getBean(stepExecutionName);
+ PartitionStep partitionStep = this.applicationContext.getBean(stepExecutionName, PartitionStep.class);
// prove that the reference in the {@link
// TaskExecutorPartitionHandler} is the step configured inline
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepListenerInStepParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepListenerInStepParserTests.java
index caf6f2f99c..6536da633c 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepListenerInStepParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepListenerInStepParserTests.java
@@ -45,7 +45,7 @@ class StepListenerInStepParserTests {
@Test
void testListenersAtStepLevel() throws Exception {
- Step step = (Step) beanFactory.getBean("s1");
+ Step step = beanFactory.getBean("s1", Step.class);
List> list = getListeners(step);
assertEquals(1, list.size());
assertTrue(list.get(0) instanceof DummyStepExecutionListener);
@@ -54,7 +54,7 @@ void testListenersAtStepLevel() throws Exception {
@Test
// TODO: BATCH-1689 (expected=BeanCreationException.class)
void testListenersAtStepLevelWrongType() throws Exception {
- Step step = (Step) beanFactory.getBean("s2");
+ Step step = beanFactory.getBean("s2", Step.class);
List> list = getListeners(step);
assertEquals(1, list.size());
assertTrue(list.get(0) instanceof DummyChunkListener);
@@ -62,7 +62,7 @@ void testListenersAtStepLevelWrongType() throws Exception {
@Test
void testListenersAtTaskletAndStepLevels() throws Exception {
- Step step = (Step) beanFactory.getBean("s3");
+ Step step = beanFactory.getBean("s3", Step.class);
List> list = getListeners(step);
assertEquals(2, list.size());
assertTrue(list.get(0) instanceof DummyStepExecutionListener);
@@ -71,7 +71,7 @@ void testListenersAtTaskletAndStepLevels() throws Exception {
@Test
void testListenersAtChunkAndStepLevels() throws Exception {
- Step step = (Step) beanFactory.getBean("s4");
+ Step step = beanFactory.getBean("s4", Step.class);
List> list = getListeners(step);
assertEquals(2, list.size());
assertTrue(list.get(0) instanceof DummyStepExecutionListener);
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepNameTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepNameTests.java
index 6932f31c12..a482c3022a 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepNameTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepNameTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2022 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,7 @@ void testStepNames(Resource resource) throws Exception {
for (String name : stepLocators.keySet()) {
StepLocator stepLocator = stepLocators.get(name);
Collection stepNames = stepLocator.getStepNames();
- Job job = (Job) context.getBean(name);
+ Job job = context.getBean(name, Job.class);
String jobName = job.getName();
assertFalse(stepNames.isEmpty(), "Job has no steps: " + jobName);
for (String registeredName : stepNames) {
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java
index 541e402264..f183cfd343 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/StepParserTests.java
@@ -98,7 +98,7 @@ void testStepParserBeanName() {
"org/springframework/batch/core/configuration/xml/StepParserBeanNameTests-context.xml");
Map beans = ctx.getBeansOfType(Step.class);
assertTrue(beans.containsKey("s1"), "'s1' bean not found");
- Step s1 = (Step) ctx.getBean("s1");
+ Step s1 = ctx.getBean("s1", Step.class);
assertEquals("s1", s1.getName(), "wrong name");
}
@@ -114,7 +114,7 @@ void testStepParserCommitInterval() throws Exception {
"org/springframework/batch/core/configuration/xml/StepParserCommitIntervalTests-context.xml");
Map beans = ctx.getBeansOfType(Step.class);
assertTrue(beans.containsKey("s1"), "'s1' bean not found");
- Step s1 = (Step) ctx.getBean("s1");
+ Step s1 = ctx.getBean("s1", Step.class);
CompletionPolicy completionPolicy = getCompletionPolicy(s1);
assertTrue(completionPolicy instanceof SimpleCompletionPolicy);
assertEquals(25, ReflectionTestUtils.getField(completionPolicy, "chunkSize"));
@@ -126,7 +126,7 @@ void testStepParserCompletionPolicy() throws Exception {
"org/springframework/batch/core/configuration/xml/StepParserCompletionPolicyTests-context.xml");
Map beans = ctx.getBeansOfType(Step.class);
assertTrue(beans.containsKey("s1"), "'s1' bean not found");
- Step s1 = (Step) ctx.getBean("s1");
+ Step s1 = ctx.getBean("s1", Step.class);
CompletionPolicy completionPolicy = getCompletionPolicy(s1);
assertTrue(completionPolicy instanceof DummyCompletionPolicy);
}
@@ -212,7 +212,7 @@ private void validateTransactionAttributesInherited(String stepName, Application
@SuppressWarnings("unchecked")
private List getListeners(String stepName, ApplicationContext ctx) throws Exception {
assertTrue(ctx.containsBean(stepName));
- Step step = (Step) ctx.getBean(stepName);
+ Step step = ctx.getBean(stepName, Step.class);
assertTrue(step instanceof TaskletStep);
Object compositeListener = ReflectionTestUtils.getField(step, "stepExecutionListener");
Object composite = ReflectionTestUtils.getField(compositeListener, "list");
@@ -236,7 +236,7 @@ private StepExecutionListener getListener(String stepName, ApplicationContext ct
private DefaultTransactionAttribute getTransactionAttribute(ApplicationContext ctx, String stepName) {
assertTrue(ctx.containsBean(stepName));
- Step step = (Step) ctx.getBean(stepName);
+ Step step = ctx.getBean(stepName, Step.class);
assertTrue(step instanceof TaskletStep);
Object transactionAttribute = ReflectionTestUtils.getField(step, "transactionAttribute");
return (DefaultTransactionAttribute) transactionAttribute;
@@ -252,7 +252,7 @@ void testInheritFromBean() {
private Tasklet getTasklet(String stepName, ApplicationContext ctx) {
assertTrue(ctx.containsBean(stepName));
- Step step = (Step) ctx.getBean(stepName);
+ Step step = ctx.getBean(stepName, Step.class);
assertTrue(step instanceof TaskletStep);
Object tasklet = ReflectionTestUtils.getField(step, "tasklet");
assertTrue(tasklet instanceof Tasklet);
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests.java
index 11823defd7..f561e58ef0 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests.java
@@ -46,7 +46,7 @@ class TaskletStepAllowStartIfCompleteTests {
@Test
void test() throws Exception {
// retrieve the step from the context and see that it's allow is set
- AbstractStep abstractStep = (AbstractStep) context.getBean("simpleJob.step1");
+ AbstractStep abstractStep = context.getBean("simpleJob.step1", AbstractStep.class);
assertTrue(abstractStep.isAllowStartIfComplete());
}
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDaoTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDaoTests.java
index f4651a2479..1bed93ab41 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDaoTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/dao/JdbcStepExecutionDaoTests.java
@@ -31,14 +31,14 @@ class JdbcStepExecutionDaoTests extends AbstractStepExecutionDaoTests {
@Override
protected StepExecutionDao getStepExecutionDao() {
- return (StepExecutionDao) applicationContext.getBean("stepExecutionDao");
+ return applicationContext.getBean("stepExecutionDao", StepExecutionDao.class);
}
@Override
protected JobRepository getJobRepository() {
deleteFromTables("BATCH_JOB_EXECUTION_CONTEXT", "BATCH_STEP_EXECUTION_CONTEXT", "BATCH_STEP_EXECUTION",
"BATCH_JOB_EXECUTION_PARAMS", "BATCH_JOB_EXECUTION", "BATCH_JOB_INSTANCE");
- return (JobRepository) applicationContext.getBean("jobRepository");
+ return applicationContext.getBean("jobRepository", JobRepository.class);
}
/**
diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java
index ec486a7636..7b9640d16b 100644
--- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java
+++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2009-2022 the original author or authors.
+ * Copyright 2009-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -339,7 +339,7 @@ private StepExecution launchStep(String stepName) throws Exception {
job.setJobRepository(jobRepository);
List stepsToExecute = new ArrayList<>();
- stepsToExecute.add((Step) applicationContext.getBean(stepName));
+ stepsToExecute.add(applicationContext.getBean(stepName, Step.class));
job.setSteps(stepsToExecute);
JobExecution jobExecution = jobLauncher.run(job,
diff --git a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc
index 22635de973..2162223f15 100644
--- a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc
+++ b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc
@@ -41,7 +41,8 @@ This implementation requires MongoDB version 4 or later and is based on Spring D
In order to use this job repository, all you need to do is define a `MongoTemplate` and a
`MongoTransactionManager` which are required by the newly added `MongoJobRepositoryFactoryBean`:
-```
+[source, java]
+----
@Bean
public JobRepository jobRepository(MongoTemplate mongoTemplate, MongoTransactionManager transactionManager) throws Exception {
MongoJobRepositoryFactoryBean jobRepositoryFactoryBean = new MongoJobRepositoryFactoryBean();
@@ -50,7 +51,7 @@ public JobRepository jobRepository(MongoTemplate mongoTemplate, MongoTransaction
jobRepositoryFactoryBean.afterPropertiesSet();
return jobRepositoryFactoryBean.getObject();
}
-```
+----
Once the MongoDB job repository defined, you can inject it in any job or step as a regular job repository.
You can find a complete example in the https://github.com/spring-projects/spring-batch/blob/main/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java[MongoDBJobRepositoryIntegrationTests].
@@ -85,7 +86,8 @@ over different resources and writing a custom reader is not an option.
A `CompositeItemReader` works like other composite artifacts, by delegating the reading operation to regular item readers
in order. Here is a quick example showing a composite reader that reads persons data from a flat file then from a database table:
-```
+[source, java]
+----
@Bean
public FlatFileItemReader itemReader1() {
return new FlatFileItemReaderBuilder()
@@ -112,12 +114,12 @@ public JdbcCursorItemReader itemReader2() {
public CompositeItemReader itemReader() {
return new CompositeItemReader<>(Arrays.asList(itemReader1(), itemReader2()));
}
-```
+----
[[new-adapters-for-java-util-function-apis]]
== New adapters for java.util.function APIs
-Similar to `FucntionItemProcessor` that adapts a `java.util.function.Function` to an item processor, this release
+Similar to `FunctionItemProcessor` that adapts a `java.util.function.Function` to an item processor, this release
introduces several new adapters for other `java.util.function` interfaces like `Supplier`, `Consumer` and `Predicate`.
The newly added adapters are: `SupplierItemReader`, `ConsumerItemWriter` and `PredicateFilteringItemProcessor`.
diff --git a/spring-batch-docs/pom.xml b/spring-batch-docs/pom.xml
index 30afe8f50b..0f6286769e 100644
--- a/spring-batch-docs/pom.xml
+++ b/spring-batch-docs/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-docs
Spring Batch Docs
diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml
index 3abe16ba46..6f46cc7a58 100644
--- a/spring-batch-infrastructure/pom.xml
+++ b/spring-batch-infrastructure/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-infrastructure
jar
@@ -553,6 +553,24 @@
${jruby.version}
test
+
+ io.lettuce
+ lettuce-core
+ ${lettuce.version}
+ test
+
+
+ redis.clients
+ jedis
+ ${jedis.version}
+ test
+
+
+ com.redis
+ testcontainers-redis
+ ${testcontainers-redis.version}
+ test
+
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
index b7aa27f375..3937005099 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/MongoItemWriter.java
@@ -16,6 +16,10 @@
package org.springframework.batch.item.data;
+import static java.util.stream.Collectors.toList;
+
+import java.util.List;
+
import org.bson.Document;
import org.bson.types.ObjectId;
@@ -92,6 +96,8 @@ public enum Mode {
private Mode mode = Mode.UPSERT;
+ private List primaryKeys = List.of(ID_KEY);
+
public MongoItemWriter() {
super();
this.bufferKey = new Object();
@@ -163,6 +169,27 @@ public String getCollection() {
return collection;
}
+ /**
+ * Set the primary keys to associate with the document being written. These fields
+ * should uniquely identify a single object.
+ * @param primaryKeys The primary keys to use.
+ * @since 5.2.3
+ */
+ public void setPrimaryKeys(List primaryKeys) {
+ Assert.notEmpty(primaryKeys, "The primaryKeys list must have one or more keys.");
+
+ this.primaryKeys = primaryKeys;
+ }
+
+ /**
+ * Get the list of primary keys associated with the document being written.
+ * @return the list of primary keys
+ * @since 5.2.3
+ */
+ public List getPrimaryKeys() {
+ return primaryKeys;
+ }
+
/**
* If a transaction is active, buffer items to be written just before commit.
* Otherwise write items using the provided template.
@@ -213,9 +240,14 @@ private void remove(Chunk extends T> chunk) {
for (Object item : chunk) {
Document document = new Document();
mongoConverter.write(item, document);
- Object objectId = document.get(ID_KEY);
- if (objectId != null) {
- Query query = new Query().addCriteria(Criteria.where(ID_KEY).is(objectId));
+
+ List criteriaList = primaryKeys.stream()
+ .filter(document::containsKey)
+ .map(key -> Criteria.where(key).is(document.get(key)))
+ .collect(toList());
+ if (!criteriaList.isEmpty()) {
+ Query query = new Query();
+ criteriaList.forEach(query::addCriteria);
bulkOperations.remove(query);
}
}
@@ -229,8 +261,21 @@ private void upsert(Chunk extends T> chunk) {
for (Object item : chunk) {
Document document = new Document();
mongoConverter.write(item, document);
- Object objectId = document.get(ID_KEY) != null ? document.get(ID_KEY) : new ObjectId();
- Query query = new Query().addCriteria(Criteria.where(ID_KEY).is(objectId));
+
+ Query query = new Query();
+ List criteriaList = primaryKeys.stream()
+ .filter(document::containsKey)
+ .map(key -> Criteria.where(key).is(document.get(key)))
+ .collect(toList());
+
+ if (criteriaList.isEmpty()) {
+ Object objectId = document.get(ID_KEY) != null ? document.get(ID_KEY) : new ObjectId();
+ query.addCriteria(Criteria.where(ID_KEY).is(objectId));
+ }
+ else {
+ criteriaList.forEach(query::addCriteria);
+ }
+
bulkOperations.replaceOne(query, document, upsert);
}
bulkOperations.execute();
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemWriterBuilder.java
index 4df60a7d4c..0b1fcc5aee 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemWriterBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/builder/MongoItemWriterBuilder.java
@@ -16,6 +16,8 @@
package org.springframework.batch.item.data.builder;
+import java.util.List;
+
import org.springframework.batch.item.data.MongoItemWriter;
import org.springframework.batch.item.data.MongoItemWriter.Mode;
import org.springframework.data.mongodb.core.MongoOperations;
@@ -37,6 +39,8 @@ public class MongoItemWriterBuilder {
private Mode mode = Mode.UPSERT;
+ private List primaryKeys = List.of();
+
/**
* Indicates if the items being passed to the writer are to be saved or removed from
* the data store. If set to false (default), the items will be saved. If set to true,
@@ -93,6 +97,32 @@ public MongoItemWriterBuilder collection(String collection) {
return this;
}
+ /**
+ * Set the primary keys to associate with the document being written. These fields
+ * should uniquely identify a single object.
+ * @param primaryKeys The keys to use.
+ * @see MongoItemWriter#setPrimaryKeys(List)
+ * @since 5.2.3
+ */
+ public MongoItemWriterBuilder primaryKeys(List primaryKeys) {
+ this.primaryKeys = List.copyOf(primaryKeys);
+
+ return this;
+ }
+
+ /**
+ * Set the primary keys to associate with the document being written. These fields
+ * should uniquely identify a single object.
+ * @param primaryKeys The keys to use.
+ * @see MongoItemWriter#setPrimaryKeys(List)
+ * @since 5.2.3
+ */
+ public MongoItemWriterBuilder primaryKeys(String... primaryKeys) {
+ this.primaryKeys = List.of(primaryKeys);
+
+ return this;
+ }
+
/**
* Validates and builds a {@link MongoItemWriter}.
* @return a {@link MongoItemWriter}
@@ -105,6 +135,10 @@ public MongoItemWriter build() {
writer.setMode(this.mode);
writer.setCollection(this.collection);
+ if (!this.primaryKeys.isEmpty()) {
+ writer.setPrimaryKeys(this.primaryKeys);
+ }
+
return writer;
}
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java
index a4014536d5..48bb8b91b8 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilder.java
@@ -164,7 +164,7 @@ public JdbcCursorItemReaderBuilder maxRows(int maxRows) {
}
/**
- * The time in milliseconds for the query to timeout
+ * The time in seconds for the query to timeout
* @param queryTimeout timeout
* @return this instance for method chaining
* @see JdbcCursorItemReader#setQueryTimeout(int)
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java
index ab8601b18c..e52d4dbde9 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilder.java
@@ -58,6 +58,8 @@
* @author Glenn Renfro
* @author Mahmoud Ben Hassine
* @author Drummond Dawson
+ * @author Patrick Baumgartner
+ * @author François Martin
* @since 4.0
* @see FlatFileItemReader
*/
@@ -459,6 +461,9 @@ else if (this.delimitedBuilder != null) {
throw new IllegalStateException("No LineTokenizer implementation was provided.");
}
+ Assert.state(this.targetType == null || this.fieldSetMapper == null,
+ "Either a TargetType or FieldSetMapper can be set, can't be both.");
+
if (this.targetType != null || StringUtils.hasText(this.prototypeBeanName)) {
if (this.targetType != null && this.targetType.isRecord()) {
RecordFieldSetMapper mapper = new RecordFieldSetMapper<>(this.targetType);
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java
index a86079cc0f..0eb449dab4 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -31,6 +31,7 @@
*
* @param type of mapped items
* @author Mahmoud Ben Hassine
+ * @author Seungyong Hong
* @since 4.3
*/
public class RecordFieldSetMapper implements FieldSetMapper {
@@ -63,6 +64,10 @@ public RecordFieldSetMapper(Class targetType, ConversionService conversionSer
this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor);
this.constructorParameterTypes = this.mappedConstructor.getParameterTypes();
}
+ else {
+ this.constructorParameterNames = new String[0];
+ this.constructorParameterTypes = new Class[0];
+ }
}
@Override
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractDatabaseItemStreamItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractDatabaseItemStreamItemReaderTests.java
index 43b9ff289e..cd2b0de1a3 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractDatabaseItemStreamItemReaderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/AbstractDatabaseItemStreamItemReaderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2009-2023 the original author or authors.
+ * Copyright 2009-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -70,7 +70,7 @@ void testReadToExhaustion() throws Exception {
}
protected DataSource getDataSource() {
- return (DataSource) ctx.getBean("dataSource");
+ return ctx.getBean("dataSource", DataSource.class);
}
}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcBatchItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcBatchItemWriterBuilderTests.java
index cdc7c5fc76..76ffb25b76 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcBatchItemWriterBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcBatchItemWriterBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2023 the original author or authors.
+ * Copyright 2016-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -60,7 +60,7 @@ class JdbcBatchItemWriterBuilderTests {
@BeforeEach
void setUp() {
this.context = new AnnotationConfigApplicationContext(TestDataSourceConfiguration.class);
- this.dataSource = (DataSource) context.getBean("dataSource");
+ this.dataSource = context.getBean("dataSource", DataSource.class);
}
@AfterEach
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java
index 16e33c6107..fa66612886 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcCursorItemReaderBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2016-2024 the original author or authors.
+ * Copyright 2016-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@ class JdbcCursorItemReaderBuilderTests {
@BeforeEach
void setUp() {
this.context = new AnnotationConfigApplicationContext(TestDataSourceConfiguration.class);
- this.dataSource = (DataSource) context.getBean("dataSource");
+ this.dataSource = context.getBean("dataSource", DataSource.class);
}
@AfterEach
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java
index a6220cbeb1..850067929d 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JdbcPagingItemReaderBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2024 the original author or authors.
+ * Copyright 2017-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -58,7 +58,7 @@ class JdbcPagingItemReaderBuilderTests {
@BeforeEach
void setUp() {
this.context = new AnnotationConfigApplicationContext(TestDataSourceConfiguration.class);
- this.dataSource = (DataSource) context.getBean("dataSource");
+ this.dataSource = context.getBean("dataSource", DataSource.class);
}
@AfterEach
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilderTests.java
index d24fca1a7a..1affc62d81 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaCursorItemReaderBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2023 the original author or authors.
+ * Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,7 @@ class JpaCursorItemReaderBuilderTests {
void setUp() {
this.context = new AnnotationConfigApplicationContext(
JpaCursorItemReaderBuilderTests.TestDataSourceConfiguration.class);
- this.entityManagerFactory = (EntityManagerFactory) context.getBean("entityManagerFactory");
+ this.entityManagerFactory = context.getBean("entityManagerFactory", EntityManagerFactory.class);
}
@AfterEach
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java
index 8fce0376be..e0f1f67e04 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2017-2023 the original author or authors.
+ * Copyright 2017-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -64,7 +64,7 @@ class JpaPagingItemReaderBuilderTests {
void setUp() {
this.context = new AnnotationConfigApplicationContext(
JpaPagingItemReaderBuilderTests.TestDataSourceConfiguration.class);
- this.entityManagerFactory = (EntityManagerFactory) context.getBean("entityManagerFactory");
+ this.entityManagerFactory = context.getBean("entityManagerFactory", EntityManagerFactory.class);
}
@AfterEach
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java
index e6c6f6c2de..736297641a 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/FlatFileItemReaderBuilderTests.java
@@ -55,6 +55,8 @@
* @author Mahmoud Ben Hassine
* @author Drummond Dawson
* @author Glenn Renfro
+ * @author Patrick Baumgartner
+ * @author François Martin
*/
class FlatFileItemReaderBuilderTests {
@@ -562,6 +564,17 @@ void testErrorMessageWhenNoLineTokenizerWasProvided() {
}
}
+ @Test
+ void testErrorWhenTargetTypeAndFieldSetMapperIsProvided() {
+ var builder = new FlatFileItemReaderBuilder().name("fooReader")
+ .resource(getResource("1;2;3"))
+ .lineTokenizer(line -> new DefaultFieldSet(line.split(";")))
+ .targetType(Foo.class)
+ .fieldSetMapper(fieldSet -> new Foo());
+ var exception = assertThrows(IllegalStateException.class, builder::build);
+ assertEquals("Either a TargetType or FieldSetMapper can be set, can't be both.", exception.getMessage());
+ }
+
@Test
void testSetupWithRecordTargetType() {
// given
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java
index a3fc68ff44..3a1dde5eef 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2024 the original author or authors.
+ * Copyright 2020-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,7 +16,6 @@
package org.springframework.batch.item.file.mapping;
import org.junit.jupiter.api.Test;
-
import org.springframework.batch.item.file.transform.DefaultFieldSet;
import org.springframework.batch.item.file.transform.FieldSet;
@@ -26,6 +25,7 @@
/**
* @author Mahmoud Ben Hassine
+ * @author Seungyong Hong
*/
class RecordFieldSetMapperTests {
@@ -68,7 +68,23 @@ void testMapFieldSetWhenFieldNamesAreNotSpecified() {
assertEquals("Field names must be specified", exception.getMessage());
}
+ @Test
+ void testMapFieldSetWhenEmptyRecord() {
+ // given
+ RecordFieldSetMapper mapper = new RecordFieldSetMapper<>(EmptyRecord.class);
+ FieldSet fieldSet = new DefaultFieldSet(new String[0], new String[0]);
+
+ // when
+ EmptyRecord empty = mapper.mapFieldSet(fieldSet);
+
+ // then
+ assertNotNull(empty);
+ }
+
record Person(int id, String name) {
}
+ record EmptyRecord() {
+ }
+
}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemReaderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemReaderIntegrationTests.java
new file mode 100644
index 0000000000..66e733fcfb
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemReaderIntegrationTests.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.redis;
+
+import com.redis.testcontainers.RedisContainer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.redis.example.Person;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.ScanOptions;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Stream;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+
+/**
+ * @author Hyunwoo Jung
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@ExtendWith(SpringExtension.class)
+class RedisItemReaderIntegrationTests {
+
+ private static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:8.0.3");
+
+ @Container
+ public static RedisContainer redis = new RedisContainer(REDIS_IMAGE);
+
+ private RedisItemReader reader;
+
+ private RedisTemplate template;
+
+ @BeforeEach
+ void setUp() {
+ this.template = setUpRedisTemplate(lettuceConnectionFactory());
+ }
+
+ @AfterEach
+ void tearDown() {
+ this.template.getConnectionFactory().getConnection().serverCommands().flushAll();
+ }
+
+ @ParameterizedTest
+ @MethodSource("connectionFactories")
+ void testRead(RedisConnectionFactory connectionFactory) throws Exception {
+ this.template.opsForValue().set("person:1", new Person(1, "foo"));
+ this.template.opsForValue().set("person:2", new Person(2, "bar"));
+ this.template.opsForValue().set("person:3", new Person(3, "baz"));
+ this.template.opsForValue().set("person:4", new Person(4, "qux"));
+ this.template.opsForValue().set("person:5", new Person(5, "quux"));
+
+ RedisTemplate redisTemplate = setUpRedisTemplate(connectionFactory);
+ ScanOptions scanOptions = ScanOptions.scanOptions().match("person:*").count(10).build();
+ this.reader = new RedisItemReader<>(redisTemplate, scanOptions);
+
+ this.reader.open(new ExecutionContext());
+
+ List items = new ArrayList<>();
+ for (int i = 0; i < 5; i++) {
+ items.add(this.reader.read());
+ }
+
+ assertThat(items, containsInAnyOrder(new Person(1, "foo"), new Person(2, "bar"), new Person(3, "baz"),
+ new Person(4, "qux"), new Person(5, "quux")));
+ }
+
+ private RedisTemplate setUpRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ RedisTemplate redisTemplate = new RedisTemplate<>();
+ redisTemplate.setConnectionFactory(redisConnectionFactory);
+ redisTemplate.setKeySerializer(new StringRedisSerializer());
+ redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
+ redisTemplate.afterPropertiesSet();
+
+ return redisTemplate;
+ }
+
+ private static Stream connectionFactories() {
+ return Stream.of(Arguments.of(lettuceConnectionFactory()), Arguments.of(jedisConnectionFactory()));
+ }
+
+ private static RedisConnectionFactory lettuceConnectionFactory() {
+ LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(
+ new RedisStandaloneConfiguration(redis.getRedisHost(), redis.getRedisPort()));
+ lettuceConnectionFactory.afterPropertiesSet();
+ return lettuceConnectionFactory;
+ }
+
+ private static JedisConnectionFactory jedisConnectionFactory() {
+ JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(
+ new RedisStandaloneConfiguration(redis.getRedisHost(), redis.getRedisPort()));
+ jedisConnectionFactory.afterPropertiesSet();
+ return jedisConnectionFactory;
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemWriterIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemWriterIntegrationTests.java
new file mode 100644
index 0000000000..f6d7edcac6
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/RedisItemWriterIntegrationTests.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.redis;
+
+import com.redis.testcontainers.RedisContainer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.batch.item.Chunk;
+import org.springframework.batch.item.redis.example.Person;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
+import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
+import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.testcontainers.junit.jupiter.Container;
+import org.testcontainers.junit.jupiter.Testcontainers;
+import org.testcontainers.utility.DockerImageName;
+
+import java.util.stream.Stream;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * @author Hyunwoo Jung
+ */
+@Testcontainers(disabledWithoutDocker = true)
+@ExtendWith(SpringExtension.class)
+class RedisItemWriterIntegrationTests {
+
+ private static final DockerImageName REDIS_IMAGE = DockerImageName.parse("redis:8.0.3");
+
+ @Container
+ public static RedisContainer redis = new RedisContainer(REDIS_IMAGE);
+
+ private RedisItemWriter writer;
+
+ private RedisTemplate template;
+
+ @BeforeEach
+ void setUp() {
+ this.template = setUpRedisTemplate(lettuceConnectionFactory());
+ }
+
+ @AfterEach
+ void tearDown() {
+ this.template.getConnectionFactory().getConnection().serverCommands().flushAll();
+ }
+
+ @ParameterizedTest
+ @MethodSource("connectionFactories")
+ void testWriteWithLettuce(RedisConnectionFactory connectionFactory) throws Exception {
+ RedisTemplate redisTemplate = setUpRedisTemplate(connectionFactory);
+ this.writer = new RedisItemWriter<>();
+ this.writer.setRedisTemplate(redisTemplate);
+ this.writer.setItemKeyMapper(p -> "person:" + p.getId());
+ this.writer.setDelete(false);
+
+ Chunk items = new Chunk<>(new Person(1, "foo"), new Person(2, "bar"), new Person(3, "baz"),
+ new Person(4, "qux"), new Person(5, "quux"));
+ this.writer.write(items);
+
+ assertEquals(new Person(1, "foo"), this.template.opsForValue().get("person:1"));
+ assertEquals(new Person(2, "bar"), this.template.opsForValue().get("person:2"));
+ assertEquals(new Person(3, "baz"), this.template.opsForValue().get("person:3"));
+ assertEquals(new Person(4, "qux"), this.template.opsForValue().get("person:4"));
+ assertEquals(new Person(5, "quux"), this.template.opsForValue().get("person:5"));
+ }
+
+ @ParameterizedTest
+ @MethodSource("connectionFactories")
+ void testDelete(RedisConnectionFactory connectionFactory) throws Exception {
+ this.template.opsForValue().set("person:1", new Person(1, "foo"));
+ this.template.opsForValue().set("person:2", new Person(2, "bar"));
+ this.template.opsForValue().set("person:3", new Person(3, "baz"));
+ this.template.opsForValue().set("person:4", new Person(4, "qux"));
+ this.template.opsForValue().set("person:5", new Person(5, "quux"));
+
+ RedisTemplate redisTemplate = setUpRedisTemplate(connectionFactory);
+ this.writer = new RedisItemWriter<>();
+ this.writer.setRedisTemplate(redisTemplate);
+ this.writer.setItemKeyMapper(p -> "person:" + p.getId());
+ this.writer.setDelete(true);
+
+ Chunk items = new Chunk<>(new Person(1, "foo"), new Person(2, "bar"), new Person(3, "baz"),
+ new Person(4, "qux"), new Person(5, "quux"));
+ this.writer.write(items);
+
+ assertFalse(this.template.hasKey("person:1"));
+ assertFalse(this.template.hasKey("person:2"));
+ assertFalse(this.template.hasKey("person:3"));
+ assertFalse(this.template.hasKey("person:4"));
+ assertFalse(this.template.hasKey("person:5"));
+ }
+
+ private RedisTemplate setUpRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
+ RedisTemplate redisTemplate = new RedisTemplate<>();
+ redisTemplate.setConnectionFactory(redisConnectionFactory);
+ redisTemplate.setKeySerializer(new StringRedisSerializer());
+ redisTemplate.setValueSerializer(new JdkSerializationRedisSerializer());
+ redisTemplate.afterPropertiesSet();
+
+ return redisTemplate;
+ }
+
+ private static Stream connectionFactories() {
+ return Stream.of(Arguments.of(lettuceConnectionFactory()), Arguments.of(jedisConnectionFactory()));
+ }
+
+ private static RedisConnectionFactory lettuceConnectionFactory() {
+ LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(
+ new RedisStandaloneConfiguration(redis.getRedisHost(), redis.getRedisPort()));
+ lettuceConnectionFactory.afterPropertiesSet();
+ return lettuceConnectionFactory;
+ }
+
+ private static JedisConnectionFactory jedisConnectionFactory() {
+ JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(
+ new RedisStandaloneConfiguration(redis.getRedisHost(), redis.getRedisPort()));
+ jedisConnectionFactory.afterPropertiesSet();
+ return jedisConnectionFactory;
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/example/Person.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/example/Person.java
new file mode 100644
index 0000000000..2cc819ab99
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/redis/example/Person.java
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2025 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.springframework.batch.item.redis.example;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Objects;
+
+/**
+ * @author Hyunwoo Jung
+ */
+public class Person implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 2396556853218591048L;
+
+ private long id;
+
+ private String name;
+
+ public Person(long id, String name) {
+ this.id = id;
+ this.name = name;
+ }
+
+ public long getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (o == null || getClass() != o.getClass())
+ return false;
+ Person person = (Person) o;
+ return id == person.id && Objects.equals(name, person.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(id, name);
+ }
+
+ @Override
+ public String toString() {
+ return "Person{id=" + id + ", name=" + name + "}";
+ }
+
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java
index bded5597a9..365ce8a5ed 100644
--- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/repeat/jms/SynchronousTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -155,7 +155,7 @@ void JpaNativeQueryProviderIntegrationTeststestPartialRollback() {
// The JmsTemplate is used elsewhere outside a transaction, so
// we need to use one here that is transaction aware.
final JmsTemplate txJmsTemplate = new JmsTemplate(
- (ConnectionFactory) applicationContext.getBean("txAwareConnectionFactory"));
+ applicationContext.getBean("txAwareConnectionFactory", ConnectionFactory.class));
txJmsTemplate.setReceiveTimeout(100L);
txJmsTemplate.setSessionTransacted(true);
diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml
index debc4e8e64..8bc95f393a 100644
--- a/spring-batch-integration/pom.xml
+++ b/spring-batch-integration/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-integration
Spring Batch Integration
diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/chunk/RemoteChunkHandlerFactoryBean.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/chunk/RemoteChunkHandlerFactoryBean.java
index 9def1ae7c3..26ffef4ff8 100644
--- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/chunk/RemoteChunkHandlerFactoryBean.java
+++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/chunk/RemoteChunkHandlerFactoryBean.java
@@ -122,12 +122,12 @@ public ChunkHandler getObject() throws Exception {
stepContributionSource = (StepContributionSource) chunkWriter;
}
- Assert.state(step instanceof TaskletStep, "Step [" + step.getName() + "] must be a TaskletStep");
+ Assert.state(step != null, "A TaskletStep must be provided");
if (logger.isDebugEnabled()) {
logger.debug("Converting TaskletStep with name=" + step.getName());
}
- Tasklet tasklet = getTasklet(step);
+ Tasklet tasklet = step.getTasklet();
Assert.state(tasklet instanceof ChunkOrientedTasklet>,
"Tasklet must be ChunkOrientedTasklet in step=" + step.getName());
@@ -227,15 +227,6 @@ private ChunkProcessor getChunkProcessor(ChunkOrientedTasklet> tasklet) {
return (ChunkProcessor) getField(tasklet, "chunkProcessor");
}
- /**
- * Pull a Tasklet out of a step.
- * @param step a TaskletStep
- * @return the Tasklet
- */
- private Tasklet getTasklet(TaskletStep step) {
- return (Tasklet) getField(step, "tasklet");
- }
-
private static Object getField(Object target, String name) {
Assert.notNull(target, "Target object must not be null");
Field field = ReflectionUtils.findField(target.getClass(), name);
diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml
index 646a150321..72fd8a5a67 100644
--- a/spring-batch-samples/pom.xml
+++ b/spring-batch-samples/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-samples
jar
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/chunking/RemoteChunkingJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/chunking/RemoteChunkingJobFunctionalTests.java
index 07d5f93da5..b800ddc027 100644
--- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/chunking/RemoteChunkingJobFunctionalTests.java
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/chunking/RemoteChunkingJobFunctionalTests.java
@@ -77,10 +77,9 @@ void testRemoteChunkingJob(@Autowired Job job) throws Exception {
JobExecution jobExecution = this.jobLauncher.run(job, new JobParameters());
// then
+ // the manager sent 2 chunks ({1, 2, 3} and {4, 5, 6}) to workers
assertEquals(ExitStatus.COMPLETED.getExitCode(), jobExecution.getExitStatus().getExitCode());
- assertEquals("Waited for 2 results.", // the manager sent 2 chunks ({1, 2,
- // 3} and {4, 5, 6}) to workers
- jobExecution.getExitStatus().getExitDescription());
+ assertEquals("Waited for 2 results.", jobExecution.getExitStatus().getExitDescription());
}
}
diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java
index 9dfcc75f44..71caa2868f 100644
--- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java
+++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java
@@ -42,6 +42,8 @@ public void testLaunchJob() throws Exception {
// then
assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
+ assertEquals(1, jobExecution.getStepExecutions().size());
+ assertEquals(BatchStatus.COMPLETED, jobExecution.getStepExecutions().iterator().next().getStatus());
}
}
diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml
index c9f92b858f..55f14ac626 100644
--- a/spring-batch-test/pom.xml
+++ b/spring-batch-test/pom.xml
@@ -4,7 +4,7 @@
org.springframework.batch
spring-batch
- 5.2.2
+ 5.2.3
spring-batch-test
Spring Batch Test
diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/JobRepositoryTestUtils.java b/spring-batch-test/src/main/java/org/springframework/batch/test/JobRepositoryTestUtils.java
index d6a7b07327..15e1d2dae5 100644
--- a/spring-batch-test/src/main/java/org/springframework/batch/test/JobRepositoryTestUtils.java
+++ b/spring-batch-test/src/main/java/org/springframework/batch/test/JobRepositoryTestUtils.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2023 the original author or authors.
+ * Copyright 2006-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -30,6 +30,7 @@
import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException;
import org.springframework.batch.core.repository.JobRepository;
import org.springframework.batch.core.repository.JobRestartException;
+import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.lang.Nullable;
/**
@@ -39,6 +40,7 @@
*
* @author Dave Syer
* @author Mahmoud Ben Hassine
+ * @author Yanming Zhou
*/
public class JobRepositoryTestUtils {
@@ -136,7 +138,12 @@ public void removeJobExecutions(Collection jobExecutions) {
removeJobExecution(jobExecution);
}
for (JobExecution jobExecution : jobExecutions) {
- this.jobRepository.deleteJobInstance(jobExecution.getJobInstance());
+ try {
+ this.jobRepository.deleteJobInstance(jobExecution.getJobInstance());
+ }
+ catch (OptimisticLockingFailureException ignore) {
+ // same job instance may be already deleted
+ }
}
}
diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/SampleStepTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/SampleStepTests.java
index e16e1410a7..a52cfd7620 100755
--- a/spring-batch-test/src/test/java/org/springframework/batch/test/SampleStepTests.java
+++ b/spring-batch-test/src/test/java/org/springframework/batch/test/SampleStepTests.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2008-2022 the original author or authors.
+ * Copyright 2008-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -61,7 +61,7 @@ void tearDown() {
@Test
void testTasklet() {
- Step step = (Step) context.getBean("s2");
+ Step step = context.getBean("s2", Step.class);
assertEquals(BatchStatus.COMPLETED, stepRunner.launchStep(step).getStatus());
assertEquals(2, jdbcTemplate.queryForObject("SELECT ID from TESTS where NAME = 'SampleTasklet2'", Integer.class)
.intValue());