Skip to content

Commit a7f090a

Browse files
committed
Make MapJobRegistry smart to auto register jobs at startup
Before this commit, it was required to populate the job registry with a different component (like a bean post processor or a smart initializing singleton) before being able to use it with the JobOperator. This commit makes the `MapJobRegistry` smart enough to auto register jobs defined in the application context. This removes the need for a distinct component to populate the registry and therefore simplifies the configuration. This commit also: - removes the usage of `JobFactory` from `JobRegistry` - deprecates JobRegistrySmartInitializingSingleton and removes its configuration from the default batch configuration Resolves #4854 Resolves #4855 Resolves #4856
1 parent 08c4cb1 commit a7f090a

18 files changed

+87
-234
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/JobRegistry.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2022 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -22,17 +22,18 @@
2222
* <code>name</code>.
2323
*
2424
* @author Dave Syer
25+
* @author Mahmoud Ben Hassine
2526
*
2627
*/
2728
public interface JobRegistry extends ListableJobLocator {
2829

2930
/**
3031
* Registers a {@link Job} at runtime.
31-
* @param jobFactory the {@link Job} to be registered
32-
* @throws DuplicateJobException if a factory with the same job name has already been
32+
* @param job the {@link Job} to be registered
33+
* @throws DuplicateJobException if a job with the same name has already been
3334
* registered.
3435
*/
35-
void register(JobFactory jobFactory) throws DuplicateJobException;
36+
void register(Job job) throws DuplicateJobException;
3637

3738
/**
3839
* Unregisters a previously registered {@link Job}. If it was not previously

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/annotation/BatchRegistrar.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222

2323
import org.springframework.batch.core.configuration.support.AutomaticJobRegistrar;
2424
import org.springframework.batch.core.configuration.support.DefaultJobLoader;
25-
import org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton;
2625
import org.springframework.batch.core.configuration.support.MapJobRegistry;
2726
import org.springframework.batch.core.launch.support.JobOperatorFactoryBean;
2827
import org.springframework.batch.core.repository.support.JdbcJobRepositoryFactoryBean;
@@ -70,7 +69,6 @@ public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, B
7069
.synthesize();
7170
registerJobRepository(registry, importingClassMetadata);
7271
registerJobRegistry(registry);
73-
registerJobRegistrySmartInitializingSingleton(registry);
7472
registerJobOperator(registry, batchAnnotation);
7573
registerAutomaticJobRegistrar(registry, batchAnnotation);
7674
watch.stop();
@@ -220,21 +218,6 @@ private void registerJobRegistry(BeanDefinitionRegistry registry) {
220218
registry.registerBeanDefinition(JOB_REGISTRY, beanDefinition);
221219
}
222220

223-
private void registerJobRegistrySmartInitializingSingleton(BeanDefinitionRegistry registry) {
224-
if (registry.containsBeanDefinition("jobRegistrySmartInitializingSingleton")) {
225-
LOGGER
226-
.info("Bean jobRegistrySmartInitializingSingleton already defined in the application context, skipping"
227-
+ " the registration of a jobRegistrySmartInitializingSingleton");
228-
return;
229-
}
230-
BeanDefinitionBuilder beanDefinitionBuilder = BeanDefinitionBuilder
231-
.genericBeanDefinition(JobRegistrySmartInitializingSingleton.class);
232-
beanDefinitionBuilder.addPropertyReference(JOB_REGISTRY, JOB_REGISTRY);
233-
234-
registry.registerBeanDefinition("jobRegistrySmartInitializingSingleton",
235-
beanDefinitionBuilder.getBeanDefinition());
236-
}
237-
238221
private void registerJobOperator(BeanDefinitionRegistry registry, EnableBatchProcessing batchAnnotation) {
239222
if (registry.containsBeanDefinition(JOB_OPERATOR)) {
240223
LOGGER.info("Bean jobOperator already defined in the application context, skipping"

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultBatchConfiguration.java

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,6 @@
5050
* <li>a {@link ResourcelessJobRepository} named "jobRepository"</li>
5151
* <li>a {@link MapJobRegistry} named "jobRegistry"</li>
5252
* <li>a {@link TaskExecutorJobOperator} named "JobOperator"</li>
53-
* <li>a {@link JobRegistrySmartInitializingSingleton} named
54-
* "jobRegistrySmartInitializingSingleton"</li>
5553
* <li>a {@link org.springframework.batch.core.scope.StepScope} named "stepScope"</li>
5654
* <li>a {@link org.springframework.batch.core.scope.JobScope} named "jobScope"</li>
5755
* </ul>
@@ -117,21 +115,6 @@ public JobOperator jobOperator(JobRepository jobRepository, JobRegistry jobRegis
117115
}
118116
}
119117

120-
@Bean
121-
public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry)
122-
throws BatchConfigurationException {
123-
JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = new JobRegistrySmartInitializingSingleton();
124-
jobRegistrySmartInitializingSingleton.setJobRegistry(jobRegistry);
125-
try {
126-
jobRegistrySmartInitializingSingleton.afterPropertiesSet();
127-
return jobRegistrySmartInitializingSingleton;
128-
}
129-
catch (Exception e) {
130-
throw new BatchConfigurationException(
131-
"Unable to configure the default job registry SmartInitializingSingleton", e);
132-
}
133-
}
134-
135118
/**
136119
* Return the transaction manager to use for the job operator. Defaults to
137120
* {@link ResourcelessTransactionManager}.

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/DefaultJobLoader.java

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,7 +27,6 @@
2727
import org.springframework.batch.core.Job;
2828
import org.springframework.batch.core.Step;
2929
import org.springframework.batch.core.configuration.DuplicateJobException;
30-
import org.springframework.batch.core.configuration.JobFactory;
3130
import org.springframework.batch.core.configuration.JobRegistry;
3231
import org.springframework.batch.core.configuration.StepRegistry;
3332
import org.springframework.batch.core.launch.NoSuchJobException;
@@ -251,8 +250,7 @@ private Collection<Step> getSteps(final StepLocator stepLocator, final Applicati
251250
* @throws DuplicateJobException if that job is already registered
252251
*/
253252
private void doRegister(ConfigurableApplicationContext context, Job job) throws DuplicateJobException {
254-
final JobFactory jobFactory = new ReferenceJobFactory(job);
255-
jobRegistry.register(jobFactory);
253+
jobRegistry.register(job);
256254

257255
if (stepRegistry != null) {
258256
if (!(job instanceof StepLocator)) {

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobFactoryRegistrationListener.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2023 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -27,6 +27,7 @@
2727
* Generic service that can bind and unbind a {@link JobFactory} in a {@link JobRegistry}.
2828
*
2929
* @author Dave Syer
30+
* @author Mahmoud Ben Hassine
3031
*
3132
*/
3233
public class JobFactoryRegistrationListener {
@@ -53,7 +54,7 @@ public void bind(JobFactory jobFactory, Map<String, ?> params) throws Exception
5354
if (logger.isInfoEnabled()) {
5455
logger.info("Binding JobFactory: " + jobFactory.getJobName());
5556
}
56-
jobRegistry.register(jobFactory);
57+
jobRegistry.register(jobFactory.createJob());
5758
}
5859

5960
/**

spring-batch-core/src/main/java/org/springframework/batch/core/configuration/support/JobRegistrySmartInitializingSingleton.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,10 @@
4545
*
4646
* @author Henning Pöttker
4747
* @since 5.1.1
48+
* @deprecated since 6.0 with no replacement. Register a {@link MapJobRegistry} as a bean,
49+
* and it will automatically register all {@link Job} beans in the application context.
4850
*/
51+
@Deprecated(since = "6.0", forRemoval = true)
4952
public class JobRegistrySmartInitializingSingleton
5053
implements SmartInitializingSingleton, BeanFactoryAware, InitializingBean, DisposableBean {
5154

@@ -143,12 +146,11 @@ private void postProcessAfterInitialization(Job job, String beanName) {
143146
groupName = getGroupName(defaultListableBeanFactory.getBeanDefinition(beanName), job);
144147
}
145148
job = groupName == null ? job : new GroupAwareJob(groupName, job);
146-
ReferenceJobFactory jobFactory = new ReferenceJobFactory(job);
147-
String name = jobFactory.getJobName();
149+
String name = job.getName();
148150
if (logger.isDebugEnabled()) {
149151
logger.debug("Registering job: " + name);
150152
}
151-
jobRegistry.register(jobFactory);
153+
jobRegistry.register(job);
152154
jobNames.add(name);
153155
}
154156
catch (DuplicateJobException e) {
Lines changed: 43 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2019 the original author or authors.
2+
* Copyright 2006-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,68 +16,89 @@
1616
package org.springframework.batch.core.configuration.support;
1717

1818
import java.util.Collections;
19+
import java.util.Map;
1920
import java.util.Set;
2021
import java.util.concurrent.ConcurrentHashMap;
2122
import java.util.concurrent.ConcurrentMap;
2223

24+
import org.apache.commons.logging.Log;
25+
import org.apache.commons.logging.LogFactory;
2326
import org.springframework.batch.core.Job;
2427
import org.springframework.batch.core.configuration.DuplicateJobException;
25-
import org.springframework.batch.core.configuration.JobFactory;
2628
import org.springframework.batch.core.configuration.JobRegistry;
2729
import org.springframework.batch.core.launch.NoSuchJobException;
30+
import org.springframework.beans.BeansException;
31+
import org.springframework.beans.factory.SmartInitializingSingleton;
32+
import org.springframework.context.ApplicationContext;
33+
import org.springframework.context.ApplicationContextAware;
2834
import org.springframework.lang.Nullable;
2935
import org.springframework.util.Assert;
3036

3137
/**
32-
* Simple, thread-safe, map-based implementation of {@link JobRegistry}.
38+
* Simple, thread-safe, map-based implementation of {@link JobRegistry}. This registry is
39+
* a {@link SmartInitializingSingleton} that is automatically populated with all
40+
* {@link Job} beans in the {@link ApplicationContext}.
3341
*
3442
* @author Dave Syer
3543
* @author Robert Fischer
3644
* @author Mahmoud Ben Hassine
3745
*/
38-
public class MapJobRegistry implements JobRegistry {
46+
public class MapJobRegistry implements JobRegistry, SmartInitializingSingleton, ApplicationContextAware {
47+
48+
protected final Log logger = LogFactory.getLog(getClass());
3949

4050
/**
41-
* The map holding the registered job factories.
51+
* The map holding the registered jobs.
4252
*/
43-
// The "final" ensures that it is visible and initialized when the constructor
44-
// resolves.
45-
private final ConcurrentMap<String, JobFactory> map = new ConcurrentHashMap<>();
53+
private final ConcurrentMap<String, Job> map = new ConcurrentHashMap<>();
54+
55+
private ApplicationContext applicationContext;
56+
57+
@Override
58+
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
59+
this.applicationContext = applicationContext;
60+
}
61+
62+
@Override
63+
public void afterSingletonsInstantiated() {
64+
Map<String, Job> jobBeans = this.applicationContext.getBeansOfType(Job.class);
65+
this.map.putAll(jobBeans);
66+
}
4667

4768
@Override
48-
public void register(JobFactory jobFactory) throws DuplicateJobException {
49-
Assert.notNull(jobFactory, "jobFactory is null");
50-
String name = jobFactory.getJobName();
51-
Assert.notNull(name, "Job configuration must have a name.");
52-
JobFactory previousValue = map.putIfAbsent(name, jobFactory);
69+
public void register(Job job) throws DuplicateJobException {
70+
Assert.notNull(job, "job must not be null");
71+
String jobName = job.getName();
72+
Assert.notNull(jobName, "Job name must not be null");
73+
Job previousValue = this.map.putIfAbsent(jobName, job);
5374
if (previousValue != null) {
54-
throw new DuplicateJobException("A job configuration with this name [" + name + "] was already registered");
75+
throw new DuplicateJobException("A job with this name [" + jobName + "] was already registered");
5576
}
5677
}
5778

5879
@Override
5980
public void unregister(String name) {
60-
Assert.notNull(name, "Job configuration must have a name.");
61-
map.remove(name);
81+
Assert.notNull(name, "Job name must not be null");
82+
this.map.remove(name);
6283
}
6384

6485
@Override
6586
public Job getJob(@Nullable String name) throws NoSuchJobException {
66-
JobFactory factory = map.get(name);
67-
if (factory == null) {
68-
throw new NoSuchJobException("No job configuration with the name [" + name + "] was registered");
87+
Job job = this.map.get(name);
88+
if (job == null) {
89+
throw new NoSuchJobException("No job with the name [" + name + "] was registered");
6990
}
7091
else {
71-
return factory.createJob();
92+
return job;
7293
}
7394
}
7495

7596
/**
76-
* Provides an unmodifiable view of the job names.
97+
* Provides an unmodifiable view of job names.
7798
*/
7899
@Override
79100
public Set<String> getJobNames() {
80-
return Collections.unmodifiableSet(map.keySet());
101+
return Collections.unmodifiableSet(this.map.keySet());
81102
}
82103

83104
}

spring-batch-core/src/test/java/org/springframework/batch/core/configuration/annotation/BatchRegistrarTests.java

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import org.springframework.batch.core.DefaultJobKeyGenerator;
2828
import org.springframework.batch.core.JobKeyGenerator;
2929
import org.springframework.batch.core.configuration.JobRegistry;
30-
import org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton;
3130
import org.springframework.batch.core.converter.DefaultJobParametersConverter;
3231
import org.springframework.batch.core.converter.JobParametersConverter;
3332
import org.springframework.batch.core.converter.JsonJobParametersConverter;
@@ -64,8 +63,6 @@ void testConfigurationWithUserDefinedBeans() {
6463
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobRepository.class)).isMock());
6564
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobRegistry.class)).isMock());
6665
Assertions.assertTrue(Mockito.mockingDetails(context.getBean(JobOperator.class)).isMock());
67-
Assertions
68-
.assertTrue(Mockito.mockingDetails(context.getBean(JobRegistrySmartInitializingSingleton.class)).isMock());
6966
}
7067

7168
@Test
@@ -147,15 +144,12 @@ void testDefaultInfrastructureBeansRegistration() {
147144
JobRepository jobRepository = context.getBean(JobRepository.class);
148145
JobRegistry jobRegistry = context.getBean(JobRegistry.class);
149146
JobOperator jobOperator = context.getBean(JobOperator.class);
150-
JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = context
151-
.getBean(JobRegistrySmartInitializingSingleton.class);
152147

153148
// then
154149
Assertions.assertNotNull(jobLauncher);
155150
Assertions.assertNotNull(jobRepository);
156151
Assertions.assertNotNull(jobRegistry);
157152
Assertions.assertNotNull(jobOperator);
158-
Assertions.assertNotNull(jobRegistrySmartInitializingSingleton);
159153
}
160154

161155
@Test
@@ -236,11 +230,6 @@ public JobOperator jobOperator() {
236230
return Mockito.mock();
237231
}
238232

239-
@Bean
240-
public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton() {
241-
return Mockito.mock();
242-
}
243-
244233
}
245234

246235
@Configuration

spring-batch-core/src/test/java/org/springframework/batch/core/configuration/support/DefaultBatchConfigurationTests.java

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2022-2024 the original author or authors.
2+
* Copyright 2022-2025 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -36,7 +36,6 @@
3636
import org.springframework.batch.core.step.builder.StepBuilder;
3737
import org.springframework.batch.core.step.tasklet.Tasklet;
3838
import org.springframework.batch.repeat.RepeatStatus;
39-
import org.springframework.beans.factory.BeanCreationException;
4039
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
4140
import org.springframework.context.annotation.Bean;
4241
import org.springframework.context.annotation.Configuration;
@@ -72,9 +71,6 @@ void testConfigurationWithCustomInfrastructureBean() {
7271
Assertions.assertEquals(1, jobRepositories.size());
7372
JobRepository jobRepository = jobRepositories.entrySet().iterator().next().getValue();
7473
Assertions.assertInstanceOf(DummyJobRepository.class, jobRepository);
75-
Map<String, JobRegistrySmartInitializingSingleton> jobRegistrySmartInitializingSingletonMap = context
76-
.getBeansOfType(JobRegistrySmartInitializingSingleton.class);
77-
Assertions.assertEquals(1, jobRegistrySmartInitializingSingletonMap.size());
7874
}
7975

