Skip to content

Commit 6ad3596

Browse files
committed
Add support for transaction customization in SimpleJobOperator
This commit adds support to configure and create a transactional proxy around SimpleJobOperator through a factory bean. The usage of `@Transactional` was removed in favor of the programmatic way of customizing the proxy through the factory bean. Resolves #1078
1 parent 40b4be3 commit 6ad3596

File tree

3 files changed

+253
-2
lines changed

3 files changed

+253
-2
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.launch.support;
17+
18+
import java.util.Properties;
19+
20+
import org.springframework.aop.framework.ProxyFactory;
21+
import org.springframework.batch.core.configuration.JobRegistry;
22+
import org.springframework.batch.core.converter.JobParametersConverter;
23+
import org.springframework.batch.core.explore.JobExplorer;
24+
import org.springframework.batch.core.launch.JobLauncher;
25+
import org.springframework.batch.core.launch.JobOperator;
26+
import org.springframework.batch.core.repository.JobRepository;
27+
import org.springframework.beans.factory.FactoryBean;
28+
import org.springframework.beans.factory.InitializingBean;
29+
import org.springframework.transaction.PlatformTransactionManager;
30+
import org.springframework.transaction.TransactionManager;
31+
import org.springframework.transaction.annotation.Isolation;
32+
import org.springframework.transaction.annotation.Propagation;
33+
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
34+
import org.springframework.transaction.interceptor.TransactionInterceptor;
35+
import org.springframework.util.Assert;
36+
37+
/**
38+
* Convenient factory bean that creates a transactional proxy around a
39+
* {@link JobOperator}.
40+
*
41+
* @see JobOperator
42+
* @see SimpleJobOperator
43+
* @author Mahmoud Ben Hassine
44+
* @since 5.0
45+
*/
46+
public class JobOperatorFactoryBean implements FactoryBean<JobOperator>, InitializingBean {
47+
48+
private static final String TRANSACTION_ISOLATION_LEVEL_PREFIX = "ISOLATION_";
49+
50+
private static final String TRANSACTION_PROPAGATION_PREFIX = "PROPAGATION_";
51+
52+
private PlatformTransactionManager transactionManager;
53+
54+
private JobRegistry jobRegistry;
55+
56+
private JobLauncher jobLauncher;
57+
58+
private JobRepository jobRepository;
59+
60+
private JobExplorer jobExplorer;
61+
62+
private JobParametersConverter jobParametersConverter;
63+
64+
private ProxyFactory proxyFactory = new ProxyFactory();
65+
66+
@Override
67+
public void afterPropertiesSet() throws Exception {
68+
Assert.notNull(this.transactionManager, "TransactionManager must not be null");
69+
Assert.notNull(this.jobLauncher, "JobLauncher must not be null");
70+
Assert.notNull(this.jobRegistry, "JobLocator must not be null");
71+
Assert.notNull(this.jobExplorer, "JobExplorer must not be null");
72+
Assert.notNull(this.jobRepository, "JobRepository must not be null");
73+
}
74+
75+
/**
76+
* Setter for the job registry.
77+
* @param jobRegistry the job registry to set
78+
*/
79+
public void setJobRegistry(JobRegistry jobRegistry) {
80+
this.jobRegistry = jobRegistry;
81+
}
82+
83+
/**
84+
* Setter for the job launcher.
85+
* @param jobLauncher the job launcher to set
86+
*/
87+
public void setJobLauncher(JobLauncher jobLauncher) {
88+
this.jobLauncher = jobLauncher;
89+
}
90+
91+
/**
92+
* Setter for the job repository.
93+
* @param jobRepository the job repository to set
94+
*/
95+
public void setJobRepository(JobRepository jobRepository) {
96+
this.jobRepository = jobRepository;
97+
}
98+
99+
/**
100+
* Setter for the job explorer.
101+
* @param jobExplorer the job explorer to set
102+
*/
103+
public void setJobExplorer(JobExplorer jobExplorer) {
104+
this.jobExplorer = jobExplorer;
105+
}
106+
107+
/**
108+
* Setter for the job parameters converter.
109+
* @param jobParametersConverter the job parameters converter to set
110+
*/
111+
public void setJobParametersConverter(JobParametersConverter jobParametersConverter) {
112+
this.jobParametersConverter = jobParametersConverter;
113+
}
114+
115+
/**
116+
* Setter for the transaction manager.
117+
* @param transactionManager the transaction manager to set
118+
*/
119+
public void setTransactionManager(PlatformTransactionManager transactionManager) {
120+
this.transactionManager = transactionManager;
121+
}
122+
123+
@Override
124+
public Class<?> getObjectType() {
125+
return JobOperator.class;
126+
}
127+
128+
@Override
129+
public boolean isSingleton() {
130+
return true;
131+
}
132+
133+
@Override
134+
public JobOperator getObject() throws Exception {
135+
Properties transactionAttributes = new Properties();
136+
String transactionProperties = String.join(",", TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRED,
137+
TRANSACTION_ISOLATION_LEVEL_PREFIX + Isolation.DEFAULT);
138+
transactionAttributes.setProperty("stop*", transactionProperties);
139+
NameMatchTransactionAttributeSource transactionAttributeSource = new NameMatchTransactionAttributeSource();
140+
transactionAttributeSource.setProperties(transactionAttributes);
141+
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) this.transactionManager,
142+
transactionAttributeSource);
143+
this.proxyFactory.addAdvice(advice);
144+
this.proxyFactory.setProxyTargetClass(false);
145+
this.proxyFactory.addInterface(JobOperator.class);
146+
this.proxyFactory.setTarget(getTarget());
147+
return (JobOperator) this.proxyFactory.getProxy(getClass().getClassLoader());
148+
}
149+
150+
private SimpleJobOperator getTarget() throws Exception {
151+
SimpleJobOperator simpleJobOperator = new SimpleJobOperator();
152+
simpleJobOperator.setJobRegistry(this.jobRegistry);
153+
simpleJobOperator.setJobExplorer(this.jobExplorer);
154+
simpleJobOperator.setJobRepository(this.jobRepository);
155+
simpleJobOperator.setJobLauncher(this.jobLauncher);
156+
simpleJobOperator.setJobParametersConverter(this.jobParametersConverter);
157+
simpleJobOperator.afterPropertiesSet();
158+
return simpleJobOperator;
159+
}
160+
161+
}

spring-batch-core/src/main/java/org/springframework/batch/core/launch/support/SimpleJobOperator.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2006-2021 the original author or authors.
2+
* Copyright 2006-2022 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.
@@ -74,6 +74,10 @@
7474
* <li>{@link JobRegistry}
7575
* </ul>
7676
*
77+
* This class can be instantiated with a {@link JobOperatorFactoryBean} to create a
78+
* transactional proxy around the job operator.
79+
*
80+
* @see JobOperatorFactoryBean
7781
* @author Dave Syer
7882
* @author Lucas Ward
7983
* @author Will Schipp
@@ -369,7 +373,6 @@ public Long startNextInstance(String jobName)
369373
* @see org.springframework.batch.core.launch.JobOperator#stop(java.lang.Long)
370374
*/
371375
@Override
372-
@Transactional
373376
public boolean stop(long executionId) throws NoSuchJobExecutionException, JobExecutionNotRunningException {
374377

375378
JobExecution jobExecution = findExecutionById(executionId);
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.springframework.batch.core.launch.support;
17+
18+
import org.junit.jupiter.api.Assertions;
19+
import org.junit.jupiter.api.Test;
20+
import org.mockito.Mockito;
21+
22+
import org.springframework.aop.Advisor;
23+
import org.springframework.aop.framework.Advised;
24+
import org.springframework.batch.core.configuration.JobRegistry;
25+
import org.springframework.batch.core.converter.JobParametersConverter;
26+
import org.springframework.batch.core.explore.JobExplorer;
27+
import org.springframework.batch.core.launch.JobLauncher;
28+
import org.springframework.batch.core.launch.JobOperator;
29+
import org.springframework.batch.core.repository.JobRepository;
30+
import org.springframework.test.util.AopTestUtils;
31+
import org.springframework.transaction.PlatformTransactionManager;
32+
import org.springframework.transaction.interceptor.TransactionAttributeSource;
33+
import org.springframework.transaction.interceptor.TransactionInterceptor;
34+
35+
/**
36+
* Test class for {@link JobOperatorFactoryBean}.
37+
*
38+
* @author Mahmoud Ben Hassine
39+
*/
40+
class JobOperatorFactoryBeanTests {
41+
42+
private PlatformTransactionManager transactionManager = Mockito.mock(PlatformTransactionManager.class);
43+
44+
private JobRepository jobRepository = Mockito.mock(JobRepository.class);
45+
46+
private JobLauncher jobLauncher = Mockito.mock(JobLauncher.class);
47+
48+
private JobRegistry jobRegistry = Mockito.mock(JobRegistry.class);
49+
50+
private JobExplorer jobExplorer = Mockito.mock(JobExplorer.class);
51+
52+
private JobParametersConverter jobParametersConverter = Mockito.mock(JobParametersConverter.class);
53+
54+
@Test
55+
public void testJobOperatorCreation() throws Exception {
56+
// given
57+
JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean();
58+
jobOperatorFactoryBean.setTransactionManager(this.transactionManager);
59+
jobOperatorFactoryBean.setJobLauncher(this.jobLauncher);
60+
jobOperatorFactoryBean.setJobExplorer(this.jobExplorer);
61+
jobOperatorFactoryBean.setJobRegistry(this.jobRegistry);
62+
jobOperatorFactoryBean.setJobRepository(this.jobRepository);
63+
jobOperatorFactoryBean.setJobParametersConverter(this.jobParametersConverter);
64+
65+
// when
66+
JobOperator jobOperator = jobOperatorFactoryBean.getObject();
67+
68+
// then
69+
Assertions.assertNotNull(jobOperator);
70+
Object targetObject = AopTestUtils.getTargetObject(jobOperator);
71+
Assertions.assertInstanceOf(SimpleJobOperator.class, targetObject);
72+
Assertions.assertEquals(this.transactionManager, getTransactionManagerSetOnJobOperator(jobOperator));
73+
}
74+
75+
private PlatformTransactionManager getTransactionManagerSetOnJobOperator(JobOperator jobOperator) {
76+
Advised target = (Advised) jobOperator; // proxy created by
77+
// AbstractJobOperatorFactoryBean
78+
Advisor[] advisors = target.getAdvisors();
79+
for (Advisor advisor : advisors) {
80+
if (advisor.getAdvice() instanceof TransactionInterceptor transactionInterceptor) {
81+
return (PlatformTransactionManager) transactionInterceptor.getTransactionManager();
82+
}
83+
}
84+
return null;
85+
}
86+
87+
}

0 commit comments

Comments
 (0)