Skip to content

Add JobRegistrySmartInitializingSingleton #4521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@
* {@link JobRegistry}. Include a bean of this type along with your job configuration and
* use the same {@link JobRegistry} as a {@link JobLocator} when you need to locate a
* {@link Job} to launch.
* <p>
* An alternative to this class is {@link JobRegistrySmartInitializingSingleton}, which is
* recommended in cases where this class may cause early bean initializations. You must
* include at most one of either of them as a bean.
*
* @author Dave Syer
* @author Mahmoud Ben Hassine
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* Copyright 2023 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.configuration.support;

import java.util.Collection;
import java.util.HashSet;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.DuplicateJobException;
import org.springframework.batch.core.configuration.JobLocator;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.SmartInitializingSingleton;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.util.Assert;

/**
* A {@link SmartInitializingSingleton} that registers {@link Job} beans with a
* {@link JobRegistry}. Include a bean of this type along with your job configuration and
* use the same {@link JobRegistry} as a {@link JobLocator} when you need to locate a
* {@link Job} to launch.
* <p>
* This class is an alternative to {@link JobRegistryBeanPostProcessor} and prevents early
* bean initializations. You must include at most one of either of them as a bean.
*
* @author Henning Pöttker
* @since 5.1.1
*/
public class JobRegistrySmartInitializingSingleton
implements SmartInitializingSingleton, BeanFactoryAware, InitializingBean, DisposableBean {

private static final Log logger = LogFactory.getLog(JobRegistrySmartInitializingSingleton.class);

// It doesn't make sense for this to have a default value...
private JobRegistry jobRegistry = null;

private final Collection<String> jobNames = new HashSet<>();

private String groupName = null;

private ListableBeanFactory beanFactory;

/**
* Default constructor.
*/
public JobRegistrySmartInitializingSingleton() {
}

/**
* Convenience constructor for setting the {@link JobRegistry}.
* @param jobRegistry the {@link JobRegistry} to register the {@link Job}s with
*/
public JobRegistrySmartInitializingSingleton(JobRegistry jobRegistry) {
this.jobRegistry = jobRegistry;
}

/**
* The group name for jobs registered by this component. Optional (defaults to null,
* which means that jobs are registered with their bean names). Useful where there is
* a hierarchy of application contexts all contributing to the same
* {@link JobRegistry}: child contexts can then define an instance with a unique group
* name to avoid clashes between job names.
* @param groupName the groupName to set
*/
public void setGroupName(String groupName) {
this.groupName = groupName;
}

/**
* Injection setter for {@link JobRegistry}.
* @param jobRegistry the {@link JobRegistry} to register the {@link Job}s with
*/
public void setJobRegistry(JobRegistry jobRegistry) {
this.jobRegistry = jobRegistry;
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ListableBeanFactory listableBeanFactory) {
this.beanFactory = listableBeanFactory;
}
}

/**
* Make sure the registry is set before use.
*/
@Override
public void afterPropertiesSet() throws Exception {
Assert.state(jobRegistry != null, "JobRegistry must not be null");
}

/**
* Unregister all the {@link Job} instances that were registered by this post
* processor.
*/
@Override
public void destroy() throws Exception {
for (String name : jobNames) {
if (logger.isDebugEnabled()) {
logger.debug("Unregistering job: " + name);
}
jobRegistry.unregister(name);
}
jobNames.clear();
}

@Override
public void afterSingletonsInstantiated() {
if (beanFactory == null) {
return;
}
Map<String, Job> jobs = beanFactory.getBeansOfType(Job.class, false, false);
for (var entry : jobs.entrySet()) {
postProcessAfterInitialization(entry.getValue(), entry.getKey());
}
}

private void postProcessAfterInitialization(Job job, String beanName) {
try {
String groupName = this.groupName;
if (beanFactory instanceof DefaultListableBeanFactory defaultListableBeanFactory
&& beanFactory.containsBean(beanName)) {
groupName = getGroupName(defaultListableBeanFactory.getBeanDefinition(beanName), job);
}
job = groupName == null ? job : new GroupAwareJob(groupName, job);
ReferenceJobFactory jobFactory = new ReferenceJobFactory(job);
String name = jobFactory.getJobName();
if (logger.isDebugEnabled()) {
logger.debug("Registering job: " + name);
}
jobRegistry.register(jobFactory);
jobNames.add(name);
}
catch (DuplicateJobException e) {
throw new FatalBeanException("Cannot register job configuration", e);
}
}

