Skip to content

Commit 7532ed8

Browse files
committed
Add JSpecify nullability annotations to Spring Cloud Task
Introduce JSpecify `@NullMarked` and `@Nullable` annotations across all modules to prevent null pointer exceptions at compile time using ErrorProne's NullAway checker. Changes include: - Add `@NullMarked` to all package-info.java files to enable null-safety checking by default for all types in each package - Mark nullable fields and method parameters with `@Nullable` in core data models (`TaskExecution`, `TaskProperties`) - Update DAO interfaces and implementations (`TaskExecutionDao`, `JdbcTaskExecutionDao`, `MapTaskExecutionDao`) with proper nullability contracts for parameters and return types - Add null checks using `Objects.requireNonNull` before dereferencing potentially nullable fields in Spring-managed beans - Handle Spring framework patterns (`@Autowired` fields, `@Value` annotations, `@PostConstruct` initialization) by marking fields as `@Nullable` and adding runtime assertions where needed - Update repository layer (`TaskRepository`, `SimpleTaskRepository`, `TaskExplorer`) with accurate null contracts - Fix configuration classes (`DefaultTaskConfigurer`, `SimpleTaskAutoConfiguration`, `TaskLifecycleConfiguration`) to properly handle nullable dependencies - Update batch module autoconfiguration and listener classes with nullability annotations - Add nullability to stream event listeners and support classes - Configure Maven with `.mvn/jvm.config` and `.mvn/maven.config` to enable ErrorProne and NullAway checking via `jspecify` profile
1 parent 75a7015 commit 7532ed8

File tree

82 files changed

+606
-299
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+606
-299
lines changed

.mvn/jvm.config

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,11 @@
11
-Xmx1024m -XX:CICompilerCount=1 -XX:TieredStopAtLevel=1 -Djava.security.egd=file:/dev/./urandom
2+
--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
3+
--add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
4+
--add-exports jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
5+
--add-exports jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
6+
--add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
7+
--add-exports jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
8+
--add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
9+
--add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
10+
--add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
11+
--add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED

.mvn/maven.config

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
-DaltSnapshotDeploymentRepository=repo.spring.io::default::https://repo.spring.io/libs-snapshot-local
2+
-Djspecify.enabled=true
23
-P spring

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/RangeConverter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cloud.task.batch.autoconfigure;
1818

19+
import org.jspecify.annotations.Nullable;
20+
1921
import org.springframework.batch.infrastructure.item.file.transform.Range;
2022
import org.springframework.core.convert.converter.Converter;
2123

