diff --git a/README.md b/README.md index a8c87850fc..a7b15015e6 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ # Latest news +* August 20, 2025: [Spring Batch 6.0.0 M2 available now](https://spring.io/blog/2025/08/20/spring-batch-6) * July 23, 2025: [Spring Batch 6.0.0 M1 is out!](https://spring.io/blog/2025/07/23/spring-batch-6) * March 19, 2025: [Spring Batch 5.2.2 available now](https://spring.io/blog/2025/03/19/spring-batch-5-2-2-available-now) * December 18, 2024: [Spring Batch 5.1.3 and 5.2.1 available now](https://spring.io/blog/2024/12/18/spring-batch-5-1-3-and-5-2-1-available-now) * November 24, 2024: [Bootiful Spring Boot 3.4: Spring Batch](https://spring.io/blog/2024/11/24/bootiful-34-batch) -* November 20, 2024: [Spring Batch 5.2.0 goes GA!](https://spring.io/blog/2024/11/20/spring-batch-5-2-0-goes-ga) diff --git a/pom.xml b/pom.xml index 9bcd904abf..cbeb251c6c 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. - 6.0.0-M2 + 6.0.0-M3 pom https://projects.spring.io/spring-batch @@ -39,15 +39,6 @@ Github Actions https://github.com/spring-projects/spring-batch/actions - - - spring-snapshots - https://repo.spring.io/libs-snapshot-local - - false - - - Apache 2.0 @@ -61,19 +52,19 @@ 17 - 7.0.0-M8 + 7.0.0-M9 2.0.12 - 7.0.0-M2 - 1.16.0-M2 + 7.0.0-M3 + 1.16.0-M3 - 4.0.0-M5 - 4.0.0-M5 - 4.0.0-M5 - 5.0.0-M5 - 4.0.0-M4 - 4.0.0-M4 - 4.0.0-M2 + 4.0.0-M6 + 4.0.0-M6 + 4.0.0-M6 + 5.0.0-M6 + 4.0.0-M5 + 4.0.0-M5 + 4.0.0-M3 2.19.2 1.12.0 @@ -92,7 +83,7 @@ 3.0.2 - 1.6.0-M2 + 1.6.0-M3 1.4.21 4.13.2 @@ -189,45 +180,6 @@ --should-stop=ifError=FLOW -Xplugin:ErrorProne - -Xep:AlmostJavadoc:OFF - -Xep:ByteBufferBackingArray:OFF - -Xep:ClassCanBeStatic:OFF - -Xep:CollectionUndefinedEquality:OFF - -Xep:DefaultCharset:OFF - -Xep:DirectInvocationOnMock:OFF - -Xep:DoNotCallSuggester:OFF - -Xep:EmptyCatch:OFF - -Xep:EqualsGetClass:OFF - -Xep:Finally:OFF - -Xep:FutureReturnValueIgnored:OFF - -Xep:HidingField:OFF - -Xep:ImmutableEnumChecker:OFF - -Xep:InlineMeSuggester:OFF - -Xep:InputStreamSlowMultibyteRead:OFF - -Xep:JavaTimeDefaultTimeZone:OFF - -Xep:JavaUtilDate:OFF - -Xep:JdkObsolete:OFF - -Xep:MissingSummary:OFF - -Xep:MixedMutabilityReturnType:OFF - -Xep:MutablePublicArray:OFF - -Xep:NonAtomicVolatileUpdate:OFF - -Xep:RedundantControlFlow:OFF - -Xep:ReferenceEquality:OFF - -Xep:StaticAssignmentInConstructor:OFF - -Xep:StaticAssignmentOfThrowable:OFF - -Xep:StaticMockMember:OFF - -Xep:StreamResourceLeak:OFF - -Xep:StringCaseLocaleUsage:OFF - -Xep:StringSplitter:OFF - -Xep:SynchronizeOnNonFinalField:OFF - -Xep:ThreadLocalUsage:OFF - -Xep:ThreadPriorityCheck:OFF - -Xep:TypeParameterUnusedInFormals:OFF - -Xep:UndefinedEquals:OFF - -Xep:UnnecessaryStringBuilder:OFF - -Xep:UnusedMethod:OFF - -Xep:UnusedVariable:OFF - -Xep:WaitNotInLoop:OFF @@ -247,6 +199,7 @@ ${surefireArgLine} **/*IntegrationTests.java + **/*FunctionalTests.java @@ -257,6 +210,7 @@ **/*IntegrationTests.java + **/*FunctionalTests.java diff --git a/spring-batch-bom/pom.xml b/spring-batch-bom/pom.xml index 51b9491a83..7cd9f5cfc6 100644 --- a/spring-batch-bom/pom.xml +++ b/spring-batch-bom/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-bom pom diff --git a/spring-batch-core/pom.xml b/spring-batch-core/pom.xml index 8a61dd00f0..1a26efda0d 100644 --- a/spring-batch-core/pom.xml +++ b/spring-batch-core/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-core jar @@ -128,6 +128,7 @@ org.springframework.data spring-data-commons ${spring-data-commons.version} + true org.mongodb diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/BeforeChunk.java b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/BeforeChunk.java index 0a575eede7..d65910710f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/BeforeChunk.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/annotation/BeforeChunk.java @@ -21,7 +21,6 @@ import java.lang.annotation.Target; import org.springframework.batch.core.listener.ChunkListener; -import org.springframework.batch.core.scope.context.ChunkContext; import org.springframework.batch.item.Chunk; /** 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 fdea238b95..ece0498bc1 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 @@ -53,10 +53,6 @@ class BatchRegistrar implements ImportBeanDefinitionRegistrar { private static final String JOB_OPERATOR = "jobOperator"; - 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(); @@ -66,7 +62,6 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B .get(EnableBatchProcessing.class) .synthesize(); registerJobRepository(registry, importingClassMetadata); - registerJobRegistry(registry); registerJobOperator(registry, batchAnnotation); registerAutomaticJobRegistrar(registry, batchAnnotation); watch.stop(); @@ -206,17 +201,6 @@ private void registerDefaultJobRepository(BeanDefinitionRegistry registry) { registry.registerBeanDefinition(JOB_REPOSITORY, beanDefinitionBuilder.getBeanDefinition()); } - private void registerJobRegistry(BeanDefinitionRegistry registry) { - 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(JOB_REGISTRY, beanDefinition); - } - private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) { if (registry.containsBeanDefinition(JOB_OPERATOR)) { LOGGER.info("Bean jobOperator already defined in the application context, skipping" @@ -226,13 +210,19 @@ private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchPro BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder .genericBeanDefinition(JobOperatorFactoryBean.class); // set mandatory properties - String transactionManagerRef = batchAnnotation.transactionManagerRef(); - beanDefinitionBuilder.addPropertyReference("transactionManager", transactionManagerRef); - beanDefinitionBuilder.addPropertyReference(JOB_REPOSITORY, JOB_REPOSITORY); - beanDefinitionBuilder.addPropertyReference(JOB_REGISTRY, JOB_REGISTRY); // set optional properties + String jobRegistryRef = batchAnnotation.jobRegistryRef(); + if (registry.containsBeanDefinition(jobRegistryRef)) { + beanDefinitionBuilder.addPropertyReference("jobRegistry", jobRegistryRef); + } + + String transactionManagerRef = batchAnnotation.transactionManagerRef(); + if (registry.containsBeanDefinition(transactionManagerRef)) { + beanDefinitionBuilder.addPropertyReference("transactionManager", transactionManagerRef); + } + String taskExecutorRef = batchAnnotation.taskExecutorRef(); if (registry.containsBeanDefinition(taskExecutorRef)) { beanDefinitionBuilder.addPropertyReference("taskExecutor", taskExecutorRef); @@ -256,12 +246,12 @@ private void registerAutomaticJobRegistrar(BeanDefinitionRegistry registry, Enab return; } BeanDefinition jobLoaderBeanDefinition = BeanDefinitionBuilder.genericBeanDefinition(DefaultJobLoader.class) - .addPropertyReference(JOB_REGISTRY, JOB_REGISTRY) + .addPropertyValue("jobRegistry", new MapJobRegistry()) .getBeanDefinition(); - registry.registerBeanDefinition(JOB_LOADER, jobLoaderBeanDefinition); + registry.registerBeanDefinition("jobLoader", jobLoaderBeanDefinition); BeanDefinition jobRegistrarBeanDefinition = BeanDefinitionBuilder .genericBeanDefinition(AutomaticJobRegistrar.class) - .addPropertyReference(JOB_LOADER, JOB_LOADER) + .addPropertyReference("jobLoader", "jobLoader") .getBeanDefinition(); registry.registerBeanDefinition("jobRegistrar", jobRegistrarBeanDefinition); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java index a8341b553d..983780f50a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/EnableBatchProcessing.java @@ -15,7 +15,6 @@ */ package org.springframework.batch.core.configuration.annotation; -import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.support.ApplicationContextFactory; import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar; import org.springframework.batch.core.configuration.support.GroupAwareJob; @@ -76,8 +75,6 @@ *
    *
  • a {@link JobRepository} (bean name "jobRepository" of type * {@link org.springframework.batch.core.repository.support.ResourcelessJobRepository})
  • - *
  • a {@link JobRegistry} (bean name "jobRegistry" of type - * {@link org.springframework.batch.core.configuration.support.MapJobRegistry})
  • *
  • a {@link org.springframework.batch.core.launch.JobOperator} (bean name * "jobOperator" of type * {@link org.springframework.batch.core.launch.support.TaskExecutorJobOperator})
  • @@ -134,7 +131,7 @@ * * * +"org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> * * * @@ -174,6 +171,13 @@ */ String taskExecutorRef() default "taskExecutor"; + /** + * Set the job registry to use in the job operator. + * @return the bean name of the job registry to use. Defaults to + * {@literal jobRegistry} + */ + String jobRegistryRef() default "jobRegistry"; + /** * Set the transaction manager to use in the job operator. * @return the bean name of the transaction manager to use. Defaults to diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java index d1b6dc9cd6..c208b06715 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java @@ -15,9 +15,8 @@ */ package org.springframework.batch.core.configuration.support; -import org.springframework.batch.core.job.DefaultJobKeyGenerator; -import org.springframework.batch.core.job.JobInstance; -import org.springframework.batch.core.job.JobKeyGenerator; +import org.springframework.batch.core.configuration.DuplicateJobException; +import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.BatchConfigurationException; import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.converter.DefaultJobParametersConverter; @@ -37,7 +36,6 @@ import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.transaction.PlatformTransactionManager; -import org.springframework.transaction.annotation.Isolation; /** * Base {@link Configuration} class that provides common infrastructure beans for enabling @@ -48,7 +46,6 @@ * *
      *
    • a {@link ResourcelessJobRepository} named "jobRepository"
    • - *
    • a {@link MapJobRegistry} named "jobRegistry"
    • *
    • a {@link TaskExecutorJobOperator} named "jobOperator"
    • *
    • a {@link org.springframework.batch.core.scope.StepScope} named "stepScope"
    • *
    • a {@link org.springframework.batch.core.scope.JobScope} named "jobScope"
    • @@ -93,16 +90,10 @@ public JobRepository jobRepository() { } @Bean - public JobRegistry jobRegistry() { - return new MapJobRegistry(); - } - - @Bean - public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegistry) - throws BatchConfigurationException { + public JobOperator jobOperator(JobRepository jobRepository) throws BatchConfigurationException { JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean(); jobOperatorFactoryBean.setJobRepository(jobRepository); - jobOperatorFactoryBean.setJobRegistry(jobRegistry); + jobOperatorFactoryBean.setJobRegistry(getJobRegistry()); jobOperatorFactoryBean.setTransactionManager(getTransactionManager()); jobOperatorFactoryBean.setJobParametersConverter(getJobParametersConverter()); jobOperatorFactoryBean.setTaskExecutor(getTaskExecutor()); @@ -115,6 +106,19 @@ public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegis } } + protected JobRegistry getJobRegistry() { + MapJobRegistry jobRegistry = new MapJobRegistry(); + this.applicationContext.getBeansOfType(Job.class).values().forEach(job -> { + try { + jobRegistry.register(job); + } + catch (DuplicateJobException e) { + throw new BatchConfigurationException(e); + } + }); + return jobRegistry; + } + /** * Return the transaction manager to use for the job operator. Defaults to * {@link ResourcelessTransactionManager}. @@ -145,33 +149,4 @@ protected JobParametersConverter getJobParametersConverter() { return new DefaultJobParametersConverter(); } - /** - * Return the value of the {@code validateTransactionState} parameter. Defaults to - * {@code true}. - * @return true if the transaction state should be validated, false otherwise - */ - protected boolean getValidateTransactionState() { - return true; - } - - /** - * Return the transaction isolation level when creating job executions. Defaults to - * {@link Isolation#SERIALIZABLE}. - * @return the transaction isolation level when creating job executions - */ - protected Isolation getIsolationLevelForCreate() { - return Isolation.SERIALIZABLE; - } - - /** - * A custom implementation of the {@link JobKeyGenerator}. The default, if not - * injected, is the {@link DefaultJobKeyGenerator}. - * @return the generator that creates the key used in identifying {@link JobInstance} - * objects - * @since 5.1 - */ - protected JobKeyGenerator getJobKeyGenerator() { - return new DefaultJobKeyGenerator(); - } - } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JdbcDefaultBatchConfiguration.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JdbcDefaultBatchConfiguration.java index 172cb98809..2b3cc40e15 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JdbcDefaultBatchConfiguration.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JdbcDefaultBatchConfiguration.java @@ -16,7 +16,6 @@ package org.springframework.batch.core.configuration.support; import org.springframework.batch.core.configuration.BatchConfigurationException; -import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.converter.DateToStringConverter; import org.springframework.batch.core.converter.LocalDateTimeToStringConverter; import org.springframework.batch.core.converter.LocalDateToStringConverter; @@ -25,6 +24,9 @@ import org.springframework.batch.core.converter.StringToLocalDateConverter; import org.springframework.batch.core.converter.StringToLocalDateTimeConverter; import org.springframework.batch.core.converter.StringToLocalTimeConverter; +import org.springframework.batch.core.job.DefaultJobKeyGenerator; +import org.springframework.batch.core.job.JobInstance; +import org.springframework.batch.core.job.JobKeyGenerator; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.ExecutionContextSerializer; import org.springframework.batch.core.repository.JobRepository; @@ -46,6 +48,7 @@ import org.springframework.jdbc.support.MetaDataAccessException; import org.springframework.jdbc.support.incrementer.DataFieldMaxValueIncrementer; import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.annotation.Isolation; import javax.sql.DataSource; import java.nio.charset.Charset; @@ -61,7 +64,6 @@ * *
        *
      • a {@link JobRepository} named "jobRepository"
      • - *
      • a {@link JobRegistry} named "jobRegistry"
      • *
      • a {@link JobOperator} named "jobOperator"
      • *
      • a {@link org.springframework.batch.core.scope.StepScope} named "stepScope"
      • *
      • a {@link org.springframework.batch.core.scope.JobScope} named "jobScope"
      • @@ -257,4 +259,33 @@ protected ConfigurableConversionService getConversionService() { return conversionService; } + /** + * Return the value of the {@code validateTransactionState} parameter. Defaults to + * {@code true}. + * @return true if the transaction state should be validated, false otherwise + */ + protected boolean getValidateTransactionState() { + return true; + } + + /** + * Return the transaction isolation level when creating job executions. Defaults to + * {@link Isolation#SERIALIZABLE}. + * @return the transaction isolation level when creating job executions + */ + protected Isolation getIsolationLevelForCreate() { + return Isolation.SERIALIZABLE; + } + + /** + * A custom implementation of the {@link JobKeyGenerator}. The default, if not + * injected, is the {@link DefaultJobKeyGenerator}. + * @return the generator that creates the key used in identifying {@link JobInstance} + * objects + * @since 5.1 + */ + protected JobKeyGenerator getJobKeyGenerator() { + return new DefaultJobKeyGenerator(); + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/MongoDefaultBatchConfiguration.java b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/MongoDefaultBatchConfiguration.java index 7f5cbb25e2..fdb18c63d3 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/MongoDefaultBatchConfiguration.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/MongoDefaultBatchConfiguration.java @@ -16,7 +16,9 @@ package org.springframework.batch.core.configuration.support; import org.springframework.batch.core.configuration.BatchConfigurationException; -import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.job.DefaultJobKeyGenerator; +import org.springframework.batch.core.job.JobInstance; +import org.springframework.batch.core.job.JobKeyGenerator; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.MongoJobRepositoryFactoryBean; @@ -24,6 +26,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.MongoTransactionManager; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.transaction.annotation.Isolation; /** * Base {@link Configuration} class that provides common MongoDB-based infrastructure @@ -34,7 +37,6 @@ * *
          *
        • a {@link JobRepository} named "jobRepository"
        • - *
        • a {@link JobRegistry} named "jobRegistry"
        • *
        • a {@link JobOperator} named "jobOperator"
        • *
        • a {@link org.springframework.batch.core.scope.StepScope} named "stepScope"
        • *
        • a {@link org.springframework.batch.core.scope.JobScope} named "jobScope"
        • @@ -117,4 +119,33 @@ protected MongoTransactionManager getTransactionManager() { return this.applicationContext.getBean("transactionManager", MongoTransactionManager.class); } + /** + * Return the value of the {@code validateTransactionState} parameter. Defaults to + * {@code true}. + * @return true if the transaction state should be validated, false otherwise + */ + protected boolean getValidateTransactionState() { + return true; + } + + /** + * Return the transaction isolation level when creating job executions. Defaults to + * {@link Isolation#SERIALIZABLE}. + * @return the transaction isolation level when creating job executions + */ + protected Isolation getIsolationLevelForCreate() { + return Isolation.SERIALIZABLE; + } + + /** + * A custom implementation of the {@link JobKeyGenerator}. The default, if not + * injected, is the {@link DefaultJobKeyGenerator}. + * @return the generator that creates the key used in identifying {@link JobInstance} + * objects + * @since 5.1 + */ + protected JobKeyGenerator getJobKeyGenerator() { + return new DefaultJobKeyGenerator(); + } + } 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 7c60e2da7a..9ca23315f4 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 @@ -37,6 +37,7 @@ import org.springframework.batch.core.job.parameters.JobParametersValidator; import org.springframework.batch.core.listener.JobExecutionListener; import org.springframework.batch.core.SpringBatchVersion; +import org.springframework.batch.core.observability.jfr.events.job.JobExecutionEvent; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.launch.NoSuchJobException; @@ -277,6 +278,9 @@ public final void execute(JobExecution execution) { } JobSynchronizationManager.register(execution); + JobExecutionEvent jobExecutionEvent = new JobExecutionEvent(execution.getJobInstance().getJobName(), + execution.getJobInstance().getId(), execution.getId()); + jobExecutionEvent.begin(); String activeJobMeterName = "job.active"; LongTaskTimer longTaskTimer = BatchMetrics.createLongTaskTimer(this.meterRegistry, activeJobMeterName, "Active jobs", Tag.of(BatchMetrics.METRICS_PREFIX + activeJobMeterName + ".name", @@ -349,6 +353,8 @@ public final void execute(JobExecution execution) { execution.setExitStatus(exitStatus.and(newExitStatus)); } stopObservation(execution, observation); + jobExecutionEvent.exitStatus = execution.getExitStatus().getExitCode(); + jobExecutionEvent.commit(); longTaskTimerSample.stop(); execution.setEndTime(LocalDateTime.now()); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/job/Job.java b/spring-batch-core/src/main/java/org/springframework/batch/core/job/Job.java index 80fdd5583b..22c91a5651 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/job/Job.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/job/Job.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,9 +28,18 @@ * @author Dave Syer * @author Mahmoud Ben Hassine */ +@FunctionalInterface public interface Job { - String getName(); + /** + * The name of the job. This is used to distinguish between different jobs and must be + * unique within the job repository. If not explicitly set, the name will default to + * the fully qualified class name. + * @return the name of the job (never {@code null}) + */ + default String getName() { + return this.getClass().getName(); + } /** * Flag to indicate if this job can be restarted, at least in principle. diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBean.java index 648e0ac85b..82c50dd82d 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBean.java @@ -22,14 +22,22 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.framework.ProxyFactory; +import org.springframework.batch.core.configuration.BatchConfigurationException; +import org.springframework.batch.core.configuration.DuplicateJobException; import org.springframework.batch.core.configuration.JobRegistry; +import org.springframework.batch.core.configuration.support.MapJobRegistry; import org.springframework.batch.core.converter.DefaultJobParametersConverter; import org.springframework.batch.core.converter.JobParametersConverter; +import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.beans.BeansException; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; import org.springframework.core.task.SyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.transaction.PlatformTransactionManager; @@ -49,10 +57,12 @@ * @author Mahmoud Ben Hassine * @since 5.0 */ -public class JobOperatorFactoryBean implements FactoryBean, InitializingBean { +public class JobOperatorFactoryBean implements FactoryBean, ApplicationContextAware, InitializingBean { protected static final Log logger = LogFactory.getLog(JobOperatorFactoryBean.class); + private ApplicationContext applicationContext; + private PlatformTransactionManager transactionManager; private TransactionAttributeSource transactionAttributeSource; @@ -69,30 +79,44 @@ public class JobOperatorFactoryBean implements FactoryBean, Initial private final ProxyFactory proxyFactory = new ProxyFactory(); + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + @Override public void afterPropertiesSet() throws Exception { Assert.notNull(this.jobRepository, "JobRepository must not be null"); - Assert.notNull(this.jobRegistry, "JobRegistry must not be null"); - Assert.notNull(this.transactionManager, "TransactionManager must not be null"); + if (this.jobRegistry == null) { + this.jobRegistry = new MapJobRegistry(); + populateJobRegistry(); + logger.info( + "No JobRegistry has been set, defaulting to a MapJobRegistry populated with jobs defined in the application context."); + } + if (this.transactionManager == null) { + this.transactionManager = new ResourcelessTransactionManager(); + logger.info("No transaction manager has been set, defaulting to ResourcelessTransactionManager."); + } if (this.taskExecutor == null) { logger.info("No TaskExecutor has been set, defaulting to synchronous executor."); this.taskExecutor = new SyncTaskExecutor(); } if (this.transactionAttributeSource == null) { - this.transactionAttributeSource = new MethodMapTransactionAttributeSource(); - DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); - Method stopMethod = TaskExecutorJobOperator.class.getMethod("stop", JobExecution.class); - Method abandonMethod = TaskExecutorJobOperator.class.getMethod("abandon", JobExecution.class); - Method recoverMethod = TaskExecutorJobOperator.class.getMethod("recover", JobExecution.class); - ((MethodMapTransactionAttributeSource) this.transactionAttributeSource).addTransactionalMethod(stopMethod, - transactionAttribute); - ((MethodMapTransactionAttributeSource) this.transactionAttributeSource) - .addTransactionalMethod(abandonMethod, transactionAttribute); - ((MethodMapTransactionAttributeSource) this.transactionAttributeSource) - .addTransactionalMethod(recoverMethod, transactionAttribute); + this.transactionAttributeSource = new DefaultJobOperatorTransactionAttributeSource(); } } + private void populateJobRegistry() { + this.applicationContext.getBeansOfType(Job.class).values().forEach(job -> { + try { + jobRegistry.register(job); + } + catch (DuplicateJobException e) { + throw new BatchConfigurationException(e); + } + }); + } + /** * Setter for the job registry. * @param jobRegistry the job registry to set @@ -189,4 +213,24 @@ private TaskExecutorJobOperator getTarget() throws Exception { return taskExecutorJobOperator; } + private static class DefaultJobOperatorTransactionAttributeSource extends MethodMapTransactionAttributeSource { + + public DefaultJobOperatorTransactionAttributeSource() { + DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); + try { + Method stopMethod = TaskExecutorJobOperator.class.getMethod("stop", JobExecution.class); + Method abandonMethod = TaskExecutorJobOperator.class.getMethod("abandon", JobExecution.class); + Method recoverMethod = TaskExecutorJobOperator.class.getMethod("recover", JobExecution.class); + addTransactionalMethod(stopMethod, transactionAttribute); + addTransactionalMethod(abandonMethod, transactionAttribute); + addTransactionalMethod(recoverMethod, transactionAttribute); + } + catch (NoSuchMethodException e) { + throw new IllegalStateException("Failed to initialize default transaction attributes for JobOperator", + e); + } + } + + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java index 047c5dd6d5..db0c1a351a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java @@ -36,6 +36,7 @@ import org.springframework.batch.core.job.parameters.JobParametersIncrementer; import org.springframework.batch.core.job.parameters.JobParametersInvalidException; import org.springframework.batch.core.step.Step; +import org.springframework.batch.core.step.StoppableStep; import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.job.UnexpectedJobExecutionException; import org.springframework.batch.core.configuration.JobRegistry; @@ -81,6 +82,7 @@ * @author Mahmoud Ben Hassine * @author Andrey Litvitski * @author Yejeong Ham + * @author Hyunsang Han * @since 2.0 * @deprecated since 6.0 in favor of {@link TaskExecutorJobOperator}. Scheduled for * removal in 6.2 or later. @@ -347,10 +349,15 @@ public boolean stop(JobExecution jobExecution) throws JobExecutionNotRunningExce Tasklet tasklet = taskletStep.getTasklet(); if (tasklet instanceof StoppableTasklet stoppableTasklet) { StepSynchronizationManager.register(stepExecution); - stoppableTasklet.stop(); + stoppableTasklet.stop(stepExecution); StepSynchronizationManager.release(); } } + if (step instanceof StoppableStep stoppableStep) { + StepSynchronizationManager.register(stepExecution); + stoppableStep.stop(stepExecution); + StepSynchronizationManager.release(); + } } catch (NoSuchStepException e) { logger.warn("Step not found", e); @@ -360,7 +367,9 @@ public boolean stop(JobExecution jobExecution) throws JobExecutionNotRunningExce } } catch (NoSuchJobException e) { - logger.warn("Cannot find Job object in the job registry. StoppableTasklet#stop() will not be called", e); + logger.warn( + "Cannot find Job object in the job registry. StoppableTasklet#stop(StepExecution stepExecution) will not be called", + e); } return true; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperator.java index 6434a22cfd..48bb126b5b 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperator.java @@ -27,6 +27,7 @@ import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.NoSuchJobException; import org.springframework.batch.core.launch.NoSuchJobExecutionException; +import org.springframework.batch.core.observability.jfr.events.job.JobLaunchEvent; import org.springframework.batch.core.repository.JobExecutionAlreadyRunningException; import org.springframework.batch.core.repository.JobInstanceAlreadyCompleteException; import org.springframework.batch.core.repository.JobRepository; @@ -92,6 +93,7 @@ public JobExecution start(Job job, JobParameters jobParameters) JobRestartException, JobParametersInvalidException { Assert.notNull(job, "Job must not be null"); Assert.notNull(jobParameters, "JobParameters must not be null"); + new JobLaunchEvent(job.getName(), jobParameters.toString()).commit(); return super.start(job, jobParameters); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListener.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListener.java index 01a4c721f3..45b91756db 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListener.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/ChunkListener.java @@ -24,6 +24,9 @@ *

          * {@link ChunkListener} shouldn't throw exceptions and expect continued processing, they * must be handled in the implementation or the step will terminate. + *

          + * Note: This listener is not called in concurrent steps. + *

          * * @author Lucas Ward * @author Michael Minella @@ -78,14 +81,16 @@ default void afterChunkError(ChunkContext context) { } /** - * Callback before the chunk is processed, inside the transaction. + * Callback before the chunk is processed, inside the transaction. This method is not + * called in concurrent steps. * @since 6.0 */ default void beforeChunk(Chunk chunk) { } /** - * Callback after the chunk is written, inside the transaction. + * Callback after the chunk is written, inside the transaction. This method is not + * called in concurrent steps. * @since 6.0 */ default void afterChunk(Chunk chunk) { @@ -95,7 +100,7 @@ default void afterChunk(Chunk chunk) { * Callback if an exception occurs while processing or writing a chunk, inside the * transaction, which is about to be rolled back. As a result, you should use * {@code PROPAGATION_REQUIRES_NEW} for any transactional operation that is called - * here. + * here. This method is not called in concurrent steps. * @param exception the exception that caused the underlying rollback. * @param chunk the processed chunk * @since 6.0 diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java index 7ceff8a96f..4fb5aad1b1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/listener/StepListenerMetaData.java @@ -46,6 +46,7 @@ * methods, their interfaces, annotation, and expected arguments. * * @author Lucas Ward + * @author Hyunsang Han * @since 2.0 * @see StepListenerFactoryBean */ @@ -53,8 +54,8 @@ public enum StepListenerMetaData implements ListenerMetaData { BEFORE_STEP("beforeStep", "before-step-method", BeforeStep.class, StepExecutionListener.class, StepExecution.class), AFTER_STEP("afterStep", "after-step-method", AfterStep.class, StepExecutionListener.class, StepExecution.class), - BEFORE_CHUNK("beforeChunk", "before-chunk-method", BeforeChunk.class, ChunkListener.class, ChunkContext.class), - AFTER_CHUNK("afterChunk", "after-chunk-method", AfterChunk.class, ChunkListener.class, ChunkContext.class), + BEFORE_CHUNK("beforeChunk", "before-chunk-method", BeforeChunk.class, ChunkListener.class, Chunk.class), + AFTER_CHUNK("afterChunk", "after-chunk-method", AfterChunk.class, ChunkListener.class, Chunk.class), AFTER_CHUNK_ERROR("afterChunkError", "after-chunk-error-method", AfterChunkError.class, ChunkListener.class, ChunkContext.class), BEFORE_READ("beforeRead", "before-read-method", BeforeRead.class, ItemReadListener.class), diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/BatchMetrics.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/BatchMetrics.java index 7e5a9e7595..6884dac28a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/BatchMetrics.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/BatchMetrics.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -55,6 +55,10 @@ public final class BatchMetrics { public static final String STATUS_FAILURE = "FAILURE"; + public static final String STATUS_COMMITTED = "COMMITTED"; + + public static final String STATUS_ROLLED_BACK = "ROLLED_BACK"; + private BatchMetrics() { } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobExecutionEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobExecutionEvent.java new file mode 100644 index 0000000000..416c524444 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobExecutionEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.job; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Job Execution") +@Description("Job Execution Event") +@Category({ "Spring Batch", "Job" }) +public class JobExecutionEvent extends Event { + + @Label("Job Name") + public String jobName; + + @Label("Job Instance Id") + public long jobInstanceId; + + @Label("Job Execution Id") + public long jobExecutionId; + + @Label("Job Exit Status") + public String exitStatus; + + public JobExecutionEvent(String jobName, long jobInstanceId, long jobExecutionId) { + this.jobName = jobName; + this.jobInstanceId = jobInstanceId; + this.jobExecutionId = jobExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobLaunchEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobLaunchEvent.java new file mode 100644 index 0000000000..269d099196 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/job/JobLaunchEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.job; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Job Launch Request") +@Description("Job Launch Request Event") +@Category({ "Spring Batch", "Job" }) +public class JobLaunchEvent extends Event { + + @Label("Job Name") + public String jobName; + + @Label("Job Parameters") + public String jobParameters; + + public JobLaunchEvent(String jobName, String jobParameters) { + this.jobParameters = jobParameters; + this.jobName = jobName; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/StepExecutionEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/StepExecutionEvent.java new file mode 100644 index 0000000000..6e7784fc79 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/StepExecutionEvent.java @@ -0,0 +1,50 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Step Execution") +@Description("Step Execution Event") +@Category({ "Spring Batch", "Step" }) +public class StepExecutionEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Job Name") + public String jobName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Job Execution Id") + public long jobExecutionId; + + @Label("Step Exit Status") + public String exitStatus; + + public StepExecutionEvent(String stepName, String jobName, long stepExecutionId, long jobExecutionId) { + this.stepName = stepName; + this.jobName = jobName; + this.stepExecutionId = stepExecutionId; + this.jobExecutionId = jobExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkScanEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkScanEvent.java new file mode 100644 index 0000000000..d5d11e7d1b --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkScanEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.chunk; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Chunk Scan") +@Description("Chunk Scan Event") +@Category({ "Spring Batch", "Step", "Chunk" }) +public class ChunkScanEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Skip Count") + public long skipCount; + + public ChunkScanEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkTransactionEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkTransactionEvent.java new file mode 100644 index 0000000000..695f3afcfa --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkTransactionEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.chunk; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Chunk Transaction") +@Description("Chunk Transaction Event") +@Category({ "Spring Batch", "Step", "Chunk" }) +public class ChunkTransactionEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Transaction Status") + public String transactionStatus; + + public ChunkTransactionEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkWriteEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkWriteEvent.java new file mode 100644 index 0000000000..6139abb60b --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ChunkWriteEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.chunk; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Chunk Write") +@Description("Chunk Write Event") +@Category({ "Spring Batch", "Step", "Chunk" }) +public class ChunkWriteEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Chunk Write Status") + public String chunkWriteStatus; + + @Label("Item Count") + public long itemCount; + + public ChunkWriteEvent(String stepName, long stepExecutionId, long itemCount) { + this.itemCount = itemCount; + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemProcessEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemProcessEvent.java new file mode 100644 index 0000000000..358794dcff --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemProcessEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.chunk; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Item Process") +@Description("Item Process Event") +@Category({ "Spring Batch", "Step", "Chunk" }) +public class ItemProcessEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Item Process Status") + public String itemProcessStatus; + + public ItemProcessEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemReadEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemReadEvent.java new file mode 100644 index 0000000000..5e55c0de3d --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/chunk/ItemReadEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.chunk; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Item Read") +@Description("Item Read Event") +@Category({ "Spring Batch", "Step", "Chunk" }) +public class ItemReadEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Item Read Status") + public String itemReadStatus; + + public ItemReadEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionAggregateEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionAggregateEvent.java new file mode 100644 index 0000000000..f516b08de1 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionAggregateEvent.java @@ -0,0 +1,39 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.partition; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Partition Aggregate") +@Description("Partition Aggregate Event") +@Category({ "Spring Batch", "Step", "Partition" }) +public class PartitionAggregateEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + public PartitionAggregateEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionSplitEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionSplitEvent.java new file mode 100644 index 0000000000..26504edc2f --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/partition/PartitionSplitEvent.java @@ -0,0 +1,42 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.partition; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Partition Split") +@Description("Partition Split Event") +@Category({ "Spring Batch", "Step", "Partition" }) +public class PartitionSplitEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Partition count") + public long partitionCount; + + public PartitionSplitEvent(String stepName, long stepExecutionId) { + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/tasklet/TaskletExecutionEvent.java b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/tasklet/TaskletExecutionEvent.java new file mode 100644 index 0000000000..97fbb7b6d7 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/observability/jfr/events/step/tasklet/TaskletExecutionEvent.java @@ -0,0 +1,46 @@ +/* + * Copyright 2025-present 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.observability.jfr.events.step.tasklet; + +import jdk.jfr.Category; +import jdk.jfr.Description; +import jdk.jfr.Event; +import jdk.jfr.Label; + +@Label("Tasklet Execution") +@Description("Tasklet Execution Event") +@Category({ "Spring Batch", "Step", "Tasklet" }) +public class TaskletExecutionEvent extends Event { + + @Label("Step Name") + public String stepName; + + @Label("Step Execution Id") + public long stepExecutionId; + + @Label("Tasklet Type") + public String taskletType; + + @Label("Tasklet Status") + public String taskletStatus; + + public TaskletExecutionEvent(String stepName, long stepExecutionId, String taskletType) { + this.taskletType = taskletType; + this.stepName = stepName; + this.stepExecutionId = stepExecutionId; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/partition/PartitionStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/partition/PartitionStep.java index 104d48e995..5600cab5f1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/partition/PartitionStep.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/partition/PartitionStep.java @@ -18,6 +18,8 @@ import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.job.JobExecutionException; +import org.springframework.batch.core.observability.jfr.events.step.partition.PartitionAggregateEvent; +import org.springframework.batch.core.observability.jfr.events.step.partition.PartitionSplitEvent; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.partition.support.DefaultStepExecutionAggregator; @@ -97,10 +99,21 @@ public void afterPropertiesSet() throws Exception { protected void doExecute(StepExecution stepExecution) throws Exception { stepExecution.getExecutionContext().put(STEP_TYPE_KEY, this.getClass().getName()); - // Wait for task completion and then aggregate the results + // Split execution into partitions and wait for task completion + PartitionSplitEvent partitionSplitEvent = new PartitionSplitEvent(stepExecution.getStepName(), + stepExecution.getId()); + partitionSplitEvent.begin(); Collection executions = partitionHandler.handle(stepExecutionSplitter, stepExecution); + partitionSplitEvent.partitionCount = executions.size(); stepExecution.upgradeStatus(BatchStatus.COMPLETED); + partitionSplitEvent.commit(); + + // aggregate the results of the executions + PartitionAggregateEvent partitionAggregateEvent = new PartitionAggregateEvent(stepExecution.getStepName(), + stepExecution.getId()); + partitionAggregateEvent.begin(); stepExecutionAggregator.aggregate(stepExecution, executions); + partitionAggregateEvent.commit(); // If anything failed or had a problem we need to crap out if (stepExecution.getStatus().isUnsuccessful()) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/AbstractJobRepositoryFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/AbstractJobRepositoryFactoryBean.java index 1d304dba63..50a5ef4186 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/AbstractJobRepositoryFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/AbstractJobRepositoryFactoryBean.java @@ -196,15 +196,8 @@ public void afterPropertiesSet() throws Exception { jobKeyGenerator = new DefaultJobKeyGenerator(); } if (this.transactionAttributeSource == null) { - Properties transactionAttributes = new Properties(); - transactionAttributes.setProperty("create*", - TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + this.isolationLevelForCreate); - transactionAttributes.setProperty("getLastJobExecution*", - TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + this.isolationLevelForCreate); - transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED"); - this.transactionAttributeSource = new NameMatchTransactionAttributeSource(); - ((NameMatchTransactionAttributeSource) this.transactionAttributeSource) - .setProperties(transactionAttributes); + this.transactionAttributeSource = new DefaultJobRepositoryTransactionAttributeSource( + this.isolationLevelForCreate); } } @@ -237,4 +230,18 @@ private Object getTarget() throws Exception { createExecutionContextDao()); } + private static class DefaultJobRepositoryTransactionAttributeSource extends NameMatchTransactionAttributeSource { + + public DefaultJobRepositoryTransactionAttributeSource(String isolationLevelForCreate) { + Properties transactionAttributes = new Properties(); + transactionAttributes.setProperty("create*", + TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + isolationLevelForCreate); + transactionAttributes.setProperty("getLastJobExecution*", + TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + isolationLevelForCreate); + transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED"); + this.setProperties(transactionAttributes); + } + + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java index 71347c3ca9..694cd471a7 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/ResourcelessJobRepository.java @@ -16,7 +16,6 @@ package org.springframework.batch.core.repository.support; import java.time.LocalDateTime; -import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -27,6 +26,7 @@ import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; +import org.springframework.lang.Nullable; /** * A {@link JobRepository} implementation that does not use or store batch meta-data. It @@ -50,6 +50,14 @@ public class ResourcelessJobRepository implements JobRepository { private JobExecution jobExecution; + private long stepExecutionIdIncrementer = 0L; + + /* + * =================================================================================== + * Job operations + * =================================================================================== + */ + @Override public List getJobNames() { if (this.jobInstance == null) { @@ -58,8 +66,41 @@ public List getJobNames() { return Collections.singletonList(this.jobInstance.getJobName()); } + /* + * =================================================================================== + * Job instance operations + * =================================================================================== + */ + + @Override + public List getJobInstances(String jobName, int start, int count) { + if (this.jobInstance == null) { + return Collections.emptyList(); + } + return Collections.singletonList(this.jobInstance); + } + + @Override + @Nullable + public JobInstance getJobInstance(@Nullable Long instanceId) { + return this.jobInstance; + } + + @Override + @Nullable + public JobInstance getLastJobInstance(String jobName) { + return this.jobInstance; + } + + @Override + @Nullable + public JobInstance getJobInstance(String jobName, JobParameters jobParameters) { + return this.jobInstance; + } + @SuppressWarnings("removal") @Override + @Deprecated(since = "6.0", forRemoval = true) public boolean isJobInstanceExists(String jobName, JobParameters jobParameters) { return false; } @@ -75,42 +116,51 @@ public JobInstance createJobInstance(String jobName, JobParameters jobParameters return this.jobInstance; } + /* + * =================================================================================== + * Job execution operations + * =================================================================================== + */ + @Override - public JobExecution createJobExecution(String jobName, JobParameters jobParameters) { - if (this.jobInstance == null) { - createJobInstance(jobName, jobParameters); - } - this.jobExecution = new JobExecution(this.jobInstance, 1L, jobParameters); + @Nullable + public JobExecution getJobExecution(Long executionId) { return this.jobExecution; } @Override - public void update(JobExecution jobExecution) { - jobExecution.setLastUpdated(LocalDateTime.now()); - this.jobExecution = jobExecution; + @Nullable + public JobExecution getLastJobExecution(String jobName, JobParameters jobParameters) { + return this.jobExecution; } @Override - public void add(StepExecution stepExecution) { - this.addAll(Collections.singletonList(stepExecution)); + @Nullable + public JobExecution getLastJobExecution(JobInstance jobInstance) { + return this.jobExecution; } @Override - public void addAll(Collection stepExecutions) { - this.jobExecution.addStepExecutions(new ArrayList<>(stepExecutions)); + public List getJobExecutions(JobInstance jobInstance) { + if (this.jobExecution == null) { + return Collections.emptyList(); + } + return Collections.singletonList(this.jobExecution); } @Override - public void update(StepExecution stepExecution) { - stepExecution.setLastUpdated(LocalDateTime.now()); - if (this.jobExecution.isStopping()) { - stepExecution.setTerminateOnly(); + public JobExecution createJobExecution(String jobName, JobParameters jobParameters) { + if (this.jobInstance == null) { + createJobInstance(jobName, jobParameters); } + this.jobExecution = new JobExecution(this.jobInstance, 1L, jobParameters); + return this.jobExecution; } @Override - public void updateExecutionContext(StepExecution stepExecution) { - stepExecution.setLastUpdated(LocalDateTime.now()); + public void update(JobExecution jobExecution) { + jobExecution.setLastUpdated(LocalDateTime.now()); + this.jobExecution = jobExecution; } @Override @@ -118,8 +168,31 @@ public void updateExecutionContext(JobExecution jobExecution) { jobExecution.setLastUpdated(LocalDateTime.now()); } + /* + * =================================================================================== + * Step execution operations + * =================================================================================== + */ + @Override + @Nullable + public StepExecution getStepExecution(Long jobExecutionId, Long stepExecutionId) { + if (this.jobExecution == null || !this.jobExecution.getId().equals(jobExecutionId)) { + return null; + } + return this.jobExecution.getStepExecutions() + .stream() + .filter(stepExecution -> stepExecution.getId().equals(stepExecutionId)) + .findFirst() + .orElse(null); + } + + @Override + @Nullable public StepExecution getLastStepExecution(JobInstance jobInstance, String stepName) { + if (this.jobExecution == null || !this.jobExecution.getJobInstance().getId().equals(jobInstance.getId())) { + return null; + } return this.jobExecution.getStepExecutions() .stream() .filter(stepExecution -> stepExecution.getStepName().equals(stepName)) @@ -136,8 +209,28 @@ public long getStepExecutionCount(JobInstance jobInstance, String stepName) { } @Override - public JobExecution getLastJobExecution(String jobName, JobParameters jobParameters) { - return this.jobExecution; + public void add(StepExecution stepExecution) { + stepExecution.setId(this.stepExecutionIdIncrementer++); + } + + @Override + public void addAll(Collection stepExecutions) { + for (StepExecution stepExecution : stepExecutions) { + this.add(stepExecution); + } + } + + @Override + public void update(StepExecution stepExecution) { + stepExecution.setLastUpdated(LocalDateTime.now()); + if (this.jobExecution.isStopping()) { + stepExecution.setTerminateOnly(); + } + } + + @Override + public void updateExecutionContext(StepExecution stepExecution) { + stepExecution.setLastUpdated(LocalDateTime.now()); } } \ No newline at end of file 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 ba83296433..bcc16338fd 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 @@ -42,6 +42,7 @@ import org.springframework.batch.core.observability.BatchStepObservation; import org.springframework.batch.core.observability.BatchStepObservationConvention; import org.springframework.batch.core.observability.DefaultBatchStepObservationConvention; +import org.springframework.batch.core.observability.jfr.events.step.StepExecutionEvent; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.scope.context.StepSynchronizationManager; import org.springframework.batch.item.ExecutionContext; @@ -63,7 +64,7 @@ * @author Mahmoud Ben Hassine * @author Jinwoo Bae */ -public abstract class AbstractStep implements Step, InitializingBean, BeanNameAware { +public abstract class AbstractStep implements StoppableStep, InitializingBean, BeanNameAware { private static final Log logger = LogFactory.getLog(AbstractStep.class); @@ -200,11 +201,17 @@ public final void execute(StepExecution stepExecution) throws JobInterruptedException, UnexpectedJobExecutionException { Assert.notNull(stepExecution, "stepExecution must not be null"); + Assert.state(stepExecution.getId() != null, + "StepExecution has no id. It must be saved before it can be executed."); stepExecution.getExecutionContext().put(SpringBatchVersion.BATCH_VERSION_KEY, SpringBatchVersion.getVersion()); if (logger.isDebugEnabled()) { logger.debug("Executing: id=" + stepExecution.getId()); } + StepExecutionEvent stepExecutionEvent = new StepExecutionEvent(stepExecution.getStepName(), + stepExecution.getJobExecution().getJobInstance().getJobName(), stepExecution.getId(), + stepExecution.getJobExecutionId()); + stepExecutionEvent.begin(); stepExecution.setStartTime(LocalDateTime.now()); stepExecution.setStatus(BatchStatus.STARTED); Observation observation = BatchMetrics @@ -291,6 +298,8 @@ public final void execute(StepExecution stepExecution) + "This job is now in an unknown state and should not be restarted.", name, stepExecution.getJobExecution().getJobInstance().getJobName()), e); } + stepExecutionEvent.exitStatus = stepExecution.getExitStatus().getExitCode(); + stepExecutionEvent.commit(); stopObservation(stepExecution, observation); stepExecution.setExitStatus(exitStatus); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/Step.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/Step.java index 071560b3cb..03447ddb0e 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/Step.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/Step.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. @@ -27,6 +27,7 @@ * @author Mahmoud Ben Hassine * */ +@FunctionalInterface public interface Step { /** @@ -35,9 +36,14 @@ public interface Step { String STEP_TYPE_KEY = "batch.stepType"; /** - * @return the name of this step. + * The name of the step. This is used to distinguish between different steps and must + * be unique within a job. If not explicitly set, the name will default to the fully + * qualified class name. + * @return the name of the step (never {@code null}) */ - String getName(); + default String getName() { + return this.getClass().getName(); + } /** * @return {@code true} if a step that is already marked as complete can be started diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/StepContribution.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/StepContribution.java index 340505c964..ae703f9277 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/StepContribution.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/StepContribution.java @@ -73,7 +73,15 @@ public ExitStatus getExitStatus() { } /** - * Increment the counter for the number of items processed. + * Increment the counter for the number of filtered items. + * @since 6.0.0 + */ + public void incrementFilterCount() { + this.incrementFilterCount(1); + } + + /** + * Increment the counter for the number of filtered items. * @param count The {@code long} amount to increment by. */ public void incrementFilterCount(long count) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/StoppableStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/StoppableStep.java new file mode 100644 index 0000000000..e455861f96 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/StoppableStep.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-present 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.step; + +/** + * Extension of the {@link Step} interface to be implemented by steps that support being + * stopped. + * + * @author Mahmoud Ben Hassine + * @since 6.0 + */ +public interface StoppableStep extends Step { + + /** + * Callback to signal the step to stop. The default implementation sets the + * {@link StepExecution} to terminate only. Concrete implementations can override this + * method to add custom stop logic. + * @param stepExecution the current step execution + */ + default void stop(StepExecution stepExecution) { + stepExecution.setTerminateOnly(); + } + +} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/ChunkOrientedStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/ChunkOrientedStepBuilder.java index 21ffbcb799..8b38d9584c 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/ChunkOrientedStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/ChunkOrientedStepBuilder.java @@ -16,10 +16,13 @@ package org.springframework.batch.core.step.builder; import java.lang.reflect.Method; +import java.util.Arrays; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.Set; +import io.micrometer.core.instrument.MeterRegistry; + import org.springframework.batch.core.annotation.AfterChunk; import org.springframework.batch.core.annotation.AfterProcess; import org.springframework.batch.core.annotation.AfterRead; @@ -44,17 +47,21 @@ import org.springframework.batch.core.step.ThreadStepInterruptionPolicy; import org.springframework.batch.core.step.item.ChunkOrientedStep; import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy; +import org.springframework.batch.core.step.skip.LimitCheckingExceptionHierarchySkipPolicy; import org.springframework.batch.core.step.skip.SkipPolicy; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemStream; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.support.ReflectionUtils; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryPolicy; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; +import org.springframework.util.Assert; /** * A builder for {@link ChunkOrientedStep}. This class extends {@link StepBuilderHelper} @@ -73,7 +80,7 @@ public class ChunkOrientedStepBuilder extends StepBuilderHelper writer; - private final PlatformTransactionManager transactionManager; + private PlatformTransactionManager transactionManager = new ResourcelessTransactionManager(); private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); @@ -85,25 +92,39 @@ public class ChunkOrientedStepBuilder extends StepBuilderHelper false; + private RetryPolicy retryPolicy; private final Set retryListeners = new LinkedHashSet<>(); - private SkipPolicy skipPolicy = new AlwaysSkipItemSkipPolicy(); + private final Set> retryableExceptions = new HashSet<>(); + + private long retryLimit = -1; + + private SkipPolicy skipPolicy; private final Set> skipListeners = new LinkedHashSet<>(); + private final Set> skippableExceptions = new HashSet<>(); + + private long skipLimit = -1; + + private AsyncTaskExecutor asyncTaskExecutor; + + private MeterRegistry meterRegistry; + + ChunkOrientedStepBuilder(StepBuilderHelper parent, int chunkSize) { + super(parent); + this.chunkSize = chunkSize; + } + /** * Create a new {@link ChunkOrientedStepBuilder} with the given job repository and * transaction manager. The step name will be assigned to the bean name. * @param jobRepository the job repository - * @param transactionManager the transaction manager * @param chunkSize the size of the chunk to be processed */ - public ChunkOrientedStepBuilder(JobRepository jobRepository, PlatformTransactionManager transactionManager, - int chunkSize) { + public ChunkOrientedStepBuilder(JobRepository jobRepository, int chunkSize) { super(jobRepository); - this.transactionManager = transactionManager; this.chunkSize = chunkSize; } @@ -112,13 +133,10 @@ public ChunkOrientedStepBuilder(JobRepository jobRepository, PlatformTransaction * repository and transaction manager. * @param name the step name * @param jobRepository the job repository - * @param transactionManager the transaction manager * @param chunkSize the size of the chunk to be processed */ - public ChunkOrientedStepBuilder(String name, JobRepository jobRepository, - PlatformTransactionManager transactionManager, int chunkSize) { + public ChunkOrientedStepBuilder(String name, JobRepository jobRepository, int chunkSize) { super(name, jobRepository); - this.transactionManager = transactionManager; this.chunkSize = chunkSize; } @@ -163,6 +181,17 @@ public ChunkOrientedStepBuilder writer(ItemWriter writer) { return self(); } + /** + * Sets the transaction manager to use for the chunk-oriented tasklet. Defaults to a + * {@link ResourcelessTransactionManager} if none is provided. + * @param transactionManager a transaction manager set + * @return this for fluent chaining + */ + public ChunkOrientedStepBuilder transactionManager(PlatformTransactionManager transactionManager) { + this.transactionManager = transactionManager; + return self(); + } + /** * Sets the transaction attributes for the tasklet execution. Defaults to the default * values for the transaction manager, but can be manipulated to provide longer @@ -256,6 +285,7 @@ public ChunkOrientedStepBuilder faultTolerant() { * @return this for fluent chaining */ public ChunkOrientedStepBuilder retryPolicy(RetryPolicy retryPolicy) { + Assert.notNull(retryPolicy, "retryPolicy must not be null"); this.retryPolicy = retryPolicy; return self(); } @@ -271,6 +301,18 @@ public ChunkOrientedStepBuilder retryListener(RetryListener retryListener) return self(); } + @SafeVarargs + public final ChunkOrientedStepBuilder retry(Class... retryableExceptions) { + this.retryableExceptions.addAll(Arrays.stream(retryableExceptions).toList()); + return self(); + } + + public ChunkOrientedStepBuilder retryLimit(long retryLimit) { + Assert.isTrue(retryLimit > 0, "retryLimit must be positive"); + this.retryLimit = retryLimit; + return self(); + } + /** * Set the skip policy for the step. This policy determines how the step handles * skipping items in case of failures. It can be used to define the conditions under @@ -280,6 +322,7 @@ public ChunkOrientedStepBuilder retryListener(RetryListener retryListener) * @return this for fluent chaining */ public ChunkOrientedStepBuilder skipPolicy(SkipPolicy skipPolicy) { + Assert.notNull(skipPolicy, "skipPolicy must not be null"); this.skipPolicy = skipPolicy; return self(); } @@ -296,16 +339,78 @@ public ChunkOrientedStepBuilder skipListener(SkipListener skipListen return self(); } + @SafeVarargs + public final ChunkOrientedStepBuilder skip(Class... skippableExceptions) { + this.skippableExceptions.addAll(Arrays.stream(skippableExceptions).toList()); + return self(); + } + + public ChunkOrientedStepBuilder skipLimit(long skipLimit) { + Assert.isTrue(skipLimit > 0, "skipLimit must be positive"); + this.skipLimit = skipLimit; + return self(); + } + + /** + * Set the asynchronous task executor to be used for processing items concurrently. + * This allows for concurrent processing of items, improving performance and + * throughput. If not set, the step will process items sequentially. + * @param asyncTaskExecutor the asynchronous task executor to use + * @return this for fluent chaining + */ + public ChunkOrientedStepBuilder taskExecutor(AsyncTaskExecutor asyncTaskExecutor) { + this.asyncTaskExecutor = asyncTaskExecutor; + return self(); + } + + /** + * Set the meter registry to be used for collecting metrics during step execution. + * This allows for monitoring and analyzing the performance of the step. If not set, + * it will default to {@link io.micrometer.core.instrument.Metrics#globalRegistry}. + * @param meterRegistry the MeterRegistry to use + * @return this for fluent chaining + */ + public ChunkOrientedStepBuilder meterRegistry(MeterRegistry meterRegistry) { + this.meterRegistry = meterRegistry; + return self(); + } + @SuppressWarnings("unchecked") public ChunkOrientedStep build() { ChunkOrientedStep chunkOrientedStep = new ChunkOrientedStep<>(this.getName(), this.chunkSize, this.reader, - this.writer, this.getJobRepository(), this.transactionManager); - chunkOrientedStep.setItemProcessor(this.processor); + this.writer, this.getJobRepository()); + if (this.processor != null) { + chunkOrientedStep.setItemProcessor(this.processor); + } + chunkOrientedStep.setTransactionManager(this.transactionManager); chunkOrientedStep.setTransactionAttribute(this.transactionAttribute); chunkOrientedStep.setInterruptionPolicy(this.interruptionPolicy); + if (this.retryPolicy == null) { + if (!this.retryableExceptions.isEmpty() || this.retryLimit > 0) { + this.retryPolicy = RetryPolicy.builder() + .maxAttempts(this.retryLimit) + .includes(this.retryableExceptions) + .build(); + } + else { + this.retryPolicy = throwable -> false; + } + } chunkOrientedStep.setRetryPolicy(this.retryPolicy); + if (this.skipPolicy == null) { + if (!this.skippableExceptions.isEmpty() || this.skipLimit > 0) { + this.skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(this.skippableExceptions, + this.skipLimit); + } + else { + this.skipPolicy = new AlwaysSkipItemSkipPolicy(); + } + } chunkOrientedStep.setSkipPolicy(this.skipPolicy); chunkOrientedStep.setFaultTolerant(this.faultTolerant); + if (this.asyncTaskExecutor != null) { + chunkOrientedStep.setTaskExecutor(this.asyncTaskExecutor); + } streams.forEach(chunkOrientedStep::registerItemStream); stepListeners.forEach(stepListener -> { if (stepListener instanceof ItemReadListener) { @@ -323,6 +428,9 @@ public ChunkOrientedStep build() { }); retryListeners.forEach(chunkOrientedStep::registerRetryListener); skipListeners.forEach(chunkOrientedStep::registerSkipListener); + if (this.meterRegistry != null) { + chunkOrientedStep.setMeterRegistry(this.meterRegistry); + } try { chunkOrientedStep.afterPropertiesSet(); } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java index b40688b58c..f18c567eb3 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/FaultTolerantStepBuilder.java @@ -92,7 +92,11 @@ * @author Mahmoud Ben Hassine * @author Ian Choi * @since 2.2 + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.builder.ChunkOrientedStepBuilder} instead. + * Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class FaultTolerantStepBuilder extends SimpleStepBuilder { private final ChunkMonitor chunkMonitor = new ChunkMonitor(); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java index 0ce25a8184..bd711e81e1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/SimpleStepBuilder.java @@ -66,7 +66,10 @@ * @author Mahmoud Ben Hassine * @author Parikshit Dutta * @since 2.2 + * @deprecated Since 6.0 in favor of {@link ChunkOrientedStepBuilder}. Scheduled for + * removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class SimpleStepBuilder extends AbstractTaskletStepBuilder> { private static final int DEFAULT_COMMIT_INTERVAL = 1; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/StepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/StepBuilder.java index 90fcca83b0..9442429842 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/StepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/StepBuilder.java @@ -65,6 +65,16 @@ public TaskletStepBuilder tasklet(Tasklet tasklet, PlatformTransactionManager tr return new TaskletStepBuilder(this).tasklet(tasklet, transactionManager); } + /** + * Build a step with a custom tasklet, not necessarily item processing. + * @param tasklet a tasklet + * @return a {@link TaskletStepBuilder} + * @since 6.0 + */ + public TaskletStepBuilder tasklet(Tasklet tasklet) { + return new TaskletStepBuilder(this).tasklet(tasklet); + } + /** * Build a step that processes items in chunks with the size provided. To extend the * step to being fault tolerant, call the {@link SimpleStepBuilder#faultTolerant()} @@ -82,11 +92,28 @@ public TaskletStepBuilder tasklet(Tasklet tasklet, PlatformTransactionManager tr * @param the type of item to be processed as input * @param the type of item to be output * @since 5.0 + * @deprecated since 6.0, use {@link #chunk(int)} instead. Scheduled for removal in + * 7.0. */ + @Deprecated(since = "6.0", forRemoval = true) public SimpleStepBuilder chunk(int chunkSize, PlatformTransactionManager transactionManager) { return new SimpleStepBuilder(this).transactionManager(transactionManager).chunk(chunkSize); } + /** + * Build a step that processes items in chunks with the size provided. To extend the + * step to being fault-tolerant, call the + * {@link ChunkOrientedStepBuilder#faultTolerant()} method on the builder. + * @param chunkSize the chunk size (commit interval) + * @return a {@link ChunkOrientedStepBuilder} for method chaining + * @param the type of item to be processed as input + * @param the type of item to be output + * @since 6.0 + */ + public ChunkOrientedStepBuilder chunk(int chunkSize) { + return new ChunkOrientedStepBuilder<>(this, chunkSize); + } + /** * Build a step that processes items in chunks with the completion policy provided. To * extend the step to being fault tolerant, call the @@ -105,8 +132,8 @@ public SimpleStepBuilder chunk(int chunkSize, PlatformTransactionMa * @param the type of item to be processed as input * @param the type of item to be output * @since 5.0 - * @deprecated since 6.0, use {@link #chunk(int, PlatformTransactionManager)} instead. - * Scheduled for removal in 6.2 or later. + * @deprecated since 6.0, use {@link #chunk(int)} instead. Scheduled for removal in + * 7.0. */ @Deprecated(since = "6.0", forRemoval = true) public SimpleStepBuilder chunk(CompletionPolicy completionPolicy, diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/TaskletStepBuilder.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/TaskletStepBuilder.java index 896fce2cea..d21ccc5a31 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/TaskletStepBuilder.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/builder/TaskletStepBuilder.java @@ -49,6 +49,16 @@ public TaskletStepBuilder tasklet(Tasklet tasklet, PlatformTransactionManager tr return this; } + /** + * @param tasklet the tasklet to use + * @return this for fluent chaining + * @since 6.0 + */ + public TaskletStepBuilder tasklet(Tasklet tasklet) { + this.tasklet = tasklet; + return this; + } + @Override protected TaskletStepBuilder self() { return this; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/BatchListenerFactoryHelper.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/BatchListenerFactoryHelper.java index 5d6d22ce0c..0204b7dfea 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/BatchListenerFactoryHelper.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/BatchListenerFactoryHelper.java @@ -22,10 +22,11 @@ /** * Package private helper for step factory beans. - * + * * @author Dave Syer - * + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) abstract class BatchListenerFactoryHelper { public static List getListeners(StepListener[] listeners, Class cls) { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java index 61e867db80..e85c960622 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/FaultTolerantStepFactoryBean.java @@ -48,8 +48,9 @@ * @author Robert Kasanicky * @author Morten Andersen-Gott * @author Ian Choi - * + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class FaultTolerantStepFactoryBean extends SimpleStepFactoryBean { private Map, Boolean> skippableExceptionClasses = new HashMap<>(); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/SimpleStepFactoryBean.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/SimpleStepFactoryBean.java index f12f4fda1a..646928d85a 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/SimpleStepFactoryBean.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/factory/SimpleStepFactoryBean.java @@ -59,8 +59,9 @@ * @author Dave Syer * @author Robert Kasanicky * @author Mahmoud Ben Hassine - * + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class SimpleStepFactoryBean implements FactoryBean, BeanNameAware { private String name; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/BatchRetryTemplate.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/BatchRetryTemplate.java index 4d16fbf665..8004deb3f1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/BatchRetryTemplate.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/BatchRetryTemplate.java @@ -53,8 +53,9 @@ * * @author Dave Syer * @author Mahmoud Ben Hassine - * + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class BatchRetryTemplate implements RetryOperations { private static class BatchRetryState extends DefaultRetryState { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkMonitor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkMonitor.java index edcb5b0a34..a1a4044b05 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkMonitor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkMonitor.java @@ -34,7 +34,9 @@ * @author Mahmoud Ben Hassine * @author Seungrae Kim * @since 2.0 + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class ChunkMonitor extends ItemStreamSupport { private final Log logger = LogFactory.getLog(getClass()); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedStep.java index a5943c96ee..720aa0556a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedStep.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedStep.java @@ -15,12 +15,18 @@ */ package org.springframework.batch.core.step.item; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Future; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.Metrics; +import io.micrometer.core.instrument.Tag; +import io.micrometer.core.instrument.Timer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; -import org.springframework.batch.core.BatchStatus; -import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.job.JobInterruptedException; import org.springframework.batch.core.listener.ChunkListener; import org.springframework.batch.core.listener.CompositeChunkListener; @@ -32,6 +38,14 @@ import org.springframework.batch.core.listener.ItemReadListener; import org.springframework.batch.core.listener.ItemWriteListener; import org.springframework.batch.core.listener.SkipListener; +import org.springframework.batch.core.observability.BatchMetrics; +import org.springframework.batch.core.observability.jfr.events.step.chunk.ChunkScanEvent; +import org.springframework.batch.core.observability.jfr.events.step.chunk.ChunkTransactionEvent; +import org.springframework.batch.core.observability.jfr.events.step.chunk.ChunkWriteEvent; +import org.springframework.batch.core.observability.jfr.events.step.chunk.ItemProcessEvent; +import org.springframework.batch.core.observability.jfr.events.step.chunk.ItemReadEvent; +import org.springframework.batch.core.scope.context.StepContext; +import org.springframework.batch.core.scope.context.StepSynchronizationManager; import org.springframework.batch.core.step.StepContribution; import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.repository.JobRepository; @@ -40,6 +54,7 @@ import org.springframework.batch.core.step.StepInterruptionPolicy; import org.springframework.batch.core.step.ThreadStepInterruptionPolicy; import org.springframework.batch.core.step.skip.AlwaysSkipItemSkipPolicy; +import org.springframework.batch.core.step.skip.SkipListenerFailedException; import org.springframework.batch.core.step.skip.SkipPolicy; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ExecutionContext; @@ -48,26 +63,29 @@ import org.springframework.batch.item.ItemStream; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.support.CompositeItemStream; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.core.retry.RetryException; import org.springframework.core.retry.RetryListener; import org.springframework.core.retry.RetryPolicy; import org.springframework.core.retry.RetryTemplate; import org.springframework.core.retry.Retryable; import org.springframework.core.retry.support.CompositeRetryListener; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; import org.springframework.transaction.interceptor.TransactionAttribute; -import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.util.Assert; /** - * Step implementation for the chunk-oriented processing model. + * Step implementation for the chunk-oriented processing model. This class also supports + * faut-tolerance features (retry and skip) as well as concurrent item processing when a + * {@link AsyncTaskExecutor} is provided. * - * @author Mahmoud Ben Hassine * @param type of input items * @param type of output items + * @author Mahmoud Ben Hassine * @since 6.0 */ public class ChunkOrientedStep extends AbstractStep { @@ -100,11 +118,11 @@ public class ChunkOrientedStep extends AbstractStep { /* * Transaction related parameters */ - private final PlatformTransactionManager transactionManager; + private PlatformTransactionManager transactionManager; private TransactionTemplate transactionTemplate; - private TransactionAttribute transactionAttribute = new DefaultTransactionAttribute(); + private TransactionAttribute transactionAttribute; /* * Chunk related parameters @@ -130,6 +148,16 @@ public class ChunkOrientedStep extends AbstractStep { private final CompositeSkipListener compositeSkipListener = new CompositeSkipListener<>(); + /* + * Concurrency parameters + */ + private AsyncTaskExecutor taskExecutor; + + /* + * Observability parameters + */ + private MeterRegistry meterRegistry; + /** * Create a new {@link ChunkOrientedStep}. * @param name the name of the step @@ -137,16 +165,14 @@ public class ChunkOrientedStep extends AbstractStep { * @param itemReader the item reader to read items * @param itemWriter the item writer to write items * @param jobRepository the job repository to use for this step - * @param transactionManager the transaction manager to use for this step */ public ChunkOrientedStep(String name, int chunkSize, ItemReader itemReader, ItemWriter itemWriter, - JobRepository jobRepository, PlatformTransactionManager transactionManager) { + JobRepository jobRepository) { super(name); this.chunkSize = chunkSize; this.itemReader = itemReader; this.itemWriter = itemWriter; setJobRepository(jobRepository); - this.transactionManager = transactionManager; } /** @@ -214,6 +240,16 @@ public void registerChunkListener(ChunkListener chunkListener) { this.compositeChunkListener.register(chunkListener); } + /** + * Set the {@link PlatformTransactionManager} to use for the chunk-oriented tasklet. + * Defaults to a {@link ResourcelessTransactionManager}. + * @param transactionManager a transaction manager set, must not be null. + */ + public void setTransactionManager(PlatformTransactionManager transactionManager) { + Assert.notNull(transactionManager, "Transaction manager must not be null"); + this.transactionManager = transactionManager; + } + /** * Set the transaction attribute for this step. * @param transactionAttribute the transaction attribute to set @@ -234,6 +270,15 @@ public void setFaultTolerant(boolean faultTolerant) { this.faultTolerant = faultTolerant; } + /** + * Set the {@link AsyncTaskExecutor} to use for processing items asynchronously. + * @param asyncTaskExecutor the asynchronous task executor to set + */ + public void setTaskExecutor(AsyncTaskExecutor asyncTaskExecutor) { + Assert.notNull(asyncTaskExecutor, "Task executor must not be null"); + this.taskExecutor = asyncTaskExecutor; + } + /** * Set the {@link RetryPolicy} for this step. * @param retryPolicy the retry policy to set @@ -273,10 +318,27 @@ public void registerSkipListener(SkipListener skipListener) { this.compositeSkipListener.register(skipListener); } + /** + * Set the meter registry to use for metrics. + * @param meterRegistry the meter registry + * @since 6.0 + */ + public void setMeterRegistry(MeterRegistry meterRegistry) { + Assert.notNull(meterRegistry, "Meter registry must not be null"); + this.meterRegistry = meterRegistry; + } + @Override public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); - Assert.notNull(this.transactionManager, "Transaction manager must not be null"); + if (this.transactionManager == null) { + logger.info("No transaction manager has been set. Defaulting to ResourcelessTransactionManager."); + this.transactionManager = new ResourcelessTransactionManager(); + } + if (this.transactionAttribute == null) { + logger.info("No transaction attribute has been set. Defaulting to DefaultTransactionAttribute."); + this.transactionAttribute = new DefaultTransactionAttribute(); + } Assert.isTrue(this.chunkSize > 0, "Chunk size must be greater than 0"); Assert.notNull(this.itemReader, "Item reader must not be null"); Assert.notNull(this.itemWriter, "Item writer must not be null"); @@ -295,6 +357,10 @@ public void afterPropertiesSet() throws Exception { this.retryTemplate.setRetryPolicy(this.retryPolicy); this.retryTemplate.setRetryListener(this.compositeRetryListener); } + if (this.meterRegistry == null) { + logger.info("No meter registry has been set. Defaulting to the global meter registry."); + this.meterRegistry = Metrics.globalRegistry; + } } @Override @@ -309,79 +375,171 @@ protected void close(ExecutionContext executionContext) throws Exception { @Override protected void doExecute(StepExecution stepExecution) throws Exception { - while (this.chunkTracker.moreItems()) { - // check interruption policy before processing next chunk - try { - this.interruptionPolicy.checkInterrupted(stepExecution); + stepExecution.getExecutionContext().put(STEP_TYPE_KEY, this.getClass().getName()); + while (this.chunkTracker.moreItems() && !interrupted(stepExecution)) { + // process next chunk in its own transaction + this.transactionTemplate.executeWithoutResult(transactionStatus -> { + ChunkTransactionEvent chunkTransactionEvent = new ChunkTransactionEvent(stepExecution.getStepName(), + stepExecution.getId()); + chunkTransactionEvent.begin(); + StepContribution contribution = stepExecution.createStepContribution(); + processNextChunk(transactionStatus, contribution, stepExecution); + chunkTransactionEvent.transactionStatus = transactionStatus.isRollbackOnly() + ? BatchMetrics.STATUS_ROLLED_BACK : BatchMetrics.STATUS_COMMITTED; + chunkTransactionEvent.commit(); + }); + } + } + + private void processNextChunk(TransactionStatus status, StepContribution contribution, + StepExecution stepExecution) { + if (isConcurrent()) { + processChunkConcurrently(status, contribution, stepExecution); + } + else { + processChunkSequentially(status, contribution, stepExecution); + } + } + + private void processChunkConcurrently(TransactionStatus status, StepContribution contribution, + StepExecution stepExecution) { + List> itemProcessingTasks = new LinkedList<>(); + try { + // read items and submit concurrent item processing tasks + for (int i = 0; i < this.chunkSize; i++) { + I item = readItem(contribution); + if (item != null) { + Future itemProcessingFuture = this.taskExecutor.submit(() -> processItem(item, contribution)); + itemProcessingTasks.add(itemProcessingFuture); + } } - catch (JobInterruptedException exception) { - stepExecution.setTerminateOnly(); - stepExecution.setStatus(BatchStatus.STOPPED); - stepExecution.setExitStatus(ExitStatus.STOPPED); + // exclude empty chunks (when the total items is a multiple of the chunk size) + if (itemProcessingTasks.isEmpty()) { return; } - // process next chunk in its own transaction - this.transactionTemplate.execute(new TransactionCallbackWithoutResult() { - @Override - protected void doInTransactionWithoutResult(TransactionStatus status) { - StepContribution contribution = stepExecution.createStepContribution(); - Chunk inputChunk = new Chunk<>(); - Chunk processedChunk = new Chunk<>(); - try { - inputChunk = read(contribution); - if (inputChunk.isEmpty()) { - return; - } - compositeChunkListener.beforeChunk(inputChunk); - processedChunk = process(inputChunk, contribution); - write(processedChunk, contribution); - compositeChunkListener.afterChunk(processedChunk); - stepExecution.apply(contribution); - stepExecution.incrementCommitCount(); - compositeItemStream.update(stepExecution.getExecutionContext()); - getJobRepository().update(stepExecution); - getJobRepository().updateExecutionContext(stepExecution); - } - catch (Exception e) { - logger.error("Rolling back chunk transaction", e); - status.setRollbackOnly(); - stepExecution.incrementRollbackCount(); - compositeChunkListener.onChunkError(e, processedChunk); - throw new FatalStepExecutionException("Unable to process chunk", e); - } + + // collect processed items + Chunk processedChunk = new Chunk<>(); + for (Future future : itemProcessingTasks) { + O processedItem = future.get(); + if (processedItem != null) { + processedChunk.add(processedItem); } - }); + } + + // write processed items + writeChunk(processedChunk, contribution); + stepExecution.incrementCommitCount(); + } + catch (Exception e) { + logger.error("Rolling back chunk transaction", e); + status.setRollbackOnly(); + stepExecution.incrementRollbackCount(); + throw new FatalStepExecutionException("Unable to process chunk", e); + } + finally { + // apply contribution and update streams + stepExecution.apply(contribution); + this.compositeItemStream.update(stepExecution.getExecutionContext()); + } + + } + + private void processChunkSequentially(TransactionStatus status, StepContribution contribution, + StepExecution stepExecution) { + Chunk inputChunk = new Chunk<>(); + Chunk processedChunk = new Chunk<>(); + try { + inputChunk = readChunk(contribution); + if (inputChunk.isEmpty()) { + return; + } + compositeChunkListener.beforeChunk(inputChunk); + processedChunk = processChunk(inputChunk, contribution); + writeChunk(processedChunk, contribution); + compositeChunkListener.afterChunk(processedChunk); + stepExecution.incrementCommitCount(); + } + catch (Exception e) { + logger.error("Rolling back chunk transaction", e); + status.setRollbackOnly(); + stepExecution.incrementRollbackCount(); + compositeChunkListener.onChunkError(e, processedChunk); + throw new FatalStepExecutionException("Unable to process chunk", e); + } + finally { + // apply contribution and update streams + stepExecution.apply(contribution); + compositeItemStream.update(stepExecution.getExecutionContext()); + } + } + + /* + * Check if the step has been interrupted either internally via user defined policy or + * externally via job operator. This will be checked at chunk boundaries. + */ + private boolean interrupted(StepExecution stepExecution) { + // check internal interruption via user defined policy + try { + this.interruptionPolicy.checkInterrupted(stepExecution); + } + catch (JobInterruptedException exception) { + return true; + } + // check external interruption via job operator + if (stepExecution.isTerminateOnly()) { + return true; } + return false; } - private Chunk read(StepContribution contribution) throws Exception { + private Chunk readChunk(StepContribution contribution) throws Exception { Chunk chunk = new Chunk<>(); for (int i = 0; i < chunkSize; i++) { + I item = readItem(contribution); + if (item != null) { + chunk.add(item); + } + } + return chunk; + } + + @Nullable private I readItem(StepContribution contribution) throws Exception { + ItemReadEvent itemReadEvent = new ItemReadEvent(contribution.getStepExecution().getStepName(), + contribution.getStepExecution().getId()); + Timer.Sample sample = startTimerSample(); + String status = BatchMetrics.STATUS_SUCCESS; + I item = null; + try { + itemReadEvent.begin(); this.compositeItemReadListener.beforeRead(); - try { - I item = doRead(); - if (item == null) { - chunkTracker.noMoreItems(); - break; - } - else { - chunk.add(item); - contribution.incrementReadCount(); - this.compositeItemReadListener.afterRead(item); - } + item = doRead(); + if (item == null) { + this.chunkTracker.noMoreItems(); } - catch (Exception exception) { - this.compositeItemReadListener.onReadError(exception); - if (this.faultTolerant && exception instanceof RetryException retryException) { - doSkipInRead(retryException, contribution); - } - else { - throw exception; - } + else { + contribution.incrementReadCount(); + this.compositeItemReadListener.afterRead(item); } - + itemReadEvent.itemReadStatus = BatchMetrics.STATUS_SUCCESS; } - return chunk; + catch (Exception exception) { + this.compositeItemReadListener.onReadError(exception); + if (this.faultTolerant && exception instanceof RetryException retryException) { + doSkipInRead(retryException, contribution); + } + else { + throw exception; + } + itemReadEvent.itemReadStatus = BatchMetrics.STATUS_FAILURE; + status = BatchMetrics.STATUS_FAILURE; + } + finally { + itemReadEvent.commit(); + stopTimerSample(sample, contribution.getStepExecution().getJobExecution().getJobInstance().getJobName(), + contribution.getStepExecution().getStepName(), "item.read", "Item reading", status); + } + return item; } @Nullable private I doRead() throws Exception { @@ -407,44 +565,80 @@ public String getName() { private void doSkipInRead(RetryException retryException, StepContribution contribution) { Throwable cause = retryException.getCause(); if (this.skipPolicy.shouldSkip(cause, contribution.getStepSkipCount())) { - this.compositeSkipListener.onSkipInRead(cause); - contribution.incrementReadSkipCount(); + try { + this.compositeSkipListener.onSkipInRead(cause); + contribution.incrementReadSkipCount(); + } + catch (Throwable throwable) { + throw new SkipListenerFailedException("Unable to apply onSkipInRead", throwable); + } } } - private Chunk process(Chunk chunk, StepContribution contribution) throws Exception { + private Chunk processChunk(Chunk chunk, StepContribution contribution) throws Exception { Chunk processedChunk = new Chunk<>(); for (I item : chunk) { - try { - this.compositeItemProcessListener.beforeProcess(item); - O processedItem = doProcess(item); - this.compositeItemProcessListener.afterProcess(item, processedItem); - if (processedItem == null) { - contribution.incrementFilterCount(1); - } - else { - processedChunk.add(processedItem); - } - } - catch (Exception exception) { - this.compositeItemProcessListener.onProcessError(item, exception); - if (this.faultTolerant && exception instanceof RetryException retryException) { - doSkipInProcess(item, retryException, contribution); - } - else { - throw exception; - } + O processedItem = processItem(item, contribution); + if (processedItem != null) { + processedChunk.add(processedItem); } } return processedChunk; } + private O processItem(I item, StepContribution contribution) throws Exception { + ItemProcessEvent itemProcessEvent = new ItemProcessEvent(contribution.getStepExecution().getStepName(), + contribution.getStepExecution().getId()); + Timer.Sample sample = startTimerSample(); + String status = BatchMetrics.STATUS_SUCCESS; + O processedItem = null; + try { + itemProcessEvent.begin(); + this.compositeItemProcessListener.beforeProcess(item); + processedItem = doProcess(item); + if (processedItem == null) { + contribution.incrementFilterCount(); + } + this.compositeItemProcessListener.afterProcess(item, processedItem); + itemProcessEvent.itemProcessStatus = BatchMetrics.STATUS_SUCCESS; + } + catch (Exception exception) { + this.compositeItemProcessListener.onProcessError(item, exception); + if (this.faultTolerant && exception instanceof RetryException retryException) { + doSkipInProcess(item, retryException, contribution); + } + else { + throw exception; + } + itemProcessEvent.itemProcessStatus = BatchMetrics.STATUS_FAILURE; + status = BatchMetrics.STATUS_FAILURE; + } + finally { + itemProcessEvent.commit(); + stopTimerSample(sample, contribution.getStepExecution().getJobExecution().getJobInstance().getJobName(), + contribution.getStepExecution().getStepName(), "item.process", "Item processing", status); + } + return processedItem; + } + @Nullable private O doProcess(I item) throws Exception { if (this.faultTolerant) { Retryable retryableProcess = new Retryable<>() { @Override public @Nullable O execute() throws Throwable { - return itemProcessor.process(item); + StepContext context = StepSynchronizationManager.getContext(); + final StepExecution stepExecution = context == null ? null : context.getStepExecution(); + if (isConcurrent() && stepExecution != null) { + StepSynchronizationManager.register(stepExecution); + } + try { + return itemProcessor.process(item); + } + finally { + if (isConcurrent() && stepExecution != null) { + StepSynchronizationManager.close(); + } + } } @Override @@ -462,29 +656,52 @@ public String getName() { private void doSkipInProcess(I item, RetryException retryException, StepContribution contribution) { Throwable cause = retryException.getCause(); if (this.skipPolicy.shouldSkip(cause, contribution.getStepSkipCount())) { - this.compositeSkipListener.onSkipInProcess(item, retryException.getCause()); - contribution.incrementProcessSkipCount(); + try { + this.compositeSkipListener.onSkipInProcess(item, retryException.getCause()); + contribution.incrementProcessSkipCount(); + } + catch (Throwable throwable) { + throw new SkipListenerFailedException("Unable to apply onSkipInProcess", throwable); + } } } - private void write(Chunk chunk, StepContribution contribution) throws Exception { + private void writeChunk(Chunk chunk, StepContribution contribution) throws Exception { + ChunkWriteEvent chunkWriteEvent = new ChunkWriteEvent(contribution.getStepExecution().getStepName(), + contribution.getStepExecution().getId(), chunk.size()); + Timer.Sample sample = startTimerSample(); + String status = BatchMetrics.STATUS_SUCCESS; try { + chunkWriteEvent.begin(); this.compositeItemWriteListener.beforeWrite(chunk); doWrite(chunk); contribution.incrementWriteCount(chunk.size()); this.compositeItemWriteListener.afterWrite(chunk); + chunkWriteEvent.chunkWriteStatus = BatchMetrics.STATUS_SUCCESS; } catch (Exception exception) { this.compositeItemWriteListener.onWriteError(exception, chunk); + chunkWriteEvent.chunkWriteStatus = BatchMetrics.STATUS_FAILURE; + status = BatchMetrics.STATUS_FAILURE; if (this.faultTolerant && exception instanceof RetryException retryException) { logger.info("Retry exhausted while attempting to write items, scanning the chunk", retryException); + ChunkScanEvent chunkScanEvent = new ChunkScanEvent(contribution.getStepExecution().getStepName(), + contribution.getStepExecution().getId()); + chunkScanEvent.begin(); scan(chunk, contribution); + chunkScanEvent.skipCount = contribution.getSkipCount(); + chunkScanEvent.commit(); logger.info("Chunk scan completed"); } else { throw exception; } } + finally { + chunkWriteEvent.commit(); + stopTimerSample(sample, contribution.getStepExecution().getJobExecution().getJobInstance().getJobName(), + contribution.getStepExecution().getStepName(), "chunk.write", "Chunk writing", status); + } } private void doWrite(Chunk chunk) throws Exception { @@ -519,8 +736,13 @@ private void scan(Chunk chunk, StepContribution contribution) { } catch (Exception exception) { if (this.skipPolicy.shouldSkip(exception, contribution.getStepSkipCount())) { - this.compositeSkipListener.onSkipInWrite(item, exception); - contribution.incrementWriteSkipCount(); + try { + this.compositeSkipListener.onSkipInWrite(item, exception); + contribution.incrementWriteSkipCount(); + } + catch (Throwable throwable) { + throw new SkipListenerFailedException("Unable to apply onSkipInWrite", throwable); + } } else { logger.error("Failed to write item: " + item, exception); @@ -530,6 +752,23 @@ private void scan(Chunk chunk, StepContribution contribution) { } } + private Timer.Sample startTimerSample() { + return BatchMetrics.createTimerSample(this.meterRegistry); + } + + private void stopTimerSample(Timer.Sample sample, String jobName, String stepName, String operation, + String description, String status) { + String fullyQualifiedMetricName = BatchMetrics.METRICS_PREFIX + operation; + sample.stop(BatchMetrics.createTimer(this.meterRegistry, operation, description + " duration", + Tag.of(fullyQualifiedMetricName + ".job.name", jobName), + Tag.of(fullyQualifiedMetricName + ".step.name", stepName), + Tag.of(fullyQualifiedMetricName + ".status", status))); + } + + private boolean isConcurrent() { + return this.taskExecutor != null; + } + private static class ChunkTracker { private boolean moreItems = true; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java index cd780c2977..d6a7bb47c8 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkOrientedTasklet.java @@ -31,7 +31,11 @@ * @author Dave Syer * @author Mahmoud Ben Hassine * @param input item type + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class ChunkOrientedTasklet implements Tasklet { private static final String INPUTS_KEY = "INPUTS"; 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 ed29a3e005..757f8422b5 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 @@ -24,7 +24,9 @@ * * @author Kyeonghoon Lee (Add FunctionalInterface annotation) * @since 2.0 + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) @FunctionalInterface public interface ChunkProcessor { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProvider.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProvider.java index 2541148a42..ceb2ef825b 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProvider.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/ChunkProvider.java @@ -25,7 +25,9 @@ * * @since 2.0 * @see ChunkOrientedTasklet + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public interface ChunkProvider { Chunk provide(StepContribution contribution) throws Exception; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/DefaultItemFailureHandler.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/DefaultItemFailureHandler.java index e51403aeb6..8d47d4e3f9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/DefaultItemFailureHandler.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/DefaultItemFailureHandler.java @@ -29,8 +29,9 @@ * * @author Lucas Ward * @author Mahmoud Ben Hassine - * + * @deprecated Since 6.0 with no replacement. Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class DefaultItemFailureHandler extends ItemListenerSupport { protected static final Log logger = LogFactory.getLog(DefaultItemFailureHandler.class); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessor.java index 04ef046685..27cb244a1b 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProcessor.java @@ -50,7 +50,11 @@ * FaultTolerant implementation of the {@link ChunkProcessor} interface, that allows for * skipping or retry of items that cause exceptions during writing. * + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class FaultTolerantChunkProcessor extends SimpleChunkProcessor { private SkipPolicy itemProcessSkipPolicy = new LimitCheckingItemSkipPolicy(); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java index e000650e44..1dfe0e5c4d 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/FaultTolerantChunkProvider.java @@ -33,7 +33,11 @@ * FaultTolerant implementation of the {@link ChunkProvider} interface, that allows for * skipping or retry of items that cause exceptions during reading or processing. * + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class FaultTolerantChunkProvider extends SimpleChunkProvider { /** diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/KeyGenerator.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/KeyGenerator.java index ee705659f3..b1926cc90f 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/KeyGenerator.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/KeyGenerator.java @@ -21,8 +21,10 @@ * * @author Dave Syer * @author Taeik Lim - * + * @deprecated Since 6.0 in favor of equals/hashcode in a wrapper type if needed. + * Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) @FunctionalInterface public interface KeyGenerator { diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java index 58ea60dcdb..246cdbd5d6 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProcessor.java @@ -40,7 +40,11 @@ * writing and processing. Any exceptions encountered will be rethrown. * * @see ChunkOrientedTasklet + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class SimpleChunkProcessor implements ChunkProcessor, InitializingBean { private ItemProcessor itemProcessor; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProvider.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProvider.java index d5420b6123..2ba00208af 100755 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProvider.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleChunkProvider.java @@ -43,7 +43,11 @@ * @author Michael Minella * @author Mahmoud Ben Hassine * @see ChunkOrientedTasklet + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class SimpleChunkProvider implements ChunkProvider { protected final Log logger = LogFactory.getLog(getClass()); diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java index 85c1d8a128..6d802f11f1 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/item/SimpleRetryExceptionHandler.java @@ -34,8 +34,11 @@ * exception handling to another {@link ExceptionHandler}. * * @author Dave Syer - * + * @deprecated Since 6.0, use + * {@link org.springframework.batch.core.step.item.ChunkOrientedStep} instead. Scheduled + * for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class SimpleRetryExceptionHandler implements RetryListener, ExceptionHandler { /** diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingExceptionHierarchySkipPolicy.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingExceptionHierarchySkipPolicy.java new file mode 100644 index 0000000000..58492c91a0 --- /dev/null +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingExceptionHierarchySkipPolicy.java @@ -0,0 +1,74 @@ +/* + * Copyright 2025-present 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.step.skip; + +import java.util.HashSet; +import java.util.Set; + +import org.springframework.util.Assert; + +/** + * A composite {@link SkipPolicy} that checks if the exception is assignable from one of + * the given skippable exceptions, and counts the number of skips to not exceed a given + * limit. + * + * @author Mahmoud Ben Hassine + * @since 6.0 + */ +public class LimitCheckingExceptionHierarchySkipPolicy implements SkipPolicy { + + private Set> skippableExceptions = new HashSet<>(); + + private long skipLimit = -1; + + /** + * Create a new {@link LimitCheckingExceptionHierarchySkipPolicy} instance. + * @param skippableExceptions exception classes that can be skipped (non-critical) + * @param skipLimit the number of skippable exceptions that are allowed to be skipped + */ + public LimitCheckingExceptionHierarchySkipPolicy(Set> skippableExceptions, + long skipLimit) { + Assert.notEmpty(skippableExceptions, "The skippableExceptions must not be empty"); + Assert.isTrue(skipLimit > 0, "The skipLimit must be greater than zero"); + this.skippableExceptions = skippableExceptions; + this.skipLimit = skipLimit; + } + + @Override + public boolean shouldSkip(Throwable t, long skipCount) throws SkipLimitExceededException { + if (!isSkippable(t)) { + return false; + } + if (skipCount < this.skipLimit) { + return true; + } + else { + throw new SkipLimitExceededException(this.skipLimit, t); + } + } + + private boolean isSkippable(Throwable t) { + boolean isSkippable = false; + for (Class skippableException : this.skippableExceptions) { + if (skippableException.isAssignableFrom(t.getClass())) { + isSkippable = true; + break; + } + } + return isSkippable; + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingItemSkipPolicy.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingItemSkipPolicy.java index 209fb969da..ceeedc374a 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingItemSkipPolicy.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/LimitCheckingItemSkipPolicy.java @@ -49,7 +49,10 @@ * @author Dave Syer * @author Dan Garrette * @author Mahmoud Ben Hassine + * @deprecated Since 6.0, use {@link LimitCheckingExceptionHierarchySkipPolicy} instead. + * Scheduled for removal in 7.0. */ +@Deprecated(since = "6.0", forRemoval = true) public class LimitCheckingItemSkipPolicy implements SkipPolicy { private long skipLimit; diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/SkipListenerFailedException.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/SkipListenerFailedException.java index f22850b279..8af3a4904b 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/SkipListenerFailedException.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/skip/SkipListenerFailedException.java @@ -28,6 +28,16 @@ */ public class SkipListenerFailedException extends UnexpectedJobExecutionException { + /** + * Create a new {@link SkipListenerFailedException}. + * @param message the exception message + * @param throwable the error that was thrown by a {@link SkipListener} + * @since 6.0 + */ + public SkipListenerFailedException(String message, Throwable throwable) { + super(message, throwable); + } + /** * @param message describes the error to the user * @param ex the exception that was thrown by a {@link SkipListener} diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/StoppableTasklet.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/StoppableTasklet.java index 5795b6cfa5..23e022d787 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/StoppableTasklet.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/StoppableTasklet.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-present the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,7 @@ */ package org.springframework.batch.core.step.tasklet; +import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.step.StepContribution; @@ -29,6 +30,7 @@ * so the appropriate thread safety and visibility controls should be put in place. * * @author Will Schipp + * @author Hyunsang Han * @since 3.0 */ public interface StoppableTasklet extends Tasklet { @@ -36,7 +38,20 @@ public interface StoppableTasklet extends Tasklet { /** * Used to signal that the job this {@link Tasklet} is executing within has been * requested to stop. + * @deprecated Since 6.0, use {@link #stop(StepExecution)} instead. */ + @Deprecated(since = "6.0", forRemoval = true) void stop(); + /** + * Used to signal that the job should stop, providing access to the current + * {@link StepExecution} context. + * @param stepExecution the current {@link StepExecution} context in which the job is + * being executed + * @since 6.0 + */ + default void stop(StepExecution stepExecution) { + stop(); + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java index 041589b035..467eb473c9 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/SystemCommandTasklet.java @@ -61,6 +61,7 @@ * @author Will Schipp * @author Mahmoud Ben Hassine * @author Injae Kim + * @author Hyunsang Han */ public class SystemCommandTasklet implements StepExecutionListener, StoppableTasklet, InitializingBean { @@ -275,4 +276,25 @@ public void stop() { stopped = true; } + /** + * Interrupts the execution of the system command if the given {@link StepExecution} + * matches the current execution context. This method allows for granular control over + * stopping specific step executions, ensuring that only the intended command is + * halted. + *

          + * This method will interrupt the thread executing the system command only if + * {@link #setInterruptOnCancel(boolean)} has been set to true. Otherwise, the + * underlying command will be allowed to finish before the tasklet ends. + * @param stepExecution the current {@link StepExecution} context; the execution is + * interrupted if it matches the ongoing one. + * @since 6.0 + * @see StoppableTasklet#stop(StepExecution) + */ + @Override + public void stop(StepExecution stepExecution) { + if (stepExecution.equals(this.execution)) { + this.stopped = true; + } + } + } diff --git a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java index 6b5c01d85b..6b97def353 100644 --- a/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java +++ b/spring-batch-core/src/main/java/org/springframework/batch/core/step/tasklet/TaskletStep.java @@ -20,6 +20,7 @@ import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.listener.ChunkListener; import org.springframework.batch.core.job.JobInterruptedException; +import org.springframework.batch.core.observability.jfr.events.step.tasklet.TaskletExecutionEvent; import org.springframework.batch.core.step.StepContribution; import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.listener.StepExecutionListener; @@ -40,6 +41,7 @@ import org.springframework.batch.repeat.RepeatOperations; import org.springframework.batch.repeat.RepeatStatus; import org.springframework.batch.repeat.support.RepeatTemplate; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.interceptor.DefaultTransactionAttribute; @@ -48,7 +50,6 @@ import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.transaction.support.TransactionTemplate; -import org.springframework.util.Assert; import java.util.concurrent.Semaphore; @@ -117,7 +118,10 @@ public TaskletStep(String name) { @Override public void afterPropertiesSet() throws Exception { super.afterPropertiesSet(); - Assert.state(transactionManager != null, "A transaction manager must be provided"); + if (this.transactionManager == null) { + logger.info("No transaction manager has been set. Defaulting to ResourcelessTransactionManager."); + this.transactionManager = new ResourcelessTransactionManager(); + } } /** @@ -222,9 +226,12 @@ public void setInterruptionPolicy(StepInterruptionPolicy interruptionPolicy) { */ @Override protected void doExecute(StepExecution stepExecution) throws Exception { - stepExecution.getExecutionContext().put(TASKLET_TYPE_KEY, tasklet.getClass().getName()); + String taskletType = tasklet.getClass().getName(); + stepExecution.getExecutionContext().put(TASKLET_TYPE_KEY, taskletType); stepExecution.getExecutionContext().put(STEP_TYPE_KEY, this.getClass().getName()); - + TaskletExecutionEvent taskletExecutionEvent = new TaskletExecutionEvent(stepExecution.getStepName(), + stepExecution.getId(), taskletType); + taskletExecutionEvent.begin(); stream.update(stepExecution.getExecutionContext()); getJobRepository().updateExecutionContext(stepExecution); @@ -266,6 +273,8 @@ public RepeatStatus doInChunkContext(RepeatContext repeatContext, ChunkContext c }); + taskletExecutionEvent.taskletStatus = stepExecution.getExitStatus().getExitCode(); + taskletExecutionEvent.commit(); } /** 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 index 65287a5f89..ec9f904daf 100644 --- 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -77,6 +78,7 @@ void testBatchVersionInExecutionContext() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class TestConfiguration { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java index 46fd03e979..39b848ef10 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java @@ -141,12 +141,10 @@ void testDefaultInfrastructureBeansRegistration() { // when JobRepository jobRepository = context.getBean(JobRepository.class); - JobRegistry jobRegistry = context.getBean(JobRegistry.class); JobOperator jobOperator = context.getBean(JobOperator.class); // then Assertions.assertNotNull(jobRepository); - Assertions.assertNotNull(jobRegistry); Assertions.assertNotNull(jobOperator); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobBuilderConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobBuilderConfigurationTests.java index 9112204db2..3d5ba56c9b 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobBuilderConfigurationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobBuilderConfigurationTests.java @@ -98,6 +98,7 @@ private void testJob(String jobName, BatchStatus status, int stepExecutionCount, @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public static class TestConfiguration { @@ -165,6 +166,7 @@ protected Step step3(JobRepository jobRepository) throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public static class BeansConfigurer { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobLoaderConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobLoaderConfigurationTests.java index 45c46d9251..c5a056c694 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobLoaderConfigurationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/JobLoaderConfigurationTests.java @@ -19,6 +19,7 @@ import jakarta.annotation.PostConstruct; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; @@ -52,6 +53,7 @@ * @author Mahmoud Ben Hassine * */ +@Disabled class JobLoaderConfigurationTests { @Test diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java index c216be342f..e5651e9c63 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java @@ -27,7 +27,6 @@ import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; import org.springframework.batch.core.step.Step; -import org.springframework.batch.core.configuration.JobRegistry; import org.springframework.batch.core.configuration.xml.DummyJobRepository; import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.launch.JobOperator; @@ -79,17 +78,15 @@ void testDefaultInfrastructureBeansRegistration() { // when JobRepository jobRepository = context.getBean(JobRepository.class); - JobRegistry jobRegistry = context.getBean(JobRegistry.class); JobOperator jobOperator = context.getBean(JobOperator.class); // then Assertions.assertNotNull(jobRepository); - Assertions.assertNotNull(jobRegistry); Assertions.assertNotNull(jobOperator); } @Configuration - static class MyJobConfiguration extends DefaultBatchConfiguration { + static class MyJobConfiguration extends JdbcDefaultBatchConfiguration { @Bean public Step myStep(JobRepository jobRepository, PlatformTransactionManager transactionManager) { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java index 1e9e13abe1..1c24477e31 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/FlowJobBuilderTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.JobInterruptedException; @@ -390,6 +391,7 @@ void testBuildWithJobScopedStep() throws Exception { @EnableBatchProcessing @Configuration + @EnableJdbcJobRepository static class JobConfiguration { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java index f39dae8d02..ee1c2f83ff 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/job/builder/JobBuilderTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.listener.JobExecutionListener; @@ -67,6 +68,7 @@ void testListeners() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class MyJobConfiguration { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBeanTests.java index 2ef929624c..6a9315326c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBeanTests.java @@ -25,6 +25,7 @@ import org.springframework.batch.core.converter.JobParametersConverter; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.test.util.AopTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.interceptor.TransactionAttributeSource; @@ -65,6 +66,26 @@ public void testJobOperatorCreation() throws Exception { Assertions.assertEquals(this.transactionManager, getTransactionManagerSetOnJobOperator(jobOperator)); } + @Test + public void testDefaultTransactionManagerConfiguration() throws Exception { + // given + JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean(); + jobOperatorFactoryBean.setJobRegistry(this.jobRegistry); + jobOperatorFactoryBean.setJobRepository(this.jobRepository); + jobOperatorFactoryBean.setJobParametersConverter(this.jobParametersConverter); + jobOperatorFactoryBean.afterPropertiesSet(); + + // when + JobOperator jobOperator = jobOperatorFactoryBean.getObject(); + + // then + Assertions.assertNotNull(jobOperator); + Object targetObject = AopTestUtils.getTargetObject(jobOperator); + Assertions.assertInstanceOf(TaskExecutorJobOperator.class, targetObject); + Assertions.assertInstanceOf(ResourcelessTransactionManager.class, + getTransactionManagerSetOnJobOperator(jobOperator)); + } + @Test public void testCustomTransactionAttributesSource() throws Exception { // given @@ -87,10 +108,7 @@ public void testCustomTransactionAttributesSource() throws Exception { } private PlatformTransactionManager getTransactionManagerSetOnJobOperator(JobOperator jobOperator) { - Advised target = (Advised) jobOperator; // proxy created by - // AbstractJobOperatorFactoryBean - Advisor[] advisors = target.getAdvisors(); - for (Advisor advisor : advisors) { + for (Advisor advisor : ((Advised) jobOperator).getAdvisors()) { if (advisor.getAdvice() instanceof TransactionInterceptor transactionInterceptor) { return (PlatformTransactionManager) transactionInterceptor.getTransactionManager(); } @@ -99,10 +117,7 @@ private PlatformTransactionManager getTransactionManagerSetOnJobOperator(JobOper } private TransactionAttributeSource getTransactionAttributesSourceSetOnJobOperator(JobOperator jobOperator) { - Advised target = (Advised) jobOperator; // proxy created by - // AbstractJobOperatorFactoryBean - Advisor[] advisors = target.getAdvisors(); - for (Advisor advisor : advisors) { + for (Advisor advisor : ((Advised) jobOperator).getAdvisors()) { if (advisor.getAdvice() instanceof TransactionInterceptor transactionInterceptor) { return transactionInterceptor.getTransactionAttributeSource(); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperatorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperatorTests.java index a841f430a9..a35e5ea63e 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperatorTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/TaskExecutorJobOperatorTests.java @@ -55,9 +55,16 @@ import org.springframework.batch.support.PropertiesConverter; import org.springframework.lang.Nullable; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * @author Dave Syer diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/ItemListenerErrorTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/ItemListenerErrorTests.java index 7c434be37c..231f9f22a4 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/listener/ItemListenerErrorTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/listener/ItemListenerErrorTests.java @@ -26,6 +26,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -127,6 +128,7 @@ void testOnProcessError() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository public static class BatchConfiguration { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/observability/BatchMetricsTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/observability/BatchMetricsTests.java index 2ad9f19f11..dcefc36ea1 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/observability/BatchMetricsTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/observability/BatchMetricsTests.java @@ -30,6 +30,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -248,6 +249,7 @@ void testBatchMetrics() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class MyJobConfiguration { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/observability/ObservabilitySampleStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/observability/ObservabilitySampleStepTests.java index bdf9d4626b..eb3af687a3 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/observability/ObservabilitySampleStepTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/observability/ObservabilitySampleStepTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -110,6 +111,7 @@ public SampleTestRunnerConsumer yourCode() { @Configuration(proxyBeanMethods = false) @EnableBatchProcessing + @EnableJdbcJobRepository static class TestConfig { @Bean 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 93d2be8a57..33a1d2b8b6 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 @@ -17,6 +17,7 @@ import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.EnableMongoJobRepository; import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; @@ -37,6 +38,7 @@ */ @Configuration @EnableBatchProcessing +@EnableMongoJobRepository class MongoDBIntegrationTestConfiguration { private static final DockerImageName MONGODB_IMAGE = DockerImageName.parse("mongo:8.0.11"); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java index dce5ea67bc..ffda9d7b22 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/AbstractStepTests.java @@ -38,7 +38,7 @@ class AbstractStepTests { void testEndTimeInListener() throws Exception { // given StepExecution execution = new StepExecution("step", - new JobExecution(new JobInstance(1L, "job"), new JobParameters())); + new JobExecution(new JobInstance(1L, "job"), 0L, new JobParameters()), 0L); AbstractStep tested = new AbstractStep() { @Override protected void doExecute(StepExecution stepExecution) { diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/NonAbstractStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/NonAbstractStepTests.java index afbe781b56..672bc95e61 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/NonAbstractStepTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/NonAbstractStepTests.java @@ -64,7 +64,7 @@ class NonAbstractStepTests { final List events = new ArrayList<>(); final StepExecution execution = new StepExecution(tested.getName(), - new JobExecution(new JobInstance(1L, "jobName"), new JobParameters())); + new JobExecution(new JobInstance(1L, "jobName"), 0L, new JobParameters()), 0L); /** * Fills the events list when abstract methods are called. diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/RegisterMultiListenerTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/RegisterMultiListenerTests.java index 7078fdd9e7..496c1cf2f7 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/RegisterMultiListenerTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/builder/RegisterMultiListenerTests.java @@ -21,6 +21,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.BatchStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.listener.ChunkListener; import org.springframework.batch.core.ExitStatus; import org.springframework.batch.core.listener.ItemWriteListener; @@ -179,6 +180,7 @@ public ItemWriter writer() { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository public static class MultiListenerFaultTolerantTestConfiguration extends MultiListenerTestConfigurationSupport { @Bean @@ -215,6 +217,7 @@ public Step step(JobRepository jobRepository) { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository public static class MultiListenerTestConfiguration extends MultiListenerTestConfigurationSupport { @Bean diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepIntegrationTests.java index 0f105f1315..1153506210 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepIntegrationTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepIntegrationTests.java @@ -38,7 +38,7 @@ import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.ChunkOrientedStepBuilder; -import org.springframework.batch.core.step.skip.LimitCheckingItemSkipPolicy; +import org.springframework.batch.core.step.skip.LimitCheckingExceptionHierarchySkipPolicy; import org.springframework.batch.core.step.skip.SkipLimitExceededException; import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; @@ -55,6 +55,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.core.retry.RetryPolicy; +import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; @@ -69,6 +70,9 @@ */ public class ChunkOrientedStepIntegrationTests { + // TODO use parameterized tests for serial and concurrent steps + // The outcome should be the same for both + @Test void testChunkOrientedStep() throws Exception { // given @@ -94,6 +98,31 @@ void testChunkOrientedStep() throws Exception { Assertions.assertEquals(5, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); } + @Test + void testConcurrentChunkOrientedStep() throws Exception { + // given + ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class, + ConcurrentChunkOrientedStepConfiguration.class); + JobOperator jobOperator = context.getBean(JobOperator.class); + Job job = context.getBean(Job.class); + JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); + + // when + JobParameters jobParameters = new JobParametersBuilder().addString("file", "data/persons.csv") + .toJobParameters(); + JobExecution jobExecution = jobOperator.start(job, jobParameters); + + // then + Assertions.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next(); + Assertions.assertEquals(ExitStatus.COMPLETED, stepExecution.getExitStatus()); + Assertions.assertEquals(5, stepExecution.getReadCount()); + Assertions.assertEquals(5, stepExecution.getWriteCount()); + Assertions.assertEquals(3, stepExecution.getCommitCount()); + Assertions.assertEquals(0, stepExecution.getRollbackCount()); + Assertions.assertEquals(5, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); + } + @Test void testChunkOrientedStepFailure() throws Exception { // given @@ -116,7 +145,37 @@ void testChunkOrientedStepFailure() throws Exception { Assertions.assertEquals(ExitStatus.FAILED.getExitCode(), stepExecutionExitStatus.getExitCode()); Assertions.assertTrue(stepExecutionExitStatus.getExitDescription() .contains("Unable to process item Person[id=1, name=foo1]")); - Assertions.assertEquals(0, stepExecution.getReadCount()); + Assertions.assertEquals(2, stepExecution.getReadCount()); + Assertions.assertEquals(0, stepExecution.getWriteCount()); + Assertions.assertEquals(0, stepExecution.getCommitCount()); + Assertions.assertEquals(1, stepExecution.getRollbackCount()); + Assertions.assertEquals(0, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); + System.clearProperty("fail"); + } + + @Test + void testConcurrentChunkOrientedStepFailure() throws Exception { + // given + System.setProperty("fail", "true"); + ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class, + ConcurrentChunkOrientedStepConfiguration.class); + JobOperator jobOperator = context.getBean(JobOperator.class); + Job job = context.getBean(Job.class); + JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); + + // when + JobParameters jobParameters = new JobParametersBuilder().addString("file", "data/persons.csv") + .toJobParameters(); + JobExecution jobExecution = jobOperator.start(job, jobParameters); + + // then + Assertions.assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode()); + StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next(); + ExitStatus stepExecutionExitStatus = stepExecution.getExitStatus(); + Assertions.assertEquals(ExitStatus.FAILED.getExitCode(), stepExecutionExitStatus.getExitCode()); + Assertions.assertTrue(stepExecutionExitStatus.getExitDescription() + .contains("Unable to process item Person[id=1, name=foo1]")); + Assertions.assertEquals(2, stepExecution.getReadCount()); Assertions.assertEquals(0, stepExecution.getWriteCount()); Assertions.assertEquals(0, stepExecution.getCommitCount()); Assertions.assertEquals(1, stepExecution.getRollbackCount()); @@ -155,6 +214,37 @@ void testFaultTolerantChunkOrientedStep() throws Exception { System.clearProperty("skipLimit"); } + @Test + void testConcurrentFaultTolerantChunkOrientedStep() throws Exception { + // given + System.setProperty("skipLimit", "3"); + ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class, + ConcurrentFaultTolerantChunkOrientedStepConfiguration.class); + JobOperator jobOperator = context.getBean(JobOperator.class); + Job job = context.getBean(Job.class); + JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); + + // when + JobParameters jobParameters = new JobParametersBuilder().addString("file", "data/persons-bad-data.csv") + .toJobParameters(); + JobExecution jobExecution = jobOperator.start(job, jobParameters); + + // then + Assertions.assertEquals(ExitStatus.COMPLETED.getExitCode(), jobExecution.getExitStatus().getExitCode()); + StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next(); + ExitStatus stepExecutionExitStatus = stepExecution.getExitStatus(); + Assertions.assertEquals(ExitStatus.COMPLETED.getExitCode(), stepExecutionExitStatus.getExitCode()); + Assertions.assertEquals(4, stepExecution.getReadCount()); + Assertions.assertEquals(3, stepExecution.getWriteCount()); + Assertions.assertEquals(3, stepExecution.getCommitCount()); + Assertions.assertEquals(0, stepExecution.getRollbackCount()); + Assertions.assertEquals(2, stepExecution.getReadSkipCount()); + Assertions.assertEquals(1, stepExecution.getWriteSkipCount()); + Assertions.assertEquals(3, stepExecution.getSkipCount()); + Assertions.assertEquals(3, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); + System.clearProperty("skipLimit"); + } + @Test void testFaultTolerantChunkOrientedStepFailure() throws Exception { // given @@ -178,14 +268,47 @@ void testFaultTolerantChunkOrientedStepFailure() throws Exception { Throwable failureException = stepExecution.getFailureExceptions().iterator().next(); Assertions.assertInstanceOf(FatalStepExecutionException.class, failureException); Assertions.assertInstanceOf(SkipLimitExceededException.class, failureException.getCause()); - Assertions.assertEquals(2, stepExecution.getReadCount()); + Assertions.assertEquals(3, stepExecution.getReadCount()); + Assertions.assertEquals(2, stepExecution.getWriteCount()); + Assertions.assertEquals(1, stepExecution.getCommitCount()); + Assertions.assertEquals(1, stepExecution.getRollbackCount()); + Assertions.assertEquals(1, stepExecution.getReadSkipCount()); + Assertions.assertEquals(0, stepExecution.getWriteSkipCount()); + Assertions.assertEquals(1, stepExecution.getSkipCount()); + Assertions.assertEquals(2, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); + System.clearProperty("skipLimit"); + } + + @Test + void testConcurrentFaultTolerantChunkOrientedStepFailure() throws Exception { + // given + System.setProperty("skipLimit", "1"); + ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class, + ConcurrentFaultTolerantChunkOrientedStepConfiguration.class); + JobOperator jobOperator = context.getBean(JobOperator.class); + Job job = context.getBean(Job.class); + JdbcTemplate jdbcTemplate = context.getBean(JdbcTemplate.class); + + // when + JobParameters jobParameters = new JobParametersBuilder().addString("file", "data/persons-bad-data.csv") + .toJobParameters(); + JobExecution jobExecution = jobOperator.start(job, jobParameters); + + // then + Assertions.assertEquals(ExitStatus.FAILED.getExitCode(), jobExecution.getExitStatus().getExitCode()); + StepExecution stepExecution = jobExecution.getStepExecutions().iterator().next(); + ExitStatus stepExecutionExitStatus = stepExecution.getExitStatus(); + Assertions.assertEquals(ExitStatus.FAILED.getExitCode(), stepExecutionExitStatus.getExitCode()); + Throwable failureException = stepExecution.getFailureExceptions().iterator().next(); + Assertions.assertInstanceOf(FatalStepExecutionException.class, failureException); + Assertions.assertInstanceOf(SkipLimitExceededException.class, failureException.getCause()); + Assertions.assertEquals(3, stepExecution.getReadCount()); Assertions.assertEquals(2, stepExecution.getWriteCount()); Assertions.assertEquals(1, stepExecution.getCommitCount()); Assertions.assertEquals(1, stepExecution.getRollbackCount()); - // contribution not applied on second chunk rollback - Assertions.assertEquals(0, stepExecution.getReadSkipCount()); + Assertions.assertEquals(1, stepExecution.getReadSkipCount()); Assertions.assertEquals(0, stepExecution.getWriteSkipCount()); - Assertions.assertEquals(0, stepExecution.getSkipCount()); + Assertions.assertEquals(1, stepExecution.getSkipCount()); Assertions.assertEquals(2, JdbcTestUtils.countRowsInTable(jdbcTemplate, "person_target")); System.clearProperty("skipLimit"); } @@ -259,9 +382,27 @@ static class ChunkOrientedStepConfiguration { public Step chunkOrientedStep(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemProcessor itemProcessor, ItemWriter itemWriter) { - return new ChunkOrientedStepBuilder(jobRepository, transactionManager, 2).reader(itemReader) + return new ChunkOrientedStepBuilder(jobRepository, 2).reader(itemReader) + .processor(itemProcessor) + .writer(itemWriter) + .transactionManager(transactionManager) + .build(); + } + + } + + @Configuration + static class ConcurrentChunkOrientedStepConfiguration { + + @Bean + public Step concurrentChunkOrientedStep(JobRepository jobRepository, JdbcTransactionManager transactionManager, + ItemReader itemReader, ItemProcessor itemProcessor, + ItemWriter itemWriter) { + return new ChunkOrientedStepBuilder(jobRepository, 2).reader(itemReader) .processor(itemProcessor) .writer(itemWriter) + .transactionManager(transactionManager) + .taskExecutor(new SimpleAsyncTaskExecutor()) .build(); } @@ -285,13 +426,51 @@ public Step faulTolerantChunkOrientedStep(JobRepository jobRepository, // skip policy configuration int skipLimit = Integer.parseInt(System.getProperty("skipLimit")); - Map, Boolean> skippableExceptions = Map.of(FlatFileParseException.class, true, - DataIntegrityViolationException.class, true); - LimitCheckingItemSkipPolicy skipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, skippableExceptions); + Set> skippableExceptions = Set.of(FlatFileParseException.class, + DataIntegrityViolationException.class); + LimitCheckingExceptionHierarchySkipPolicy skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy( + skippableExceptions, skipLimit); + + return new ChunkOrientedStepBuilder(jobRepository, 2).reader(itemReader) + .processor(itemProcessor) + .writer(itemWriter) + .transactionManager(transactionManager) + .faultTolerant() + .retryPolicy(retryPolicy) + .skipPolicy(skipPolicy) + .build(); + } + + } + + @Configuration + static class ConcurrentFaultTolerantChunkOrientedStepConfiguration { + + @Bean + public Step concurrentFaulTolerantChunkOrientedStep(JobRepository jobRepository, + JdbcTransactionManager transactionManager, ItemReader itemReader, + ItemProcessor itemProcessor, ItemWriter itemWriter) { + // retry policy configuration + int retryLimit = 3; + Set> nonRetrybaleExceptions = Set.of(FlatFileParseException.class, + DataIntegrityViolationException.class); + RetryPolicy retryPolicy = RetryPolicy.builder() + .maxAttempts(retryLimit) + .excludes(nonRetrybaleExceptions) + .build(); + + // skip policy configuration + int skipLimit = Integer.parseInt(System.getProperty("skipLimit")); + Set> skippableExceptions = Set.of(FlatFileParseException.class, + DataIntegrityViolationException.class); + LimitCheckingExceptionHierarchySkipPolicy skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy( + skippableExceptions, skipLimit); - return new ChunkOrientedStepBuilder(jobRepository, transactionManager, 2).reader(itemReader) + return new ChunkOrientedStepBuilder(jobRepository, 2).reader(itemReader) .processor(itemProcessor) .writer(itemWriter) + .transactionManager(transactionManager) + .taskExecutor(new SimpleAsyncTaskExecutor()) .faultTolerant() .retryPolicy(retryPolicy) .skipPolicy(skipPolicy) diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepObservabilityIntegrationTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepObservabilityIntegrationTests.java new file mode 100644 index 0000000000..f77b961536 --- /dev/null +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/ChunkOrientedStepObservabilityIntegrationTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2025-present 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.step.item; + +import java.util.List; + +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.job.Job; +import org.springframework.batch.core.job.JobExecution; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.job.parameters.JobParameters; +import org.springframework.batch.core.launch.JobOperator; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.Step; +import org.springframework.batch.core.step.builder.ChunkOrientedStepBuilder; +import org.springframework.batch.item.support.ListItemReader; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * Integration tests for observability features in {@link ChunkOrientedStep}. + * + * @author Mahmoud Ben Hassine + */ +public class ChunkOrientedStepObservabilityIntegrationTests { + + @Test + void testChunkOrientedStepMetrics() throws Exception { + // given + ApplicationContext context = new AnnotationConfigApplicationContext(TestConfiguration.class); + SimpleMeterRegistry meterRegistry = context.getBean(SimpleMeterRegistry.class); + JobOperator jobOperator = context.getBean(JobOperator.class); + Job job = context.getBean(Job.class); + + // when + JobExecution jobExecution = jobOperator.start(job, new JobParameters()); + + // then + Assertions.assertEquals(ExitStatus.COMPLETED, jobExecution.getExitStatus()); + Assertions.assertEquals(3, meterRegistry.getMeters().size()); + assertDoesNotThrow( + () -> meterRegistry.get("spring.batch.item.read") + .tag("spring.batch.item.read.job.name", "job") + .tag("spring.batch.item.read.step.name", "step") + .tag("spring.batch.item.read.status", "SUCCESS") + .timer(), + "There should be a meter of type TIMER named spring.batch.item.read registered in the meter registry"); + assertDoesNotThrow( + () -> meterRegistry.get("spring.batch.item.process") + .tag("spring.batch.item.process.job.name", "job") + .tag("spring.batch.item.process.step.name", "step") + .tag("spring.batch.item.process.status", "SUCCESS") + .timer(), + "There should be a meter of type TIMER named spring.batch.item.process registered in the meter registry"); + assertDoesNotThrow( + () -> meterRegistry.get("spring.batch.chunk.write") + .tag("spring.batch.chunk.write.job.name", "job") + .tag("spring.batch.chunk.write.step.name", "step") + .tag("spring.batch.chunk.write.status", "SUCCESS") + .timer(), + "There should be a meter of type TIMER named spring.batch.chunk.write registered in the meter registry"); + } + + @Configuration + @EnableBatchProcessing + static class TestConfiguration { + + @Bean + public Job job(JobRepository jobRepository, Step step) { + return new JobBuilder(jobRepository).start(step).build(); + } + + @Bean + public Step step(JobRepository jobRepository, MeterRegistry meterRegistry) { + return new ChunkOrientedStepBuilder(jobRepository, 2) + .reader(new ListItemReader<>(List.of("one", "two", "three", "four", "five"))) + .processor(String::toUpperCase) + .writer(items -> { + }) + .meterRegistry(meterRegistry) + .build(); + } + + @Bean + public SimpleMeterRegistry meterRegistry() { + return new SimpleMeterRegistry(); + } + + } + +} \ No newline at end of file diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanNonBufferingTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanNonBufferingTests.java index 91b5342158..dcec489a5c 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanNonBufferingTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/FaultTolerantStepFactoryBeanNonBufferingTests.java @@ -79,7 +79,7 @@ void setUp() throws Exception { factory.setIsReaderTransactionalQueue(true); JobInstance jobInstance = new JobInstance(1L, "skipJob"); - jobExecution = new JobExecution(jobInstance, new JobParameters()); + jobExecution = new JobExecution(jobInstance, 0L, new JobParameters()); } /** @@ -94,7 +94,7 @@ void testSkip() throws Exception { factory.setListeners(new SkipListener[] { skipListener }); Step step = factory.getObject(); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java index 4815ff7000..c4755b2fd6 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/RepeatOperationsStepFactoryBeanTests.java @@ -49,7 +49,7 @@ class RepeatOperationsStepFactoryBeanTests { private List list; - private final JobExecution jobExecution = new JobExecution(new JobInstance(0L, "job"), new JobParameters()); + private final JobExecution jobExecution = new JobExecution(new JobInstance(0L, "job"), 0L, new JobParameters()); @BeforeEach void setUp() { @@ -86,7 +86,7 @@ void testStepOperationsWithoutChunkListener() throws Exception { }); Step step = factory.getObject(); - step.execute(new StepExecution(step.getName(), jobExecution)); + step.execute(new StepExecution(step.getName(), jobExecution, 0L)); assertEquals(1, list.size()); } diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java index ca18ecc295..4925db24d8 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/item/SimpleStepFactoryBeanTests.java @@ -36,7 +36,6 @@ import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; import org.springframework.batch.core.step.Step; -import org.springframework.batch.core.step.StepExecution; import org.springframework.batch.core.listener.StepListener; import org.springframework.batch.core.job.SimpleJob; import org.springframework.batch.core.listener.ItemListenerSupport; @@ -54,7 +53,6 @@ import org.springframework.batch.repeat.exception.SimpleLimitExceptionHandler; import org.springframework.batch.repeat.policy.SimpleCompletionPolicy; import org.springframework.batch.support.transaction.ResourcelessTransactionManager; -import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.jdbc.support.JdbcTransactionManager; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; @@ -146,24 +144,6 @@ void testSimpleJob() throws Exception { assertTrue(written.contains("foo")); } - @Test - void testSimpleConcurrentJob() throws Exception { - - SimpleStepFactoryBean factory = getStepFactory("foo", "bar"); - factory.setTaskExecutor(new SimpleAsyncTaskExecutor()); - - AbstractStep step = (AbstractStep) factory.getObject(); - step.setName("step1"); - - JobExecution jobExecution = repository.createJobExecution(job.getName(), new JobParameters()); - StepExecution stepExecution = jobExecution.createStepExecution(step.getName()); - repository.add(stepExecution); - step.execute(stepExecution); - assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); - assertEquals(2, written.size()); - assertTrue(written.contains("foo")); - } - @Test void testSimpleJobWithItemListeners() throws Exception { 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 b1caf81dfb..ecef7d0f2b 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 @@ -86,8 +86,8 @@ void init() { taskletStep.setTransactionManager(new ResourcelessTransactionManager()); JobInstance jobInstance = new JobInstance(1L, "testJob"); - JobExecution jobExecution = new JobExecution(jobInstance, new JobParameters()); - stepExecution = new StepExecution("testStep", jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, new JobParameters()); + stepExecution = new StepExecution("testStep", jobExecution, 0L); } @Test diff --git a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/AsyncTaskletStepTests.java b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/AsyncTaskletStepTests.java index 2f274009ac..09d7d1935e 100644 --- a/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/AsyncTaskletStepTests.java +++ b/spring-batch-core/src/test/java/org/springframework/batch/core/step/tasklet/AsyncTaskletStepTests.java @@ -119,7 +119,7 @@ void testStepExecutionUpdates() throws Exception { JobExecution jobExecution = jobRepository.createJobExecution("JOB", new JobParameters()); StepExecution stepExecution = jobExecution.createStepExecution(step.getName()); - + stepExecution.setId(0L); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); @@ -148,6 +148,7 @@ void testStepExecutionFails() throws Exception { JobExecution jobExecution = jobRepository.createJobExecution("JOB", new JobParameters()); StepExecution stepExecution = jobExecution.createStepExecution(step.getName()); + stepExecution.setId(0L); step.execute(stepExecution); @@ -178,7 +179,7 @@ void testStepExecutionFailsWithProcessor() throws Exception { JobExecution jobExecution = jobRepository.createJobExecution("JOB", new JobParameters()); StepExecution stepExecution = jobExecution.createStepExecution(step.getName()); - + stepExecution.setId(0L); step.execute(stepExecution); assertEquals(BatchStatus.FAILED, stepExecution.getStatus()); @@ -200,6 +201,7 @@ void testStepExecutionFailsOnLastItem() throws Exception { JobExecution jobExecution = jobRepository.createJobExecution("JOB", new JobParameters()); StepExecution stepExecution = jobExecution.createStepExecution(step.getName()); + stepExecution.setId(0L); step.execute(stepExecution); 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 e5ca60fb42..1eb9934642 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 @@ -127,8 +127,8 @@ void setUp() throws Exception { @Test void testStepExecutor() throws Exception { - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); assertEquals(1, processed.size()); assertEquals(1, stepExecution.getReadCount()); @@ -137,10 +137,10 @@ void testStepExecutor() throws Exception { @Test void testCommitCount_Even() throws Exception { - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); step = getStep(new String[] { "foo", "bar", "spam", "eggs" }, 2); step.setTransactionManager(transactionManager); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); assertEquals(4, processed.size()); assertEquals(4, stepExecution.getReadCount()); @@ -151,10 +151,10 @@ void testCommitCount_Even() throws Exception { @Test void testCommitCount_Uneven() throws Exception { - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); step = getStep(new String[] { "foo", "bar", "spam" }, 2); step.setTransactionManager(transactionManager); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); assertEquals(3, processed.size()); assertEquals(3, stepExecution.getReadCount()); @@ -164,8 +164,8 @@ void testCommitCount_Uneven() throws Exception { @Test void testEmptyReader() throws Exception { - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step = getStep(new String[0]); step.setTasklet(new TestingChunkOrientedTasklet<>(getReader(new String[0]), itemWriter, new RepeatTemplate())); step.setStepOperations(new RepeatTemplate()); @@ -184,8 +184,8 @@ void testEmptyReader() throws Exception { @Test void testStepExecutionUpdates() throws Exception { - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.setStepOperations(new RepeatTemplate()); @@ -205,8 +205,8 @@ void testStepExecutionUpdates() throws Exception { @Test void testStepExecutionUpdateFailure() throws Exception { - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); JobRepository repository = new JobRepositoryFailedUpdateStub(); @@ -247,8 +247,8 @@ void testIncrementRollbackCount() { }; step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); try { step.execute(stepExecution); @@ -268,8 +268,8 @@ void testExitCodeDefaultClassification() { }; step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); try { step.execute(stepExecution); @@ -296,8 +296,8 @@ public ExitStatus afterStep(StepExecution stepExecution) { return ExitStatus.FAILED.addExitDescription("FOO"); } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); try { step.execute(stepExecution); @@ -319,8 +319,8 @@ void testNonRestartedJob() throws Exception { MockRestartableItemReader tasklet = new MockRestartableItemReader(); step.setTasklet(new TestingChunkOrientedTasklet<>(tasklet, itemWriter)); step.registerStream(tasklet); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); @@ -330,8 +330,8 @@ void testNonRestartedJob() throws Exception { @Test void testSuccessfulExecutionWithExecutionContext() throws Exception { - final JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - final StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + final JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + final StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.setJobRepository(new JobRepositorySupport() { @Override public void updateExecutionContext(StepExecution stepExecution) { @@ -347,8 +347,8 @@ public void updateExecutionContext(StepExecution stepExecution) { @Test void testSuccessfulExecutionWithFailureOnSaveOfExecutionContext() throws Exception { - final JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - final StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + final JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + final StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.setJobRepository(new JobRepositorySupport() { private int counter = 0; @@ -376,8 +376,8 @@ public void updateExecutionContext(StepExecution stepExecution) { void testNoSaveExecutionAttributesRestartableJob() { MockRestartableItemReader tasklet = new MockRestartableItemReader(); step.setTasklet(new TestingChunkOrientedTasklet<>(tasklet, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); assertDoesNotThrow(() -> step.execute(stepExecution)); assertFalse(tasklet.isRestoreFromCalled()); @@ -390,8 +390,8 @@ void testNoSaveExecutionAttributesRestartableJob() { @Test void testRestartJobOnNonRestartableTasklet() throws Exception { step.setTasklet(new TestingChunkOrientedTasklet<>(() -> "foo", itemWriter)); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.execute(stepExecution); } @@ -413,8 +413,8 @@ public void update(ExecutionContext executionContext) { }; step.setTasklet(new TestingChunkOrientedTasklet<>(reader, itemWriter)); step.registerStream(reader); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); assertFalse(stepExecution.getExecutionContext().containsKey("foo")); @@ -434,8 +434,8 @@ public void update(ExecutionContext executionContext) { executionContext.putString("foo", "bar"); } } }); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); assertFalse(stepExecution.getExecutionContext().containsKey("foo")); @@ -459,8 +459,8 @@ public ExitStatus afterStep(StepExecution stepExecution) { return null; } }); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.execute(stepExecution); assertEquals(2, list.size()); } @@ -481,7 +481,8 @@ public void open(ExecutionContext executionContext) throws ItemStreamException { }; step.setStreams(new ItemStream[] { reader }); step.registerStepExecutionListener(reader); - StepExecution stepExecution = new StepExecution(step.getName(), new JobExecution(jobInstance, jobParameters)); + StepExecution stepExecution = new StepExecution(step.getName(), + new JobExecution(jobInstance, 0L, jobParameters), 0L); step.execute(stepExecution); assertEquals(1, list.size()); } @@ -504,8 +505,8 @@ public ExitStatus afterStep(StepExecution stepExecution) { stepTemplate.setCompletionPolicy(new SimpleCompletionPolicy(5)); step.setStepOperations(stepTemplate); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.execute(stepExecution); assertEquals(1, list.size()); ExitStatus returnedStatus = stepExecution.getExitStatus(); @@ -530,8 +531,8 @@ public String read() throws RuntimeException { throw new RuntimeException("FOO"); } }, itemWriter)); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); step.execute(stepExecution); assertEquals("FOO", stepExecution.getFailureExceptions().get(0).getMessage()); assertEquals(1, list.size()); @@ -554,8 +555,8 @@ public void update(ExecutionContext executionContext) { }; step.setTasklet(new TestingChunkOrientedTasklet<>(reader, itemWriter)); step.setStreams(new ItemStream[] { reader }); - JobExecution jobExecution = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecution); + JobExecution jobExecution = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecution, 0L); assertFalse(stepExecution.getExecutionContext().containsKey("foo")); @@ -582,8 +583,8 @@ void testStatusForInterruptedException() throws Exception { step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); @@ -603,8 +604,8 @@ void testStatusForNormalFailure() throws Exception { }; step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); // step.setLastExecution(stepExecution); @@ -624,8 +625,8 @@ void testStatusForErrorFailure() throws Exception { }; step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); // step.setLastExecution(stepExecution); @@ -653,8 +654,8 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); // step.setLastExecution(stepExecution); @@ -683,8 +684,8 @@ protected void doRollback(DefaultTransactionStatus status) throws TransactionExc } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); // step.setLastExecution(stepExecution); @@ -708,8 +709,8 @@ public void close() throws ItemStreamException { } } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); // The job actually completed, but the streams couldn't be closed. @@ -736,8 +737,8 @@ public void close() throws ItemStreamException { step.setTasklet(new TestingChunkOrientedTasklet<>(itemReader, itemWriter)); step.registerStream(itemReader); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); stepExecution.setExecutionContext(foobarEc); // step.setLastExecution(stepExecution); @@ -769,7 +770,8 @@ public String read() throws RuntimeException { step.setTasklet(new TestingChunkOrientedTasklet<>(reader, itemWriter)); step.registerStream(reader); - StepExecution stepExecution = new StepExecution(step.getName(), new JobExecution(jobInstance, jobParameters)); + StepExecution stepExecution = new StepExecution(step.getName(), + new JobExecution(jobInstance, 0L, jobParameters), 0L); step.execute(stepExecution); assertEquals(BatchStatus.FAILED, stepExecution.getStatus()); @@ -788,8 +790,8 @@ void testStepToCompletion() throws Exception { template.setCompletionPolicy(new DefaultResultCompletionPolicy()); step.setStepOperations(template); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); assertEquals(3, processed.size()); @@ -810,7 +812,8 @@ public ExitStatus afterStep(StepExecution stepExecution) { } }; step.setStepExecutionListeners(new StepExecutionListener[] { listener }); - StepExecution stepExecution = new StepExecution(step.getName(), new JobExecution(jobInstance, jobParameters)); + StepExecution stepExecution = new StepExecution(step.getName(), + new JobExecution(jobInstance, 0L, jobParameters), 0L); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); @@ -828,8 +831,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); DefaultTransactionAttribute transactionAttribute = new DefaultTransactionAttribute() { @Override @@ -852,8 +855,8 @@ public RepeatStatus execute(StepContribution contribution, ChunkContext chunkCon return null; } }); - JobExecution jobExecutionContext = new JobExecution(jobInstance, jobParameters); - StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext); + JobExecution jobExecutionContext = new JobExecution(jobInstance, 0L, jobParameters); + StepExecution stepExecution = new StepExecution(step.getName(), jobExecutionContext, 0L); step.execute(stepExecution); assertEquals(BatchStatus.COMPLETED, stepExecution.getStatus()); } diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/trivial-context-autoregister.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/trivial-context-autoregister.xml index 7ef2ee26ed..e73d1861f5 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/trivial-context-autoregister.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/support/trivial-context-autoregister.xml @@ -11,6 +11,4 @@ - - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests-context.xml index ffcebb51ef..fc7c37b53a 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/configuration/xml/TaskletStepAllowStartIfCompleteTests-context.xml @@ -36,6 +36,4 @@ - - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/partition/launch-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/partition/launch-context.xml index 40a88d4e3b..16e6ad60da 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/partition/launch-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/partition/launch-context.xml @@ -62,12 +62,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - - - - - - - - - + - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/repository/dao/TablePrefixTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/repository/dao/TablePrefixTests-context.xml index 437c6c5921..e756500308 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/repository/dao/TablePrefixTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/repository/dao/TablePrefixTests-context.xml @@ -16,11 +16,8 @@ - - - + - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/context/CommitIntervalJobParameter-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/context/CommitIntervalJobParameter-context.xml index 7d7ecd5eca..3001f87717 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/context/CommitIntervalJobParameter-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/scope/context/CommitIntervalJobParameter-context.xml @@ -9,12 +9,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - \ No newline at end of file diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests-context.xml index 31f1db5bb7..88d4275ef5 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/FaultTolerantExceptionClassesTests-context.xml @@ -142,12 +142,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTests-context.xml index 798a82c2af..dda0ba4d01 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/item/ScriptItemProcessorTests-context.xml @@ -19,16 +19,8 @@ - - - - - - - - + - diff --git a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/skip/ReprocessExceptionTests-context.xml b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/skip/ReprocessExceptionTests-context.xml index bba1cff094..f85e093f5f 100644 --- a/spring-batch-core/src/test/resources/org/springframework/batch/core/step/skip/ReprocessExceptionTests-context.xml +++ b/spring-batch-core/src/test/resources/org/springframework/batch/core/step/skip/ReprocessExceptionTests-context.xml @@ -6,12 +6,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - diff --git a/spring-batch-core/src/test/resources/simple-job-launcher-context.xml b/spring-batch-core/src/test/resources/simple-job-launcher-context.xml index db6b23764c..de0eace8fd 100644 --- a/spring-batch-core/src/test/resources/simple-job-launcher-context.xml +++ b/spring-batch-core/src/test/resources/simple-job-launcher-context.xml @@ -11,15 +11,8 @@ p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager" /> - - - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean" + p:jobRepository-ref="jobRepository"/> diff --git a/spring-batch-docs/modules/ROOT/nav.adoc b/spring-batch-docs/modules/ROOT/nav.adoc index 01406bc879..540c0f8a0d 100644 --- a/spring-batch-docs/modules/ROOT/nav.adoc +++ b/spring-batch-docs/modules/ROOT/nav.adoc @@ -53,8 +53,9 @@ ** xref:spring-batch-integration/launching-jobs-through-messages.adoc[] ** xref:spring-batch-integration/available-attributes-of-the-job-launching-gateway.adoc[] ** xref:spring-batch-integration/sub-elements.adoc[] -* xref:monitoring-and-metrics.adoc[] -* xref:tracing.adoc[] +* xref:spring-batch-observability.adoc[] +** xref:spring-batch-observability/micrometer.adoc[] +** xref:spring-batch-observability/jfr.adoc[] * Appendices ** xref:appendix.adoc[] ** xref:schema-appendix.adoc[] diff --git a/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc b/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc index 5e25cddbb7..d1b05fe799 100644 --- a/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc +++ b/spring-batch-docs/modules/ROOT/pages/common-patterns.adoc @@ -766,7 +766,7 @@ public Job job1(JobRepository jobRepository, Step step1, Step step2) { @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(reader()) .writer(savingWriter()) .listener(promotionListener()) diff --git a/spring-batch-docs/modules/ROOT/pages/index.adoc b/spring-batch-docs/modules/ROOT/pages/index.adoc index 8557d07958..0a28755815 100644 --- a/spring-batch-docs/modules/ROOT/pages/index.adoc +++ b/spring-batch-docs/modules/ROOT/pages/index.adoc @@ -28,9 +28,8 @@ xref:common-patterns.adoc#commonPatterns[Common Patterns] :: Common batch proces and guidelines. xref:spring-batch-integration.adoc[Spring Batch Integration] :: Integration between Spring Batch and Spring Integration projects. -xref:monitoring-and-metrics.adoc[Monitoring and metrics] :: Batch jobs +xref:spring-batch-observability.adoc[Spring Batch Observability] :: Batch jobs monitoring and metrics. -xref:tracing.adoc[Tracing] :: Tracing with Micrometer. The following appendices are available: diff --git a/spring-batch-docs/modules/ROOT/pages/job/configuring-operator.adoc b/spring-batch-docs/modules/ROOT/pages/job/configuring-operator.adoc index 0c0d4c15d9..fc0f61b9ba 100644 --- a/spring-batch-docs/modules/ROOT/pages/job/configuring-operator.adoc +++ b/spring-batch-docs/modules/ROOT/pages/job/configuring-operator.adoc @@ -2,40 +2,40 @@ = Configuring a JobOperator The most basic implementation of the `JobOperator` interface is the `TaskExecutorJobOperator`. -It only requires two dependencies: a `JobRepository` and a `JobRegistry`. - +It requires only one dependency: a `JobRepository`. All other dependencies like `JobRegistry`, +`MeterRegistry`, `TransactionManager`, etc are optional. Spring Batch provides a factory bean +to simplify the configuration of this operator: `JobOperatorFactoryBean`. This factory bean +creates a transactional proxy around the `TaskExecutorJobOperator` to ensure that all its public methods +are executed within a transaction. [tabs] ==== Java:: + -The following example shows a `TaskExecutorJobLauncher` in Java: +The following example shows how to configure a `TaskExecutorJobOperator` in Java: + .Java Configuration [source, java] ---- ... @Bean -public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegistry) throws Exception { - TaskExecutorJobJobOperator jobOperator = new TaskExecutorJobOperator(); +public JobOperatorFactoryBean jobOperator(JobRepository jobRepository) { + JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean(); jobOperator.setJobRepository(jobRepository); - jobOperator.setJobRegistry(jobRegistry); - jobOperator.afterPropertiesSet(); - return jobOperator; + return jobOperatorFactoryBean; } ... ---- XML:: + -The following example shows a `TaskExecutorJobOperator` in XML: +The following example shows how to configure a `TaskExecutorJobOperator` in XML: + .XML Configuration [source, xml] ---- - + - ---- @@ -72,13 +72,11 @@ The following Java example configures a `TaskExecutorJobOperator` to return imme [source, java] ---- @Bean -public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegistry) throws Exception { - TaskExecutorJobJobOperator jobOperator = new TaskExecutorJobOperator(); +public JobOperatorFactoryBean jobOperator(JobRepository jobRepository) { + JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean(); jobOperator.setJobRepository(jobRepository); - jobOperator.setJobRegistry(jobRegistry); jobOperator.setTaskExecutor(new SimpleAsyncTaskExecutor()); - jobOperator.afterPropertiesSet(); - return jobOperator; + return jobOperatorFactoryBean; } ---- @@ -89,9 +87,8 @@ The following XML example configures a `TaskExecutorJobOperator` to return immed .XML Configuration [source, xml] ---- - + - diff --git a/spring-batch-docs/modules/ROOT/pages/processor.adoc b/spring-batch-docs/modules/ROOT/pages/processor.adoc index 02993846de..8e24984dcf 100644 --- a/spring-batch-docs/modules/ROOT/pages/processor.adoc +++ b/spring-batch-docs/modules/ROOT/pages/processor.adoc @@ -101,7 +101,7 @@ public Job ioSampleJob(JobRepository jobRepository, Step step1) { @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(fooReader()) .processor(fooProcessor()) .writer(barWriter()) @@ -205,7 +205,7 @@ public Job ioSampleJob(JobRepository jobRepository, Step step1) { @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(fooReader()) .processor(compositeProcessor()) .writer(foobarWriter()) diff --git a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/delegate-pattern-registering.adoc b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/delegate-pattern-registering.adoc index c89e4f2498..aaeb0f56b9 100644 --- a/spring-batch-docs/modules/ROOT/pages/readers-and-writers/delegate-pattern-registering.adoc +++ b/spring-batch-docs/modules/ROOT/pages/readers-and-writers/delegate-pattern-registering.adoc @@ -30,7 +30,7 @@ public Job ioSampleJob(JobRepository jobRepository, Step step1) { @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(fooReader()) .processor(fooProcessor()) .writer(compositeItemWriter()) diff --git a/spring-batch-docs/modules/ROOT/pages/scalability.adoc b/spring-batch-docs/modules/ROOT/pages/scalability.adoc index 48f711e6b2..697d9f2d77 100644 --- a/spring-batch-docs/modules/ROOT/pages/scalability.adoc +++ b/spring-batch-docs/modules/ROOT/pages/scalability.adoc @@ -52,7 +52,7 @@ public TaskExecutor taskExecutor() { @Bean public Step sampleStep(TaskExecutor taskExecutor, JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("sampleStep", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .taskExecutor(taskExecutor) @@ -103,7 +103,7 @@ follows: @Bean public Step sampleStep(TaskExecutor taskExecutor, JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("sampleStep", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .taskExecutor(taskExecutor) diff --git a/spring-batch-docs/modules/ROOT/pages/spring-batch-observability.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability.adoc new file mode 100644 index 0000000000..5c8f4135cf --- /dev/null +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability.adoc @@ -0,0 +1,11 @@ + +[[springBatchObservability]] += Spring Batch Observability + +Observability is a critical aspect of modern applications, and Spring Batch provides robust support for monitoring and tracing batch jobs. + +This section covers the integration of Spring Batch with popular observability tools such as Micrometer and Java Flight Recorder (JFR): + +[role="xmlContent"] +* xref:spring-batch-observability/micrometer.adoc[Micrometer Support] +* xref:spring-batch-observability/jfr.adoc[Java Flight Recorder Support] \ No newline at end of file diff --git a/spring-batch-docs/modules/ROOT/pages/spring-batch-observability/jfr.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability/jfr.adoc new file mode 100644 index 0000000000..411f51b6d9 --- /dev/null +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability/jfr.adoc @@ -0,0 +1,13 @@ +[[jfr]] += Java Flight Recorder (JFR) support + +As of version 6, Spring Batch provides support for Java Flight Recorder (JFR) to help you monitor and troubleshoot batch jobs. JFR is a low-overhead, event-based profiling tool built into the Java Virtual Machine (JVM) that allows developers to collect detailed information about the performance and behavior of their applications. + +JFR can be enabled by adding the following JVM options when starting your Spring Batch application: + +[source, bash] +---- +java -XX:StartFlightRecording:filename=my-batch-job.jfr,dumponexit=true -jar my-batch-job.jar +---- + +Once JFR is enabled, Spring Batch will automatically create JFR events for key batch processing activities, such as job and step executions, item reads and writes, as well as transaction boundaries. These events can be viewed and analyzed using tools such as Java Mission Control (JMC) or other JFR-compatible tools. \ No newline at end of file diff --git a/spring-batch-docs/modules/ROOT/pages/monitoring-and-metrics.adoc b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability/micrometer.adoc similarity index 84% rename from spring-batch-docs/modules/ROOT/pages/monitoring-and-metrics.adoc rename to spring-batch-docs/modules/ROOT/pages/spring-batch-observability/micrometer.adoc index 0d22ebcabb..0eb49fc31e 100644 --- a/spring-batch-docs/modules/ROOT/pages/monitoring-and-metrics.adoc +++ b/spring-batch-docs/modules/ROOT/pages/spring-batch-observability/micrometer.adoc @@ -1,7 +1,9 @@ +[[micrometer]] += Micrometer support -[[monitoring-and-metrics]] -= Monitoring and metrics +[[monitoring-and-metrics]] +== Monitoring and metrics Since version 4.2, Spring Batch provides support for batch monitoring and metrics based on link:$$https://micrometer.io/$$[Micrometer]. This section describes @@ -82,4 +84,14 @@ Metrics.globalRegistry.config().meterFilter(MeterFilter.denyNameStartsWith("spri ---- See Micrometer's link:$$http://micrometer.io/docs/concepts#_meter_filters$$[reference documentation] -for more details. \ No newline at end of file +for more details. + +[[tracing]] +== Tracing + +As of version 5, Spring Batch provides tracing through Micrometer's `Observation` API. By default, tracing is enabled +when using `@EnableBatchProcessing`. Spring Batch will create a trace for each job execution and a span for each +step execution. + +If you do not use `EnableBatchProcessing`, you need to register a `BatchObservabilityBeanPostProcessor` in your +application context, which will automatically setup Micrometer's observability in your jobs and steps beans. diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/commit-interval.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/commit-interval.adoc index 60528a2c28..dd56270eef 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/commit-interval.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/commit-interval.adoc @@ -29,7 +29,7 @@ public Job sampleJob(JobRepository jobRepository, Step step1) { @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .build(); diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc index 5c5136c825..7eef3ac47c 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring-skip.adoc @@ -22,7 +22,7 @@ The following Java example shows an example of using a skip limit: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(flatFileItemReader()) .writer(itemWriter()) .faultTolerant() @@ -83,7 +83,7 @@ The following Java example shows an example excluding a particular exception: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(flatFileItemReader()) .writer(itemWriter()) .faultTolerant() diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring.adoc index 100782cd63..95b3af9a31 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/configuring.adoc @@ -33,7 +33,7 @@ public Job sampleJob(JobRepository jobRepository, Step sampleStep) { public Step sampleStep(JobRepository jobRepository, // <2> PlatformTransactionManager transactionManager) { // <1> return new StepBuilder("sampleStep", jobRepository) - .chunk(10, transactionManager) // <3> + .chunk(10).transactionManager(transactionManager) // <3> .reader(itemReader()) .writer(itemWriter()) .build(); diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/controlling-rollback.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/controlling-rollback.adoc index 69b704c5ae..a4e21ee625 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/controlling-rollback.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/controlling-rollback.adoc @@ -21,7 +21,7 @@ In Java, you can control rollback as follows: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .faultTolerant() @@ -75,7 +75,7 @@ The following example shows how to create a reader that does not buffer items in @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .readerIsTransactionalQueue() 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 d07884516a..3bb0748bc2 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 @@ -27,7 +27,7 @@ The following example shows a listener applied at the chunk level in Java: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(reader()) .writer(writer()) .listener(chunkListener()) diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/registering-item-streams.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/registering-item-streams.adoc index 643087d7d6..ae2c2118ac 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/registering-item-streams.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/registering-item-streams.adoc @@ -25,7 +25,7 @@ The following example shows how to register a `stream` on a `step` in Java: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(itemReader()) .writer(compositeItemWriter()) .stream(fileItemWriter1()) diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc index 20e80bd72d..b59e949372 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/restart.adoc @@ -28,7 +28,7 @@ The following code fragment shows an example of a start limit configuration in J @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .startLimit(1) @@ -80,7 +80,7 @@ The following code fragment shows how to define a restartable job in Java: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .allowStartIfComplete(true) @@ -132,7 +132,7 @@ public Job footballJob(JobRepository jobRepository, Step playerLoad, Step gameLo @Bean public Step playerLoad(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("playerLoad", jobRepository) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(playerFileItemReader()) .writer(playerWriter()) .build(); @@ -142,7 +142,7 @@ public Step playerLoad(JobRepository jobRepository, PlatformTransactionManager t public Step gameLoad(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("gameLoad", jobRepository) .allowStartIfComplete(true) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(gameFileItemReader()) .writer(gameWriter()) .build(); @@ -152,7 +152,7 @@ public Step gameLoad(JobRepository jobRepository, PlatformTransactionManager tra public Step playerSummarization(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("playerSummarization", jobRepository) .startLimit(2) - .chunk(10, transactionManager) + .chunk(10).transactionManager(transactionManager) .reader(playerSummarizationSource()) .writer(summaryWriter()) .build(); diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/retry-logic.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/retry-logic.adoc index c841e94a7c..e81fcc228d 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/retry-logic.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/retry-logic.adoc @@ -20,7 +20,7 @@ In Java, retry should be configured as follows: @Bean public Step step1(JobRepository jobRepository, PlatformTransactionManager transactionManager) { return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .faultTolerant() diff --git a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/transaction-attributes.adoc b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/transaction-attributes.adoc index fdd7fcc327..f48f0e3895 100644 --- a/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/transaction-attributes.adoc +++ b/spring-batch-docs/modules/ROOT/pages/step/chunk-oriented-processing/transaction-attributes.adoc @@ -25,7 +25,7 @@ public Step step1(JobRepository jobRepository, PlatformTransactionManager transa attribute.setTimeout(30); return new StepBuilder("step1", jobRepository) - .chunk(2, transactionManager) + .chunk(2).transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .transactionAttribute(attribute) diff --git a/spring-batch-docs/modules/ROOT/pages/tracing.adoc b/spring-batch-docs/modules/ROOT/pages/tracing.adoc deleted file mode 100644 index 113190feeb..0000000000 --- a/spring-batch-docs/modules/ROOT/pages/tracing.adoc +++ /dev/null @@ -1,9 +0,0 @@ -[[tracing]] -= Tracing - -As of version 5, Spring Batch provides tracing through Micrometer's `Observation` API. By default, tracing is enabled -when using `@EnableBatchProcessing`. Spring Batch will create a trace for each job execution and a span for each -step execution. - -If you do not use `EnableBatchProcessing`, you need to register a `BatchObservabilityBeanPostProcessor` in your -application context, which will automatically setup Micrometer's observability in your jobs and steps beans. diff --git a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc index 7ec1625480..4b4991aaa0 100644 --- a/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc +++ b/spring-batch-docs/modules/ROOT/pages/whatsnew.adoc @@ -3,13 +3,16 @@ This section highlights the major changes in Spring Batch 6.0. For the complete list of changes, please refer to the https://github.com/spring-projects/spring-batch/releases[release notes]. -Spring Batch 6.0 includes the following features: +Spring Batch 6.0 includes the following features and improvements: * xref:whatsnew.adoc#dependencies-upgrade[Dependencies upgrade] * xref:whatsnew.adoc#batch-infrastrucutre-configuration-improvements[Batch infrastructure configuration improvements] * xref:whatsnew.adoc#new-implementation-of-the-chunk-oriented-processing-model[New implementation of the chunk-oriented processing model] +* xref:whatsnew.adoc#new-concurrency-model[New concurrency model] * xref:whatsnew.adoc#new-command-line-operator[New command line operator] * xref:whatsnew.adoc#ability-to-recover-failed-job-executions[Ability to recover failed job executions] +* xref:whatsnew.adoc#ability-to-stop-all-kind-of-steps[Ability to stop all kinds of steps] +* xref:whatsnew.adoc#observability-with-jfr[Observability support with the Java Flight Recorder (JFR)] * xref:whatsnew.adoc#deprecations-and-pruning[Deprecations and pruning] [[dependencies-upgrade]] @@ -70,7 +73,8 @@ In this release, several changes have been made to simplify the batch infrastruc * The `JobRepository` now extends the `JobExplorer` interface, so there is no need to define a separate `JobExplorer` bean. * The `JobOperator` now extends the `JobLauncher` interface, so there is no need to define a separate `JobLauncher` bean. -* The `JobRegistry` is now smart enough to register jobs automatically, so there is no need to define a separate `JobRegistrySmartInitializingSingleton` bean. +* The `JobRegistry` is now optional, and smart enough to register jobs automatically, so there is no need to define a separate `JobRegistrySmartInitializingSingleton` bean. +* The transaction manager is now optional, and a default `ResourcelessTransactionManager` is used if none is provided. This reduces the number of beans required for a typical batch application and simplifies the configuration code. @@ -121,7 +125,7 @@ public Step faulTolerantChunkOrientedStep(JobRepository jobRepository, JdbcTrans // skip policy configuration int skipLimit = 50; var skippableExceptions = Set.of(FlatFileParseException.class); - SkipPolicy skipPolicy = new LimitCheckingItemSkipPolicy(skipLimit, skippableExceptions); + SkipPolicy skipPolicy = new LimitCheckingExceptionHierarchySkipPolicy(skippableExceptions, skipLimit); // step configuration int chunkSize = 100; @@ -138,6 +142,15 @@ public Step faulTolerantChunkOrientedStep(JobRepository jobRepository, JdbcTrans Please refer to the https://github.com/spring-projects/spring-batch/wiki/Spring-Batch-6.0-Migration-Guide[migration guide] for more details on how to migrate from the previous implementation to the new one. +[[new-concurrency-model]] +== New concurrency model + +Prior to this release, the concurrency model based on the "parallel iteration" concept required a lot of state synchronization at different levels and had several limitations related to throttling and backpressure leading to confusing transaction semantics and poor performance. + +This release revisits that model and comes with a new, simplified approach to concurrency based on the producer-consumer pattern. A concurrent chunk-oriented step now uses a bounded internal queue between the producer thread and consumer threads. Items are put in the queue as soon as they are ready to be processed, and consumer threads take items from the queue as soon as they are available for processing. Once a chunk is ready to be written, the producer thread pauses until the chunk is written, and then resumes producing items. + +This new model is more efficient, easier to understand and provides better performance for concurrent executions. + [[new-command-line-operator]] == New command line operator @@ -154,6 +167,23 @@ Prior to this release, if a job execution fails abruptly, it was not possible to This release introduces a new method named `recover` in the `JobOperator` interface that allows you to recover failed job executions consistently across all job repositories. +[[ability-to-stop-all-kind-of-steps]] +== Ability to stop all kinds of steps + +As of v5.2, it is only possible to externally stop `Tasklet` steps through `JobOperator#stop`. +If a custom `Step` implementation wants to handle external stop signals, it just can't. + +This release adds a new interface, named `StoppableStep`, that extends `Step` and which can be implemented by any step that is able to handle stop signals. + +[[observability-with-jfr]] +== Observability with the Java Flight Recorder (JFR) + +In addition to the existing Micrometer metrics, Spring Batch 6.0 introduces support for the Java Flight Recorder (JFR) to provide enhanced observability capabilities. + +JFR is a powerful profiling and event collection framework built into the Java Virtual Machine (JVM). It allows you to capture detailed information about the runtime behavior of your applications with minimal performance overhead. + +This release introduces several JFR events to monitor key aspects of a batch job execution, including job and step executions, item reads and writes, as well as transaction boundaries. + [[deprecations-and-pruning]] == Deprecations and pruning diff --git a/spring-batch-docs/pom.xml b/spring-batch-docs/pom.xml index a11dee44d1..bcf3a9bed9 100644 --- a/spring-batch-docs/pom.xml +++ b/spring-batch-docs/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-docs Spring Batch Docs diff --git a/spring-batch-infrastructure/pom.xml b/spring-batch-infrastructure/pom.xml index a4de25b3fc..da6d4c71a9 100644 --- a/spring-batch-infrastructure/pom.xml +++ b/spring-batch-infrastructure/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-infrastructure jar diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java index a86079cc0f..0eb449dab4 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -31,6 +31,7 @@ * * @param type of mapped items * @author Mahmoud Ben Hassine + * @author Seungyong Hong * @since 4.3 */ public class RecordFieldSetMapper implements FieldSetMapper { @@ -63,6 +64,10 @@ public RecordFieldSetMapper(Class targetType, ConversionService conversionSer this.constructorParameterNames = BeanUtils.getParameterNames(this.mappedConstructor); this.constructorParameterTypes = this.mappedConstructor.getParameterTypes(); } + else { + this.constructorParameterNames = new String[0]; + this.constructorParameterTypes = new Class[0]; + } } @Override diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java index a3fc68ff44..3a1dde5eef 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/file/mapping/RecordFieldSetMapperTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.batch.item.file.mapping; import org.junit.jupiter.api.Test; - import org.springframework.batch.item.file.transform.DefaultFieldSet; import org.springframework.batch.item.file.transform.FieldSet; @@ -26,6 +25,7 @@ /** * @author Mahmoud Ben Hassine + * @author Seungyong Hong */ class RecordFieldSetMapperTests { @@ -68,7 +68,23 @@ void testMapFieldSetWhenFieldNamesAreNotSpecified() { assertEquals("Field names must be specified", exception.getMessage()); } + @Test + void testMapFieldSetWhenEmptyRecord() { + // given + RecordFieldSetMapper mapper = new RecordFieldSetMapper<>(EmptyRecord.class); + FieldSet fieldSet = new DefaultFieldSet(new String[0], new String[0]); + + // when + EmptyRecord empty = mapper.mapFieldSet(fieldSet); + + // then + assertNotNull(empty); + } + record Person(int id, String name) { } + record EmptyRecord() { + } + } diff --git a/spring-batch-integration/pom.xml b/spring-batch-integration/pom.xml index 3a51a2acac..0f639f5858 100644 --- a/spring-batch-integration/pom.xml +++ b/spring-batch-integration/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-integration Spring Batch Integration diff --git a/spring-batch-integration/src/test/java/org/springframework/batch/integration/chunk/RemoteChunkingManagerStepBuilderTests.java b/spring-batch-integration/src/test/java/org/springframework/batch/integration/chunk/RemoteChunkingManagerStepBuilderTests.java index f5911e16ab..704b262215 100644 --- a/spring-batch-integration/src/test/java/org/springframework/batch/integration/chunk/RemoteChunkingManagerStepBuilderTests.java +++ b/spring-batch-integration/src/test/java/org/springframework/batch/integration/chunk/RemoteChunkingManagerStepBuilderTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.listener.ChunkListener; import org.springframework.batch.core.listener.ItemReadListener; import org.springframework.batch.core.listener.ItemWriteListener; @@ -356,6 +357,7 @@ else if (count < items.size()) { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class BatchConfiguration { @Bean diff --git a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepIntegrationTests-context.xml b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepIntegrationTests-context.xml index 9f40ab4b64..90ac290c6d 100644 --- a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepIntegrationTests-context.xml +++ b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepIntegrationTests-context.xml @@ -75,12 +75,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - diff --git a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepJmsIntegrationTests-context.xml b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepJmsIntegrationTests-context.xml index 61ce74d2ce..702dec1c86 100644 --- a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepJmsIntegrationTests-context.xml +++ b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkFaultTolerantStepJmsIntegrationTests-context.xml @@ -109,12 +109,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - \ No newline at end of file diff --git a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkStepIntegrationTests-context.xml b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkStepIntegrationTests-context.xml index e2dc82f365..0ce037dd99 100644 --- a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkStepIntegrationTests-context.xml +++ b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/chunk/RemoteChunkStepIntegrationTests-context.xml @@ -62,12 +62,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - \ No newline at end of file diff --git a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/config/xml/JobLaunchingGatewayParserTests-context.xml b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/config/xml/JobLaunchingGatewayParserTests-context.xml index e0821915e4..1d3772de55 100644 --- a/spring-batch-integration/src/test/resources/org/springframework/batch/integration/config/xml/JobLaunchingGatewayParserTests-context.xml +++ b/spring-batch-integration/src/test/resources/org/springframework/batch/integration/config/xml/JobLaunchingGatewayParserTests-context.xml @@ -15,12 +15,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - - - - + - diff --git a/spring-batch-samples/pom.xml b/spring-batch-samples/pom.xml index a528ecc12d..e3a3ce43dd 100644 --- a/spring-batch-samples/pom.xml +++ b/spring-batch-samples/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-samples jar diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/amqp/AmqpJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/amqp/AmqpJobConfiguration.java index 4e2238969e..9fcb89077e 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/amqp/AmqpJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/amqp/AmqpJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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. @@ -54,7 +54,8 @@ public Job job(JobRepository jobRepository, Step step) { @Bean public Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager, RabbitTemplate rabbitInputTemplate, RabbitTemplate rabbitOutputTemplate) { - return new StepBuilder("step", jobRepository).chunk(1, transactionManager) + return new StepBuilder("step", jobRepository).chunk(1) + .transactionManager(transactionManager) .reader(amqpItemReader(rabbitInputTemplate)) .processor(new MessageProcessor()) .writer(amqpItemWriter(rabbitOutputTemplate)) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/chunking/ManagerConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/chunking/ManagerConfiguration.java index e09bfe6dbe..9ba06ca753 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/chunking/ManagerConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/chunking/ManagerConfiguration.java @@ -20,6 +20,7 @@ import jakarta.jms.JMSException; import org.apache.activemq.artemis.jms.client.ActiveMQConnectionFactory; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.job.builder.JobBuilder; @@ -50,6 +51,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @EnableBatchIntegration @EnableIntegration @PropertySource("classpath:org/springframework/batch/samples/chunking/remote-chunking.properties") diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/delimited/DelimitedJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/delimited/DelimitedJobConfiguration.java index d0c2f118d7..d3b059fd36 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/delimited/DelimitedJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/delimited/DelimitedJobConfiguration.java @@ -1,5 +1,21 @@ +/* + * Copyright 2023-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.file.delimited; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -25,6 +41,7 @@ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class DelimitedJobConfiguration { @@ -54,7 +71,8 @@ public FlatFileItemWriter itemWriter( public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(new CustomerCreditIncreaseProcessor()) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/fixed/FixedLengthJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/fixed/FixedLengthJobConfiguration.java index 19466c8e01..d9db0e8eea 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/fixed/FixedLengthJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/fixed/FixedLengthJobConfiguration.java @@ -1,5 +1,21 @@ +/* + * Copyright 2023-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.file.fixed; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -26,6 +42,7 @@ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class FixedLengthJobConfiguration { @@ -57,7 +74,8 @@ public FlatFileItemWriter itemWriter( public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(new CustomerCreditIncreaseProcessor()) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/json/JsonJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/json/JsonJobConfiguration.java index 135a9ec821..a00c4b9c96 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/json/JsonJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/json/JsonJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2018-2023 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. @@ -15,6 +15,7 @@ */ package org.springframework.batch.samples.file.json; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -43,6 +44,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JsonJobConfiguration { @@ -69,7 +71,8 @@ public JsonFileItemWriter itemWriter(@Value("#{jobParameters[outputFile]} @Bean public Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager, JsonItemReader itemReader, JsonFileItemWriter itemWriter) { - return new StepBuilder("step", jobRepository).chunk(2, transactionManager) + return new StepBuilder("step", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .writer(itemWriter) .build(); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiline/MultiLineJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiline/MultiLineJobConfiguration.java index c8283b1be1..981785c62b 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiline/MultiLineJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiline/MultiLineJobConfiguration.java @@ -1,5 +1,21 @@ +/* + * Copyright 2023-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.file.multiline; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -19,18 +35,19 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.io.Resource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.WritableResource; import org.springframework.jdbc.support.JdbcTransactionManager; @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class MultiLineJobConfiguration { @Bean @StepScope - public MultiLineTradeItemReader itemReader(@Value("#{jobParameters[inputFile]}") Resource resource) { + public MultiLineTradeItemReader itemReader(@Value("#{jobParameters[inputFile]}") FileSystemResource resource) { FlatFileItemReader

          delegate = new FlatFileItemReaderBuilder
          ().name("delegateItemReader") .resource(resource) .lineTokenizer(new DelimitedLineTokenizer()) @@ -57,7 +74,8 @@ public MultiLineTradeItemWriter itemWriter(@Value("#{jobParameters[outputFile]}" public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, MultiLineTradeItemReader itemReader, MultiLineTradeItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .writer(itemWriter) .build()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeJobConfiguration.java index c85d4cf2ef..f5aaf0e463 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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,6 +17,7 @@ import java.util.Map; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -41,7 +42,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; -import org.springframework.core.io.Resource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.WritableResource; import org.springframework.jdbc.support.JdbcTransactionManager; @@ -50,13 +51,14 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class MultiRecordTypeJobConfiguration { @Bean @StepScope public FlatFileItemReader itemReader(PatternMatchingCompositeLineMapper lineMapper, - @Value("#{jobParameters[inputFile]}") Resource resource) { + @Value("#{jobParameters[inputFile]}") FileSystemResource resource) { return new FlatFileItemReaderBuilder().name("itemReader").resource(resource).lineMapper(lineMapper).build(); } @@ -129,7 +131,8 @@ public FormatterLineAggregator customerLineAggregator() { public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, FlatFileItemReader itemReader, FlatFileItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .writer(itemWriter) .build()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiresource/MultiResourceJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiresource/MultiResourceJobConfiguration.java index 1888961de2..0ea241c961 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiresource/MultiResourceJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/multiresource/MultiResourceJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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.samples.file.multiresource; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -47,6 +48,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class MultiResourceJobConfiguration { @@ -92,7 +94,8 @@ public FlatFileItemWriter delegateWriter() { public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(new CustomerCreditIncreaseProcessor()) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/xml/XmlJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/xml/XmlJobConfiguration.java index 520818c62d..47dbe6397a 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/xml/XmlJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/file/xml/XmlJobConfiguration.java @@ -1,3 +1,18 @@ +/* + * Copyright 2023-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.file.xml; import java.math.BigDecimal; @@ -5,6 +20,7 @@ import com.thoughtworks.xstream.security.ExplicitTypePermission; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.configuration.annotation.StepScope; @@ -31,6 +47,7 @@ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class XmlJobConfiguration { @@ -69,7 +86,8 @@ public StaxEventItemWriter itemWriter( public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(new CustomerCreditIncreaseProcessor()) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/football/FootballJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/football/FootballJobConfiguration.java index fe81064cba..d6f5e9a051 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/football/FootballJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/football/FootballJobConfiguration.java @@ -1,7 +1,23 @@ +/* + * Copyright 2023-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.football; import javax.sql.DataSource; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -28,6 +44,7 @@ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class FootballJobConfiguration { @@ -55,7 +72,8 @@ public PlayerItemWriter playerWriter(DataSource dataSource) { @Bean public Step playerLoad(JobRepository jobRepository, JdbcTransactionManager transactionManager, FlatFileItemReader playerFileItemReader, PlayerItemWriter playerWriter) { - return new StepBuilder("playerLoad", jobRepository).chunk(2, transactionManager) + return new StepBuilder("playerLoad", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(playerFileItemReader) .writer(playerWriter) .build(); @@ -84,7 +102,8 @@ public JdbcGameDao gameWriter(DataSource dataSource) { @Bean public Step gameLoad(JobRepository jobRepository, JdbcTransactionManager transactionManager, FlatFileItemReader gameFileItemReader, JdbcGameDao gameWriter) { - return new StepBuilder("gameLoad", jobRepository).chunk(2, transactionManager) + return new StepBuilder("gameLoad", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(gameFileItemReader) .writer(gameWriter) .build(); @@ -120,8 +139,8 @@ public JdbcPlayerSummaryDao summaryWriter(DataSource dataSource) { @Bean public Step summarizationStep(JobRepository jobRepository, JdbcTransactionManager transactionManager, JdbcCursorItemReader playerSummarizationSource, JdbcPlayerSummaryDao summaryWriter) { - return new StepBuilder("summarizationStep", jobRepository) - .chunk(2, transactionManager) + return new StepBuilder("summarizationStep", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(playerSummarizationSource) .writer(summaryWriter) .build(); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/helloworld/HelloWorldJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/helloworld/HelloWorldJobConfiguration.java index 388e1af0de..1be6378d09 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/helloworld/HelloWorldJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/helloworld/HelloWorldJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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,31 +15,26 @@ */ package org.springframework.batch.samples.helloworld; +import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.job.Job; -import org.springframework.batch.core.step.Step; -import org.springframework.batch.core.configuration.annotation.*; import org.springframework.batch.core.job.builder.JobBuilder; import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.Step; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.repeat.RepeatStatus; -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.jdbc.support.JdbcTransactionManager; @Configuration @EnableBatchProcessing -@EnableJdbcJobRepository -@Import(DataSourceConfiguration.class) public class HelloWorldJobConfiguration { @Bean - public Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager) { + public Step step(JobRepository jobRepository) { return new StepBuilder("step", jobRepository).tasklet((contribution, chunkContext) -> { System.out.println("Hello world!"); return RepeatStatus.FINISHED; - }, transactionManager).build(); + }).build(); } @Bean diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/jdbc/JdbcReaderBatchWriterSampleJob.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/jdbc/JdbcReaderBatchWriterSampleJob.java index 6e18f63573..34e6522ec8 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/jdbc/JdbcReaderBatchWriterSampleJob.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/jdbc/JdbcReaderBatchWriterSampleJob.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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,6 +17,7 @@ import javax.sql.DataSource; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.job.builder.JobBuilder; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JdbcReaderBatchWriterSampleJob { @@ -56,7 +58,8 @@ public JdbcBatchItemWriter itemWriter(DataSource dataSource) { public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, JdbcBatchItemWriter itemWriter) { return new JobBuilder("ioSampleJob", jobRepository) - .start(new StepBuilder("step1", jobRepository).chunk(2, transactionManager) + .start(new StepBuilder("step1", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(new CustomerCreditIncreaseProcessor()) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForAsynchronousItemProcessingWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForAsynchronousItemProcessingWithVirtualThreads.java index d5862c7aa4..d51f3efcd7 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForAsynchronousItemProcessingWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForAsynchronousItemProcessingWithVirtualThreads.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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.util.Arrays; import java.util.concurrent.Future; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -43,6 +44,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForAsynchronousItemProcessingWithVirtualThreads { @@ -77,7 +79,8 @@ public AsyncItemWriter itemWriter() { public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, AsyncItemProcessor itemProcessor, AsyncItemWriter itemWriter) { - Step step = new StepBuilder("step", jobRepository).>chunk(2, transactionManager) + Step step = new StepBuilder("step", jobRepository).>chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .processor(itemProcessor) .writer(itemWriter) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForLaunchingJobsWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForLaunchingJobsWithVirtualThreads.java index 2332ff4d14..8006c3fb48 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForLaunchingJobsWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForLaunchingJobsWithVirtualThreads.java @@ -15,6 +15,7 @@ */ package org.springframework.batch.samples.loom; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.step.Step; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForLaunchingJobsWithVirtualThreads { diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningConcurrentStepsWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningConcurrentStepsWithVirtualThreads.java index 129cfc7d69..39a4a7f161 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningConcurrentStepsWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningConcurrentStepsWithVirtualThreads.java @@ -1,5 +1,5 @@ /* - * Copyright 2023 the original author or authors. + * Copyright 2023-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,6 +19,7 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -43,6 +44,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForRunningConcurrentStepsWithVirtualThreads { @@ -78,7 +80,8 @@ public ItemWriter itemWriter() { @Bean public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, ItemReader itemReader, ItemWriter itemWriter) { - Step step = new StepBuilder("step", jobRepository).chunk(2, transactionManager) + Step step = new StepBuilder("step", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(itemReader) .writer(itemWriter) .taskExecutor(new VirtualThreadTaskExecutor("spring-batch-")) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningParallelStepsWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningParallelStepsWithVirtualThreads.java index 17e183b4e1..f19c757bae 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningParallelStepsWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningParallelStepsWithVirtualThreads.java @@ -15,6 +15,7 @@ */ package org.springframework.batch.samples.loom; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForRunningParallelStepsWithVirtualThreads { diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningPartitionedStepsWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningPartitionedStepsWithVirtualThreads.java index a3fd73e4d9..742494c8e8 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningPartitionedStepsWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningPartitionedStepsWithVirtualThreads.java @@ -18,6 +18,7 @@ import java.util.HashMap; import java.util.Map; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -45,6 +46,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForRunningPartitionedStepsWithVirtualThreads { diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningSystemCommandTaskletsWithVirtualThreads.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningSystemCommandTaskletsWithVirtualThreads.java index e4b1feb8c0..d64985f0df 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningSystemCommandTaskletsWithVirtualThreads.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/loom/JobConfigurationForRunningSystemCommandTaskletsWithVirtualThreads.java @@ -19,6 +19,7 @@ import java.io.IOException; import java.util.Arrays; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -42,6 +43,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class JobConfigurationForRunningSystemCommandTaskletsWithVirtualThreads { diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/BatchMetricsApplication.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/BatchMetricsApplication.java index 80ea2f8211..4ae4b72d68 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/BatchMetricsApplication.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/BatchMetricsApplication.java @@ -16,6 +16,7 @@ package org.springframework.batch.samples.metrics; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.samples.common.DataSourceConfiguration; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -27,6 +28,7 @@ @EnableScheduling @EnableBatchProcessing +@EnableJdbcJobRepository @Import({ Job1Configuration.class, Job2Configuration.class, JobScheduler.class, PrometheusConfiguration.class, DataSourceConfiguration.class }) @PropertySource("metrics-sample.properties") diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/Job2Configuration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/Job2Configuration.java index c6c4cbd587..e0f59048ff 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/Job2Configuration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/metrics/Job2Configuration.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. @@ -47,7 +47,8 @@ public Job job2(JobRepository jobRepository, PlatformTransactionManager transact @Bean public Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager) { - return new StepBuilder("step1", jobRepository).chunk(3, transactionManager) + return new StepBuilder("step1", jobRepository).chunk(3) + .transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .build(); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java index 81cae7737d..483b500181 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/DeletionJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -67,7 +67,8 @@ public MongoItemWriter mongoPersonRemover(MongoTemplate mongoTemplate) { @Bean public Step deletionStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, MongoPagingItemReader mongoPersonReader, MongoItemWriter mongoPersonRemover) { - return new StepBuilder("step", jobRepository).chunk(2, transactionManager) + return new StepBuilder("step", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(mongoPersonReader) .writer(mongoPersonRemover) .build(); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java index 38b9319f8b..b245b02084 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/InsertionJobConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2024 the original author or authors. + * Copyright 2020-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -61,7 +61,8 @@ public MongoItemWriter mongoItemWriter(MongoTemplate mongoTemplate) { @Bean public Step step(JobRepository jobRepository, PlatformTransactionManager transactionManager, MongoPagingItemReader mongoItemReader, MongoItemWriter mongoItemWriter) { - return new StepBuilder("step", jobRepository).chunk(2, transactionManager) + return new StepBuilder("step", jobRepository).chunk(2) + .transactionManager(transactionManager) .reader(mongoItemReader) .writer(mongoItemWriter) .build(); diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java index 33dc7f94a6..a795e66789 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/mongodb/MongoDBConfiguration.java @@ -19,6 +19,7 @@ import com.mongodb.client.MongoClients; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; +import org.springframework.batch.core.configuration.annotation.EnableMongoJobRepository; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.MongoJobRepositoryFactoryBean; import org.springframework.beans.factory.annotation.Value; @@ -34,6 +35,7 @@ @Configuration @PropertySource("classpath:/org/springframework/batch/samples/mongodb/mongodb-sample.properties") @EnableBatchProcessing +@EnableMongoJobRepository public class MongoDBConfiguration { @Value("${mongodb.host}") 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 index 40a096ff3b..c87122f7e3 100644 --- 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 @@ -17,6 +17,7 @@ import javax.sql.DataSource; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; import org.springframework.batch.core.job.builder.JobBuilder; @@ -36,6 +37,7 @@ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class OwnersExportJobConfiguration { @@ -61,7 +63,8 @@ public FlatFileItemWriter ownersWriter() { public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager, JdbcCursorItemReader ownersReader, FlatFileItemWriter ownersWriter) { return new JobBuilder("ownersExportJob", jobRepository) - .start(new StepBuilder("ownersExportStep", jobRepository).chunk(5, transactionManager) + .start(new StepBuilder("ownersExportStep", jobRepository).chunk(5) + .transactionManager(transactionManager) .reader(ownersReader) .writer(ownersWriter) .build()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/retry/RetrySampleConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/retry/RetrySampleConfiguration.java index 593bd39a8b..3984bc96fa 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/retry/RetrySampleConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/retry/RetrySampleConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-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.samples.retry; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -37,6 +38,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class RetrySampleConfiguration { @@ -47,7 +49,8 @@ public Job retrySample(JobRepository jobRepository, Step step) { @Bean protected Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager) { - return new StepBuilder("step", jobRepository).chunk(1, transactionManager) + return new StepBuilder("step", jobRepository).chunk(1) + .transactionManager(transactionManager) .reader(reader()) .writer(writer()) .faultTolerant() diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringProcessSample.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringProcessSample.java index 3808ccf02e..f1ec9c18e4 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringProcessSample.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringProcessSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,6 +18,7 @@ import java.util.Arrays; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class SkippableExceptionDuringProcessSample { @@ -84,7 +86,8 @@ public ItemWriter itemWriter() { @Bean public Step step(JobRepository jobRepository) { - return new StepBuilder("step", jobRepository).chunk(3, this.transactionManager) + return new StepBuilder("step", jobRepository).chunk(3) + .transactionManager(this.transactionManager) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringReadSample.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringReadSample.java index 14a9e5c266..2d09447828 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringReadSample.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringReadSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,6 +18,7 @@ import java.util.Arrays; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class SkippableExceptionDuringReadSample { @@ -84,7 +86,8 @@ public ItemWriter itemWriter() { @Bean public Step step(JobRepository jobRepository) { - return new StepBuilder("step", jobRepository).chunk(3, this.transactionManager) + return new StepBuilder("step", jobRepository).chunk(3) + .transactionManager(this.transactionManager) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringWriteSample.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringWriteSample.java index 39b217c459..45b15c7d20 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringWriteSample.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/skip/SkippableExceptionDuringWriteSample.java @@ -1,5 +1,5 @@ /* - * Copyright 2019-2023 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. @@ -18,6 +18,7 @@ import java.util.Arrays; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class SkippableExceptionDuringWriteSample { @@ -84,7 +86,8 @@ public ItemWriter itemWriter() { @Bean public Step step(JobRepository jobRepository) { - return new StepBuilder("step", jobRepository).chunk(3, this.transactionManager) + return new StepBuilder("step", jobRepository).chunk(3) + .transactionManager(this.transactionManager) .reader(itemReader()) .processor(itemProcessor()) .writer(itemWriter()) diff --git a/spring-batch-samples/src/main/java/org/springframework/batch/samples/validation/ValidationSampleConfiguration.java b/spring-batch-samples/src/main/java/org/springframework/batch/samples/validation/ValidationSampleConfiguration.java index 39c5aaff24..37f15d3c17 100644 --- a/spring-batch-samples/src/main/java/org/springframework/batch/samples/validation/ValidationSampleConfiguration.java +++ b/spring-batch-samples/src/main/java/org/springframework/batch/samples/validation/ValidationSampleConfiguration.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,6 +18,7 @@ import java.util.Arrays; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.step.Step; import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing; @@ -39,6 +40,7 @@ */ @Configuration @EnableBatchProcessing +@EnableJdbcJobRepository @Import(DataSourceConfiguration.class) public class ValidationSampleConfiguration { @@ -65,7 +67,8 @@ public BeanValidatingItemProcessor itemValidator() throws Exception { @Bean public Step step(JobRepository jobRepository, JdbcTransactionManager transactionManager) throws Exception { - return new StepBuilder("step", jobRepository).chunk(1, transactionManager) + return new StepBuilder("step", jobRepository).chunk(1) + .transactionManager(transactionManager) .reader(itemReader()) .processor(itemValidator()) .writer(itemWriter()) diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multiline/job/multiLine.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multiline/job/multiLine.xml index a21502bd22..879db2bd43 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multiline/job/multiLine.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multiline/job/multiLine.xml @@ -14,10 +14,14 @@ + + + + - + diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multirecordtype/job/multiRecordType.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multirecordtype/job/multiRecordType.xml index d28c4382b4..dd1ca5b5fd 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multirecordtype/job/multiRecordType.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/file/multirecordtype/job/multiRecordType.xml @@ -14,8 +14,12 @@ - - + + + + + + diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/headerfooter/job/headerFooterSample.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/headerfooter/job/headerFooterSample.xml index 05658f6e05..b734b6a773 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/headerfooter/job/headerFooterSample.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/headerfooter/job/headerFooterSample.xml @@ -55,9 +55,9 @@ - + + value="src/main/resources/org/springframework/batch/samples/headerfooter/data/input.txt" /> - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/jpa/job/repository.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/jpa/job/repository.xml index ee5720533b..13ac9a73f3 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/jpa/job/repository.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/jpa/job/repository.xml @@ -75,12 +75,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - diff --git a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/restart/stop/stopRestartSample.xml b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/restart/stop/stopRestartSample.xml index cfd657f588..5ec9d360bf 100644 --- a/spring-batch-samples/src/main/resources/org/springframework/batch/samples/restart/stop/stopRestartSample.xml +++ b/spring-batch-samples/src/main/resources/org/springframework/batch/samples/restart/stop/stopRestartSample.xml @@ -10,16 +10,13 @@ p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager"/> + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean" + p:jobRepository-ref="jobRepository"> - - diff --git a/spring-batch-samples/src/main/resources/simple-job-launcher-context.xml b/spring-batch-samples/src/main/resources/simple-job-launcher-context.xml index bfec5974bc..8872e36eca 100644 --- a/spring-batch-samples/src/main/resources/simple-job-launcher-context.xml +++ b/spring-batch-samples/src/main/resources/simple-job-launcher-context.xml @@ -11,11 +11,8 @@ p:dataSource-ref="dataSource" p:transactionManager-ref="transactionManager"/> - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean" + p:jobRepository-ref="jobRepository"/> diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java index 0119ab58c9..e537b7a3ce 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/compositereader/CompositeItemReaderSampleFunctionalTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -74,6 +75,7 @@ void testJobLaunch() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class JobConfiguration { @Bean @@ -120,7 +122,8 @@ public JdbcBatchItemWriter itemWriter() { @Bean public Job job(JobRepository jobRepository, JdbcTransactionManager transactionManager) { return new JobBuilder("job", jobRepository) - .start(new StepBuilder("step", jobRepository).chunk(5, transactionManager) + .start(new StepBuilder("step", jobRepository).chunk(5) + .transactionManager(transactionManager) .reader(itemReader()) .writer(itemWriter()) .build()) diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiline/MultiLineFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiline/MultiLineFunctionalTests.java index 06e7baa871..188ade2aac 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiline/MultiLineFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multiline/MultiLineFunctionalTests.java @@ -32,7 +32,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -48,7 +47,7 @@ "/simple-job-launcher-context.xml" }) class MultiLineFunctionalTests { - private static final String INPUT_FILE = "org/springframework/batch/samples/file/multiline/data/multiLine.txt"; + private static final String INPUT_FILE = "src/main/resources/org/springframework/batch/samples/file/multiline/data/multiLine.txt"; private static final String OUTPUT_FILE = "target/test-outputs/multiLineOutput.txt"; @@ -66,7 +65,7 @@ void testLaunchJobWithXmlConfig() throws Exception { // then assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); - Path inputFile = new ClassPathResource(INPUT_FILE).getFile().toPath(); + Path inputFile = new FileSystemResource(INPUT_FILE).getFile().toPath(); Path outputFile = new FileSystemResource(OUTPUT_FILE).getFile().toPath(); Assertions.assertLinesMatch(Files.lines(inputFile), Files.lines(outputFile)); } @@ -86,7 +85,7 @@ public void testLaunchJobWithJavaConfig() throws Exception { // then assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); - Path inputFile = new ClassPathResource(INPUT_FILE).getFile().toPath(); + Path inputFile = new FileSystemResource(INPUT_FILE).getFile().toPath(); Path outputFile = new FileSystemResource(OUTPUT_FILE).getFile().toPath(); Assertions.assertLinesMatch(Files.lines(inputFile), Files.lines(outputFile)); } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeFunctionalTests.java index e5ecf3d65c..b3afa611b3 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/multirecordtype/MultiRecordTypeFunctionalTests.java @@ -32,7 +32,6 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -50,7 +49,7 @@ class MultiRecordTypeFunctionalTests { private static final String OUTPUT_FILE = "target/test-outputs/multiRecordTypeOutput.txt"; - private static final String INPUT_FILE = "org/springframework/batch/samples/file/multirecordtype/data/multiRecordType.txt"; + private static final String INPUT_FILE = "src/main/resources/org/springframework/batch/samples/file/multirecordtype/data/multiRecordType.txt"; @Autowired private JobOperatorTestUtils jobOperatorTestUtils; @@ -67,7 +66,7 @@ void testLaunchJobWithXmlConfig() throws Exception { // then assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); - Path inputFile = new ClassPathResource(INPUT_FILE).getFile().toPath(); + Path inputFile = new FileSystemResource(INPUT_FILE).getFile().toPath(); Path outputFile = new FileSystemResource(OUTPUT_FILE).getFile().toPath(); Assertions.assertLinesMatch(Files.lines(inputFile), Files.lines(outputFile)); } @@ -87,7 +86,7 @@ public void testLaunchJobWithJavaConfig() throws Exception { // then assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); - Path inputFile = new ClassPathResource(INPUT_FILE).getFile().toPath(); + Path inputFile = new FileSystemResource(INPUT_FILE).getFile().toPath(); Path outputFile = new FileSystemResource(OUTPUT_FILE).getFile().toPath(); Assertions.assertLinesMatch(Files.lines(inputFile), Files.lines(outputFile)); } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/patternmatching/PatternMatchingJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/patternmatching/PatternMatchingJobFunctionalTests.java index d256ce902a..093b8eca6a 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/patternmatching/PatternMatchingJobFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/file/patternmatching/PatternMatchingJobFunctionalTests.java @@ -23,7 +23,6 @@ import org.springframework.batch.test.JobOperatorTestUtils; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; @@ -35,7 +34,7 @@ class PatternMatchingJobFunctionalTests { private static final String ACTUAL = "target/test-outputs/multilineOrderOutput.txt"; - private static final String EXPECTED = "org/springframework/batch/samples/file/patternmatching/data/multilineOrderOutput.txt"; + private static final String EXPECTED = "src/main/resources/org/springframework/batch/samples/file/patternmatching/data/multilineOrderOutput.txt"; @Autowired private JobOperatorTestUtils jobOperatorTestUtils; @@ -43,7 +42,7 @@ class PatternMatchingJobFunctionalTests { @Test void testJobLaunch() throws Exception { this.jobOperatorTestUtils.startJob(); - Path expectedFile = new ClassPathResource(EXPECTED).getFile().toPath(); + Path expectedFile = new FileSystemResource(EXPECTED).getFile().toPath(); Path actualFile = new FileSystemResource(ACTUAL).getFile().toPath(); assertLinesMatch(Files.lines(expectedFile), Files.lines(actualFile)); } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java index d03bfe88b2..6516443138 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/helloworld/HelloWorldJobFunctionalTests.java @@ -42,6 +42,8 @@ public void testLaunchJob() throws Exception { // then assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus()); + assertEquals(1, jobExecution.getStepExecutions().size()); + assertEquals(BatchStatus.COMPLETED, jobExecution.getStepExecutions().iterator().next().getStatus()); } } diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportFunctionalTests.java similarity index 99% rename from spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportTests.java rename to spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportFunctionalTests.java index 21022b16ff..ae7e247a47 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/loom/VirtualThreadsSupportFunctionalTests.java @@ -65,7 +65,7 @@ * @author Mahmoud Ben Hassine */ @EnabledForJreRange(min = JRE.JAVA_21) -public class VirtualThreadsSupportTests { +public class VirtualThreadsSupportFunctionalTests { @Test public void testJobLaunchingWithVirtualThreads() throws Exception { diff --git a/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java b/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherFunctionalTests.java similarity index 98% rename from spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java rename to spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherFunctionalTests.java index f573060466..0e656fc01f 100644 --- a/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherTests.java +++ b/spring-batch-samples/src/test/java/org/springframework/batch/samples/misc/jmx/RemoteLauncherFunctionalTests.java @@ -46,9 +46,9 @@ * */ @SuppressWarnings("removal") -class RemoteLauncherTests { +class RemoteLauncherFunctionalTests { - private static final Log logger = LogFactory.getLog(RemoteLauncherTests.class); + private static final Log logger = LogFactory.getLog(RemoteLauncherFunctionalTests.class); private static final List errors = new ArrayList<>(); diff --git a/spring-batch-test/pom.xml b/spring-batch-test/pom.xml index b1c309c129..92196a272b 100644 --- a/spring-batch-test/pom.xml +++ b/spring-batch-test/pom.xml @@ -4,7 +4,7 @@ org.springframework.batch spring-batch - 6.0.0-M2 + 6.0.0-M3 spring-batch-test Spring Batch Test diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/JobLauncherTestUtilsTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/JobLauncherTestUtilsTests.java index 2eb32abefb..60ed6feeb3 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/JobLauncherTestUtilsTests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/JobLauncherTestUtilsTests.java @@ -18,6 +18,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -78,6 +79,7 @@ void getUniqueJobParameters_doesNotRepeatJobParameters() { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class TestJobConfiguration { @Bean diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit4Tests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit4Tests.java index 9f83750cf2..1ab1c7d1df 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit4Tests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit4Tests.java @@ -24,6 +24,7 @@ import org.junit.runner.RunWith; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.step.StepExecution; @@ -107,6 +108,7 @@ public void testJob() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository public static class JobConfiguration { @Bean diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit5Tests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit5Tests.java index b901605e11..bd18a3d750 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit5Tests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/SpringBatchTestJUnit5Tests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.job.parameters.JobParameters; @@ -110,6 +111,7 @@ JobExecution getJobExecution() { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository public static class JobConfiguration { @Bean diff --git a/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeAnnotatedListenerIntegrationTests.java b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeAnnotatedListenerIntegrationTests.java index f12cc4bb35..57b2315fb9 100644 --- a/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeAnnotatedListenerIntegrationTests.java +++ b/spring-batch-test/src/test/java/org/springframework/batch/test/StepScopeAnnotatedListenerIntegrationTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.Test; import org.springframework.batch.core.ExitStatus; +import org.springframework.batch.core.configuration.annotation.EnableJdbcJobRepository; import org.springframework.batch.core.job.Job; import org.springframework.batch.core.job.JobExecution; import org.springframework.batch.core.launch.JobOperator; @@ -96,6 +97,7 @@ public String read() throws Exception { @Configuration @EnableBatchProcessing + @EnableJdbcJobRepository static class TestConfig { @Autowired 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 index d4de807f90..adbcbeee8d 100644 --- 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 @@ -18,13 +18,9 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.batch.core.configuration.JobRegistry; -import org.springframework.batch.core.configuration.support.MapJobRegistry; -import org.springframework.batch.core.launch.JobOperator; import org.springframework.batch.core.launch.support.JobOperatorFactoryBean; import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.repository.support.ResourcelessJobRepository; -import org.springframework.batch.support.transaction.ResourcelessTransactionManager; import org.springframework.batch.test.JobOperatorTestUtils; import org.springframework.batch.test.JobRepositoryTestUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -38,6 +34,7 @@ /** * @author Stefano Cordio + * @author Mahmoud Ben Hassine */ @SpringJUnitConfig @SpringBatchTest @@ -86,18 +83,10 @@ public JobRepository jobRepository() { } @Bean - public JobRegistry jobRegistry() { - return new MapJobRegistry(); - } - - @Bean - public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegistry) throws Exception { + public JobOperatorFactoryBean jobOperator(JobRepository jobRepository) throws Exception { JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean(); jobOperatorFactoryBean.setJobRepository(jobRepository); - jobOperatorFactoryBean.setJobRegistry(jobRegistry); - jobOperatorFactoryBean.setTransactionManager(new ResourcelessTransactionManager()); - jobOperatorFactoryBean.afterPropertiesSet(); - return jobOperatorFactoryBean.getObject(); + return jobOperatorFactoryBean; } } diff --git a/spring-batch-test/src/test/resources/simple-job-launcher-context.xml b/spring-batch-test/src/test/resources/simple-job-launcher-context.xml index 80e3509026..499aa0501d 100755 --- a/spring-batch-test/src/test/resources/simple-job-launcher-context.xml +++ b/spring-batch-test/src/test/resources/simple-job-launcher-context.xml @@ -14,12 +14,9 @@ - - + class="org.springframework.batch.core.launch.support.JobOperatorFactoryBean"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -