Skip to content

Commit e33659c

Browse files
committed
Add support to customize transaction attributes in infrastructure beans
Before this commit, the transaction attributes were hardcoded and not configurable in the proxy created by Job explorer/repository/operator factory beans. This commit adds the possibility to customize the transaction attributes in addition to the transaction manager. It is now possible to configure the transaction's isolation level as well as its propagation for each method if needed. Resolves #4195
1 parent 6ad3596 commit e33659c

File tree

6 files changed

+198
-62
lines changed

6 files changed

+198
-62
lines changed

spring-batch-core/src/main/java/org/springframework/batch/core/explore/support/AbstractJobExplorerFactoryBean.java

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.transaction.annotation.Isolation;
3232
import org.springframework.transaction.annotation.Propagation;
3333
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
34+
import org.springframework.transaction.interceptor.TransactionAttributeSource;
3435
import org.springframework.transaction.interceptor.TransactionInterceptor;
3536
import org.springframework.util.Assert;
3637

@@ -51,6 +52,8 @@ public abstract class AbstractJobExplorerFactoryBean implements FactoryBean<JobE
5152

5253
private PlatformTransactionManager transactionManager;
5354

55+
private TransactionAttributeSource transactionAttributeSource;
56+
5457
private ProxyFactory proxyFactory = new ProxyFactory();
5558

5659
/**
@@ -100,9 +103,30 @@ public PlatformTransactionManager getTransactionManager() {
100103
return this.transactionManager;
101104
}
102105

106+
/**
107+
* Set the transaction attributes source to use in the created proxy.
108+
* @param transactionAttributeSource the transaction attributes source to use in the
109+
* created proxy.
110+
* @since 5.0
111+
*/
112+
public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) {
113+
Assert.notNull(transactionAttributeSource, "transactionAttributeSource must not be null.");
114+
this.transactionAttributeSource = transactionAttributeSource;
115+
}
116+
103117
@Override
104118
public void afterPropertiesSet() throws Exception {
105119
Assert.notNull(this.transactionManager, "TransactionManager must not be null.");
120+
if (this.transactionAttributeSource == null) {
121+
Properties transactionAttributes = new Properties();
122+
String transactionProperties = String.join(",", TRANSACTION_PROPAGATION_PREFIX + Propagation.SUPPORTS,
123+
TRANSACTION_ISOLATION_LEVEL_PREFIX + Isolation.READ_COMMITTED);
124+
transactionAttributes.setProperty("get*", transactionProperties);
125+
transactionAttributes.setProperty("find*", transactionProperties);
126+
this.transactionAttributeSource = new NameMatchTransactionAttributeSource();
127+
((NameMatchTransactionAttributeSource) this.transactionAttributeSource)
128+
.setProperties(transactionAttributes);
129+
}
106130
}
107131

