Skip to content

Commit da85749

Browse files
Thread-safe property resolution for concurrent execution of the same job: Added JobBuilder.createJob(String, Consumer<PropertyResolver>>) and deprecated JobProperties
1 parent d8189c0 commit da85749

File tree

16 files changed

+355
-194
lines changed

16 files changed

+355
-194
lines changed

README.md

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
REST API for <a href="https://spring.io/projects/spring-batch">Spring Batch</a> based on <a href="https://github.com/spring-projects/spring-boot">Spring Boot 2.2</a> and <a href="https://github.com/spring-projects/spring-hateoas">Spring HATOEAS</a>. It comes with an OpenAPI 3 documentation provided by <a href="https://github.com/springdoc/springdoc-openapi">Springdoc</a>.
1010

11-
Supports Java 8 and above. Tested using OpenJDK 8, 11, and 14.
11+
Supports Java 8 and above. Tested on OpenJDK 8, 11, and 14.
1212

1313
## Features
1414
- Get information on jobs, job executions, and Quartz schedules
@@ -227,29 +227,60 @@ This disables the global exception handling via `com.github.chrisgleissner.sprin
227227

228228
## Job Property Overrides
229229

230-
Properties can be overridden when starting a job via REST. These overrides can then be accessed from a job either via:
230+
Properties can be overridden when starting a job via REST. You can then access these overrides in one of the following ways.
231+
232+
### @Value
233+
234+
Annotate your Spring bean method with `@StepScope` and use the `@Value("#{jobParameters['name']}")` annotation on a method parameter
235+
to specify the desired job parameter name.
236+
237+
Please note that this approach won't transparently fall back to Spring environment properties. Thus,
238+
if this is desired, you should manually check if a job parameter is `null` and in this case return it from the Spring `Environment` instance.
239+
240+
Example:
241+
```java
242+
@Bean @StepScope
243+
ItemWriter<Object> writer(@Value("#{jobParameters['propName']}") String prop) {
244+
// ...
245+
}
246+
```
247+
248+
### PropertyResolver
249+
250+
When using `AdhocStarter`, you can create a `Job` using a `JobBuilder` and pass in a `Consumer<PropertyResolver>`.
251+
252+
Properties looked up from this `PropertyResolver` transparently fall back to the Spring environment if properties can't be found in the job parameters.
253+
254+
Example:
255+
```java
256+
Job job = jobBuilder.createJob("sampleJob", propertyResolver -> {
257+
String propertyValue = propertyResolver.getProperty("sampleProperty");
258+
...
259+
});
260+
```
261+
### JobProperties
262+
263+
In case you don't execute the same job concurrently, you may also look up properties from the `JobProperties` singleton.
264+
265+
Properties looked up from this singleton transparently fall back to the Spring environment if properties can't be found in the
266+
job parameters.
267+
268+
This approach is *deprecated* as it doesn't work with concurrent execution of the same job. Therefore, it is recommended to use one
269+
of the other approaches.
270+
271+
Example:
231272
```java
232273
@Bean
233274
ItemWriter<Object> writer() {
234275
return new ItemWriter<Object>() {
235276
@Override
236277
public void write(List<?> items) throws Exception {
237-
String prop = JobPropertyResolvers.JobProperties.of("jobName").getProperty("propName");
278+
String prop = JobPropertyResolvers.JobProperties.of("sampleJob").getProperty("sampleProperty");
238279
// ...
239280
}
240281
}
241282
}
242283
```
243-
or alternatively by using `@StepScope`-annotated beans:
244-
```java
245-
@StepScope
246-
@Bean
247-
ItemWriter<Object> writer(@Value("#{jobParameters['propName']}") String prop) {
248-
// ...
249-
}
250-
```
251-
252-
If a property is not overridden, it is resolved against the Spring environment. All overrides are reverted on job completion.
253284

254285
## Utilities
255286

api/src/test/java/com/github/chrisgleissner/springbatchrest/api/core/RestTest.java

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
import com.github.chrisgleissner.springbatchrest.util.core.JobBuilder;
66
import com.github.chrisgleissner.springbatchrest.util.core.JobConfig;
77
import com.github.chrisgleissner.springbatchrest.util.core.config.AdHocBatchConfig;
8-
98
import org.junit.Before;
109
import org.junit.Test;
1110
import org.junit.runner.RunWith;
@@ -26,7 +25,6 @@
2625
import java.util.concurrent.CountDownLatch;
2726
import java.util.concurrent.atomic.AtomicBoolean;
2827

29-
import static com.github.chrisgleissner.springbatchrest.util.core.property.JobPropertyResolvers.JobProperties;
3028
import static org.assertj.core.api.Assertions.assertThat;
3129
import static org.springframework.batch.core.ExitStatus.COMPLETED;
3230
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
@@ -56,11 +54,11 @@ public class RestTest {
5654
@Before
5755
public void setUp() {
5856
if (firstExecution.compareAndSet(true, false)) {
59-
Job job = jobBuilder.createJob(JOB_NAME, () -> {
60-
String propertyValue = JobProperties.of(JOB_NAME).getProperty(PROPERTY_NAME);
57+
Job job = jobBuilder.createJob(JOB_NAME, propertyResolver -> {
58+
String propertyValue = propertyResolver.getProperty(PROPERTY_NAME);
6159
propertyValues.add(propertyValue);
6260

63-
String exceptionMessage = JobProperties.of(JOB_NAME).getProperty(EXCEPTION_MESSAGE_PROPERTY_NAME);
61+
String exceptionMessage = propertyResolver.getProperty(EXCEPTION_MESSAGE_PROPERTY_NAME);
6462
if (exceptionMessage != null)
6563
throw new RuntimeException(exceptionMessage);
6664

example/api/src/main/java/com/github/chrisgleissner/springbatchrest/example/core/PersonJobConfig.java

Lines changed: 27 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,10 @@
44
import lombok.AllArgsConstructor;
55
import lombok.Data;
66
import lombok.NoArgsConstructor;
7+
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
89
import org.springframework.batch.core.Job;
910
import org.springframework.batch.core.Step;
10-
import org.springframework.batch.core.configuration.DuplicateJobException;
11-
import org.springframework.batch.core.configuration.JobFactory;
1211
import org.springframework.batch.core.configuration.JobRegistry;
1312
import org.springframework.batch.core.configuration.annotation.EnableBatchProcessing;
1413
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
@@ -22,39 +21,32 @@
2221
import org.springframework.batch.item.file.mapping.BeanWrapperFieldSetMapper;
2322
import org.springframework.batch.item.function.FunctionItemProcessor;
2423
import org.springframework.batch.item.support.CompositeItemProcessor;
25-
import org.springframework.beans.factory.annotation.Autowired;
2624
import org.springframework.beans.factory.annotation.Qualifier;
2725
import org.springframework.beans.factory.annotation.Value;
2826
import org.springframework.context.annotation.Bean;
2927
import org.springframework.context.annotation.Configuration;
28+
import org.springframework.core.env.Environment;
3029
import org.springframework.core.io.ClassPathResource;
3130

3231
import java.util.LinkedList;
3332
import java.util.List;
33+
import java.util.Optional;
3434

35-
import static com.github.chrisgleissner.springbatchrest.util.core.property.JobPropertyResolvers.JobProperties;
3635
import static com.google.common.collect.Lists.newArrayList;
3736
import static java.util.Collections.synchronizedList;
3837

39-
@Slf4j
40-
@Configuration
41-
@EnableBatchProcessing
38+
@Configuration @EnableBatchProcessing @RequiredArgsConstructor @Slf4j
4239
public class PersonJobConfig {
43-
4440
static final String JOB_NAME = "personJob";
4541
static final String LAST_NAME_PREFIX = "lastNamePrefix";
4642

47-
@Autowired
48-
JobBuilderFactory jobs;
49-
50-
@Autowired
51-
StepBuilderFactory steps;
52-
53-
@Autowired
54-
JobRegistry jobRegistry;
43+
private final JobBuilderFactory jobs;
44+
private final StepBuilderFactory steps;
45+
private final JobRegistry jobRegistry;
46+
private final Environment environment;
5547

5648
@Bean
57-
Job personJob(@Qualifier("personStep") Step personStep) throws DuplicateJobException {
49+
Job personJob(@Qualifier("personStep") Step personStep) {
5850
return JobBuilder.registerJob(jobRegistry, jobs.get(JOB_NAME)
5951
.incrementer(new RunIdIncrementer())
6052
.start(personStep)
@@ -78,31 +70,32 @@ FlatFileItemReader<Person> personReader() {
7870
.name("personItemReader")
7971
.resource(new ClassPathResource("person.csv"))
8072
.delimited()
81-
.names(new String[]{"firstName", "lastName"})
73+
.names("firstName", "lastName")
8274
.fieldSetMapper(new BeanWrapperFieldSetMapper<Person>() {{
8375
setTargetType(Person.class);
8476
}})
8577
.build();
8678
}
8779

88-
@Bean
89-
ItemProcessor personProcessor(@Qualifier("personNameCaseChange") ItemProcessor personNameCaseChange) {
80+
@Bean @StepScope
81+
ItemProcessor personProcessor(
82+
@Qualifier("personNameCaseChange") ItemProcessor personNameCaseChange,
83+
@Value("#{jobParameters['" + LAST_NAME_PREFIX + "']}") String lastNamePrefix) {
9084
CompositeItemProcessor p = new CompositeItemProcessor();
91-
p.setDelegates(newArrayList(personNameFilter(), personNameCaseChange));
85+
p.setDelegates(newArrayList(
86+
personNameFilter(Optional.ofNullable(lastNamePrefix).orElseGet(() -> environment.getProperty(LAST_NAME_PREFIX))),
87+
personNameCaseChange));
9288
return p;
9389
}
9490

95-
@Bean
96-
ItemProcessor personNameFilter() {
91+
private ItemProcessor personNameFilter(String lastNamePrefix) {
9792
return new FunctionItemProcessor<Person, Person>(p -> {
98-
String lastNamePrefix = JobProperties.of(PersonJobConfig.JOB_NAME).getProperty(LAST_NAME_PREFIX);
9993
log.info("Last name prefix: {}", lastNamePrefix);
10094
return p.lastName != null && p.lastName.startsWith(lastNamePrefix) ? p : null;
10195
});
10296
}
10397

104-
@StepScope
105-
@Bean
98+
@Bean @StepScope
10699
ItemProcessor personNameCaseChange(@Value("#{jobParameters['upperCase']}") Boolean upperCaseParam) {
107100
boolean upperCase = upperCaseParam == null ? false : upperCaseParam;
108101
log.info("personNameCaseChange(upperCase={})", upperCase);
@@ -116,8 +109,14 @@ CacheItemWriter<Person> personWriter() {
116109
return new CacheItemWriter<>();
117110
}
118111

119-
public class CacheItemWriter<T> implements ItemWriter<T> {
120-
private List<T> items = synchronizedList(new LinkedList<>());
112+
@Data @NoArgsConstructor @AllArgsConstructor
113+
public static class Person {
114+
private String firstName;
115+
private String lastName;
116+
}
117+
118+
public static class CacheItemWriter<T> implements ItemWriter<T> {
119+
private final List<T> items = synchronizedList(new LinkedList<>());
121120

122121
@Override
123122
public void write(List<? extends T> items) {
@@ -132,12 +131,4 @@ public void clear() {
132131
items.clear();
133132
}
134133
}
135-
136-
@Data
137-
@NoArgsConstructor
138-
@AllArgsConstructor
139-
public static class Person {
140-
private String firstName;
141-
private String lastName;
142-
}
143134
}

example/api/src/test/java/com/github/chrisgleissner/springbatchrest/example/core/PersonJobTest.java

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,18 +31,12 @@
3131
@SpringBootTest(webEnvironment = DEFINED_PORT)
3232
@EnableAutoConfiguration
3333
public class PersonJobTest {
34-
35-
@LocalServerPort
36-
private int port;
37-
38-
@Autowired
39-
private TestRestTemplate restTemplate;
40-
41-
@Autowired
42-
private PersonJobConfig.CacheItemWriter<PersonJobConfig.Person> cacheItemWriter;
34+
@LocalServerPort private int port;
35+
@Autowired private TestRestTemplate restTemplate;
36+
@Autowired private PersonJobConfig.CacheItemWriter<PersonJobConfig.Person> cacheItemWriter;
4337

4438
@Test
45-
public void canStartJob() throws NoSuchJobException {
39+
public void canStartJob() {
4640
cacheItemWriter.clear();
4741
startJob(Optional.empty(), Optional.empty());
4842
assertThat(cacheItemWriter.getItems()).hasSize(5);

example/quartz-api/src/main/java/com/github/chrisgleissner/springbatchrest/example/quartz/PersonJobConfig.java

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import lombok.AllArgsConstructor;
55
import lombok.Data;
66
import lombok.NoArgsConstructor;
7+
import lombok.RequiredArgsConstructor;
78
import lombok.extern.slf4j.Slf4j;
89
import org.springframework.batch.core.Job;
910
import org.springframework.batch.core.Step;
@@ -23,30 +24,25 @@
2324
import org.springframework.beans.factory.annotation.Value;
2425
import org.springframework.context.annotation.Bean;
2526
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.core.env.Environment;
2628
import org.springframework.core.io.ClassPathResource;
2729

2830
import java.util.LinkedList;
2931
import java.util.List;
32+
import java.util.Optional;
3033

31-
import static com.github.chrisgleissner.springbatchrest.util.core.property.JobPropertyResolvers.JobProperties;
3234
import static com.google.common.collect.Lists.newArrayList;
3335
import static java.util.Collections.synchronizedList;
3436

35-
@Slf4j
36-
@Configuration
37+
@Configuration @RequiredArgsConstructor @Slf4j
3738
public class PersonJobConfig {
38-
3939
static final String JOB_NAME = "personJob";
4040
static final String LAST_NAME_PREFIX = "lastNamePrefix";
4141

42-
@Autowired
43-
JobBuilderFactory jobs;
44-
45-
@Autowired
46-
StepBuilderFactory steps;
47-
48-
@Autowired
49-
private AdHocScheduler adHocScheduler;
42+
private final JobBuilderFactory jobs;
43+
private final StepBuilderFactory steps;
44+
private final AdHocScheduler adHocScheduler;
45+
private final Environment environment;
5046

5147
@Bean
5248
Job personJob(@Qualifier("personStep") Step personStep) {
@@ -81,24 +77,25 @@ FlatFileItemReader<Person> personReader() {
8177
.build();
8278
}
8379

84-
@Bean
85-
ItemProcessor personProcessor(@Qualifier("personNameCaseChange") ItemProcessor personNameCaseChange) {
80+
@Bean @StepScope
81+
ItemProcessor personProcessor(
82+
@Qualifier("personNameCaseChange") ItemProcessor personNameCaseChange,
83+
@Value("#{jobParameters['" + LAST_NAME_PREFIX + "']}") String lastNamePrefix) {
8684
CompositeItemProcessor p = new CompositeItemProcessor();
87-
p.setDelegates(newArrayList(personNameFilter(), personNameCaseChange));
85+
p.setDelegates(newArrayList(
86+
personNameFilter(Optional.ofNullable(lastNamePrefix).orElseGet(() -> environment.getProperty(LAST_NAME_PREFIX))),
87+
personNameCaseChange));
8888
return p;
8989
}
9090

91-
@Bean
92-
ItemProcessor personNameFilter() {
91+
private ItemProcessor personNameFilter(String lastNamePrefix) {
9392
return new FunctionItemProcessor<Person, Person>(p -> {
94-
String lastNamePrefix = JobProperties.of(PersonJobConfig.JOB_NAME).getProperty(LAST_NAME_PREFIX);
9593
log.info("Last name prefix: {}", lastNamePrefix);
9694
return p.lastName != null && p.lastName.startsWith(lastNamePrefix) ? p : null;
9795
});
9896
}
9997

100-
@StepScope
101-
@Bean
98+
@Bean @StepScope
10299
ItemProcessor personNameCaseChange(@Value("#{jobParameters['upperCase']}") Boolean upperCaseParam) {
103100
boolean upperCase = upperCaseParam == null ? false : upperCaseParam;
104101
log.info("personNameCaseChange(upperCase={})", upperCase);

example/quartz-api/src/test/java/com/github/chrisgleissner/springbatchrest/example/quartz/PersonJobTest.java

Lines changed: 8 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,10 @@
2323
@RunWith(SpringRunner.class)
2424
@SpringBootTest(webEnvironment = DEFINED_PORT)
2525
public class PersonJobTest {
26-
27-
@LocalServerPort
28-
private int port;
29-
30-
@Autowired
31-
private TestRestTemplate restTemplate;
32-
33-
@Autowired
34-
private PersonJobConfig.CacheItemWriter<PersonJobConfig.Person> cacheItemWriter;
35-
36-
@Autowired
37-
private JobRegistry jobRegistry;
26+
@LocalServerPort private int port;
27+
@Autowired private TestRestTemplate restTemplate;
28+
@Autowired private PersonJobConfig.CacheItemWriter<PersonJobConfig.Person> cacheItemWriter;
29+
@Autowired private JobRegistry jobRegistry;
3830

3931
@Test
4032
public void canStartJob() throws NoSuchJobException {
@@ -58,16 +50,11 @@ public void canStartJob() throws NoSuchJobException {
5850
}
5951

6052
private JobExecution startJob(Optional<String> lastNamePrefix, Optional<Boolean> upperCase) {
61-
JobConfig.JobConfigBuilder jobConfigBuilder = JobConfig.builder()
62-
.name(PersonJobConfig.JOB_NAME).asynchronous(false);
63-
if (lastNamePrefix.isPresent())
64-
jobConfigBuilder.property(PersonJobConfig.LAST_NAME_PREFIX, lastNamePrefix.get());
65-
if (upperCase.isPresent())
66-
jobConfigBuilder.property("upperCase", "" + upperCase.get());
67-
JobConfig jobConfig = jobConfigBuilder.build();
68-
53+
JobConfig.JobConfigBuilder jobConfigBuilder = JobConfig.builder().name(PersonJobConfig.JOB_NAME).asynchronous(false);
54+
lastNamePrefix.ifPresent(s -> jobConfigBuilder.property(PersonJobConfig.LAST_NAME_PREFIX, s));
55+
upperCase.ifPresent(aBoolean -> jobConfigBuilder.property("upperCase", "" + aBoolean));
6956
ResponseEntity<JobExecutionResource> responseEntity = restTemplate.postForEntity("http://localhost:" + port + "/jobExecutions",
70-
jobConfig, JobExecutionResource.class);
57+
jobConfigBuilder.build(), JobExecutionResource.class);
7158
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
7259
return responseEntity.getBody().getJobExecution();
7360
}

0 commit comments

Comments
 (0)