@@ -31,7 +33,7 @@
3133
public class RangeConverter implements Converter<String, Range> {
3234

3335
@Override
34-
public Range convert(String source) {
36+
public @Nullable Range convert(String source) {
3537
if (source == null) {
3638
return null;
3739
}

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/SingleStepJobAutoConfiguration.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,7 @@
2222
import org.springframework.batch.core.job.builder.JobBuilder;
2323
import org.springframework.batch.core.repository.JobRepository;
2424
import org.springframework.batch.core.step.Step;
25-
import org.springframework.batch.core.step.builder.SimpleStepBuilder;
26-
import org.springframework.batch.core.step.builder.StepBuilder;
25+
import org.springframework.batch.core.step.builder.ChunkOrientedStepBuilder;
2726
import org.springframework.batch.infrastructure.item.ItemProcessor;
2827
import org.springframework.batch.infrastructure.item.ItemReader;
2928
import org.springframework.batch.infrastructure.item.ItemWriter;
@@ -78,16 +77,16 @@ private void validateProperties(SingleStepJobProperties properties) {
7877
@ConditionalOnMissingBean
7978
@ConditionalOnProperty(prefix = "spring.batch.job", name = "job-name")
8079
public Job job(ItemReader<Map<String, Object>> itemReader, ItemWriter<Map<String, Object>> itemWriter) {
81-
82-
SimpleStepBuilder<Map<String, Object>, Map<String, Object>> stepBuilder = new StepBuilder(
83-
this.properties.getStepName(), this.jobRepository)
84-
.<Map<String, Object>, Map<String, Object>>chunk(this.properties.getChunkSize(), this.transactionManager)
80+
Assert.state(properties.getStepName() != null, "A step name is required");
81+
Assert.state(properties.getChunkSize() != null, "A chunkSize is required");
82+
var chunkOrientedStepBuilder = new ChunkOrientedStepBuilder(properties.getStepName(), this.jobRepository,
83+
this.properties.getChunkSize())
84+
.transactionManager(this.transactionManager)
8585
.reader(itemReader);
86+
chunkOrientedStepBuilder.processor(this.itemProcessor);
87+
Step step = chunkOrientedStepBuilder.writer(itemWriter).build();
8688

87-
stepBuilder.processor(this.itemProcessor);
88-
89-
Step step = stepBuilder.writer(itemWriter).build();
90-
89+
Assert.state(this.properties.getJobName() != null, "A job name is required");
9190
return new JobBuilder(this.properties.getJobName(), this.jobRepository).start(step).build();
9291
}
9392

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/SingleStepJobProperties.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.cloud.task.batch.autoconfigure;
1818

19+
import org.jspecify.annotations.Nullable;
20+
1921
import org.springframework.boot.context.properties.ConfigurationProperties;
2022

2123
/**
@@ -30,63 +32,63 @@ public class SingleStepJobProperties {
3032
/**
3133
* Name of the step in the single step job.
3234
*/
33-
private String stepName;
35+
private @Nullable String stepName;
3436

3537
/**
3638
* The number of items to process per transaction or chunk.
3739
*/
38-
private Integer chunkSize;
40+
private @Nullable Integer chunkSize;
3941

4042
/**
4143
* The name of the job.
4244
*/
43-
private String jobName;
45+
private @Nullable String jobName;
4446

4547
/**
4648
* Name of the step in the single step job.
4749
* @return name
4850
*/
49-
public String getStepName() {
51+
public @Nullable String getStepName() {
5052
return stepName;
5153
}
5254

5355
/**
5456
* Set the name of the step.
5557
* @param stepName name
5658
*/
57-
public void setStepName(String stepName) {
59+
public void setStepName(@Nullable String stepName) {
5860
this.stepName = stepName;
5961
}
6062

6163
/**
6264
* The number of items to process per transaction/chunk.
6365
* @return number of items
6466
*/
65-
public Integer getChunkSize() {
67+
public @Nullable Integer getChunkSize() {
6668
return chunkSize;
6769
}
6870

6971
/**
7072
* Set the number of items within a transaction/chunk.
7173
* @param chunkSize number of items
7274
*/
73-
public void setChunkSize(Integer chunkSize) {
75+
public void setChunkSize(@Nullable Integer chunkSize) {
7476
this.chunkSize = chunkSize;
7577
}
7678

7779
/**
7880
* The name of the job.
7981
* @return name
8082
*/
81-
public String getJobName() {
83+
public @Nullable String getJobName() {
8284
return jobName;
8385
}
8486

8587
/**
8688
* Set the name of the job.
8789
* @param jobName name
8890
*/
89-
public void setJobName(String jobName) {
91+
public void setJobName(@Nullable String jobName) {
9092
this.jobName = jobName;
9193
}
9294

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/flatfile/FlatFileItemReaderAutoConfiguration.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
import org.springframework.boot.context.properties.EnableConfigurationProperties;
4343
import org.springframework.cloud.task.batch.autoconfigure.RangeConverter;
4444
import org.springframework.context.annotation.Bean;
45+
import org.springframework.util.Assert;
4546

4647
/**
4748
* Autconfiguration for a {@code FlatFileItemReader}.
@@ -71,6 +72,8 @@ public FlatFileItemReader<Map<String, Object>> itemReader(@Autowired(required =
7172
@Autowired(required = false) LineMapper<Map<String, Object>> lineMapper,
7273
@Autowired(required = false) LineCallbackHandler skippedLinesCallback,
7374
@Autowired(required = false) RecordSeparatorPolicy recordSeparatorPolicy) {
75+
Assert.state(this.properties.getName() != null, "A name is required");
76+
Assert.state(this.properties.getResource() != null, "A resource is required");
7477
FlatFileItemReaderBuilder<Map<String, Object>> mapFlatFileItemReaderBuilder = new FlatFileItemReaderBuilder<Map<String, Object>>()
7578
.name(this.properties.getName())
7679
.resource(this.properties.getResource())
@@ -90,6 +93,7 @@ public FlatFileItemReader<Map<String, Object>> itemReader(@Autowired(required =
9093
mapFlatFileItemReaderBuilder.skippedLinesCallback(skippedLinesCallback);
9194

9295
if (this.properties.isDelimited()) {
96+
Assert.state(this.properties.getNames() != null, "Names are required");
9397
mapFlatFileItemReaderBuilder.delimited()
9498
.quoteCharacter(this.properties.getQuoteCharacter())
9599
.delimiter(this.properties.getDelimiter())
@@ -99,9 +103,14 @@ public FlatFileItemReader<Map<String, Object>> itemReader(@Autowired(required =
99103
.fieldSetMapper(new MapFieldSetMapper());
100104
}
101105
else if (this.properties.isFixedLength()) {
106+
Assert.state(this.properties.getNames() != null, "Names are required");
102107
RangeConverter rangeConverter = new RangeConverter();
103108
List<Range> ranges = new ArrayList<>();
104-
this.properties.getRanges().forEach(range -> ranges.add(rangeConverter.convert(range)));
109+
this.properties.getRanges().forEach(range -> {
110+
Range result = rangeConverter.convert(range);
111+
Assert.state(result != null, "Range String could not converted to non-null range");
112+
ranges.add(result);
113+
});
105114
mapFlatFileItemReaderBuilder.fixedLength()
106115
.columns(ranges.toArray(new Range[0]))
107116
.names(this.properties.getNames())

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/flatfile/FlatFileItemReaderProperties.java

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.ArrayList;
2020
import java.util.List;
2121

22+
import org.jspecify.annotations.Nullable;
23+
2224
import org.springframework.batch.infrastructure.item.file.FlatFileItemReader;
2325
import org.springframework.batch.infrastructure.item.file.transform.DelimitedLineTokenizer;
2426
import org.springframework.batch.infrastructure.item.file.transform.Range;
@@ -44,7 +46,7 @@ public class FlatFileItemReaderProperties {
4446
* {@link org.springframework.batch.infrastructure.item.ExecutionContext}. Required if
4547
* {@link #setSaveState} is set to {@code true}.
4648
*/
47-
private String name;
49+
private @Nullable String name;
4850

4951
/**
5052
* Configure the maximum number of items to be read.
@@ -64,7 +66,7 @@ public class FlatFileItemReaderProperties {
6466
/**
6567
* The {@link Resource} to be used as input.
6668
*/
67-
private Resource resource;
69+
private @Nullable Resource resource;
6870

6971
/**
7072
* Configure whether the reader should be in strict mode (require the input
@@ -118,7 +120,7 @@ public class FlatFileItemReaderProperties {
118120
/**
119121
* The names of the fields to be parsed from the file.
120122
*/
121-
private String[] names;
123+
private String @Nullable [] names;
122124

123125
/**
124126
* Indicates whether the number of tokens must match the number of configured fields.
@@ -150,7 +152,7 @@ public void setSaveState(boolean saveState) {
150152
* keys.
151153
* @return the name
152154
*/
153-
public String getName() {
155+
public @Nullable String getName() {
154156
return this.name;
155157
}
156158

@@ -161,7 +163,7 @@ public String getName() {
161163
* @param name name of the reader instance
162164
* @see org.springframework.batch.infrastructure.item.ItemStreamSupport#setName(String)
163165
*/
164-
public void setName(String name) {
166+
public void setName(@Nullable String name) {
165167
this.name = name;
166168
}
167169

@@ -219,7 +221,7 @@ public void setComments(List<String> comments) {
219221
* The input file for the {@code FlatFileItemReader}.
220222
* @return a Resource
221223
*/
222-
public Resource getResource() {
224+
public @Nullable Resource getResource() {
223225
return this.resource;
224226
}
225227

@@ -228,7 +230,7 @@ public Resource getResource() {
228230
* @param resource the input to the reader.
229231
* @see FlatFileItemReader#setResource(Resource)
230232
*/
231-
public void setResource(Resource resource) {
233+
public void setResource(@Nullable Resource resource) {
232234
this.resource = resource;
233235
}
234236

@@ -391,15 +393,15 @@ public void setRanges(List<String> ranges) {
391393
* Names of each column.
392394
* @return names
393395
*/
394-
public String[] getNames() {
396+
public String @Nullable [] getNames() {
395397
return this.names;
396398
}
397399

398400
/**
399401
* The names of the fields to be parsed from the file.
400402
* @param names names of fields
401403
*/
402-
public void setNames(String[] names) {
404+
public void setNames(String @Nullable [] names) {
403405
this.names = names;
404406
}
405407

spring-cloud-starter-single-step-batch-job/src/main/java/org/springframework/cloud/task/batch/autoconfigure/flatfile/FlatFileItemWriterAutoConfiguration.java

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.springframework.boot.context.properties.EnableConfigurationProperties;
3636
import org.springframework.context.annotation.Bean;
3737
import org.springframework.core.io.WritableResource;
38+
import org.springframework.util.Assert;
3839

3940
/**
4041
* Autoconfiguration for a {@code FlatFileItemWriter}.
@@ -47,7 +48,7 @@
4748
@AutoConfigureAfter(BatchAutoConfiguration.class)
4849
public class FlatFileItemWriterAutoConfiguration {
4950

50-
private FlatFileItemWriterProperties properties;
51+
private final FlatFileItemWriterProperties properties;
5152

5253
@Autowired(required = false)
5354
private LineAggregator<Map<String, Object>> lineAggregator;
@@ -78,7 +79,8 @@ else if ((this.properties.isFormatted() || this.properties.isDelimited()) && thi
7879
throw new IllegalStateException(
7980
"A LineAggregator must be configured if the " + "output is not formatted or delimited");
8081
}
81-
82+
Assert.state(this.properties.getName() != null, "name must not be null");
83+
Assert.state(this.properties.getResource() != null, "resource must not be null");
8284
FlatFileItemWriterBuilder<Map<String, Object>> builder = new FlatFileItemWriterBuilder<Map<String, Object>>()
8385
.name(this.properties.getName())
8486
.resource((WritableResource) this.properties.getResource())
@@ -101,10 +103,13 @@ else if ((this.properties.isFormatted() || this.properties.isDelimited()) && thi
101103
delimitedBuilder.fieldExtractor(this.fieldExtractor);
102104
}
103105
else {
106+
Assert.state(this.properties.getNames() != null, "names must not be null");
104107
delimitedBuilder.fieldExtractor(new MapFieldExtractor(this.properties.getNames()));
105108
}
106109
}
107110
else if (this.properties.isFormatted()) {
111+
Assert.state(this.properties.getFormat() != null, "format must not be null");
112+
108113
FlatFileItemWriterBuilder.FormattedBuilder<Map<String, Object>> formattedBuilder = builder.formatted()
109114
.format(this.properties.getFormat())
110115
.locale(this.properties.getLocale())
@@ -115,6 +120,7 @@ else if (this.properties.isFormatted()) {
115120
formattedBuilder.fieldExtractor(this.fieldExtractor);
116121
}
117122
else {
123+
Assert.state(this.properties.getNames() != null, "names must not be null");
118124
formattedBuilder.fieldExtractor(new MapFieldExtractor(this.properties.getNames()));
119125
}
120126
}
@@ -131,7 +137,7 @@ else if (this.lineAggregator != null) {
131137
*/
132138
public static class MapFieldExtractor implements FieldExtractor<Map<String, Object>> {
133139

134-
private String[] names;
140+
private final String[] names;
135141

136142
public MapFieldExtractor(String[] names) {
137143
this.names = names;

0 commit comments

Comments
 (0)