/**
* Determine a group name for the job to be registered. The default implementation
* returns the {@link #setGroupName(String) groupName} configured. Provides an
* extension point for specialised subclasses.
* @param beanDefinition the bean definition for the job
* @param job the job
* @return a group name for the job (or null if not needed)
*/
protected String getGroupName(BeanDefinition beanDefinition, Job job) {
return groupName;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* Copyright 2023 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.configuration.support;

import java.util.Collection;
import java.util.Map;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.configuration.DuplicateJobException;
import org.springframework.batch.core.configuration.JobRegistry;
import org.springframework.batch.core.job.JobSupport;
import org.springframework.beans.FatalBeanException;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;

/**
* @author Henning Pöttker
*/
class JobRegistrySmartInitializingSingletonTests {

private final JobRegistry jobRegistry = new MapJobRegistry();

private final JobRegistrySmartInitializingSingleton singleton = new JobRegistrySmartInitializingSingleton(
jobRegistry);

private final ListableBeanFactory beanFactory = mock(ListableBeanFactory.class);

@BeforeEach
void setUp() {
var job = new JobSupport();
job.setName("foo");
lenient().when(beanFactory.getBeansOfType(Job.class, false, false)).thenReturn(Map.of("bar", job));
singleton.setBeanFactory(beanFactory);
}

@Test
void testInitializationFails() {
singleton.setJobRegistry(null);
var exception = assertThrows(IllegalStateException.class, singleton::afterPropertiesSet);
assertTrue(exception.getMessage().contains("JobRegistry"));
}

@Test
void testAfterSingletonsInstantiated() {
singleton.afterSingletonsInstantiated();
assertEquals("[foo]", jobRegistry.getJobNames().toString());
}

@Test
void testAfterSingletonsInstantiatedWithGroupName() {
singleton.setGroupName("jobs");
singleton.afterSingletonsInstantiated();
assertEquals("[jobs.foo]", jobRegistry.getJobNames().toString());
}

@Test
void testAfterSingletonsInstantiatedWithDuplicate() {
singleton.afterSingletonsInstantiated();
var exception = assertThrows(FatalBeanException.class, singleton::afterSingletonsInstantiated);
assertTrue(exception.getCause() instanceof DuplicateJobException);
}

@Test
void testUnregisterOnDestroy() throws Exception {
singleton.afterSingletonsInstantiated();
singleton.destroy();
assertEquals("[]", jobRegistry.getJobNames().toString());
}

@Test
void testExecutionWithApplicationContext() throws Exception {
var context = new ClassPathXmlApplicationContext("test-context-with-smart-initializing-singleton.xml",
getClass());
var registry = context.getBean("registry", JobRegistry.class);
Collection<String> jobNames = registry.getJobNames();
String[] names = context.getBeanNamesForType(JobSupport.class);
int count = names.length;
// Each concrete bean of type JobConfiguration is registered...
assertEquals(count, jobNames.size());
// N.B. there is a failure / wonky mode where a parent bean is given an
// explicit name or beanName (using property setter): in this case then
// child beans will have the same name and will be re-registered (and
// override, if the registry supports that).
assertNotNull(registry.getJob("test-job"));
assertEquals(context.getBean("test-job-with-name"), registry.getJob("foo"));
assertEquals(context.getBean("test-job-with-bean-name"), registry.getJob("bar"));
assertEquals(context.getBean("test-job-with-parent-and-name"), registry.getJob("spam"));
assertEquals(context.getBean("test-job-with-parent-and-bean-name"), registry.getJob("bucket"));
assertEquals(context.getBean("test-job-with-concrete-parent"), registry.getJob("maps"));
assertEquals(context.getBean("test-job-with-concrete-parent-and-name"), registry.getJob("oof"));
assertEquals(context.getBean("test-job-with-concrete-parent-and-bean-name"), registry.getJob("rab"));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean
class="org.springframework.batch.core.configuration.support.JobRegistrySmartInitializingSingleton">
<property name="jobRegistry" ref="registry" />
</bean>

<bean id="registry"
class="org.springframework.batch.core.configuration.support.MapJobRegistry" />

<bean id="test-job"
class="org.springframework.batch.core.job.JobSupport">
<property name="steps">
<bean id="step1" class="org.springframework.batch.core.step.factory.SimpleStepFactoryBean">
<property name="itemReader" ref="itemReader" />
<property name="itemWriter" ref="itemWriter" />
<property name="jobRepository" ref="jobRepository" />
<property name="transactionManager" ref="transactionManager"/>
</bean>
</property>
</bean>

<bean id="itemReader"
class="org.springframework.batch.item.support.ListItemReader">
<constructor-arg value="foo,bar,spam" />
</bean>

<bean id="itemWriter"
class="org.springframework.batch.core.launch.EmptyItemWriter" />

<bean id="jobRepository"
class="org.springframework.batch.core.step.JobRepositorySupport" />

<bean id="transactionManager"
class="org.springframework.batch.support.transaction.ResourcelessTransactionManager" />

<bean id="test-job-with-name"
class="org.springframework.batch.core.job.JobSupport">
<property name="name" value="foo" />
</bean>

<bean id="test-job-with-bean-name"
class="org.springframework.batch.core.job.JobSupport">
<property name="beanName" value="bar" />
</bean>

<bean id="abstract-job"
class="org.springframework.batch.core.job.JobSupport"
abstract="true" />

<bean id="test-job-with-parent" parent="abstract-job" />

<bean id="test-job-with-parent-and-name" parent="abstract-job"
p:name="spam" />

<bean id="test-job-with-parent-and-bean-name" parent="abstract-job"
p:name="bucket" />

<bean id="parent-job"
class="org.springframework.batch.core.job.JobSupport" />

<bean id="test-job-with-concrete-parent" parent="parent-job"
p:name="maps" />

<bean id="test-job-with-concrete-parent-and-name"
parent="parent-job" p:name="oof" />

<bean id="test-job-with-concrete-parent-and-bean-name"
parent="parent-job" p:beanName="rab" />

</beans>