diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/SpringBatchVersion.java b/spring-batch-core/src/main/java/org/springframework/batch/core/SpringBatchVersion.java new file mode 100644 index 000000000..57493f3ee --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/SpringBatchVersion.java @@ -0,0 +1,55 @@ +/* + * Copyright 2022 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.core; + +import org.springframework.lang.Nullable; + +/** + * Class that exposes the Spring Batch version. Fetches the "Implementation-Version" + * manifest attribute from the jar file. + * + *

+ * Note that some ClassLoaders do not expose the package metadata, hence this class might + * not be able to determine the Spring Batch version in all environments. + * + * @author Mahmoud Ben Hassine + * @since 5.0 + */ +public final class SpringBatchVersion { + + /** + * The key to use in the execution context for batch version. + */ + public static final String BATCH_VERSION_KEY = "batch.version"; + + private SpringBatchVersion() { + } + + /** + * Return the full version string of the present Spring Batch codebase, or + * {@code "N/A"} if it cannot be determined. + * @see Package#getImplementationVersion() + */ + @Nullable + public static String getVersion() { + Package pkg = SpringBatchVersion.class.getPackage(); + if (pkg != null && pkg.getImplementationVersion() != null) { + return pkg.getImplementationVersion(); + } + return "N/A"; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java index e3997a03e..4df2422c0 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/AbstractJob.java @@ -36,6 +36,7 @@ import org.springframework.batch.core.JobExecutionListener; import org.springframework.batch.core.JobInterruptedException; import org.springframework.batch.core.JobParametersIncrementer; import org.springframework.batch.core.JobParametersValidator; +import org.springframework.batch.core.SpringBatchVersion; import org.springframework.batch.core.StartLimitExceededException; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; @@ -276,6 +277,7 @@ public abstract class AbstractJob implements Job, StepLocator, BeanNameAware, In public final void execute(JobExecution execution) { Assert.notNull(execution, "jobExecution must not be null"); + execution.getExecutionContext().put(SpringBatchVersion.BATCH_VERSION_KEY, SpringBatchVersion.getVersion()); if (logger.isDebugEnabled()) { logger.debug("Job execution starting: " + execution); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java index 170339fdc..92aecda11 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/AbstractStep.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.JobInterruptedException; +import org.springframework.batch.core.SpringBatchVersion; import org.springframework.batch.core.Step; import org.springframework.batch.core.StepExecution; import org.springframework.batch.core.StepExecutionListener; @@ -193,6 +194,7 @@ public abstract class AbstractStep implements Step, InitializingBean, BeanNameAw throws JobInterruptedException, UnexpectedJobExecutionException { Assert.notNull(stepExecution, "stepExecution must not be null"); + stepExecution.getExecutionContext().put(SpringBatchVersion.BATCH_VERSION_KEY, SpringBatchVersion.getVersion()); if (logger.isDebugEnabled()) { logger.debug("Executing: id=" + stepExecution.getId()); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBatchVersionTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBatchVersionTests.java new file mode 100644 index 000000000..101e431fd --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/SpringBatchVersionTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 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.core; + +import javax.sql.DataSource; + +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.repeat.RepeatStatus; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType; +import org.springframework.jdbc.support.JdbcTransactionManager; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.transaction.PlatformTransactionManager; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test class for {@link SpringBatchVersion}. + * + * @author Mahmoud Ben Hassine + */ +@SpringJUnitConfig +public class SpringBatchVersionTests { + + @Autowired + private JobLauncher jobLauncher; + + @Autowired + private Job job; + + @Test + void testBatchVersionInExecutionContext() throws Exception { + // given + JobParameters jobParameters = new JobParametersBuilder().toJobParameters(); + + // when + JobExecution jobExecution = this.jobLauncher.run(this.job, jobParameters); + + // then + assertNotNull(jobExecution); + assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + assertTrue(jobExecution.getExecutionContext().containsKey(SpringBatchVersion.BATCH_VERSION_KEY)); + assertTrue(jobExecution.getStepExecutions().iterator().next().getExecutionContext() + .containsKey(SpringBatchVersion.BATCH_VERSION_KEY)); + } + + @Configuration + @EnableBatchProcessing + static class TestConfiguration { + + @Bean + public DataSource dataSource() { + return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.HSQL) + .addScript("/org/springframework/batch/core/schema-hsqldb.sql").generateUniqueName(true).build(); + } + + @Bean + public JdbcTransactionManager transactionManager(DataSource dataSource) { + return new JdbcTransactionManager(dataSource); + } + + @Bean + public Job job(JobRepository jobRepository, PlatformTransactionManager transactionManager) { + return new JobBuilder("job", jobRepository) + .start(new StepBuilder("step", jobRepository) + .tasklet((contribution, chunkContext) -> RepeatStatus.FINISHED, transactionManager).build()) + .build(); + } + + } + +} diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java index a5dee3cfb..c6665b758 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/SimpleJobTests.java @@ -214,7 +214,7 @@ class SimpleJobTests { assertNotNull(jobExecution.getEndTime()); assertNotNull(jobExecution.getStartTime()); - assertTrue(step1.passedInJobContext.isEmpty()); + assertEquals(1, step1.passedInJobContext.size()); assertFalse(step2.passedInJobContext.isEmpty()); // Observability @@ -442,7 +442,7 @@ class SimpleJobTests { Throwable e = jobExecution.getAllFailureExceptions().get(0); assertSame(exception, e); - assertTrue(step1.passedInJobContext.isEmpty()); + assertEquals(1, step1.passedInJobContext.size()); assertFalse(step2.passedInJobContext.isEmpty()); assertFalse(jobExecution.getExecutionContext().isEmpty()); 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 d869fdac9..44da78b2a 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 @@ -251,7 +251,7 @@ class TaskletStepExceptionTests { assertEquals(1, stepExecution.getRollbackCount()); // Failed transaction // counts as // rollback - assertEquals(2, stepExecution.getExecutionContext().size()); + assertEquals(3, stepExecution.getExecutionContext().size()); assertTrue(stepExecution.getExecutionContext().containsKey(Step.STEP_TYPE_KEY)); assertTrue(stepExecution.getExecutionContext().containsKey(TaskletStep.TASKLET_TYPE_KEY)); } @@ -286,7 +286,7 @@ class TaskletStepExceptionTests { assertEquals(1, stepExecution.getRollbackCount()); // Failed transaction // counts as // rollback - assertEquals(2, stepExecution.getExecutionContext().size()); + assertEquals(3, stepExecution.getExecutionContext().size()); assertTrue(stepExecution.getExecutionContext().containsKey(Step.STEP_TYPE_KEY)); assertTrue(stepExecution.getExecutionContext().containsKey(TaskletStep.TASKLET_TYPE_KEY)); }