From 70b85764267fbdc253abd44f06fedb1c0be77b1c Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Wed, 18 Dec 2024 21:38:44 +0100 Subject: [PATCH 01/37] Next development version --- pom.xml | 2 +- spring-batch-bom/pom.xml | 2 +- spring-batch-core/pom.xml | 2 +- spring-batch-docs/pom.xml | 2 +- spring-batch-infrastructure/pom.xml | 2 +- spring-batch-integration/pom.xml | 2 +- spring-batch-samples/pom.xml | 2 +- spring-batch-test/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 2cef3d05b8..eb01fbdcc5 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.1 + 5.2.2-SNAPSHOT pom https://projects.spring.io/spring-batch diff --git a/spring-batch-bom/pom.xml b/spring-batch-bom/pom.xml index d5a013c25d..a9680681b6 100644 --- a/spring-batch-bom/pom.xml +++ b/spring-batch-bom/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-bom pom diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index 447807636d..5a4187bb5d 100644 --- a/spring-batch-core/pom.xml +++ b/spring-batch-core/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-core jar diff --git a/spring-batch-docs/pom.xml b/spring-batch-docs/pom.xml index ccaeb1ae84..d31d9dd8cd 100644 --- a/spring-batch-docs/pom.xml +++ b/spring-batch-docs/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-docs Spring Batch Docs diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index fc411fe428..dd208f8379 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-infrastructure jar diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml index 05649454c0..a57e5f58c5 100644 --- a/spring-batch-integration/pom.xml +++ b/spring-batch-integration/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-integration Spring Batch Integration diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml index 3fefe28a8c..9bf671940f 100644 --- a/spring-batch-samples/pom.xml +++ b/spring-batch-samples/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-samples jar diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml index cf8b468ccd..ab9e8b20ba 100644 --- a/spring-batch-test/pom.xml +++ b/spring-batch-test/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 5.2.1 + 5.2.2-SNAPSHOT spring-batch-test Spring Batch Test From aa8fb75f1c2a961414172341439d5736e74ea73f Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Thu, 19 Dec 2024 05:59:30 +0100 Subject: [PATCH 02/37] Update latest news in README.md Signed-off-by: Mahmoud Ben Hassine --- README.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 7110ad6524..5cc775db30 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ # Latest news +* 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) -* October 25, 2024: [Spring Batch 5.2.0-RC1 is out!](https://spring.io/blog/2024/10/25/spring-batch-5-2-0-rc1-is-out) -* October 11, 2024: [Spring Batch 5.2.0-M2 is available now!](https://spring.io/blog/2024/10/11/spring-batch-5-2-0-m2-is-available-now) -* September 18, 2024: [Spring Batch 5.2.0-M1 is out!](https://spring.io/blog/2024/09/18/spring-batch-5-2-0-m1-is-out) +* November 20, 2024: [Spring Batch 5.2.0 goes GA!](https://spring.io/blog/2024/11/20/spring-batch-5-2-0-goes-ga) From 90e7e9fefacfe8cae1ee188e114d402ef7e7e3f1 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 7 Jan 2025 12:07:32 +0100 Subject: [PATCH 03/37] Add sample job based on PetClinic application Signed-off-by: Mahmoud Ben Hassine --- spring-batch-samples/README.md | 11 +++ .../batch/samples/petclinic/Owner.java | 19 +++++ .../OwnersExportJobConfiguration.java | 71 ++++++++++++++++++ .../batch/samples/petclinic/README.md | 21 ++++++ .../samples/common/business-schema-hsqldb.sql | 23 ++++++ .../samples/petclinic/job/ownersExportJob.xml | 42 +++++++++++ .../PetClinicJobFunctionalTests.java | 73 +++++++++++++++++++ 7 files changed, 260 insertions(+) create mode 100644 spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java create mode 100644 spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java create mode 100644 spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md create mode 100644 spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml create mode 100644 spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java diff --git a/spring-batch-samples/README.md b/spring-batch-samples/README.md index eb26857fc5..770c7fd938 100644 --- a/spring-batch-samples/README.md +++ b/spring-batch-samples/README.md @@ -61,6 +61,7 @@ The IO Sample Job has a number of special instances that show different IO featu | [multiResource Sample](#multiresource-input-output-job) | x | | | | | | | x | | x | | x | | [XML Input Output Sample](#xml-input-output) | | | x | | | | | | | | | | | [MongoDB sample](#mongodb-sample) | | | | | x | | | | x | | | | +| [PetClinic sample](#petclinic-sample) | | | | | x | x | | | | | | | ### Common Sample Source Structures @@ -615,6 +616,16 @@ $>docker run --name mongodb --rm -d -p 27017:27017 mongo Once MongoDB is up and running, run the `org.springframework.batch.samples.mongodb.MongoDBSampleApp` class without any argument to start the sample. +### PetClinic sample + +This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use +Spring Batch to export data from a relational database table to a flat file. + +The job in this sample is a single-step job that exports data from the `owners` table +to a flat file named `owners.csv`. + +[PetClinic Sample](src/main/java/org/springframework/batch/samples/petclinic/README.md) + ### Adhoc Loop and JMX Sample This job is simply an infinite loop. It runs forever so it is diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java new file mode 100644 index 0000000000..7a66d7d296 --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/Owner.java @@ -0,0 +1,19 @@ +/* + * 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.samples.petclinic; + +public record Owner(int id, String firstname, String lastname, String address, String city, String telephone) { +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java new file mode 100644 index 0000000000..4a27ffb23f --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/OwnersExportJobConfiguration.java @@ -0,0 +1,71 @@ +/* + * 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.samples.petclinic; + +import javax.sql.DataSource; + +import org.springframework.batch.core.Job; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.database.JdbcCursorItemReader; +import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; +import org.springframework.batch.samples.common.DataSourceConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.io.FileSystemResource; +import org.springframework.jdbc.core.DataClassRowMapper; +import org.springframework.jdbc.support.JdbcTransactionManager; + +@Configuration +@EnableBatchProcessing +@Import(DataSourceConfiguration.class) +public class OwnersExportJobConfiguration { + + @Bean + public JdbcCursorItemReader ownersReader(DataSource dataSource) { + return new JdbcCursorItemReaderBuilder().name("ownersReader") + .sql("SELECT * FROM OWNERS") + .dataSource(dataSource) + .rowMapper(new DataClassRowMapper<>(Owner.class)) + .build(); + } + + @Bean + public FlatFileItemWriter ownersWriter() { + return new FlatFileItemWriterBuilder().name("ownersWriter") + .resource(new FileSystemResource("owners.csv")) + .delimited() + .names("id", "firstname", "lastname", "address", "city", "telephone") + .build(); + } + + @Bean + public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, + JdbcCursorItemReader ownersReader, FlatFileItemWriter ownersWriter) { + return new JobBuilder("ownersExportJob", jobRepository) + .start(new StepBuilder("ownersExportStep", jobRepository).chunk(5, transactionManager) + .reader(ownersReader) + .writer(ownersWriter) + .build()) + .build(); + } + +} diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md new file mode 100644 index 0000000000..12be08e09b --- /dev/null +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/petclinic/README.md @@ -0,0 +1,21 @@ +# PetClinic Job + +## About the sample + +This sample uses the [PetClinic Spring application](https://github.com/spring-projects/spring-petclinic) to show how to use +Spring Batch to export data from a relational database table to a flat file. + +The job in this sample is a single-step job that exports data from the `owners` table +to a flat file named `owners.csv`. + +## Run the sample + +You can run the sample from the command line as following: + +``` +$>cd spring-batch-samples +# Launch the sample using the XML configuration +$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithXmlConfiguration test +# Launch the sample using the Java configuration +$>../mvnw -Dtest=PetClinicJobFunctionalTests#testLaunchJobWithJavaConfiguration test +``` \ No newline at end of file diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql index b02b0b89a5..ffd6823049 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql @@ -100,3 +100,26 @@ CREATE TABLE ERROR_LOG ( STEP_NAME CHAR(20) , MESSAGE VARCHAR(300) NOT NULL ) ; + +-- PetClinic sample tables + +CREATE TABLE OWNERS ( + ID INTEGER IDENTITY PRIMARY KEY, + FIRSTNAME VARCHAR(30), + LASTNAME VARCHAR(30), + ADDRESS VARCHAR(255), + CITY VARCHAR(80), + TELEPHONE VARCHAR(20) +); + +INSERT INTO OWNERS VALUES (1, 'George', 'Franklin', '110 W. Liberty St.', 'Madison', '6085551023'); +INSERT INTO OWNERS VALUES (2, 'Betty', 'Davis', '638 Cardinal Ave.', 'Sun Prairie', '6085551749'); +INSERT INTO OWNERS VALUES (3, 'Eduardo', 'Rodriquez', '2693 Commerce St.', 'McFarland', '6085558763'); +INSERT INTO OWNERS VALUES (4, 'Harold', 'Davis', '563 Friendly St.', 'Windsor', '6085553198'); +INSERT INTO OWNERS VALUES (5, 'Peter', 'McTavish', '2387 S. Fair Way', 'Madison', '6085552765'); +INSERT INTO OWNERS VALUES (6, 'Jean', 'Coleman', '105 N. Lake St.', 'Monona', '6085552654'); +INSERT INTO OWNERS VALUES (7, 'Jeff', 'Black', '1450 Oak Blvd.', 'Monona', '6085555387'); +INSERT INTO OWNERS VALUES (8, 'Maria', 'Escobito', '345 Maple St.', 'Madison', '6085557683'); +INSERT INTO OWNERS VALUES (9, 'David', 'Schroeder', '2749 Blackhawk Trail', 'Madison', '6085559435'); +INSERT INTO OWNERS VALUES (10, 'Carlos', 'Estaban', '2335 Independence La.', 'Waunakee', '6085555487'); + diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml new file mode 100644 index 0000000000..0247f5511f --- /dev/null +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java new file mode 100644 index 0000000000..5f790cba9f --- /dev/null +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java @@ -0,0 +1,73 @@ +/* + * 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.samples.petclinic; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobExecution; +import org.springframework.batch.core.JobParameters; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SpringJUnitConfig(locations = { "/simple-job-launcher-context.xml", + "/org/springframework/batch/samples/petclinic/job/ownersExportJob.xml" }) +class PetClinicJobFunctionalTests { + + @Autowired + private JobLauncherTestUtils jobLauncherTestUtils; + + @BeforeEach + public void setup() throws IOException { + Files.deleteIfExists(Paths.get("owners.csv")); + } + + @Test + void testLaunchJobWithXmlConfiguration() throws Exception { + // when + JobExecution jobExecution = jobLauncherTestUtils.launchJob(); + + // then + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + } + + @Test + void testLaunchJobWithJavaConfiguration() throws Exception { + // given + ApplicationContext context = new AnnotationConfigApplicationContext(OwnersExportJobConfiguration.class); + JobLauncher jobLauncher = context.getBean(JobLauncher.class); + Job job = context.getBean(Job.class); + + // when + JobExecution jobExecution = jobLauncher.run(job, new JobParameters()); + + // then + assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + } + +} From e706957eb7fd2687682d2850d4685b9de96f844f Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 7 Jan 2025 13:20:22 +0100 Subject: [PATCH 04/37] Fix tests Signed-off-by: Mahmoud Ben Hassine --- .../batch/samples/common/business-schema-hsqldb.sql | 1 + .../batch/samples/petclinic/PetClinicJobFunctionalTests.java | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql index ffd6823049..52a8c890f0 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/common/business-schema-hsqldb.sql @@ -9,6 +9,7 @@ DROP TABLE PLAYERS IF EXISTS; DROP TABLE GAMES IF EXISTS; DROP TABLE PLAYER_SUMMARY IF EXISTS; DROP TABLE ERROR_LOG IF EXISTS; +DROP TABLE OWNERS IF EXISTS; -- Autogenerated: do not edit this file diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java index 5f790cba9f..dc8bfce26b 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/petclinic/PetClinicJobFunctionalTests.java @@ -19,6 +19,7 @@ import java.nio.file.Files; import java.nio.file.Paths; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,7 +44,8 @@ class PetClinicJobFunctionalTests { private JobLauncherTestUtils jobLauncherTestUtils; @BeforeEach - public void setup() throws IOException { + @AfterEach + public void deleteOwnersFile() throws IOException { Files.deleteIfExists(Paths.get("owners.csv")); } From 60f490556dcae73f35fe4ba7a3a12ddbe0c69ddb Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Fri, 24 Jan 2025 17:24:43 +0100 Subject: [PATCH 05/37] Add dco.yml Signed-off-by: Mahmoud Ben Hassine --- .github/dco.yml | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 .github/dco.yml diff --git a/.github/dco.yml b/.github/dco.yml new file mode 100644 index 0000000000..0c4b142e9a --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,2 @@ +require: + members: false From 8e91adb9ab95d66a1dc127f0b52134603517b589 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Fri, 24 Jan 2025 17:31:48 +0100 Subject: [PATCH 06/37] Update CONTRIBUTING.md to include details about the DCO Signed-off-by: Mahmoud Ben Hassine --- CONTRIBUTING.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 779b711d58..c6ad7d3a70 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,9 +26,11 @@ about how to report issues. Not sure what a *pull request* is, or how to submit one? Take a look at the excellent [GitHub help documentation][] first. Please create a new issue *before* submitting a pull request unless the change is truly trivial, e.g. typo fixes, removing compiler warnings, etc. -### Sign the contributor license agreement +### Sign-off commits according to the Developer Certificate of Origin -If you have not previously done so, please fill out and submit the [Contributor License Agreement](https://cla.pivotal.io/sign/spring). +All commits must include a Signed-off-by trailer at the end of each commit message to indicate that the contributor agrees to the [Developer Certificate of Origin](https://developercertificate.org). + +For additional details, please refer to the blog post [Hello DCO, Goodbye CLA: Simplifying Contributions to Spring](https://spring.io/blog/2025/01/06/hello-dco-goodbye-cla-simplifying-contributions-to-spring). ### Fork the Repository From f888ebb43f70d925c028721db0b3d71306089038 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Fri, 31 Jan 2025 12:39:36 +0100 Subject: [PATCH 07/37] Add test cases to cover key generation for empty identifying job parameters set Related to #4755 --- .../core/DefaultJobKeyGeneratorTests.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java index 74e280f1ef..f5e9983011 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/DefaultJobKeyGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2022 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. @@ -65,4 +65,22 @@ void testCreateJobKeyOrdering() { assertEquals(key1, key2); } + @Test + public void testCreateJobKeyForEmptyParameters() { + JobParameters jobParameters1 = new JobParameters(); + JobParameters jobParameters2 = new JobParameters(); + String key1 = jobKeyGenerator.generateKey(jobParameters1); + String key2 = jobKeyGenerator.generateKey(jobParameters2); + assertEquals(key1, key2); + } + + @Test + public void testCreateJobKeyForEmptyParametersAndNonIdentifying() { + JobParameters jobParameters1 = new JobParameters(); + JobParameters jobParameters2 = new JobParametersBuilder().addString("name", "foo", false).toJobParameters(); + String key1 = jobKeyGenerator.generateKey(jobParameters1); + String key2 = jobKeyGenerator.generateKey(jobParameters2); + assertEquals(key1, key2); + } + } From 3b0868db5d76927e2a09a57f157b8f9e90f162ea Mon Sep 17 00:00:00 2001 From: kimjg Date: Sun, 9 Feb 2025 17:08:42 +0900 Subject: [PATCH 08/37] Extract bean name string literals to constants - Extract hardcoded bean name strings to static final constants - Improve code maintainability and reduce the risk of typos - No functional changes Signed-off-by: kimjg --- .../annotation/BatchRegistrar.java | 46 +++++++++++-------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java index d261384ef0..3d23f6bcf7 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2024 the original author or authors. + * Copyright 2022-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. @@ -51,6 +51,16 @@ class BatchRegistrar implements ImportBeanDefinitionRegistrar { private static final String MISSING_ANNOTATION_ERROR_MESSAGE = "EnableBatchProcessing is not present on importing class '%s' as expected"; + private static final String JOB_REPOSITORY = "jobRepository"; + + private static final String JOB_EXPLORER = "jobExplorer"; + + private static final String JOB_LAUNCHER = "jobLauncher"; + + private static final String JOB_REGISTRY = "jobRegistry"; + + private static final String JOB_LOADER = "jobLoader"; + @Override public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { StopWatch watch = new StopWatch(); @@ -80,7 +90,7 @@ private void validateState(AnnotationMetadata importingClassMetadata) { } private void registerJobRepository(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) { - if (registry.containsBeanDefinition("jobRepository")) { + if (registry.containsBeanDefinition(JOB_REPOSITORY)) { LOGGER.info("Bean jobRepository already defined in the application context, skipping" + " the registration of a jobRepository"); return; @@ -143,11 +153,11 @@ private void registerJobRepository(BeanDefinitionRegistry registry, EnableBatchP beanDefinitionBuilder.addPropertyValue("maxVarCharLength", batchAnnotation.maxVarCharLength()); beanDefinitionBuilder.addPropertyValue("clobType", batchAnnotation.clobType()); - registry.registerBeanDefinition("jobRepository", beanDefinitionBuilder.getBeanDefinition()); + registry.registerBeanDefinition(JOB_REPOSITORY, beanDefinitionBuilder.getBeanDefinition()); } private void registerJobExplorer(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) { - if (registry.containsBeanDefinition("jobExplorer")) { + if (registry.containsBeanDefinition(JOB_EXPLORER)) { LOGGER.info("Bean jobExplorer already defined in the application context, skipping" + " the registration of a jobExplorer"); return; @@ -192,11 +202,11 @@ private void registerJobExplorer(BeanDefinitionRegistry registry, EnableBatchPro if (tablePrefix != null) { beanDefinitionBuilder.addPropertyValue("tablePrefix", tablePrefix); } - registry.registerBeanDefinition("jobExplorer", beanDefinitionBuilder.getBeanDefinition()); + registry.registerBeanDefinition(JOB_EXPLORER, beanDefinitionBuilder.getBeanDefinition()); } private void registerJobLauncher(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) { - if (registry.containsBeanDefinition("jobLauncher")) { + if (registry.containsBeanDefinition(JOB_LAUNCHER)) { LOGGER.info("Bean jobLauncher already defined in the application context, skipping" + " the registration of a jobLauncher"); return; @@ -204,25 +214,25 @@ private void registerJobLauncher(BeanDefinitionRegistry registry, EnableBatchPro BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(TaskExecutorJobLauncher.class); // set mandatory properties - beanDefinitionBuilder.addPropertyReference("jobRepository", "jobRepository"); + beanDefinitionBuilder.addPropertyReference(JOB_REPOSITORY, JOB_REPOSITORY); // set optional properties String taskExecutorRef = batchAnnotation.taskExecutorRef(); if (registry.containsBeanDefinition(taskExecutorRef)) { beanDefinitionBuilder.addPropertyReference("taskExecutor", taskExecutorRef); } - registry.registerBeanDefinition("jobLauncher", beanDefinitionBuilder.getBeanDefinition()); + registry.registerBeanDefinition(JOB_LAUNCHER, beanDefinitionBuilder.getBeanDefinition()); } private void registerJobRegistry(BeanDefinitionRegistry registry) { - if (registry.containsBeanDefinition("jobRegistry")) { + if (registry.containsBeanDefinition(JOB_REGISTRY)) { LOGGER.info("Bean jobRegistry already defined in the application context, skipping" + " the registration of a jobRegistry"); return; } BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MapJobRegistry.class) .getBeanDefinition(); - registry.registerBeanDefinition("jobRegistry", beanDefinition); + registry.registerBeanDefinition(JOB_REGISTRY, beanDefinition); } private void registerJobRegistrySmartInitializingSingleton(BeanDefinitionRegistry registry) { @@ -234,7 +244,7 @@ private void registerJobRegistrySmartInitializingSingleton(BeanDefinitionRegistr } BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(JobRegistrySmartInitializingSingleton.class); - beanDefinitionBuilder.addPropertyReference("jobRegistry", "jobRegistry"); + beanDefinitionBuilder.addPropertyReference(JOB_REGISTRY, JOB_REGISTRY); registry.registerBeanDefinition("jobRegistrySmartInitializingSingleton", beanDefinitionBuilder.getBeanDefinition()); @@ -252,10 +262,10 @@ private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchPro String transactionManagerRef = batchAnnotation.transactionManagerRef(); beanDefinitionBuilder.addPropertyReference("transactionManager", transactionManagerRef); - beanDefinitionBuilder.addPropertyReference("jobRepository", "jobRepository"); - beanDefinitionBuilder.addPropertyReference("jobLauncher", "jobLauncher"); - beanDefinitionBuilder.addPropertyReference("jobExplorer", "jobExplorer"); - beanDefinitionBuilder.addPropertyReference("jobRegistry", "jobRegistry"); + beanDefinitionBuilder.addPropertyReference(JOB_REPOSITORY, JOB_REPOSITORY); + beanDefinitionBuilder.addPropertyReference(JOB_LAUNCHER, JOB_LAUNCHER); + beanDefinitionBuilder.addPropertyReference(JOB_EXPLORER, JOB_EXPLORER); + beanDefinitionBuilder.addPropertyReference(JOB_REGISTRY, JOB_REGISTRY); // set optional properties String jobParametersConverterRef = batchAnnotation.jobParametersConverterRef(); @@ -276,12 +286,12 @@ private void registerAutomaticJobRegistrar(BeanDefinitionRegistry registry, Enab return; } BeanDefinition jobLoaderBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(DefaultJobLoader.class) - .addPropertyReference("jobRegistry", "jobRegistry") + .addPropertyReference(JOB_REGISTRY, JOB_REGISTRY) .getBeanDefinition(); - registry.registerBeanDefinition("jobLoader", jobLoaderBeanDefinition); + registry.registerBeanDefinition(JOB_LOADER, jobLoaderBeanDefinition); BeanDefinition jobRegistrarBeanDefinition = BeanDefinitionBuilder .genericBeanDefinition(AutomaticJobRegistrar.class) - .addPropertyReference("jobLoader", "jobLoader") + .addPropertyReference(JOB_LOADER, JOB_LOADER) .getBeanDefinition(); registry.registerBeanDefinition("jobRegistrar", jobRegistrarBeanDefinition); } From f758d14ee6b704a5a59e6f9ab8cfe1f4d62a05d0 Mon Sep 17 00:00:00 2001 From: Henning Poettker Date: Tue, 7 Jan 2025 22:16:43 +0100 Subject: [PATCH 09/37] Refactor `MultiResourceItemWriter` The `MultiResourceItemWriter` now writes at most `itemCountLimitPerResource` items per resource where it previously allowed more items when they were written within the same chunk. Resolves #1722 --- .../item/file/MultiResourceItemWriter.java | 46 ++++--- .../MultiResourceItemWriterFlatFileTests.java | 129 +++++++++--------- .../file/MultiResourceItemWriterXmlTests.java | 19 ++- .../MultiResourceItemWriterBuilderTests.java | 93 ++++++------- 4 files changed, 150 insertions(+), 137 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java index 835abb3527..d07cbda99d 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/MultiResourceItemWriter.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. @@ -34,9 +34,6 @@ * {@link #setItemCountLimitPerResource(int)}. Suffix creation can be customized with * {@link #setResourceSuffixCreator(ResourceSuffixCreator)}. *