108132
/**
@@ -122,15 +146,8 @@ public boolean isSingleton() {
122146

123147
@Override
124148
public JobExplorer getObject() throws Exception {
125-
Properties transactionAttributes = new Properties();
126-
String transactionProperties = String.join(",", TRANSACTION_PROPAGATION_PREFIX + Propagation.SUPPORTS,
127-
TRANSACTION_ISOLATION_LEVEL_PREFIX + Isolation.READ_COMMITTED);
128-
transactionAttributes.setProperty("get*", transactionProperties);
129-
transactionAttributes.setProperty("find*", transactionProperties);
130-
NameMatchTransactionAttributeSource transactionAttributeSource = new NameMatchTransactionAttributeSource();
131-
transactionAttributeSource.setProperties(transactionAttributes);
132-
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) transactionManager,
133-
transactionAttributeSource);
149+
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) this.transactionManager,
150+
this.transactionAttributeSource);
134151
proxyFactory.addAdvice(advice);
135152
proxyFactory.setProxyTargetClass(false);
136153
proxyFactory.addInterface(JobExplorer.class);

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

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import org.springframework.transaction.annotation.Isolation;
3232
import org.springframework.transaction.annotation.Propagation;
3333
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
34+
import org.springframework.transaction.interceptor.TransactionAttributeSource;
3435
import org.springframework.transaction.interceptor.TransactionInterceptor;
3536
import org.springframework.util.Assert;
3637

@@ -51,6 +52,8 @@ public class JobOperatorFactoryBean implements FactoryBean<JobOperator>, Initial
5152

5253
private PlatformTransactionManager transactionManager;
5354

55+
private TransactionAttributeSource transactionAttributeSource;
56+
5457
private JobRegistry jobRegistry;
5558

5659
private JobLauncher jobLauncher;
@@ -70,6 +73,14 @@ public void afterPropertiesSet() throws Exception {
7073
Assert.notNull(this.jobRegistry, "JobLocator must not be null");
7174
Assert.notNull(this.jobExplorer, "JobExplorer must not be null");
7275
Assert.notNull(this.jobRepository, "JobRepository must not be null");
76+
if (this.transactionAttributeSource == null) {
77+
Properties transactionAttributes = new Properties();
78+
String transactionProperties = String.join(",", TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRED,
79+
TRANSACTION_ISOLATION_LEVEL_PREFIX + Isolation.DEFAULT);
80+
transactionAttributes.setProperty("stop*", transactionProperties);
81+
this.transactionAttributeSource = new NameMatchTransactionAttributeSource();
82+
((NameMatchTransactionAttributeSource) transactionAttributeSource).setProperties(transactionAttributes);
83+
}
7384
}
7485

7586
/**
@@ -120,6 +131,16 @@ public void setTransactionManager(PlatformTransactionManager transactionManager)
120131
this.transactionManager = transactionManager;
121132
}
122133

134+
/**
135+
* Set the transaction attributes source to use in the created proxy.
136+
* @param transactionAttributeSource the transaction attributes source to use in the
137+
* created proxy.
138+
*/
139+
public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) {
140+
Assert.notNull(transactionAttributeSource, "transactionAttributeSource must not be null.");
141+
this.transactionAttributeSource = transactionAttributeSource;
142+
}
143+
123144
@Override
124145
public Class<?> getObjectType() {
125146
return JobOperator.class;
@@ -132,14 +153,8 @@ public boolean isSingleton() {
132153

133154
@Override
134155
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);
141156
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) this.transactionManager,
142-
transactionAttributeSource);
157+
this.transactionAttributeSource);
143158
this.proxyFactory.addAdvice(advice);
144159
this.proxyFactory.setProxyTargetClass(false);
145160
this.proxyFactory.addInterface(JobOperator.class);

spring-batch-core/src/main/java/org/springframework/batch/core/repository/support/AbstractJobRepositoryFactoryBean.java

Lines changed: 56 additions & 46 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.
@@ -33,7 +33,9 @@
3333
import org.springframework.transaction.PlatformTransactionManager;
3434
import org.springframework.transaction.TransactionManager;
3535
import org.springframework.transaction.annotation.Isolation;
36+
import org.springframework.transaction.annotation.Propagation;
3637
import org.springframework.transaction.interceptor.NameMatchTransactionAttributeSource;
38+
import org.springframework.transaction.interceptor.TransactionAttributeSource;
3739
import org.springframework.transaction.interceptor.TransactionInterceptor;
3840
import org.springframework.transaction.support.TransactionSynchronizationManager;
3941
import org.springframework.util.Assert;
@@ -52,18 +54,22 @@ public abstract class AbstractJobRepositoryFactoryBean implements FactoryBean<Jo
5254

5355
private PlatformTransactionManager transactionManager;
5456

55-
private ProxyFactory proxyFactory;
57+
private TransactionAttributeSource transactionAttributeSource;
58+
59+
private ProxyFactory proxyFactory = new ProxyFactory();
5660

5761
private String isolationLevelForCreate = DEFAULT_ISOLATION_LEVEL;
5862

