Skip to content

Commit a8d1d31

Browse files
tiborsulyansnicoll
authored andcommitted
Add option to allow Spring Batch custom isolation levels
See gh-28859
1 parent fc794f1 commit a8d1d31

File tree

6 files changed

+123
-6
lines changed

6 files changed

+123
-6
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BasicBatchConfigurer.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ protected JobRepository createJobRepository() throws Exception {
139139
* @return the isolation level or {@code null} to use the default
140140
*/
141141
protected String determineIsolationLevel() {
142-
return null;
142+
return this.properties.getJdbc().getIsolationLevelForCreate();
143143
}
144144

145145
protected PlatformTransactionManager createTransactionManager() {

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/BatchProperties.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,11 @@ public static class Jdbc {
8787
*/
8888
private DatabaseInitializationMode initializeSchema = DatabaseInitializationMode.EMBEDDED;
8989

90+
/**
91+
* Transaction isolation level to use when creating job meta-data for new jobs.
92+
*/
93+
private String isolationLevelForCreate;
94+
9095
public String getSchema() {
9196
return this.schema;
9297
}
@@ -119,6 +124,14 @@ public void setInitializeSchema(DatabaseInitializationMode initializeSchema) {
119124
this.initializeSchema = initializeSchema;
120125
}
121126

127+
public String getIsolationLevelForCreate() {
128+
return this.isolationLevelForCreate;
129+
}
130+
131+
public void setIsolationLevelForCreate(String isolationLevelForCreate) {
132+
this.isolationLevelForCreate = isolationLevelForCreate;
133+
}
134+
122135
}
123136

124137
}

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/batch/JpaBatchConfigurer.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ public class JpaBatchConfigurer extends BasicBatchConfigurer {
3838

3939
private final EntityManagerFactory entityManagerFactory;
4040

41+
private final String isolationLevelForCreate;
42+
4143
/**
4244
* Create a new {@link BasicBatchConfigurer} instance.
4345
* @param properties the batch properties
@@ -50,12 +52,18 @@ protected JpaBatchConfigurer(BatchProperties properties, DataSource dataSource,
5052
TransactionManagerCustomizers transactionManagerCustomizers, EntityManagerFactory entityManagerFactory) {
5153
super(properties, dataSource, transactionManagerCustomizers);
5254
this.entityManagerFactory = entityManagerFactory;
55+
this.isolationLevelForCreate = properties.getJdbc().getIsolationLevelForCreate();
5356
}
5457

5558
@Override
5659
protected String determineIsolationLevel() {
57-
logger.warn("JPA does not support custom isolation levels, so locks may not be taken when launching Jobs");
58-
return "ISOLATION_DEFAULT";
60+
if (this.isolationLevelForCreate == null) {
61+
logger.warn(
62+
"JPA does not support custom isolation levels, so locks may not be taken when launching Jobs. Define spring.batch.jdbc.isolation-level-for-create property to force a custom isolation level.");
63+
return "ISOLATION_DEFAULT";
64+
}
65+
66+
return this.isolationLevelForCreate;
5967
}
6068

6169
@Override

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationTests.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,8 @@ void testUsingJpa() {
208208
// level)
209209
assertThat(context.getBean(JobRepository.class).getLastJobExecution("job", new JobParameters()))
210210
.isNull();
211+
assertThat(context.getBean(JobRepository.class))
212+
.satisfies(JobRepositoryTestingSupport.isolationLevelRequirements("ISOLATION_DEFAULT"));
211213
});
212214
}
213215

@@ -232,6 +234,16 @@ void testRenamePrefix() {
232234
});
233235
}
234236

237+
@Test
238+
void testCustomIsolationLevelForCreate() {
239+
this.contextRunner
240+
.withUserConfiguration(TestConfiguration.class, EmbeddedDataSourceConfiguration.class,
241+
HibernateJpaAutoConfiguration.class)
242+
.withPropertyValues("spring.batch.jdbc.isolation-level-for-create:ISOLATION_READ_COMMITTED")
243+
.run((context) -> assertThat(context.getBean(JobRepository.class))
244+
.satisfies(JobRepositoryTestingSupport.isolationLevelRequirements("ISOLATION_READ_COMMITTED")));
245+
}
246+
235247
@Test
236248
void testCustomizeJpaTransactionManagerUsingProperties() {
237249
this.contextRunner

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/batch/BatchAutoConfigurationWithoutJpaTests.java

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.springframework.batch.core.repository.JobRepository;
2828
import org.springframework.boot.autoconfigure.AutoConfigurations;
2929
import org.springframework.boot.autoconfigure.TestAutoConfigurationPackage;
30+
import org.springframework.boot.autoconfigure.batch.BatchProperties.Jdbc;
3031
import org.springframework.boot.autoconfigure.jdbc.EmbeddedDataSourceConfiguration;
3132
import org.springframework.boot.autoconfigure.orm.jpa.test.City;
3233
import org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration;
@@ -37,6 +38,7 @@
3738
import org.springframework.transaction.PlatformTransactionManager;
3839

3940
import static org.assertj.core.api.Assertions.assertThat;
41+
import static org.assertj.core.api.Assertions.from;
4042

4143
/**
4244
* Tests for {@link BatchAutoConfiguration} when JPA is not on the classpath.
@@ -59,18 +61,24 @@ void jdbcWithDefaultSettings() {
5961
assertThat(context).hasSingleBean(PlatformTransactionManager.class);
6062
assertThat(context.getBean(PlatformTransactionManager.class).toString())
6163
.contains("DataSourceTransactionManager");
62-
assertThat(context.getBean(BatchProperties.class).getJdbc().getInitializeSchema())
63-
.isEqualTo(DatabaseInitializationMode.EMBEDDED);
64+
assertThat(context.getBean(BatchProperties.class).getJdbc())
65+
.returns("classpath:org/springframework/batch/core/schema-@@platform@@.sql",
66+
from(Jdbc::getSchema))
67+
.returns(DatabaseInitializationMode.EMBEDDED, from(Jdbc::getInitializeSchema))
68+
.returns(null, from(Jdbc::getIsolationLevelForCreate));
6469
assertThat(new JdbcTemplate(context.getBean(DataSource.class))
6570
.queryForList("select * from BATCH_JOB_EXECUTION")).isEmpty();
6671
assertThat(context.getBean(JobExplorer.class).findRunningJobExecutions("test")).isEmpty();
6772
assertThat(context.getBean(JobRepository.class).getLastJobExecution("test", new JobParameters()))
6873
.isNull();
74+
75+
assertThat(context.getBean(JobRepository.class)).satisfies(
76+
JobRepositoryTestingSupport.isolationLevelRequirements("ISOLATION_SERIALIZABLE"));
6977
});
7078
}
7179

7280
@Test
73-
void jdbcWithCustomPrefix() {
81+
void jdbcWithCustomSettings() {
7482
this.contextRunner.withUserConfiguration(DefaultConfiguration.class, EmbeddedDataSourceConfiguration.class)
7583
.withPropertyValues("spring.datasource.generate-unique-name=true",
7684
"spring.batch.jdbc.schema:classpath:batch/custom-schema-hsql.sql",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/*
2+
* Copyright 2012-2021 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+
17+
package org.springframework.boot.autoconfigure.batch;
18+
19+
import java.util.Arrays;
20+
import java.util.function.Consumer;
21+
import java.util.stream.Stream;
22+
import java.util.stream.Stream.Builder;
23+
24+
import org.aopalliance.aop.Advice;
25+
import org.assertj.core.api.InstanceOfAssertFactories;
26+
27+
import org.springframework.aop.Advisor;
28+
import org.springframework.aop.framework.Advised;
29+
import org.springframework.aop.support.AopUtils;
30+
import org.springframework.batch.core.repository.JobRepository;
31+
import org.springframework.transaction.interceptor.TransactionAspectSupport;
32+
33+
import static org.assertj.core.api.Assertions.as;
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
final class JobRepositoryTestingSupport {
37+
38+
private JobRepositoryTestingSupport() {
39+
40+
}
41+
42+
static Consumer<JobRepository> isolationLevelRequirements(String isolationLevel) {
43+
return (jobRepository) ->
44+
// jobRepository is proxied twice, the inner proxy has the transaction advice.
45+
// This logic does not assume anything about proxy hierarchy, but it does about
46+
// the advice itself.
47+
assertThat(getTransactionAdvices(jobRepository))
48+
.anySatisfy((advice) -> assertThat(advice).extracting("transactionAttributeSource")
49+
.extracting(Object::toString, as(InstanceOfAssertFactories.STRING))
50+
.contains("create*=PROPAGATION_REQUIRES_NEW," + isolationLevel)
51+
.contains("getLastJobExecution*=PROPAGATION_REQUIRES_NEW," + isolationLevel));
52+
}
53+
54+
private static Stream<Advice> getTransactionAdvices(Object candidate) {
55+
Builder<Advice> builder = Stream.builder();
56+
getTransactionAdvices(candidate, builder);
57+
return builder.build();
58+
}
59+
60+
private static void getTransactionAdvices(Object candidate, Builder<Advice> builder) {
61+
try {
62+
if (AopUtils.isAopProxy(candidate) && candidate instanceof Advised) {
63+
Arrays.stream(((Advised) candidate).getAdvisors()).map(Advisor::getAdvice)
64+
.filter(TransactionAspectSupport.class::isInstance).forEach(builder::add);
65+
Object target = ((Advised) candidate).getTargetSource().getTarget();
66+
if (target != null) {
67+
getTransactionAdvices(target, builder);
68+
}
69+
}
70+
}
71+
catch (Exception ex) {
72+
throw new IllegalStateException("Failed to unwrap proxied object", ex);
73+
}
74+
}
75+
76+
}

0 commit comments

Comments
 (0)