- * Note that new resources are created only at chunk boundaries i.e. the number of items - * written into one resource is between the limit set by - *

* This writer will create an output file only when there are items to write, which means * there would be no empty file created if no items are passed (for example when all items * are filtered or skipped during the processing phase). @@ -45,6 +42,7 @@ * @param item type * @author Robert Kasanicky * @author Mahmoud Ben Hassine + * @author Henning Pöttker */ public class MultiResourceItemWriter extends AbstractItemStreamItemWriter { @@ -74,22 +72,30 @@ public MultiResourceItemWriter() { @Override public void write(Chunk items) throws Exception { - if (!opened) { - File file = setResourceToDelegate(); - // create only if write is called - file.createNewFile(); - Assert.state(file.canWrite(), "Output resource " + file.getAbsolutePath() + " must be writable"); - delegate.open(new ExecutionContext()); - opened = true; - } - delegate.write(items); - currentResourceItemCount += items.size(); - if (currentResourceItemCount >= itemCountLimitPerResource) { - delegate.close(); - resourceIndex++; - currentResourceItemCount = 0; - setResourceToDelegate(); - opened = false; + int writtenItems = 0; + while (writtenItems < items.size()) { + if (!opened) { + File file = setResourceToDelegate(); + // create only if write is called + file.createNewFile(); + Assert.state(file.canWrite(), "Output resource " + file.getAbsolutePath() + " must be writable"); + delegate.open(new ExecutionContext()); + opened = true; + } + + int itemsToWrite = Math.min(itemCountLimitPerResource - currentResourceItemCount, + items.size() - writtenItems); + delegate.write(new Chunk(items.getItems().subList(writtenItems, writtenItems + itemsToWrite))); + currentResourceItemCount += itemsToWrite; + writtenItems += itemsToWrite; + + if (currentResourceItemCount >= itemCountLimitPerResource) { + delegate.close(); + resourceIndex++; + currentResourceItemCount = 0; + setResourceToDelegate(); + opened = false; + } } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java index ffe91317e9..ab23affa63 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterFlatFileTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2023 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. @@ -78,22 +78,22 @@ void testBasicMultiResourceWriteScenario() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34"); tested.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + + assertFileExistsAndContains(3, "5"); tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -107,7 +107,7 @@ void testUpdateAfterDelegateClose() throws Exception { assertEquals(1, executionContext.getInt(tested.getExecutionContextKey("resource.index"))); tested.write(Chunk.of("1", "2", "3")); tested.update(executionContext); - assertEquals(0, executionContext.getInt(tested.getExecutionContextKey("resource.item.count"))); + assertEquals(1, executionContext.getInt(tested.getExecutionContextKey("resource.item.count"))); assertEquals(2, executionContext.getInt(tested.getExecutionContextKey("resource.index"))); } @@ -121,17 +121,22 @@ void testMultiResourceWriteScenarioWithFooter() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3"); tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); + + assertFileExistsAndContains(2, "34f"); + + tested.write(Chunk.of("5")); + + assertFileExistsAndContains(3, "5"); tested.close(); - assertEquals("123f", readFile(part1)); - assertEquals("4f", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "34f"); + assertFileExistsAndContains(3, "5f"); } @@ -144,19 +149,18 @@ void testTransactionalMultiResourceWriteScenarioWithFooter() throws Exception { ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2", "3"))); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2"))); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); + assertFileExistsAndContains(1, "12f"); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("3"))); + + assertFileExistsAndContains(2, "3"); tested.close(); - assertEquals("123f", readFile(part1)); - assertEquals("4f", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3f"); } @@ -168,27 +172,23 @@ void testRestart() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - tested.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + tested.write(Chunk.of("4")); - tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + assertFileExistsAndContains(2, "34"); + + tested.write(Chunk.of("5", "6", "7", "8", "9")); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -201,27 +201,24 @@ void testRestartWithFooter() throws Exception { tested.write(Chunk.of("1", "2", "3")); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123f", readFile(part1)); - - tested.write(Chunk.of("4")); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12f"); + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - tested.write(Chunk.of("5")); - assertEquals("45f", readFile(part2)); + tested.write(Chunk.of("4")); - tested.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789f", readFile(part3)); + assertFileExistsAndContains(2, "34f"); + + tested.write(Chunk.of("5", "6", "7", "8", "9")); + tested.close(); + + assertFileExistsAndContains(3, "56f"); + assertFileExistsAndContains(4, "78f"); + assertFileExistsAndContains(5, "9f"); } @Test @@ -233,24 +230,28 @@ void testTransactionalRestartWithFooter() throws Exception { ResourcelessTransactionManager transactionManager = new ResourcelessTransactionManager(); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2", "3"))); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("1", "2"))); - File part1 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123f", readFile(part1)); + assertFileExistsAndContains(1, "12f"); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); - File part2 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("3"))); + + assertFileExistsAndContains(2, "3"); tested.update(executionContext); tested.close(); tested.open(executionContext); - new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("5"))); - assertEquals("45f", readFile(part2)); + new TransactionTemplate(transactionManager).execute(new WriterCallback(Chunk.of("4"))); + + assertFileExistsAndContains(2, "34f"); + } + + private void assertFileExistsAndContains(int index, String expected) throws Exception { + File part = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(index)); + assertTrue(part.exists()); + assertEquals(expected, readFile(part)); } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java index 38760361c6..f6485adb80 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/MultiResourceItemWriterXmlTests.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. @@ -108,21 +108,26 @@ void multiResourceWritingWithRestart() throws Exception { tested.update(executionContext); tested.close(); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part1)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part1)); tested.open(executionContext); tested.write(Chunk.of("5")); - - tested.write(Chunk.of("6", "7", "8", "9")); File part3 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(3)); assertTrue(part3.exists()); + tested.write(Chunk.of("6", "7", "8", "9")); + File part4 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(4)); + assertTrue(part4.exists()); + File part5 = new File(file.getAbsolutePath() + suffixCreator.getSuffix(5)); + assertTrue(part5.exists()); + tested.close(); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part2)); - assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part3)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part3)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part4)); + assertEquals(xmlDocStart + "" + xmlDocEnd, readFile(part5)); } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java index 6eb75b9ed8..ce1ec6b8f4 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/builder/MultiResourceItemWriterBuilderTests.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. @@ -84,22 +84,22 @@ void testBasicMultiResourceWriteScenario() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34"); this.writer.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + + assertFileExistsAndContains(3, "5"); this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); + assertFileExistsAndContains(5, "9"); } @Test @@ -117,14 +117,12 @@ void testBasicDefaultSuffixCreator() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + simpleResourceSuffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); + assertFileExistsAndContains(1, "12", simpleResourceSuffixCreator); + assertFileExistsAndContains(2, "3", simpleResourceSuffixCreator); this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + simpleResourceSuffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + + assertFileExistsAndContains(2, "34", simpleResourceSuffixCreator); } @Test @@ -143,7 +141,7 @@ void testUpdateAfterDelegateClose() throws Exception { assertEquals(1, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.index"))); this.writer.write(Chunk.of("1", "2", "3")); this.writer.update(this.executionContext); - assertEquals(0, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.item.count"))); + assertEquals(1, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.item.count"))); assertEquals(2, this.executionContext.getInt(this.writer.getExecutionContextKey("resource.index"))); } @@ -160,26 +158,21 @@ void testRestart() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.update(this.executionContext); this.writer.close(); this.writer.open(this.executionContext); - this.writer.write(Chunk.of("5")); - assertEquals("45", readFile(part2)); + this.writer.write(Chunk.of("4")); - this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(3)); - assertTrue(part3.exists()); - assertEquals("6789", readFile(part3)); + assertFileExistsAndContains(2, "34"); + + this.writer.write(Chunk.of("5", "6", "7", "8")); + + assertFileExistsAndContains(3, "56"); + assertFileExistsAndContains(4, "78"); } @Test @@ -195,26 +188,23 @@ void testRestartNoSaveState() throws Exception { this.writer.write(Chunk.of("1", "2", "3")); - File part1 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part1.exists()); - assertEquals("123", readFile(part1)); - - this.writer.write(Chunk.of("4")); - File part2 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(2)); - assertTrue(part2.exists()); - assertEquals("4", readFile(part2)); + assertFileExistsAndContains(1, "12"); + assertFileExistsAndContains(2, "3"); this.writer.update(this.executionContext); this.writer.close(); this.writer.open(this.executionContext); - this.writer.write(Chunk.of("5")); - assertEquals("4", readFile(part2)); + this.writer.write(Chunk.of("4")); - this.writer.write(Chunk.of("6", "7", "8", "9")); - File part3 = new File(this.file.getAbsolutePath() + this.suffixCreator.getSuffix(1)); - assertTrue(part3.exists()); - assertEquals("56789", readFile(part3)); + assertFileExistsAndContains(2, "3"); + assertFileExistsAndContains(1, "4"); + + this.writer.write(Chunk.of("5", "6", "7", "8")); + + assertFileExistsAndContains(1, "45"); + assertFileExistsAndContains(2, "67"); + assertFileExistsAndContains(3, "8"); } @Test @@ -265,4 +255,15 @@ private String readFile(File f) throws Exception { return result.toString(); } + private void assertFileExistsAndContains(int index, String expected) throws Exception { + assertFileExistsAndContains(index, expected, this.suffixCreator); + } + + private void assertFileExistsAndContains(int index, String expected, ResourceSuffixCreator suffixCreator) + throws Exception { + File part = new File(this.file.getAbsolutePath() + suffixCreator.getSuffix(index)); + assertTrue(part.exists()); + assertEquals(expected, readFile(part)); + } + } From bd8f9a8d7603fba5fcf5fc03252c13d095cec175 Mon Sep 17 00:00:00 2001 From: Elimelec Burghelea Date: Thu, 23 Jan 2025 01:59:10 +0200 Subject: [PATCH 10/37] Attempt to close all delegate writers even when some fail Issue #4750 Signed-off-by: Elimelec Burghelea --- .../item/support/CompositeItemWriter.java | 20 +++++++++- .../support/CompositeItemWriterTests.java | 39 ++++++++++++++++++- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java index d76d5c33e0..8666112769 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.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. @@ -25,6 +25,7 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; @@ -37,6 +38,7 @@ * @author Robert Kasanicky * @author Dave Syer * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea */ public class CompositeItemWriter implements ItemStreamWriter, InitializingBean { @@ -105,11 +107,25 @@ public void setDelegates(List> delegates) { @Override public void close() throws ItemStreamException { + List exceptions = new ArrayList<>(); + for (ItemWriter writer : delegates) { if (!ignoreItemStream && (writer instanceof ItemStream)) { - ((ItemStream) writer).close(); + try { + ((ItemStream) writer).close(); + } + catch (Exception e) { + exceptions.add(e); + } } } + + if (!exceptions.isEmpty()) { + String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size()); + ItemStreamException holder = new ItemStreamException(message); + exceptions.forEach(holder::addSuppressed); + throw holder; + } } @Override diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java index 8d5d3f7b62..89db324007 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemWriterTests.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. @@ -18,14 +18,18 @@ import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemStreamWriter; import org.springframework.batch.item.ItemWriter; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link CompositeItemWriter} @@ -33,6 +37,7 @@ * @author Robert Kasanicky * @author Will Schipp * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea */ class CompositeItemWriterTests { @@ -94,4 +99,36 @@ private void doTestItemStream(boolean expectOpen) throws Exception { itemWriter.write(data); } + @Test + void testCloseWithMultipleDelegate() { + AbstractFileItemWriter delegate1 = mock(); + AbstractFileItemWriter delegate2 = mock(); + CompositeItemWriter itemWriter = new CompositeItemWriter<>(List.of(delegate1, delegate2)); + + itemWriter.close(); + + verify(delegate1).close(); + verify(delegate2).close(); + } + + @Test + void testCloseWithMultipleDelegatesThatThrow() { + AbstractFileItemWriter delegate1 = mock(); + AbstractFileItemWriter delegate2 = mock(); + CompositeItemWriter itemWriter = new CompositeItemWriter<>(List.of(delegate1, delegate2)); + + doThrow(new ItemStreamException("A failure")).when(delegate1).close(); + + try { + itemWriter.close(); + Assertions.fail("Expected an ItemStreamException"); + } + catch (ItemStreamException ignored) { + + } + + verify(delegate1).close(); + verify(delegate2).close(); + } + } From 98a42288446a9fc97071b13218d1655da0b4f996 Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Sun, 22 Dec 2024 10:41:49 +0100 Subject: [PATCH 11/37] Allow CI on any branch, skip CD on forks Signed-off-by: Stefano Cordio --- .github/workflows/continuous-integration.yml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 37dc5a6925..9171cc9021 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -1,9 +1,6 @@ name: CI/CD build -on: - workflow_dispatch: - push: - branches: [ "main" ] +on: [push, pull_request, workflow_dispatch] jobs: build: @@ -20,7 +17,12 @@ jobs: distribution: 'temurin' cache: 'maven' + - name: Build with Maven + if: ${{ github.repository != 'spring-projects/spring-batch' || github.ref_name != 'main' }} + 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' }} env: ARTIFACTORY_USERNAME: ${{ secrets.ARTIFACTORY_USERNAME }} ARTIFACTORY_PASSWORD: ${{ secrets.ARTIFACTORY_PASSWORD }} @@ -37,6 +39,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' }} env: DOCS_SSH_KEY: ${{ secrets.DOCS_SSH_KEY }} DOCS_SSH_HOST_KEY: ${{ secrets.DOCS_SSH_HOST_KEY }} @@ -47,6 +50,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' }} env: DOCS_HOST: ${{ secrets.DOCS_HOST }} DOCS_PATH: ${{ secrets.DOCS_PATH }} From bbd46fb29d69026a60314ad44c8834462f2dad9c Mon Sep 17 00:00:00 2001 From: Henning Poettker Date: Tue, 7 Jan 2025 19:38:57 +0100 Subject: [PATCH 12/37] Polish integration tests In the Mongo integration tests, the application context and in particular the Mongo client are now closed after each test, respectively. Thread-safety of the fault-tolerance integration steps is improved as they are intermittingly stalling. --- .../MongoDBIntegrationTestConfiguration.java | 7 +- .../MongoDBJobExplorerIntegrationTests.java | 4 +- .../MongoDBJobRepositoryIntegrationTests.java | 4 +- ...goExecutionContextDaoIntegrationTests.java | 4 +- ...lerantStepFactoryBeanIntegrationTests.java | 103 ++++------------- ...epFactoryBeanRollbackIntegrationTests.java | 106 ++++-------------- 6 files changed, 55 insertions(+), 173 deletions(-) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java index 015a90e034..31ea7439dd 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBIntegrationTestConfiguration.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. @@ -15,8 +15,6 @@ */ package org.springframework.batch.core.repository.support; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; import org.springframework.batch.core.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.explore.JobExplorer; @@ -63,8 +61,7 @@ public JobExplorer jobExplorer(MongoTemplate mongoTemplate, MongoTransactionMana @Bean public MongoDatabaseFactory mongoDatabaseFactory(@Value("${mongo.connectionString}") String connectionString) { - MongoClient mongoClient = MongoClients.create(connectionString); - return new SimpleMongoClientDatabaseFactory(mongoClient, "test"); + return new SimpleMongoClientDatabaseFactory(connectionString + "/test"); } @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java index a6ed1c9bb9..f47c731990 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobExplorerIntegrationTests.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. @@ -30,6 +30,7 @@ import org.springframework.batch.core.launch.JobLauncher; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -45,6 +46,7 @@ /** * @author Henning Pöttker */ +@DirtiesContext @Testcontainers(disabledWithoutDocker = true) @SpringJUnitConfig(MongoDBIntegrationTestConfiguration.class) public class MongoDBJobExplorerIntegrationTests { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java index b45aa7bd19..6b70f0b3c7 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.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. @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -43,6 +44,7 @@ /** * @author Mahmoud Ben Hassine */ +@DirtiesContext @Testcontainers(disabledWithoutDocker = true) @SpringJUnitConfig(MongoDBIntegrationTestConfiguration.class) public class MongoDBJobRepositoryIntegrationTests { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java index 7b71ca8505..a04795928f 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoExecutionContextDaoIntegrationTests.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. @@ -36,6 +36,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -51,6 +52,7 @@ /** * @author Henning Pöttker */ +@DirtiesContext @Testcontainers(disabledWithoutDocker = true) @SpringJUnitConfig({ MongoDBIntegrationTestConfiguration.class, ExecutionContextDaoConfiguration.class }) public class MongoExecutionContextDaoIntegrationTests { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java index 67d24fcefa..2cf59d33c1 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2024 the original author or authors. + * Copyright 2010-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,17 +16,14 @@ package org.springframework.batch.core.test.step; import java.util.ArrayList; -import java.util.Collection; import java.util.Collections; import java.util.List; -import javax.sql.DataSource; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.JobExecution; @@ -39,8 +36,8 @@ import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.ParseException; -import org.springframework.batch.item.UnexpectedInputException; +import org.springframework.batch.item.support.ListItemReader; +import org.springframework.batch.item.support.SynchronizedItemReader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.lang.Nullable; @@ -48,15 +45,14 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.util.Assert; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Timeout.ThreadMode.SEPARATE_THREAD; /** * Tests for {@link FaultTolerantStepFactoryBean}. */ @SpringJUnitConfig(locations = "/simple-job-launcher-context.xml") -@Disabled("Randomly failing/hanging") // FIXME This test is randomly failing/hanging class FaultTolerantStepFactoryBeanIntegrationTests { private static final int MAX_COUNT = 1000; @@ -69,12 +65,8 @@ class FaultTolerantStepFactoryBeanIntegrationTests { private SkipWriterStub writer; - private JobExecution jobExecution; - - private StepExecution stepExecution; - @Autowired - private DataSource dataSource; + private JdbcTemplate jdbcTemplate; @Autowired private JobRepository repository; @@ -85,8 +77,8 @@ class FaultTolerantStepFactoryBeanIntegrationTests { @BeforeEach void setUp() { - writer = new SkipWriterStub(dataSource); - processor = new SkipProcessorStub(dataSource); + writer = new SkipWriterStub(jdbcTemplate); + processor = new SkipProcessorStub(jdbcTemplate); factory = new FaultTolerantStepFactoryBean<>(); @@ -101,14 +93,12 @@ void setUp() { taskExecutor.afterPropertiesSet(); factory.setTaskExecutor(taskExecutor); - JdbcTestUtils.deleteFromTables(new JdbcTemplate(dataSource), "ERROR_LOG"); + JdbcTestUtils.deleteFromTables(jdbcTemplate, "ERROR_LOG"); } @Test - void testUpdatesNoRollback() throws Exception { - - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + void testUpdatesNoRollback() { writer.write(Chunk.of("foo", "bar")); processor.process("spam"); @@ -121,17 +111,15 @@ void testUpdatesNoRollback() throws Exception { } @Test + @Timeout(value = 30, threadMode = SEPARATE_THREAD) void testMultithreadedSunnyDay() throws Throwable { - jobExecution = repository.createJobExecution("vanillaJob", new JobParameters()); + JobExecution jobExecution = repository.createJobExecution("vanillaJob", new JobParameters()); for (int i = 0; i < MAX_COUNT; i++) { - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); - - SkipReaderStub reader = new SkipReaderStub(); - reader.clear(); - reader.setItems("1", "2", "3", "4", "5"); + ItemReader reader = new SynchronizedItemReader<>( + new ListItemReader<>(List.of("1", "2", "3", "4", "5"))); factory.setItemReader(reader); writer.clear(); factory.setItemWriter(writer); @@ -144,7 +132,7 @@ void testMultithreadedSunnyDay() throws Throwable { Step step = factory.getObject(); - stepExecution = jobExecution.createStepExecution(factory.getName()); + StepExecution stepExecution = jobExecution.createStepExecution(factory.getName()); repository.add(stepExecution); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); @@ -167,48 +155,12 @@ void testMultithreadedSunnyDay() throws Throwable { } - private static class SkipReaderStub implements ItemReader { - - private String[] items; - - private int counter = -1; - - public SkipReaderStub() throws Exception { - super(); - } - - public void setItems(String... items) { - Assert.isTrue(counter < 0, "Items cannot be set once reading has started"); - this.items = items; - } - - public void clear() { - counter = -1; - } - - @Nullable - @Override - public synchronized String read() throws Exception, UnexpectedInputException, ParseException { - counter++; - if (counter >= items.length) { - return null; - } - String item = items[counter]; - return item; - } - - } - private static class SkipWriterStub implements ItemWriter { - private final List written = new ArrayList<>(); - - private final Collection failures = Collections.emptySet(); - private final JdbcTemplate jdbcTemplate; - public SkipWriterStub(DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); + public SkipWriterStub(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; } public List getCommitted() { @@ -217,22 +169,13 @@ public List getCommitted() { } public void clear() { - written.clear(); JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "ERROR_LOG", "STEP_NAME='written'"); } @Override - public void write(Chunk items) throws Exception { + public void write(Chunk items) { for (String item : items) { - written.add(item); jdbcTemplate.update("INSERT INTO ERROR_LOG (MESSAGE, STEP_NAME) VALUES (?, ?)", item, "written"); - checkFailure(item); - } - } - - private void checkFailure(String item) { - if (failures.contains(item)) { - throw new RuntimeException("Planned failure"); } } @@ -242,12 +185,10 @@ private static class SkipProcessorStub implements ItemProcessor private final Log logger = LogFactory.getLog(getClass()); - private final List processed = new ArrayList<>(); - private final JdbcTemplate jdbcTemplate; - public SkipProcessorStub(DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); + public SkipProcessorStub(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; } public List getCommitted() { @@ -256,14 +197,12 @@ public List getCommitted() { } public void clear() { - processed.clear(); JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "ERROR_LOG", "STEP_NAME='processed'"); } @Nullable @Override - public String process(String item) throws Exception { - processed.add(item); + public String process(String item) { logger.debug("Processed item: " + item); jdbcTemplate.update("INSERT INTO ERROR_LOG (MESSAGE, STEP_NAME) VALUES (?, ?)", item, "processed"); return item; diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java index 6eb416fa06..eeaeba2365 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2010-2023 the original author or authors. + * Copyright 2010-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. @@ -19,16 +19,15 @@ import java.util.Arrays; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import javax.sql.DataSource; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.JobExecution; @@ -41,8 +40,8 @@ import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.ParseException; -import org.springframework.batch.item.UnexpectedInputException; +import org.springframework.batch.item.support.ListItemReader; +import org.springframework.batch.item.support.SynchronizedItemReader; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.lang.Nullable; @@ -50,9 +49,9 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.util.Assert; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Timeout.ThreadMode.SEPARATE_THREAD; /** * Tests for {@link FaultTolerantStepFactoryBean}. @@ -70,12 +69,8 @@ class FaultTolerantStepFactoryBeanRollbackIntegrationTests { private SkipWriterStub writer; - private JobExecution jobExecution; - - private StepExecution stepExecution; - @Autowired - private DataSource dataSource; + private JdbcTemplate jdbcTemplate; @Autowired private JobRepository repository; @@ -86,8 +81,8 @@ class FaultTolerantStepFactoryBeanRollbackIntegrationTests { @BeforeEach void setUp() { - writer = new SkipWriterStub(dataSource); - processor = new SkipProcessorStub(dataSource); + writer = new SkipWriterStub(jdbcTemplate, "1", "2", "3", "4", "5"); + processor = new SkipProcessorStub(jdbcTemplate); factory = new FaultTolerantStepFactoryBean<>(); @@ -97,14 +92,12 @@ void setUp() { factory.setCommitInterval(3); factory.setSkipLimit(10); - JdbcTestUtils.deleteFromTables(new JdbcTemplate(dataSource), "ERROR_LOG"); + JdbcTestUtils.deleteFromTables(jdbcTemplate, "ERROR_LOG"); } @Test - void testUpdatesNoRollback() throws Exception { - - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); + void testUpdatesNoRollback() { writer.write(Chunk.of("foo", "bar")); processor.process("spam"); @@ -117,6 +110,7 @@ void testUpdatesNoRollback() throws Exception { } @Test + @Timeout(value = 30, threadMode = SEPARATE_THREAD) void testMultithreadedSkipInWriter() throws Throwable { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); @@ -126,11 +120,9 @@ void testMultithreadedSkipInWriter() throws Throwable { taskExecutor.afterPropertiesSet(); factory.setTaskExecutor(taskExecutor); - @SuppressWarnings("unchecked") - Map, Boolean> skippable = getExceptionMap(Exception.class); - factory.setSkippableExceptionClasses(skippable); + factory.setSkippableExceptionClasses(Map.of(Exception.class, true)); - jobExecution = repository.createJobExecution("skipJob", new JobParameters()); + JobExecution jobExecution = repository.createJobExecution("skipJob", new JobParameters()); for (int i = 0; i < MAX_COUNT; i++) { @@ -138,25 +130,21 @@ void testMultithreadedSkipInWriter() throws Throwable { logger.info("Starting step: " + i); } - JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource); assertEquals(0, JdbcTestUtils.countRowsInTable(jdbcTemplate, "ERROR_LOG")); try { - SkipReaderStub reader = new SkipReaderStub(); - reader.clear(); - reader.setItems("1", "2", "3", "4", "5"); + ItemReader reader = new SynchronizedItemReader<>( + new ListItemReader<>(List.of("1", "2", "3", "4", "5"))); factory.setItemReader(reader); writer.clear(); factory.setItemWriter(writer); processor.clear(); factory.setItemProcessor(processor); - writer.setFailures("1", "2", "3", "4", "5"); - Step step = factory.getObject(); - stepExecution = jobExecution.createStepExecution(factory.getName()); + StepExecution stepExecution = jobExecution.createStepExecution(factory.getName()); repository.add(stepExecution); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); @@ -178,61 +166,15 @@ void testMultithreadedSkipInWriter() throws Throwable { } - @SuppressWarnings("unchecked") - private Map, Boolean> getExceptionMap(Class... args) { - Map, Boolean> map = new HashMap<>(); - for (Class arg : args) { - map.put(arg, true); - } - return map; - } - - private static class SkipReaderStub implements ItemReader { - - private String[] items; - - private int counter = -1; - - public SkipReaderStub() throws Exception { - super(); - } - - public void setItems(String... items) { - Assert.isTrue(counter < 0, "Items cannot be set once reading has started"); - this.items = items; - } - - public void clear() { - counter = -1; - } - - @Nullable - @Override - public synchronized String read() throws Exception, UnexpectedInputException, ParseException { - counter++; - if (counter >= items.length) { - return null; - } - String item = items[counter]; - return item; - } - - } - private static class SkipWriterStub implements ItemWriter { - private final List written = new CopyOnWriteArrayList<>(); - - private Collection failures = Collections.emptySet(); + private final Collection failures; private final JdbcTemplate jdbcTemplate; - public SkipWriterStub(DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); - } - - public void setFailures(String... failures) { + public SkipWriterStub(JdbcTemplate jdbcTemplate, String... failures) { this.failures = Arrays.asList(failures); + this.jdbcTemplate = jdbcTemplate; } public List getCommitted() { @@ -241,14 +183,12 @@ public List getCommitted() { } public void clear() { - written.clear(); JdbcTestUtils.deleteFromTableWhere(jdbcTemplate, "ERROR_LOG", "STEP_NAME='written'"); } @Override - public void write(Chunk items) throws Exception { + public void write(Chunk items) { for (String item : items) { - written.add(item); jdbcTemplate.update("INSERT INTO ERROR_LOG (MESSAGE, STEP_NAME) VALUES (?, ?)", item, "written"); checkFailure(item); } @@ -270,8 +210,8 @@ private static class SkipProcessorStub implements ItemProcessor private final JdbcTemplate jdbcTemplate; - public SkipProcessorStub(DataSource dataSource) { - jdbcTemplate = new JdbcTemplate(dataSource); + public SkipProcessorStub(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; } /** @@ -293,7 +233,7 @@ public void clear() { @Nullable @Override - public String process(String item) throws Exception { + public String process(String item) { processed.add(item); logger.debug("Processed item: " + item); jdbcTemplate.update("INSERT INTO ERROR_LOG (MESSAGE, STEP_NAME) VALUES (?, ?)", item, "processed"); From b4835ef52ea5b628c2c8e83e91e66306ee3abff5 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Mon, 24 Feb 2025 08:39:36 +0100 Subject: [PATCH 13/37] Upgrade the job execution status when appropriate in MongoJobExecutionDao Before this commit, the mongo implementation of job execution DAO did not upgrade the status of the job execution when synchronizing the state with the database. This commit fixes the issue by upgrading the status when appropriate, similar to the jdbc implementation. Resolves #4760 --- .../core/repository/dao/MongoJobExecutionDao.java | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java index 90d3326a9a..da1d81ff78 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/dao/MongoJobExecutionDao.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. @@ -26,7 +26,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.mongodb.core.query.Update; import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import static org.springframework.data.mongodb.core.query.Criteria.where; @@ -143,14 +142,13 @@ public JobExecution getJobExecution(Long executionId) { @Override public void synchronizeStatus(JobExecution jobExecution) { - Query query = query(where("jobExecutionId").is(jobExecution.getId())); - Update update = Update.update("status", jobExecution.getStatus()); + JobExecution currentJobExecution = getJobExecution(jobExecution.getId()); + if (currentJobExecution != null && currentJobExecution.getStatus().isGreaterThan(jobExecution.getStatus())) { + jobExecution.upgradeStatus(currentJobExecution.getStatus()); + } // TODO the contract mentions to update the version as well. Double check if this // is needed as the version is not used in the tests following the call sites of // synchronizeStatus - this.mongoOperations.updateFirst(query, update, - org.springframework.batch.core.repository.persistence.JobExecution.class, - JOB_EXECUTIONS_COLLECTION_NAME); } } From 982ccc1c1c7e62bdd2c922ebf80f9ce1f3aa4956 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Mon, 24 Feb 2025 09:42:37 +0100 Subject: [PATCH 14/37] Disable randomly failing/hanging test --- .../test/step/FaultTolerantStepFactoryBeanIntegrationTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java index 2cf59d33c1..704c3fc22c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanIntegrationTests.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; @@ -53,6 +54,7 @@ * Tests for {@link FaultTolerantStepFactoryBean}. */ @SpringJUnitConfig(locations = "/simple-job-launcher-context.xml") +@Disabled("Randomly failing/hanging") // FIXME This test is randomly failing/hanging class FaultTolerantStepFactoryBeanIntegrationTests { private static final int MAX_COUNT = 1000; From 1eac9e9feea36320a17140033c7a7c56024a6b11 Mon Sep 17 00:00:00 2001 From: Elimelec Burghelea Date: Thu, 20 Feb 2025 21:00:38 +0200 Subject: [PATCH 15/37] Attempt to close all delegate readers even when some fail Signed-off-by: Elimelec Burghelea --- .../item/support/CompositeItemReader.java | 20 +++++++++++-- .../support/CompositeItemReaderTests.java | 29 ++++++++++++++++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java index 06148a346c..8da25504fc 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.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. @@ -15,6 +15,7 @@ */ package org.springframework.batch.item.support; +import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -27,6 +28,7 @@ * implementation is not thread-safe. * * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea * @param type of objects to read * @since 5.2 */ @@ -79,8 +81,22 @@ public void update(ExecutionContext executionContext) throws ItemStreamException @Override public void close() throws ItemStreamException { + List exceptions = new ArrayList<>(); + for (ItemStreamReader delegate : delegates) { - delegate.close(); + try { + delegate.close(); + } + catch (Exception e) { + exceptions.add(e); + } + } + + if (!exceptions.isEmpty()) { + String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size()); + ItemStreamException holder = new ItemStreamException(message); + exceptions.forEach(holder::addSuppressed); + throw holder; } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java index 3775c4299c..70091a0afc 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemReaderTests.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. @@ -17,11 +17,14 @@ import java.util.Arrays; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemStreamReader; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -32,6 +35,7 @@ * Test class for {@link CompositeItemReader}. * * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea */ public class CompositeItemReaderTests { @@ -107,4 +111,27 @@ void testCompositeItemReaderClose() { verify(reader2).close(); } + @Test + void testCompositeItemReaderCloseWithDelegateThatThrowsException() { + // given + ItemStreamReader reader1 = mock(); + ItemStreamReader reader2 = mock(); + CompositeItemReader compositeItemReader = new CompositeItemReader<>(Arrays.asList(reader1, reader2)); + + doThrow(new ItemStreamException("A failure")).when(reader1).close(); + + // when + try { + compositeItemReader.close(); + Assertions.fail("Expected an ItemStreamException"); + } + catch (ItemStreamException ignored) { + + } + + // then + verify(reader1).close(); + verify(reader2).close(); + } + } \ No newline at end of file From f40ab202a0326348c98217260106964f25dd1c85 Mon Sep 17 00:00:00 2001 From: Elimelec Burghelea Date: Thu, 20 Feb 2025 21:52:35 +0200 Subject: [PATCH 16/37] Attempt to close all delegate stream readers even when some fail Signed-off-by: Elimelec Burghelea --- .../step/item/TaskletStepExceptionTests.java | 7 +-- .../core/step/tasklet/TaskletStepTests.java | 6 +-- .../item/support/CompositeItemStream.java | 23 +++++++-- .../support/CompositeItemStreamTests.java | 48 +++++++++++++++++-- 4 files changed, 69 insertions(+), 15 deletions(-) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/TaskletStepExceptionTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/TaskletStepExceptionTests.java index 5bc1fc695f..d4a01e8fc9 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/TaskletStepExceptionTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/TaskletStepExceptionTests.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. @@ -63,6 +63,7 @@ * @author David Turanski * @author Mahmoud Ben Hassine * @author Parikshit Dutta + * @author Elimelec Burghelea */ class TaskletStepExceptionTests { @@ -212,8 +213,8 @@ public void close() throws ItemStreamException { taskletStep.execute(stepExecution); assertEquals(FAILED, stepExecution.getStatus()); - assertTrue(stepExecution.getFailureExceptions().contains(taskletException)); - assertTrue(stepExecution.getFailureExceptions().contains(exception)); + assertEquals(stepExecution.getFailureExceptions().get(0), taskletException); + assertEquals(stepExecution.getFailureExceptions().get(1).getSuppressed()[0], exception); assertEquals(2, jobRepository.getUpdateCount()); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletStepTests.java index ef6e917b70..e429a30e42 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletStepTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/TaskletStepTests.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. @@ -761,7 +761,7 @@ public void close() throws ItemStreamException { Throwable ex = stepExecution.getFailureExceptions().get(0); // The original rollback was caused by this one: - assertEquals("Bar", ex.getMessage()); + assertEquals("Bar", ex.getSuppressed()[0].getMessage()); } @Test @@ -791,7 +791,7 @@ public void close() throws ItemStreamException { assertEquals("", msg); Throwable ex = stepExecution.getFailureExceptions().get(0); // The original rollback was caused by this one: - assertEquals("Bar", ex.getMessage()); + assertEquals("Bar", ex.getSuppressed()[0].getMessage()); } /** diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java index e773bf8616..8cb2a1c83c 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.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. @@ -28,7 +28,7 @@ * * @author Dave Syer * @author Mahmoud Ben Hassine - * + * @author Elimelec Burghelea */ public class CompositeItemStream implements ItemStream { @@ -102,13 +102,26 @@ public void update(ExecutionContext executionContext) { /** * Broadcast the call to close. * @throws ItemStreamException thrown if one of the {@link ItemStream}s in the list - * fails to close. This is a sequential operation so all itemStreams in the list after - * the one that failed to close will remain open. + * fails to close. */ @Override public void close() throws ItemStreamException { + List exceptions = new ArrayList<>(); + for (ItemStream itemStream : streams) { - itemStream.close(); + try { + itemStream.close(); + } + catch (Exception e) { + exceptions.add(e); + } + } + + if (!exceptions.isEmpty()) { + String message = String.format("Failed to close %d delegate(s) due to exceptions", exceptions.size()); + ItemStreamException holder = new ItemStreamException(message); + exceptions.forEach(holder::addSuppressed); + throw holder; } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java index 5f1be03821..3861ca0f8d 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/CompositeItemStreamTests.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. @@ -15,19 +15,25 @@ */ package org.springframework.batch.item.support; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + import java.util.ArrayList; import java.util.List; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.batch.item.ExecutionContext; import org.springframework.batch.item.ItemStream; +import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.ItemStreamSupport; -import static org.junit.jupiter.api.Assertions.assertEquals; - /** * @author Dave Syer - * + * @author Elimelec Burghelea */ class CompositeItemStreamTests { @@ -90,6 +96,40 @@ public void close() { assertEquals(1, list.size()); } + @Test + void testClose2Delegates() { + ItemStream reader1 = Mockito.mock(ItemStream.class); + ItemStream reader2 = Mockito.mock(ItemStream.class); + manager.register(reader1); + manager.register(reader2); + + manager.close(); + + verify(reader1, times(1)).close(); + verify(reader2, times(1)).close(); + } + + @Test + void testClose2DelegatesThatThrowsException() { + ItemStream reader1 = Mockito.mock(ItemStream.class); + ItemStream reader2 = Mockito.mock(ItemStream.class); + manager.register(reader1); + manager.register(reader2); + + doThrow(new ItemStreamException("A failure")).when(reader1).close(); + + try { + manager.close(); + Assertions.fail("Expected an ItemStreamException"); + } + catch (ItemStreamException ignored) { + + } + + verify(reader1, times(1)).close(); + verify(reader2, times(1)).close(); + } + @Test void testCloseDoesNotUnregister() { manager.setStreams(new ItemStream[] { new ItemStreamSupport() { From 75edb29d28b4b852cf8e1edc6b987d4fc68a1cbf Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Mon, 24 Feb 2025 10:49:07 +0100 Subject: [PATCH 17/37] Update Javadocs about exception handling when closing composite streams Related to #4764 and #4750 --- .../batch/item/support/CompositeItemReader.java | 6 ++++++ .../batch/item/support/CompositeItemStream.java | 3 ++- .../batch/item/support/CompositeItemWriter.java | 6 ++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java index 8da25504fc..73a92aa57a 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemReader.java @@ -79,6 +79,12 @@ public void update(ExecutionContext executionContext) throws ItemStreamException } } + /** + * Close all delegates. + * @throws ItemStreamException thrown if one of the delegates fails to close. Original + * exceptions thrown by delegates are added as suppressed exceptions into this one, in + * the same order as delegates were registered. + */ @Override public void close() throws ItemStreamException { List exceptions = new ArrayList<>(); diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java index 8cb2a1c83c..82f55750e8 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemStream.java @@ -102,7 +102,8 @@ public void update(ExecutionContext executionContext) { /** * Broadcast the call to close. * @throws ItemStreamException thrown if one of the {@link ItemStream}s in the list - * fails to close. + * fails to close. Original exceptions thrown by delegates are added as suppressed + * exceptions into this one, in the same order as delegates were registered. */ @Override public void close() throws ItemStreamException { diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java index 8666112769..730213c965 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/CompositeItemWriter.java @@ -105,6 +105,12 @@ public void setDelegates(List> delegates) { this.delegates = delegates; } + /** + * Close all delegates. + * @throws ItemStreamException thrown if one of the delegates fails to close. Original + * exceptions thrown by delegates are added as suppressed exceptions into this one, in + * the same order as delegates were registered. + */ @Override public void close() throws ItemStreamException { List exceptions = new ArrayList<>(); From 6701606f68997a7f7c18d8c1f31bd1428c37d626 Mon Sep 17 00:00:00 2001 From: charlie881007 <65711157+charlie881007@users.noreply.github.com> Date: Mon, 24 Feb 2025 10:49:19 +0800 Subject: [PATCH 18/37] fix typo Signed-off-by: charlie881007 <65711157+charlie881007@users.noreply.github.com> --- .../step/chunk-oriented-processing/intercepting-execution.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc index 023dcaee4f..d07884516a 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/intercepting-execution.adoc @@ -89,7 +89,7 @@ public interface StepExecutionListener extends StepListener { } ---- -`ExitStatus` has a return type of `afterStep`, to give listeners the chance to +`afterStep` has a return type of `ExitStatus`, to give listeners the chance to modify the exit code that is returned upon completion of a `Step`. The annotations corresponding to this interface are: From e366dcb1978652437ff0480fcb9eeff3eea8b7ce Mon Sep 17 00:00:00 2001 From: Stefano Cordio Date: Fri, 3 Jan 2025 22:33:56 +0100 Subject: [PATCH 19/37] Honor `@NestedTestConfiguration` semantic in `BatchTestContextCustomizerFactory` Signed-off-by: Stefano Cordio --- .../BatchTestContextCustomizerFactory.java | 10 +-- ...atchTestContextCustomizerFactoryTests.java | 23 ++++-- .../SpringBatchTestIntegrationTests.java | 70 +++++++++++++++++++ 3 files changed, 92 insertions(+), 11 deletions(-) create mode 100644 spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java diff --git a/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java b/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java index 12625b0dea..3c4888d7a1 100644 --- a/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java +++ b/spring-batch-test/src/main/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018 the original author or authors. + * Copyright 2018-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. @@ -17,23 +17,25 @@ import java.util.List; -import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; +import org.springframework.test.context.TestContextAnnotationUtils; /** * Factory for {@link BatchTestContextCustomizer}. * * @author Mahmoud Ben Hassine + * @author Stefano Cordio * @since 4.1 */ public class BatchTestContextCustomizerFactory implements ContextCustomizerFactory { @Override - public ContextCustomizer createContextCustomizer(Class testClass, + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { - if (AnnotatedElementUtils.hasAnnotation(testClass, SpringBatchTest.class)) { + if (TestContextAnnotationUtils.hasAnnotation(testClass, SpringBatchTest.class)) { return new BatchTestContextCustomizer(); } return null; diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java index 4c693d05ec..7d393fde47 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/context/BatchTestContextCustomizerFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-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. @@ -18,38 +18,42 @@ import java.util.Collections; import java.util.List; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; -import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNull; /** * @author Mahmoud Ben Hassine + * @author Stefano Cordio */ class BatchTestContextCustomizerFactoryTests { private final BatchTestContextCustomizerFactory factory = new BatchTestContextCustomizerFactory(); - @Test - void testCreateContextCustomizer_whenAnnotationIsPresent() { + @ParameterizedTest + @ValueSource(classes = { MyJobTest.class, MyJobTest.MyNestedTest.class }) + void testCreateContextCustomizer_whenAnnotationIsPresent(Class testClass) { // given - Class testClass = MyJobTest.class; List configAttributes = Collections.emptyList(); // when ContextCustomizer contextCustomizer = this.factory.createContextCustomizer(testClass, configAttributes); // then - assertNotNull(contextCustomizer); + assertInstanceOf(BatchTestContextCustomizer.class, contextCustomizer); } @Test void testCreateContextCustomizer_whenAnnotationIsAbsent() { // given - Class testClass = MyOtherJobTest.class; + Class testClass = MyOtherJobTest.class; List configAttributes = Collections.emptyList(); // when @@ -62,6 +66,11 @@ void testCreateContextCustomizer_whenAnnotationIsAbsent() { @SpringBatchTest private static class MyJobTest { + @Nested + class MyNestedTest { + + } + } private static class MyOtherJobTest { diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java new file mode 100644 index 0000000000..b48c39214d --- /dev/null +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/context/SpringBatchTestIntegrationTests.java @@ -0,0 +1,70 @@ +/* + * 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.test.context; + +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.batch.test.JobLauncherTestUtils; +import org.springframework.batch.test.JobRepositoryTestUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * @author Stefano Cordio + */ +@SpringJUnitConfig +@SpringBatchTest +class SpringBatchTestIntegrationTests { + + @Autowired + ApplicationContext context; + + @Nested + class InnerWithoutSpringBatchTest { + + @Autowired + ApplicationContext context; + + @Test + void test() { + assertSame(SpringBatchTestIntegrationTests.this.context, context); + assertNotNull(context.getBean(JobLauncherTestUtils.class)); + assertNotNull(context.getBean(JobRepositoryTestUtils.class)); + } + + } + + @Nested + @SpringBatchTest + class InnerWithSpringBatchTest { + + @Autowired + ApplicationContext context; + + @Test + void test() { + assertSame(SpringBatchTestIntegrationTests.this.context, context); + assertNotNull(context.getBean(JobLauncherTestUtils.class)); + assertNotNull(context.getBean(JobRepositoryTestUtils.class)); + } + + } + +} From 9fbdf1ed827c880e046432e4e6bbd555bf1cf175 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 25 Feb 2025 14:45:37 +0100 Subject: [PATCH 20/37] Revert thread mode in FaultTolerantStepFactoryBeanRollbackIntegrationTests --- .../FaultTolerantStepFactoryBeanRollbackIntegrationTests.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java index eeaeba2365..e17332dac9 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/step/FaultTolerantStepFactoryBeanRollbackIntegrationTests.java @@ -51,7 +51,6 @@ import org.springframework.transaction.PlatformTransactionManager; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Timeout.ThreadMode.SEPARATE_THREAD; /** * Tests for {@link FaultTolerantStepFactoryBean}. @@ -110,7 +109,7 @@ void testUpdatesNoRollback() { } @Test - @Timeout(value = 30, threadMode = SEPARATE_THREAD) + @Timeout(value = 30) void testMultithreadedSkipInWriter() throws Throwable { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); From a79b6f75500c669293e1eb7b8066b5100d3656bb Mon Sep 17 00:00:00 2001 From: yoseplee Date: Wed, 5 Feb 2025 20:09:35 +0900 Subject: [PATCH 21/37] Fix index creation statements in MongoDB DDL script Signed-off-by: yoseplee --- .../batch/core/schema-mongodb.js | 12 +++++------ .../MongoDBJobRepositoryIntegrationTests.java | 21 +++++++++++++++++++ 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js index e3a971ad8a..eb10033e8c 100644 --- a/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js +++ b/spring-batch-core/src/main/resources/org/springframework/batch/core/schema-mongodb.js @@ -10,9 +10,9 @@ db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_JOB_EXECUTION_SEQ", c db.getCollection("BATCH_SEQUENCES").insertOne({_id: "BATCH_STEP_EXECUTION_SEQ", count: Long(0)}); // INDICES -db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_name_idx", {"jobName": 1}, {}); -db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_name_key_idx", {"jobName": 1, "jobKey": 1}, {}); -db.getCollection("BATCH_JOB_INSTANCE").createIndex("job_instance_idx", {"jobInstanceId": -1}, {}); -db.getCollection("BATCH_JOB_EXECUTION").createIndex("job_instance_idx", {"jobInstanceId": 1}, {}); -db.getCollection("BATCH_JOB_EXECUTION").createIndex("job_instance_idx", {"jobInstanceId": 1, "status": 1}, {}); -db.getCollection("BATCH_STEP_EXECUTION").createIndex("step_execution_idx", {"stepExecutionId": 1}, {}); +db.getCollection("BATCH_JOB_INSTANCE").createIndex( {"jobName": 1}, {"name": "job_name_idx"}); +db.getCollection("BATCH_JOB_INSTANCE").createIndex( {"jobName": 1, "jobKey": 1}, {"name": "job_name_key_idx"}); +db.getCollection("BATCH_JOB_INSTANCE").createIndex( {"jobInstanceId": -1}, {"name": "job_instance_idx"}); +db.getCollection("BATCH_JOB_EXECUTION").createIndex( {"jobInstanceId": 1}, {"name": "job_instance_idx"}); +db.getCollection("BATCH_JOB_EXECUTION").createIndex( {"jobInstanceId": 1, "status": 1}, {"name": "job_instance_status_idx"}); +db.getCollection("BATCH_STEP_EXECUTION").createIndex( {"stepExecutionId": 1}, {"name": "step_execution_idx"}); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java index 6b70f0b3c7..b70b80281c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/repository/support/MongoDBJobRepositoryIntegrationTests.java @@ -24,6 +24,8 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.annotation.DirtiesContext; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.index.Index; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -64,9 +66,11 @@ static void setMongoDbConnectionString(DynamicPropertyRegistry registry) { @BeforeEach public void setUp() { + // collections mongoTemplate.createCollection("BATCH_JOB_INSTANCE"); mongoTemplate.createCollection("BATCH_JOB_EXECUTION"); mongoTemplate.createCollection("BATCH_STEP_EXECUTION"); + // sequences mongoTemplate.createCollection("BATCH_SEQUENCES"); mongoTemplate.getCollection("BATCH_SEQUENCES") .insertOne(new Document(Map.of("_id", "BATCH_JOB_INSTANCE_SEQ", "count", 0L))); @@ -74,6 +78,23 @@ public void setUp() { .insertOne(new Document(Map.of("_id", "BATCH_JOB_EXECUTION_SEQ", "count", 0L))); mongoTemplate.getCollection("BATCH_SEQUENCES") .insertOne(new Document(Map.of("_id", "BATCH_STEP_EXECUTION_SEQ", "count", 0L))); + // indices + mongoTemplate.indexOps("BATCH_JOB_INSTANCE") + .ensureIndex(new Index().on("jobName", Sort.Direction.ASC).named("job_name_idx")); + mongoTemplate.indexOps("BATCH_JOB_INSTANCE") + .ensureIndex(new Index().on("jobName", Sort.Direction.ASC) + .on("jobKey", Sort.Direction.ASC) + .named("job_name_key_idx")); + mongoTemplate.indexOps("BATCH_JOB_INSTANCE") + .ensureIndex(new Index().on("jobInstanceId", Sort.Direction.DESC).named("job_instance_idx")); + mongoTemplate.indexOps("BATCH_JOB_EXECUTION") + .ensureIndex(new Index().on("jobInstanceId", Sort.Direction.ASC).named("job_instance_idx")); + mongoTemplate.indexOps("BATCH_JOB_EXECUTION") + .ensureIndex(new Index().on("jobInstanceId", Sort.Direction.ASC) + .on("status", Sort.Direction.ASC) + .named("job_instance_status_idx")); + mongoTemplate.indexOps("BATCH_STEP_EXECUTION") + .ensureIndex(new Index().on("stepExecutionId", Sort.Direction.ASC).named("step_execution_idx")); } @Test From 42b1464a0994460112c854c4f93903f59c2a9bc9 Mon Sep 17 00:00:00 2001 From: HeoSeokMun Date: Sun, 27 Oct 2024 15:55:22 +0900 Subject: [PATCH 22/37] Fix dirty flag on ExecutionContext Before this commit, the dirty flag was reset on any put operation, even those that replace existing keys with the same values. This commit fixes the dirty flag to be cleared only by meaningful put operations. Resolves #4685 Resolves #4692 --- .../batch/item/ExecutionContext.java | 13 ++++++++----- .../batch/item/ExecutionContextTests.java | 17 ++++++++++++++--- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java index 71e56ce4d5..8f000c4656 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/ExecutionContext.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. @@ -37,6 +37,7 @@ * @author Lucas Ward * @author Douglas Kaminsky * @author Mahmoud Ben Hassine + * @author Seokmun Heo */ public class ExecutionContext implements Serializable { @@ -124,19 +125,21 @@ public void putDouble(String key, double value) { public void put(String key, @Nullable Object value) { if (value != null) { Object result = this.map.put(key, value); - this.dirty = result == null || !result.equals(value); + this.dirty = this.dirty || result == null || !result.equals(value); } else { Object result = this.map.remove(key); - this.dirty = result != null; + this.dirty = this.dirty || result != null; } } /** * Indicates if context has been changed with a "put" operation since the dirty flag * was last cleared. Note that the last time the flag was cleared might correspond to - * creation of the context. - * @return True if "put" operation has occurred since flag was last cleared + * creation of the context. A context is only dirty if a new value is put or an old + * one is removed. + * @return True if a new value was put or an old one was removed since the last time + * the flag was cleared */ public boolean isDirty() { return this.dirty; diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java index 96e19dfc43..581369b822 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/ExecutionContextTests.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. @@ -36,7 +36,7 @@ /** * @author Lucas Ward * @author Mahmoud Ben Hassine - * + * @author Seokmun Heo */ class ExecutionContextTests { @@ -94,11 +94,13 @@ void testNotDirtyWithDuplicate() { } @Test - void testNotDirtyWithRemoveMissing() { + void testDirtyWithRemoveMissing() { context.putString("1", "test"); assertTrue(context.isDirty()); context.putString("1", null); // remove an item that was present assertTrue(context.isDirty()); + + context.clearDirtyFlag(); context.putString("1", null); // remove a non-existent item assertFalse(context.isDirty()); } @@ -167,6 +169,15 @@ void testCopyConstructorNullInput() { assertTrue(context.isEmpty()); } + @Test + void testDirtyWithDuplicate() { + ExecutionContext context = new ExecutionContext(); + context.put("1", "testString1"); + assertTrue(context.isDirty()); + context.put("1", "testString1"); // put the same value + assertTrue(context.isDirty()); + } + /** * Value object for testing serialization */ From e110b359ef01cb297257ba72cf57eaa59efee772 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 18 Mar 2025 09:15:44 +0100 Subject: [PATCH 23/37] Add AOT runtime hints for infrastructure artifacts This commit adds AOT runtime hints for common classes and interfaces of the infrastructure module. Technology specific APIs (mongodb, kafka, redis, etc) are not included on purpose and are left for users when needed. Resolves #4785 --- .../aot/InfrastructureRuntimeHints.java | 104 ++++++++++++++++++ .../resources/META-INF/spring/aot.factories | 1 + 2 files changed, 105 insertions(+) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/aot/InfrastructureRuntimeHints.java create mode 100644 spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/aot/InfrastructureRuntimeHints.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/aot/InfrastructureRuntimeHints.java new file mode 100644 index 0000000000..8d67cefb0f --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/infrastructure/aot/InfrastructureRuntimeHints.java @@ -0,0 +1,104 @@ +/* + * 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.infrastructure.aot; + +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.batch.item.ItemStreamSupport; +import org.springframework.batch.item.amqp.AmqpItemReader; +import org.springframework.batch.item.amqp.AmqpItemWriter; +import org.springframework.batch.item.amqp.builder.AmqpItemReaderBuilder; +import org.springframework.batch.item.amqp.builder.AmqpItemWriterBuilder; +import org.springframework.batch.item.database.JdbcBatchItemWriter; +import org.springframework.batch.item.database.JdbcCursorItemReader; +import org.springframework.batch.item.database.JdbcPagingItemReader; +import org.springframework.batch.item.database.JpaCursorItemReader; +import org.springframework.batch.item.database.JpaItemWriter; +import org.springframework.batch.item.database.JpaPagingItemReader; +import org.springframework.batch.item.database.builder.JdbcBatchItemWriterBuilder; +import org.springframework.batch.item.database.builder.JdbcCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JdbcPagingItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaCursorItemReaderBuilder; +import org.springframework.batch.item.database.builder.JpaItemWriterBuilder; +import org.springframework.batch.item.database.builder.JpaPagingItemReaderBuilder; +import org.springframework.batch.item.file.FlatFileItemReader; +import org.springframework.batch.item.file.FlatFileItemWriter; +import org.springframework.batch.item.file.builder.FlatFileItemReaderBuilder; +import org.springframework.batch.item.file.builder.FlatFileItemWriterBuilder; +import org.springframework.batch.item.jms.JmsItemReader; +import org.springframework.batch.item.jms.JmsItemWriter; +import org.springframework.batch.item.jms.builder.JmsItemReaderBuilder; +import org.springframework.batch.item.jms.builder.JmsItemWriterBuilder; +import org.springframework.batch.item.json.JsonFileItemWriter; +import org.springframework.batch.item.json.JsonItemReader; +import org.springframework.batch.item.json.builder.JsonFileItemWriterBuilder; +import org.springframework.batch.item.json.builder.JsonItemReaderBuilder; +import org.springframework.batch.item.queue.BlockingQueueItemReader; +import org.springframework.batch.item.queue.BlockingQueueItemWriter; +import org.springframework.batch.item.queue.builder.BlockingQueueItemReaderBuilder; +import org.springframework.batch.item.queue.builder.BlockingQueueItemWriterBuilder; +import org.springframework.batch.item.support.AbstractFileItemWriter; +import org.springframework.batch.item.support.AbstractItemCountingItemStreamItemReader; +import org.springframework.batch.item.support.AbstractItemStreamItemReader; +import org.springframework.batch.item.support.AbstractItemStreamItemWriter; +import org.springframework.batch.item.xml.StaxEventItemReader; +import org.springframework.batch.item.xml.StaxEventItemWriter; +import org.springframework.batch.item.xml.builder.StaxEventItemReaderBuilder; +import org.springframework.batch.item.xml.builder.StaxEventItemWriterBuilder; + +import java.util.Set; + +/** + * {@link RuntimeHintsRegistrar} for Spring Batch infrastructure module. + * + * @author Mahmoud Ben Hassine + * @since 5.2.2 + */ +public class InfrastructureRuntimeHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + // reflection hints + Set> classes = Set.of( + // File IO APIs + FlatFileItemReader.class, FlatFileItemReaderBuilder.class, FlatFileItemWriter.class, + FlatFileItemWriterBuilder.class, JsonItemReader.class, JsonItemReaderBuilder.class, + JsonFileItemWriter.class, JsonFileItemWriterBuilder.class, StaxEventItemReader.class, + StaxEventItemReaderBuilder.class, StaxEventItemWriter.class, StaxEventItemWriterBuilder.class, + + // Database IO APIs + JdbcCursorItemReader.class, JdbcCursorItemReaderBuilder.class, JdbcPagingItemReader.class, + JdbcPagingItemReaderBuilder.class, JdbcBatchItemWriter.class, JdbcBatchItemWriterBuilder.class, + JpaCursorItemReader.class, JpaCursorItemReaderBuilder.class, JpaPagingItemReader.class, + JpaPagingItemReaderBuilder.class, JpaItemWriter.class, JpaItemWriterBuilder.class, + + // Queue IO APIs + BlockingQueueItemReader.class, BlockingQueueItemReaderBuilder.class, BlockingQueueItemWriter.class, + BlockingQueueItemWriterBuilder.class, JmsItemReader.class, JmsItemReaderBuilder.class, + JmsItemWriter.class, JmsItemWriterBuilder.class, AmqpItemReader.class, AmqpItemReaderBuilder.class, + AmqpItemWriter.class, AmqpItemWriterBuilder.class, + + // Support classes + AbstractFileItemWriter.class, AbstractItemStreamItemWriter.class, + AbstractItemCountingItemStreamItemReader.class, AbstractItemStreamItemReader.class, + ItemStreamSupport.class); + for (Class type : classes) { + hints.reflection().registerType(type, MemberCategory.values()); + } + } + +} diff --git a/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories b/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 0000000000..efa2f70c11 --- /dev/null +++ b/spring-batch-infrastructure/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=org.springframework.batch.infrastructure.aot.InfrastructureRuntimeHints From 618e03b00bbe74092cd9ee4975d7b666b5435937 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 18 Mar 2025 09:20:24 +0100 Subject: [PATCH 24/37] Fix incorrect test class name This test class is about the composite item reader and not the composite item writer. --- ...Tests.java => CompositeItemReaderSampleFunctionalTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/{CompositeItemWriterSampleFunctionalTests.java => CompositeItemReaderSampleFunctionalTests.java} (99%) diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java similarity index 99% rename from spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java rename to spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java index 8c90257b6e..03db277a99 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemWriterSampleFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java @@ -50,7 +50,7 @@ import org.springframework.jdbc.support.JdbcTransactionManager; import org.springframework.test.jdbc.JdbcTestUtils; -public class CompositeItemWriterSampleFunctionalTests { +public class CompositeItemReaderSampleFunctionalTests { record Person(int id, String name) { } From 60f83438e95901f7861c9eb139d8e39384ac37f9 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 18 Mar 2025 09:47:29 +0100 Subject: [PATCH 25/37] Disable test failing on CI but not locally --- .../file/multiresource/MultiResourceFunctionalTests.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java index 209ac5ce39..b7522968b5 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiresource/MultiResourceFunctionalTests.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. @@ -16,6 +16,7 @@ package org.springframework.batch.samples.file.multiresource; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; @@ -38,6 +39,7 @@ * @author Mahmoud Ben Hassine * @since 2.0 */ +@Disabled("Failing on the CI platform but not locally") @SpringJUnitConfig(locations = { "/org/springframework/batch/samples/file/multiresource/job/multiResource.xml", "/simple-job-launcher-context.xml" }) class MultiResourceFunctionalTests { From 42056e714d4346bd9a10f2da7f426c7de0d892e4 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Tue, 18 Mar 2025 10:38:05 +0100 Subject: [PATCH 26/37] Update version of IBM DB2 docker image and jdbc driver --- pom.xml | 2 +- .../test/repository/Db2JobRepositoryIntegrationTests.java | 4 ++-- .../support/Db2PagingQueryProviderIntegrationTests.java | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/pom.xml b/pom.xml index eb01fbdcc5..4aa29bb280 100644 --- a/pom.xml +++ b/pom.xml @@ -123,7 +123,7 @@ 9.1.0 3.5.1 42.7.4 - 11.5.9.0 + 12.1.0.0 19.24.0.0 11.2.3.jre17 1.3.1 diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/Db2JobRepositoryIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/Db2JobRepositoryIntegrationTests.java index 22b6d109bb..4f7e9041c1 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/Db2JobRepositoryIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/test/repository/Db2JobRepositoryIntegrationTests.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. @@ -57,7 +57,7 @@ class Db2JobRepositoryIntegrationTests { // TODO find the best way to externalize and manage image versions - private static final DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.5.1"); + private static final DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.9.0"); @Container public static Db2Container db2 = new Db2Container(DB2_IMAGE).acceptLicense(); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java index 19d876b9d1..18353345b6 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/support/Db2PagingQueryProviderIntegrationTests.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. @@ -32,6 +32,7 @@ /** * @author Henning Pöttker + * @author Mahmoud Ben Hassine */ @Testcontainers(disabledWithoutDocker = true) @SpringJUnitConfig @@ -39,7 +40,7 @@ class Db2PagingQueryProviderIntegrationTests extends AbstractPagingQueryProviderIntegrationTests { // TODO find the best way to externalize and manage image versions - private static final DockerImageName DB2_IMAGE = DockerImageName.parse("ibmcom/db2:11.5.5.1"); + private static final DockerImageName DB2_IMAGE = DockerImageName.parse("icr.io/db2_community/db2:11.5.9.0"); @Container public static Db2Container db2 = new Db2Container(DB2_IMAGE).acceptLicense(); From 4c9b88eb528eace294ac640cb709154a433c82e3 Mon Sep 17 00:00:00 2001 From: KyeongHoon Lee Date: Thu, 9 Jan 2025 13:00:42 +0900 Subject: [PATCH 27/37] Add FunctionalInterface annotation to ChunkProcessor Signed-off-by: KyeongHoon Lee --- .../springframework/batch/core/step/item/ChunkProcessor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProcessor.java index 3bab818b81..51034c867e 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProcessor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProcessor.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. @@ -22,8 +22,10 @@ /** * Interface defined for processing {@link org.springframework.batch.item.Chunk}s. * + * @author Kyeonghoon Lee (Add FunctionalInterface annotation) * @since 2.0 */ +@FunctionalInterface public interface ChunkProcessor { void process(StepContribution contribution, Chunk chunk) throws Exception; From f1ac0c01c51b50f454b5e59011f5569b80599a12 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Wed, 26 Feb 2025 12:54:36 +0800 Subject: [PATCH 28/37] Fix wrong statement in Javadoc of SimplePartitioner Signed-off-by: Yanming Zhou --- .../batch/core/partition/support/SimplePartitioner.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/partition/support/SimplePartitioner.java b/spring-batch-core/src/main/java/org/springframework/batch/core/partition/support/SimplePartitioner.java index 9e3ebbaa10..de0b44f7b4 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/partition/support/SimplePartitioner.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/partition/support/SimplePartitioner.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2013 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. @@ -25,7 +25,7 @@ * Simplest possible implementation of {@link Partitioner}. Just creates a set of empty * {@link ExecutionContext} instances, and labels them as * {partition0, partition1, ..., partitionN}, where N is the - * grid size. + * grid size - 1. * * @author Dave Syer * @since 2.0 From a06f39b76d3479a28b8d3cf93904af8a3e37b850 Mon Sep 17 00:00:00 2001 From: Ludovic Bertin Date: Tue, 25 Feb 2025 08:33:57 +0100 Subject: [PATCH 29/37] Add AOT runtime hints for core listeners Signed-off-by: Ludovic Bertin --- .../batch/core/aot/CoreRuntimeHints.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/aot/CoreRuntimeHints.java b/spring-batch-core/src/main/java/org/springframework/batch/core/aot/CoreRuntimeHints.java index 84a3c6e885..5c818578c8 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/aot/CoreRuntimeHints.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/aot/CoreRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2023 the original author or authors. + * Copyright 2022-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. @@ -98,6 +98,27 @@ public void registerHints(RuntimeHints hints, ClassLoader classLoader) { // proxy hints hints.proxies() + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.StepExecutionListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.ItemReadListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.ItemProcessListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.ItemWriteListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.ChunkListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.SkipListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) + .registerJdkProxy(builder -> builder + .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.JobExecutionListener")) + .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) .registerJdkProxy(builder -> builder .proxiedInterfaces(TypeReference.of("org.springframework.batch.core.repository.JobRepository")) .proxiedInterfaces(SpringProxy.class, Advised.class, DecoratingProxy.class)) From 69615714069d13b09976f0a3d001f2d4ba6fd352 Mon Sep 17 00:00:00 2001 From: yeonnex Date: Fri, 11 Oct 2024 20:17:06 +0900 Subject: [PATCH 30/37] Fix variable usage in ScriptItemProcessorTests This commit also adds test dependencies for groovy, javascript, bean shell and jruby script engines. --- pom.xml | 4 ++++ spring-batch-infrastructure/pom.xml | 24 +++++++++++++++++++ .../support/ScriptItemProcessorTests.java | 4 ++-- 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 4aa29bb280..d297febf6e 100644 --- a/pom.xml +++ b/pom.xml @@ -129,6 +129,10 @@ 1.3.1 1.20.4 1.5.3 + 4.0.23 + 15.4 + 2.0b6 + 9.4.8.0 ${spring-amqp.version} diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index dd208f8379..1f00352b32 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -529,6 +529,30 @@ ${angus-mail.version} test + + org.apache.groovy + groovy-jsr223 + ${groovy-jsr223.version} + test + + + org.openjdk.nashorn + nashorn-core + ${nashorn.version} + test + + + org.apache-extras.beanshell + bsh + ${beanshell.version} + test + + + org.jruby + jruby + ${jruby.version} + test + diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java index bdbb6205c4..5370d7b74f 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/ScriptItemProcessorTests.java @@ -82,7 +82,7 @@ void testJRubyScriptSourceSimple() throws Exception { assumeTrue(languageExists("jruby")); ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor<>(); - scriptItemProcessor.setScriptSource("$item.upcase", "jruby"); + scriptItemProcessor.setScriptSource("item.upcase", "jruby"); scriptItemProcessor.afterPropertiesSet(); assertEquals("SS", scriptItemProcessor.process("ss"), "Incorrect transformed value"); @@ -93,7 +93,7 @@ void testJRubyScriptSourceMethod() throws Exception { assumeTrue(languageExists("jruby")); ScriptItemProcessor scriptItemProcessor = new ScriptItemProcessor<>(); - scriptItemProcessor.setScriptSource("def process(item) $item.upcase end \n process($item)", "jruby"); + scriptItemProcessor.setScriptSource("def process(item) item.upcase end \n process(item)", "jruby"); scriptItemProcessor.afterPropertiesSet(); assertEquals("SS", scriptItemProcessor.process("ss"), "Incorrect transformed value"); From 4671b62b14d87e33480c02762aa8b6c5964aca09 Mon Sep 17 00:00:00 2001 From: Elimelec Burghelea Date: Thu, 27 Feb 2025 00:38:45 +0200 Subject: [PATCH 31/37] Use Files.delete() for better error reporting Signed-off-by: Elimelec Burghelea --- .../item/support/AbstractFileItemWriter.java | 10 +-- .../batch/item/util/FileUtils.java | 11 ++- .../batch/item/xml/StaxEventItemWriter.java | 10 +-- .../support/AbstractFileItemWriterTest.java | 75 +++++++++++++++++++ .../batch/item/util/FileUtilsTests.java | 41 +++++++++- .../item/xml/StaxEventItemWriterTests.java | 26 ++++++- 6 files changed, 158 insertions(+), 15 deletions(-) create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java index 0396ca8cc7..c3d3a00bbb 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/support/AbstractFileItemWriter.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. @@ -25,6 +25,7 @@ import java.nio.channels.FileChannel; import java.nio.charset.StandardCharsets; import java.nio.charset.UnsupportedCharsetException; +import java.nio.file.Files; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -61,6 +62,7 @@ * @author Mahmoud Ben Hassine * @author Glenn Renfro * @author Remi Kaeffer + * @author Elimelec Burghelea * @since 4.1 */ public abstract class AbstractFileItemWriter extends AbstractItemStreamItemWriter @@ -268,11 +270,9 @@ public void close() { state.close(); if (state.linesWritten == 0 && shouldDeleteIfEmpty) { try { - if (!resource.getFile().delete()) { - throw new ItemStreamException("Failed to delete empty file on close"); - } + Files.delete(resource.getFile().toPath()); } - catch (IOException e) { + catch (IOException | SecurityException e) { throw new ItemStreamException("Failed to delete empty file on close", e); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java index 1b82ae1634..c14d9470b3 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/util/FileUtils.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. @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; +import java.nio.file.Files; import org.springframework.batch.item.ItemStreamException; import org.springframework.util.Assert; @@ -28,6 +29,7 @@ * @author Peter Zozom * @author Mahmoud Ben Hassine * @author Taeik Lim + * @author Elimelec Burghelea */ public abstract class FileUtils { @@ -57,8 +59,11 @@ public static void setUpOutputFile(File file, boolean restarted, boolean append, if (!overwriteOutputFile) { throw new ItemStreamException("File already exists: [" + file.getAbsolutePath() + "]"); } - if (!file.delete()) { - throw new IOException("Could not delete file: " + file); + try { + Files.delete(file.toPath()); + } + catch (IOException | SecurityException e) { + throw new IOException("Could not delete file: " + file, e); } } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java index fef239f809..2c6e803773 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/xml/StaxEventItemWriter.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. @@ -24,6 +24,7 @@ import java.io.UnsupportedEncodingException; import java.io.Writer; import java.nio.channels.FileChannel; +import java.nio.file.Files; import java.util.Collections; import java.util.List; import java.util.Map; @@ -75,6 +76,7 @@ * @author Michael Minella * @author Parikshit Dutta * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea */ public class StaxEventItemWriter extends AbstractItemStreamItemWriter implements ResourceAwareItemWriterItemStream, InitializingBean { @@ -726,11 +728,9 @@ public void close() { } if (currentRecordCount == 0 && shouldDeleteIfEmpty) { try { - if (!resource.getFile().delete()) { - throw new ItemStreamException("Failed to delete empty file on close"); - } + Files.delete(resource.getFile().toPath()); } - catch (IOException e) { + catch (IOException | SecurityException e) { throw new ItemStreamException("Failed to delete empty file on close", e); } } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java new file mode 100644 index 0000000000..aacc67e716 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/support/AbstractFileItemWriterTest.java @@ -0,0 +1,75 @@ +/* + * 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.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.when; + +import java.io.File; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.batch.item.Chunk; +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; +import org.springframework.core.io.FileSystemResource; + +/** + * Tests for common methods from {@link AbstractFileItemWriter}. + * + * @author Elimelec Burghelea + */ +class AbstractFileItemWriterTests { + + @Test + void testFailedFileDeletionThrowsException() { + File outputFile = new File("target/data/output.tmp"); + File mocked = Mockito.spy(outputFile); + + TestFileItemWriter writer = new TestFileItemWriter(); + + writer.setResource(new FileSystemResource(mocked)); + writer.setShouldDeleteIfEmpty(true); + writer.setName(writer.getClass().getSimpleName()); + writer.open(new ExecutionContext()); + + when(mocked.delete()).thenReturn(false); + + ItemStreamException exception = assertThrows(ItemStreamException.class, writer::close, + "Expected exception when file deletion fails"); + + assertEquals("Failed to delete empty file on close", exception.getMessage(), "Wrong exception message"); + assertNotNull(exception.getCause(), "Exception should have a cause"); + } + + private static class TestFileItemWriter extends AbstractFileItemWriter { + + @Override + protected String doWrite(Chunk items) { + return String.join("\n", items); + } + + @Override + public void afterPropertiesSet() { + + } + + } + +} \ No newline at end of file diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java index 311ef986ba..6faae21e61 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/util/FileUtilsTests.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. @@ -28,6 +28,7 @@ import org.springframework.util.Assert; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -36,6 +37,7 @@ * Tests for {@link FileUtils} * * @author Robert Kasanicky + * @author Elimelec Burghelea */ class FileUtilsTests { @@ -178,6 +180,43 @@ public boolean exists() { } } + @Test + void testCannotDeleteFile() { + + File file = new File("new file") { + + @Override + public boolean createNewFile() { + return true; + } + + @Override + public boolean exists() { + return true; + } + + @Override + public boolean delete() { + return false; + } + + }; + try { + FileUtils.setUpOutputFile(file, false, false, true); + fail("Expected ItemStreamException because file cannot be deleted"); + } + catch (ItemStreamException ex) { + String message = ex.getMessage(); + assertTrue(message.startsWith("Unable to create file"), "Wrong message: " + message); + assertTrue(ex.getCause() instanceof IOException); + assertTrue(ex.getCause().getMessage().startsWith("Could not delete file"), "Wrong message: " + message); + assertNotNull(ex.getCause().getCause(), "Exception should have a cause"); + } + finally { + file.delete(); + } + } + @BeforeEach void setUp() { file.delete(); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java index f904c59441..08fada774e 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/xml/StaxEventItemWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2008-2023 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. @@ -30,6 +30,7 @@ import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.ItemStreamException; import org.springframework.batch.item.UnexpectedInputException; import org.springframework.batch.item.WriterNotOpenException; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; @@ -47,9 +48,11 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; /** @@ -57,6 +60,7 @@ * * @author Parikshit Dutta * @author Mahmoud Ben Hassine + * @author Elimelec Burghelea */ class StaxEventItemWriterTests { @@ -831,6 +835,26 @@ void testOpenAndCloseTagsInComplexCallbacksRestart() throws Exception { + "", content, "Wrong content: " + content); } + /** + * Tests that if file.delete() returns false, an appropriate exception is thrown to + * indicate the deletion attempt failed. + */ + @Test + void testFailedFileDeletionThrowsException() throws IOException { + File mockedFile = spy(resource.getFile()); + writer.setResource(new FileSystemResource(mockedFile)); + writer.setShouldDeleteIfEmpty(true); + writer.open(executionContext); + + when(mockedFile.delete()).thenReturn(false); + + ItemStreamException exception = assertThrows(ItemStreamException.class, () -> writer.close(), + "Expected exception when file deletion fails"); + + assertEquals("Failed to delete empty file on close", exception.getMessage(), "Wrong exception message"); + assertNotNull(exception.getCause(), "Exception should have a cause"); + } + private void initWriterForSimpleCallbackTests() throws Exception { writer = createItemWriter(); writer.setHeaderCallback(writer -> { From 30e9dd79f87cfd926ff09e49567c2073b0c2885e Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Mon, 17 Mar 2025 15:09:50 +0800 Subject: [PATCH 32/37] Stop using deprecated StepExecutionListenerSupport in document Signed-off-by: Yanming Zhou --- spring-batch-docs/modules/ROOT/pages/common-patterns.adoc | 2 +- spring-batch-docs/modules/ROOT/pages/testing.adoc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc b/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc index d5442d1ddb..5e25cddbb7 100644 --- a/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc +++ b/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc @@ -686,7 +686,7 @@ the class definition for `NoWorkFoundStepExecutionListener`: [source, java] ---- -public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { +public class NoWorkFoundStepExecutionListener implements StepExecutionListener { public ExitStatus afterStep(StepExecution stepExecution) { if (stepExecution.getReadCount() == 0) { diff --git a/spring-batch-docs/modules/ROOT/pages/testing.adoc b/spring-batch-docs/modules/ROOT/pages/testing.adoc index f6b3d7e523..3066d034ab 100644 --- a/spring-batch-docs/modules/ROOT/pages/testing.adoc +++ b/spring-batch-docs/modules/ROOT/pages/testing.adoc @@ -303,7 +303,7 @@ the following code snippet shows: [source, java] ---- -public class NoWorkFoundStepExecutionListener extends StepExecutionListenerSupport { +public class NoWorkFoundStepExecutionListener implements StepExecutionListener { public ExitStatus afterStep(StepExecution stepExecution) { if (stepExecution.getReadCount() == 0) { From f9108ee56f6c370f582fccd14e38024abff34eb0 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Fri, 14 Mar 2025 09:04:16 +0800 Subject: [PATCH 33/37] Fix typo Signed-off-by: Yanming Zhou --- .../batch/item/file/builder/FlatFileItemWriterBuilder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java index 30233fd89c..7de7de5301 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/builder/FlatFileItemWriterBuilder.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. @@ -97,7 +97,7 @@ public FlatFileItemWriterBuilder saveState(boolean saveState) { * The name used to calculate the key within the * {@link org.springframework.batch.item.ExecutionContext}. Required if * {@link #saveState(boolean)} is set to true. - * @param name name of the reader instance + * @param name name of the writer instance * @return The current instance of the builder. * @see org.springframework.batch.item.ItemStreamSupport#setName(String) */ From 2b1b5d1c9d0dbd682154c40ee2215d604585cc2f Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Wed, 19 Mar 2025 08:20:39 +0100 Subject: [PATCH 34/37] Remove outdated usage of AssertFile in documentation Resolves #4754 --- .../modules/ROOT/pages/testing.adoc | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/spring-batch-docs/modules/ROOT/pages/testing.adoc b/spring-batch-docs/modules/ROOT/pages/testing.adoc index 3066d034ab..7030be5005 100644 --- a/spring-batch-docs/modules/ROOT/pages/testing.adoc +++ b/spring-batch-docs/modules/ROOT/pages/testing.adoc @@ -274,26 +274,6 @@ int count = StepScopeTestUtils.doInStepScope(stepExecution, }); ---- -[[validatingOutputFiles]] -== Validating Output Files - -When a batch job writes to the database, it is easy to query the database to verify that -the output is as expected. However, if the batch job writes to a file, it is equally -important that the output be verified. Spring Batch provides a class called `AssertFile` -to facilitate the verification of output files. The method called `assertFileEquals` takes -two `File` objects (or two `Resource` objects) and asserts, line by line, that the two -files have the same content. Therefore, it is possible to create a file with the expected -output and to compare it to the actual result, as the following example shows: - -[source, java] ----- -private static final String EXPECTED_FILE = "src/main/resources/data/input.txt"; -private static final String OUTPUT_FILE = "target/test-outputs/output.txt"; - -AssertFile.assertFileEquals(new FileSystemResource(EXPECTED_FILE), - new FileSystemResource(OUTPUT_FILE)); ----- - [[mockingDomainObjects]] == Mocking Domain Objects From e1b0f156e4db9ae2c3b60b83ec372dac8bddad68 Mon Sep 17 00:00:00 2001 From: Yanming Zhou Date: Tue, 4 Mar 2025 14:44:10 +0800 Subject: [PATCH 35/37] Fix incorrect Javadoc Signed-off-by: Yanming Zhou --- .../RemotePartitioningManagerStepBuilderFactory.java | 6 ++---- .../RemotePartitioningWorkerStepBuilderFactory.java | 6 ++---- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java index 60a1f8d019..8a3c4995b0 100644 --- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java +++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningManagerStepBuilderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2022 the original author or authors. + * Copyright 2019-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. @@ -21,12 +21,10 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.transaction.PlatformTransactionManager; /** * Convenient factory for a {@link RemotePartitioningManagerStepBuilder} which sets the - * {@link JobRepository}, {@link JobExplorer}, {@link BeanFactory} and - * {@link PlatformTransactionManager} automatically. + * {@link JobRepository}, {@link JobExplorer} and {@link BeanFactory} automatically. * * @since 4.2 * @author Mahmoud Ben Hassine diff --git a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java index b3c13a1f72..7246b0d259 100644 --- a/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java +++ b/spring-batch-integration/src/main/java/org/springframework/batch/integration/partition/RemotePartitioningWorkerStepBuilderFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2022 the original author or authors. + * Copyright 2018-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. @@ -21,12 +21,10 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.transaction.PlatformTransactionManager; /** * Convenient factory for a {@link RemotePartitioningWorkerStepBuilder} which sets the - * {@link JobRepository}, {@link JobExplorer}, {@link BeanFactory} and - * {@link PlatformTransactionManager} automatically. + * {@link JobRepository}, {@link JobExplorer} and {@link BeanFactory} automatically. * * @since 4.1 * @author Mahmoud Ben Hassine From 2c7178aafd31c94ff6a09bd25ee91b6930ac8506 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Wed, 19 Mar 2025 15:20:19 +0100 Subject: [PATCH 36/37] Prepare release 5.2.2 --- pom.xml | 84 ++++++++++++++++++++++++++++----------------------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/pom.xml b/pom.xml index d297febf6e..37b279a639 100644 --- a/pom.xml +++ b/pom.xml @@ -61,55 +61,55 @@ 17 - 6.2.1 + 6.2.4 2.0.11 - 6.4.1 - 1.14.2 + 6.4.3 + 1.14.5 - 3.4.1 - 3.4.1 - 3.4.1 - 4.4.1 - 3.3.1 - 3.2.1 - 3.2.9 + 3.4.4 + 3.4.4 + 3.4.4 + 4.4.4 + 3.3.4 + 3.2.4 + 3.2.11 - 2.18.2 + 2.18.3 1.12.0 - 2.11.0 - 6.6.3.Final + 2.12.1 + 6.6.11.Final 3.0.0 2.1.3 3.1.0 - 3.1.0 - 3.1.0 - 4.0.13 - 5.2.1 - 5.11.4 + 3.1.1 + 3.1.0 + 4.0.16 + 5.3.1 + 5.11.4 3.0.2 - 1.4.1 + 1.4.4 - 1.4.20 + 1.4.21 4.13.2 ${junit-jupiter.version} 3.0 - 3.26.3 - 5.14.2 + 3.27.3 + 5.16.1 2.10.0 2.18.0 2.13.0 - 2.0.16 + 2.0.17 2.7.4 2.3.232 - 3.47.1.0 + 3.49.1.0 10.16.1.1 - 2.21.11 - 2.38.0 + 2.24.6 + 2.40.0 4.0.5 2.24.3 8.0.2.Final @@ -119,24 +119,24 @@ 4.0.2 2.0.3 7.1.0 - 1.9.22.1 - 9.1.0 - 3.5.1 - 42.7.4 + 1.9.23 + 9.2.0 + 3.5.2 + 42.7.5 12.1.0.0 - 19.24.0.0 + 19.26.0.0 11.2.3.jre17 1.3.1 - 1.20.4 + 1.20.6 1.5.3 - 4.0.23 - 15.4 + 4.0.26 + 15.6 2.0b6 - 9.4.8.0 + 9.4.12.0 ${spring-amqp.version} - 2.3.2 + 2.5.0 0.16.0 3.0.22 @@ -144,13 +144,13 @@ 0.0.4 - 3.13.0 - 3.5.0 - 3.5.0 - 3.10.0 + 3.14.0 + 3.5.2 + 3.5.2 + 3.11.2 3.3.1 - 1.6.0 - 3.1.3 + 1.7.0 + 3.1.4 3.7.1 3.4.2 0.0.39 From 503adc232445ecce3aefeb488bfed96ef564a741 Mon Sep 17 00:00:00 2001 From: Mahmoud Ben Hassine Date: Wed, 19 Mar 2025 16:31:09 +0100 Subject: [PATCH 37/37] Release version 5.2.2 --- pom.xml | 2 +- spring-batch-bom/pom.xml | 2 +- spring-batch-core/pom.xml | 2 +- spring-batch-docs/pom.xml | 2 +- spring-batch-infrastructure/pom.xml | 2 +- spring-batch-integration/pom.xml | 2 +- spring-batch-samples/pom.xml | 2 +- spring-batch-test/pom.xml | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 37b279a639..9ad73b8107 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-SNAPSHOT + 5.2.2 pom https://projects.spring.io/spring-batch diff --git a/spring-batch-bom/pom.xml b/spring-batch-bom/pom.xml index a9680681b6..35c27a533f 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-SNAPSHOT + 5.2.2 spring-batch-bom pom diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index 5a4187bb5d..783b995348 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-SNAPSHOT + 5.2.2 spring-batch-core jar diff --git a/spring-batch-docs/pom.xml b/spring-batch-docs/pom.xml index d31d9dd8cd..30afe8f50b 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-SNAPSHOT + 5.2.2 spring-batch-docs Spring Batch Docs diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index 1f00352b32..3abe16ba46 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-SNAPSHOT + 5.2.2 spring-batch-infrastructure jar diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml index a57e5f58c5..debc4e8e64 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-SNAPSHOT + 5.2.2 spring-batch-integration Spring Batch Integration diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml index 9bf671940f..646a150321 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-SNAPSHOT + 5.2.2 spring-batch-samples jar diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml index ab9e8b20ba..c9f92b858f 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-SNAPSHOT + 5.2.2 spring-batch-test Spring Batch Test