5963
private boolean validateTransactionState = true;
6064

61-
private static final String ISOLATION_LEVEL_PREFIX = "ISOLATION_";
65+
private static final String TRANSACTION_ISOLATION_LEVEL_PREFIX = "ISOLATION_";
66+
67+
private static final String TRANSACTION_PROPAGATION_PREFIX = "PROPAGATION_";
6268

6369
/**
6470
* Default value for isolation level in create* method.
6571
*/
66-
private static final String DEFAULT_ISOLATION_LEVEL = ISOLATION_LEVEL_PREFIX + "SERIALIZABLE";
72+
private static final String DEFAULT_ISOLATION_LEVEL = TRANSACTION_ISOLATION_LEVEL_PREFIX + "SERIALIZABLE";
6773

6874
/**
6975
* @return fully configured {@link JobInstanceDao} implementation.
@@ -139,7 +145,7 @@ public void setIsolationLevelForCreate(String isolationLevelForCreate) {
139145
* org.springframework.batch.core.JobParameters)
140146
*/
141147
public void setIsolationLevelForCreate(Isolation isolationLevelForCreate) {
142-
this.setIsolationLevelForCreate(ISOLATION_LEVEL_PREFIX + isolationLevelForCreate.name());
148+
this.setIsolationLevelForCreate(TRANSACTION_ISOLATION_LEVEL_PREFIX + isolationLevelForCreate.name());
143149
}
144150

145151
/**
@@ -159,59 +165,63 @@ public PlatformTransactionManager getTransactionManager() {
159165
return transactionManager;
160166
}
161167

162-
private void initializeProxy() throws Exception {
163-
if (proxyFactory == null) {
164-
proxyFactory = new ProxyFactory();
168+
/**
169+
* Set the transaction attributes source to use in the created proxy.
170+
* @param transactionAttributeSource the transaction attributes source to use in the
171+
* created proxy.
172+
* @since 5.0
173+
*/
174+
public void setTransactionAttributeSource(TransactionAttributeSource transactionAttributeSource) {
175+
Assert.notNull(transactionAttributeSource, "transactionAttributeSource must not be null.");
176+
this.transactionAttributeSource = transactionAttributeSource;
177+
}
178+
179+
@Override
180+
public void afterPropertiesSet() throws Exception {
181+
Assert.notNull(transactionManager, "TransactionManager must not be null.");
182+
if (this.transactionAttributeSource == null) {
165183
Properties transactionAttributes = new Properties();
166-
transactionAttributes.setProperty("create*", "PROPAGATION_REQUIRES_NEW," + isolationLevelForCreate);
184+
transactionAttributes.setProperty("create*",
185+
TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + this.isolationLevelForCreate);
167186
transactionAttributes.setProperty("getLastJobExecution*",
168-
"PROPAGATION_REQUIRES_NEW," + isolationLevelForCreate);
187+
TRANSACTION_PROPAGATION_PREFIX + Propagation.REQUIRES_NEW + "," + this.isolationLevelForCreate);
169188
transactionAttributes.setProperty("*", "PROPAGATION_REQUIRED");
170-
NameMatchTransactionAttributeSource transactionAttributeSource = new NameMatchTransactionAttributeSource();
171-
transactionAttributeSource.setProperties(transactionAttributes);
172-
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) transactionManager,
173-
transactionAttributeSource);
174-
if (validateTransactionState) {
175-
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MethodInterceptor() {
176-
@Override
177-
public Object invoke(MethodInvocation invocation) throws Throwable {
178-
if (TransactionSynchronizationManager.isActualTransactionActive()) {
179-
throw new IllegalStateException("Existing transaction detected in JobRepository. "
180-
+ "Please fix this and try again (e.g. remove @Transactional annotations from client).");
181-
}
182-
return invocation.proceed();
183-
}
184-
});
185-
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
186-
pointcut.addMethodName("create*");
187-
advisor.setPointcut(pointcut);
188-
proxyFactory.addAdvisor(advisor);
189-
}
190-
proxyFactory.addAdvice(advice);
191-
proxyFactory.setProxyTargetClass(false);
192-
proxyFactory.addInterface(JobRepository.class);
193-
proxyFactory.setTarget(getTarget());
189+
this.transactionAttributeSource = new NameMatchTransactionAttributeSource();
190+
((NameMatchTransactionAttributeSource) this.transactionAttributeSource)
191+
.setProperties(transactionAttributes);
194192
}
195193
}
196194