8076
@Test
@@ -87,15 +83,12 @@ void testDefaultInfrastructureBeansRegistration() {
8783
JobRepository jobRepository = context.getBean(JobRepository.class);
8884
JobRegistry jobRegistry = context.getBean(JobRegistry.class);
8985
JobOperator jobOperator = context.getBean(JobOperator.class);
90-
JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton = context
91-
.getBean(JobRegistrySmartInitializingSingleton.class);
9286

9387
// then
9488
Assertions.assertNotNull(jobLauncher);
9589
Assertions.assertNotNull(jobRepository);
9690
Assertions.assertNotNull(jobRegistry);
9791
Assertions.assertNotNull(jobOperator);
98-
Assertions.assertNotNull(jobRegistrySmartInitializingSingleton);
9992
}
10093

10194
@Configuration
@@ -136,13 +129,6 @@ public JobRepository jobRepository() {
136129
return new DummyJobRepository();
137130
}
138131

139-
@Bean
140-
public JobRegistrySmartInitializingSingleton jobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
141-
JobRegistrySmartInitializingSingleton smartInitializingSingleton = new JobRegistrySmartInitializingSingleton();
142-
smartInitializingSingleton.setJobRegistry(jobRegistry);
143-
return smartInitializingSingleton;
144-
}
145-
146132
}
147133

148134
}

0 commit comments

Comments
 (0)