197195
@Override
198-
public void afterPropertiesSet() throws Exception {
199-
Assert.notNull(transactionManager, "TransactionManager must not be null.");
200-
201-
initializeProxy();
196+
public JobRepository getObject() throws Exception {
197+
TransactionInterceptor advice = new TransactionInterceptor((TransactionManager) this.transactionManager,
198+
this.transactionAttributeSource);
199+
if (this.validateTransactionState) {
200+
DefaultPointcutAdvisor advisor = new DefaultPointcutAdvisor(new MethodInterceptor() {
201+
@Override
202+
public Object invoke(MethodInvocation invocation) throws Throwable {
203+
if (TransactionSynchronizationManager.isActualTransactionActive()) {
204+
throw new IllegalStateException("Existing transaction detected in JobRepository. "
205+
+ "Please fix this and try again (e.g. remove @Transactional annotations from client).");
206+
}
207+
return invocation.proceed();
208+
}
209+
});
210+
NameMatchMethodPointcut pointcut = new NameMatchMethodPointcut();
211+
pointcut.addMethodName("create*");
212+
advisor.setPointcut(pointcut);
213+
this.proxyFactory.addAdvisor(advisor);
214+
}
215+
this.proxyFactory.addAdvice(advice);
216+
this.proxyFactory.setProxyTargetClass(false);
217+
this.proxyFactory.addInterface(JobRepository.class);
218+
this.proxyFactory.setTarget(getTarget());
219+
return (JobRepository) this.proxyFactory.getProxy(getClass().getClassLoader());
202220
}
203221

204222
private Object getTarget() throws Exception {
205223
return new SimpleJobRepository(createJobInstanceDao(), createJobExecutionDao(), createStepExecutionDao(),
206224
createExecutionContextDao());
207225
}
208226

209-
@Override
210-
public JobRepository getObject() throws Exception {
211-
if (proxyFactory == null) {
212-
afterPropertiesSet();
213-
}
214-
return (JobRepository) proxyFactory.getProxy(getClass().getClassLoader());
215-
}
216-
217227
}

spring-batch-core/src/test/java/org/springframework/batch/core/explore/support/JobExplorerFactoryBeanTests.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,21 @@
2323

2424
import javax.sql.DataSource;
2525

26+
import org.junit.jupiter.api.Assertions;
2627
import org.junit.jupiter.api.BeforeEach;
2728
import org.junit.jupiter.api.Test;
29+
import org.mockito.Mockito;
30+
31+
import org.springframework.aop.Advisor;
32+
import org.springframework.aop.framework.Advised;
2833
import org.springframework.batch.core.explore.JobExplorer;
34+
import org.springframework.batch.core.repository.JobRepository;
2935
import org.springframework.jdbc.core.JdbcOperations;
3036
import org.springframework.jdbc.core.JdbcTemplate;
3137
import org.springframework.test.util.ReflectionTestUtils;
3238
import org.springframework.transaction.PlatformTransactionManager;
39+
import org.springframework.transaction.interceptor.TransactionAttributeSource;
40+
import org.springframework.transaction.interceptor.TransactionInterceptor;
3341

3442
/**
3543
* @author Dave Syer
@@ -101,4 +109,25 @@ void testCreateExplorer() throws Exception {
101109

102110
}
103111

112+
@Test
113+
public void testCustomTransactionAttributesSource() throws Exception {
114+
// given
115+
TransactionAttributeSource transactionAttributeSource = Mockito.mock(TransactionAttributeSource.class);
116+
this.factory.setTransactionAttributeSource(transactionAttributeSource);
117+
this.factory.afterPropertiesSet();
118+
119+
// when
120+
JobExplorer explorer = this.factory.getObject();
121+
122+
// then
123+
Advised target = (Advised) explorer;
124+
Advisor[] advisors = target.getAdvisors();
125+
for (Advisor advisor : advisors) {
126+
if (advisor.getAdvice() instanceof TransactionInterceptor transactionInterceptor) {
127+
Assertions.assertEquals(transactionAttributeSource,
128+
transactionInterceptor.getTransactionAttributeSource());
129+
}
130+
}
131+
}
132+
104133
}

spring-batch-core/src/test/java/org/springframework/batch/core/launch/support/JobOperatorFactoryBeanTests.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ public void testJobOperatorCreation() throws Exception {
6161
jobOperatorFactoryBean.setJobRegistry(this.jobRegistry);
6262
jobOperatorFactoryBean.setJobRepository(this.jobRepository);
6363
jobOperatorFactoryBean.setJobParametersConverter(this.jobParametersConverter);
64+
jobOperatorFactoryBean.afterPropertiesSet();
6465

6566
// when
6667
JobOperator jobOperator = jobOperatorFactoryBean.getObject();
@@ -72,6 +73,29 @@ public void testJobOperatorCreation() throws Exception {
7273
Assertions.assertEquals(this.transactionManager, getTransactionManagerSetOnJobOperator(jobOperator));
7374
}
7475

76+
@Test
77+
public void testCustomTransactionAttributesSource() throws Exception {
78+
// given
79+
TransactionAttributeSource transactionAttributeSource = Mockito.mock(TransactionAttributeSource.class);
80+
JobOperatorFactoryBean jobOperatorFactoryBean = new JobOperatorFactoryBean();
81+
jobOperatorFactoryBean.setTransactionManager(this.transactionManager);
82+
jobOperatorFactoryBean.setJobLauncher(this.jobLauncher);
83+
jobOperatorFactoryBean.setJobExplorer(this.jobExplorer);
84+
jobOperatorFactoryBean.setJobRegistry(this.jobRegistry);
85+
jobOperatorFactoryBean.setJobRepository(this.jobRepository);
86+
jobOperatorFactoryBean.setJobParametersConverter(this.jobParametersConverter);
87+
jobOperatorFactoryBean.setTransactionAttributeSource(transactionAttributeSource);
88+
jobOperatorFactoryBean.afterPropertiesSet();
89+
90+
// when
91+
JobOperator jobOperator = jobOperatorFactoryBean.getObject();
92+
93+
// then
94+
Assertions.assertEquals(transactionAttributeSource,
95+
getTransactionAttributesSourceSetOnJobOperator(jobOperator));
96+
97+
}
98+
7599
private PlatformTransactionManager getTransactionManagerSetOnJobOperator(JobOperator jobOperator) {
76100
Advised target = (Advised) jobOperator; // proxy created by
77101
// AbstractJobOperatorFactoryBean
@@ -84,4 +108,16 @@ private PlatformTransactionManager getTransactionManagerSetOnJobOperator(JobOper
84108
return null;
85109
}
86110

111+
private TransactionAttributeSource getTransactionAttributesSourceSetOnJobOperator(JobOperator jobOperator) {
112+
Advised target = (Advised) jobOperator; // proxy created by
113+
// AbstractJobOperatorFactoryBean
114+
Advisor[] advisors = target.getAdvisors();
115+
for (Advisor advisor : advisors) {
116+
if (advisor.getAdvice() instanceof TransactionInterceptor transactionInterceptor) {
117+
return transactionInterceptor.getTransactionAttributeSource();
118+
}
119+
}
120+
return null;
121+
}
122+
87123
}

0 commit comments

Comments
 (0)