Skip to content

Commit bac4a38

Browse files
committed
Add nullability annotations to core/spring-boot-test
See gh-46587
1 parent 419a1c3 commit bac4a38

33 files changed

+260
-144
lines changed

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/AnnotatedClassFinder.java

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.Map;
2323
import java.util.Set;
2424

25+
import org.jspecify.annotations.Nullable;
26+
2527
import org.springframework.beans.factory.config.BeanDefinition;
2628
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
2729
import org.springframework.core.type.filter.AnnotationTypeFilter;
@@ -63,7 +65,7 @@ public AnnotatedClassFinder(Class<? extends Annotation> annotationType) {
6365
* @return the first {@link Class} annotated with the target annotation within the
6466
* hierarchy defined by the given {@code source} or {@code null} if none is found.
6567
*/
66-
public Class<?> findFromClass(Class<?> source) {
68+
public @Nullable Class<?> findFromClass(Class<?> source) {
6769
Assert.notNull(source, "'source' must not be null");
6870
return findFromPackage(ClassUtils.getPackageName(source));
6971
}
@@ -75,7 +77,7 @@ public Class<?> findFromClass(Class<?> source) {
7577
* @return the first {@link Class} annotated with the target annotation within the
7678
* hierarchy defined by the given {@code source} or {@code null} if none is found.
7779
*/
78-
public Class<?> findFromPackage(String source) {
80+
public @Nullable Class<?> findFromPackage(String source) {
7981
Assert.notNull(source, "'source' must not be null");
8082
Class<?> configuration = cache.get(source);
8183
if (configuration == null) {
@@ -85,13 +87,15 @@ public Class<?> findFromPackage(String source) {
8587
return configuration;
8688
}
8789

88-
private Class<?> scanPackage(String source) {
90+
private @Nullable Class<?> scanPackage(String source) {
8991
while (!source.isEmpty()) {
9092
Set<BeanDefinition> components = this.scanner.findCandidateComponents(source);
9193
if (!components.isEmpty()) {
9294
Assert.state(components.size() == 1, () -> "Found multiple @" + this.annotationType.getSimpleName()
9395
+ " annotated classes " + components);
94-
return ClassUtils.resolveClassName(components.iterator().next().getBeanClassName(), null);
96+
String beanClassName = components.iterator().next().getBeanClassName();
97+
Assert.state(beanClassName != null, "'beanClassName' must not be null");
98+
return ClassUtils.resolveClassName(beanClassName, null);
9599
}
96100
source = getParentPackage(source);
97101
}

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/FilteredClassLoader.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import java.util.Enumeration;
2828
import java.util.function.Predicate;
2929

30+
import org.jspecify.annotations.Nullable;
31+
3032
import org.springframework.core.SmartClassLoader;
3133
import org.springframework.core.io.ClassPathResource;
3234

@@ -117,7 +119,7 @@ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundE
117119
}
118120

119121
@Override
120-
public URL getResource(String name) {
122+
public @Nullable URL getResource(String name) {
121123
for (Predicate<String> filter : this.resourcesFilters) {
122124
if (filter.test(name)) {
123125
return null;
@@ -137,7 +139,7 @@ public Enumeration<URL> getResources(String name) throws IOException {
137139
}
138140

139141
@Override
140-
public InputStream getResourceAsStream(String name) {
142+
public @Nullable InputStream getResourceAsStream(String name) {
141143
for (Predicate<String> filter : this.resourcesFilters) {
142144
if (filter.test(name)) {
143145
return null;
@@ -147,7 +149,7 @@ public InputStream getResourceAsStream(String name) {
147149
}
148150

149151
@Override
150-
public Class<?> publicDefineClass(String name, byte[] b, ProtectionDomain protectionDomain) {
152+
public Class<?> publicDefineClass(String name, byte[] b, @Nullable ProtectionDomain protectionDomain) {
151153
for (Predicate<String> filter : this.classesFilters) {
152154
if (filter.test(name)) {
153155
throw new IllegalArgumentException(String.format("Defining class with name %s is not supported", name));

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import java.util.Set;
2525
import java.util.stream.Collectors;
2626

27+
import org.jspecify.annotations.Nullable;
28+
2729
import org.springframework.beans.BeansException;
2830
import org.springframework.beans.factory.BeanFactory;
2931
import org.springframework.beans.factory.BeanFactoryAware;
@@ -156,6 +158,7 @@ static class ImportsSelector implements ImportSelector, BeanFactoryAware {
156158

157159
private static final String[] NO_IMPORTS = {};
158160

161+
@SuppressWarnings("NullAway.Init")
159162
private ConfigurableListableBeanFactory beanFactory;
160163

161164
@Override
@@ -254,7 +257,7 @@ private boolean isFilteredAnnotation(String typeName) {
254257
return ANNOTATION_FILTERS.stream().anyMatch((filter) -> filter.matches(typeName));
255258
}
256259

257-
private Set<Object> determineImports(MergedAnnotations annotations, Class<?> testClass) {
260+
private @Nullable Set<Object> determineImports(MergedAnnotations annotations, Class<?> testClass) {
258261
Set<Object> determinedImports = new LinkedHashSet<>();
259262
AnnotationMetadata metadata = AnnotationMetadata.introspect(testClass);
260263
for (MergedAnnotation<Import> annotation : annotations.stream(Import.class).toList()) {
@@ -269,7 +272,7 @@ private Set<Object> determineImports(MergedAnnotations annotations, Class<?> tes
269272
return determinedImports;
270273
}
271274

272-
private Set<Object> determineImports(Class<?> source, AnnotationMetadata metadata) {
275+
private @Nullable Set<Object> determineImports(Class<?> source, AnnotationMetadata metadata) {
273276
if (DeterminableImports.class.isAssignableFrom(source)) {
274277
// We can determine the imports
275278
return ((DeterminableImports) instantiate(source)).determineImports(metadata);

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.lang.reflect.Method;
2020
import java.util.List;
2121

22+
import org.jspecify.annotations.Nullable;
23+
2224
import org.springframework.aot.AotDetector;
2325
import org.springframework.context.annotation.Bean;
2426
import org.springframework.context.annotation.Import;
@@ -41,7 +43,7 @@
4143
class ImportsContextCustomizerFactory implements ContextCustomizerFactory {
4244

4345
@Override
44-
public ContextCustomizer createContextCustomizer(Class<?> testClass,
46+
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
4547
List<ContextConfigurationAttributes> configAttributes) {
4648
if (AotDetector.useGeneratedArtifacts()) {
4749
return null;

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootContextLoader.java

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
import java.util.List;
2424
import java.util.function.Consumer;
2525

26+
import org.jspecify.annotations.Nullable;
27+
2628
import org.springframework.aot.hint.ExecutableMode;
2729
import org.springframework.aot.hint.RuntimeHints;
2830
import org.springframework.beans.BeanUtils;
@@ -105,6 +107,8 @@ public class SpringBootContextLoader extends AbstractContextLoader implements Ao
105107
private static final Consumer<SpringApplication> ALREADY_CONFIGURED = (springApplication) -> {
106108
};
107109

110+
private static final Object NONE = new Object();
111+
108112
@Override
109113
public ApplicationContext loadContext(MergedContextConfiguration mergedConfig) throws Exception {
110114
return loadContext(mergedConfig, Mode.STANDARD, null, null);
@@ -123,8 +127,8 @@ public ApplicationContext loadContextForAotRuntime(MergedContextConfiguration me
123127
}
124128

125129
private ApplicationContext loadContext(MergedContextConfiguration mergedConfig, Mode mode,
126-
ApplicationContextInitializer<ConfigurableApplicationContext> initializer, RuntimeHints runtimeHints)
127-
throws Exception {
130+
@Nullable ApplicationContextInitializer<ConfigurableApplicationContext> initializer,
131+
@Nullable RuntimeHints runtimeHints) throws Exception {
128132
assertHasClassesOrLocations(mergedConfig);
129133
SpringBootTestAnnotation annotation = SpringBootTestAnnotation.get(mergedConfig);
130134
String[] args = annotation.getArgs();
@@ -153,7 +157,7 @@ private void assertHasClassesOrLocations(MergedContextConfiguration mergedConfig
153157
+ SpringVersion.getVersion() + ").");
154158
}
155159

156-
private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMethod useMainMethod) {
160+
private @Nullable Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMethod useMainMethod) {
157161
if (useMainMethod == UseMainMethod.NEVER) {
158162
return null;
159163
}
@@ -167,14 +171,16 @@ private Method getMainMethod(MergedContextConfiguration mergedConfig, UseMainMet
167171
"Cannot use main method as no @SpringBootConfiguration-annotated class is available");
168172
Method mainMethod = findMainMethod(springBootConfiguration);
169173
Assert.state(mainMethod != null || useMainMethod == UseMainMethod.WHEN_AVAILABLE,
170-
() -> "Main method not found on '%s'".formatted(springBootConfiguration.getName()));
174+
() -> "Main method not found on '%s'"
175+
.formatted((springBootConfiguration != null) ? springBootConfiguration.getName() : null));
171176
return mainMethod;
172177
}
173178

174-
private static Method findMainMethod(Class<?> type) {
179+
private static @Nullable Method findMainMethod(@Nullable Class<?> type) {
175180
Method mainMethod = (type != null) ? ReflectionUtils.findMethod(type, "main", String[].class) : null;
176181
if (mainMethod == null && KotlinDetector.isKotlinPresent()) {
177182
try {
183+
Assert.state(type != null, "'type' must not be null");
178184
Class<?> kotlinClass = ClassUtils.forName(type.getName() + "Kt", type.getClassLoader());
179185
mainMethod = ReflectionUtils.findMethod(kotlinClass, "main", String[].class);
180186
}
@@ -285,7 +291,7 @@ protected SpringApplication getSpringApplication() {
285291
* method if you need a custom environment.
286292
* @return a {@link ConfigurableEnvironment} instance
287293
*/
288-
protected ConfigurableEnvironment getEnvironment() {
294+
protected @Nullable ConfigurableEnvironment getEnvironment() {
289295
return null;
290296
}
291297

@@ -462,9 +468,9 @@ public void initialize(ConfigurableApplicationContext applicationContext) {
462468
private static class ParentContextApplicationContextInitializer
463469
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
464470

465-
private final ApplicationContext parent;
471+
private final @Nullable ApplicationContext parent;
466472

467-
ParentContextApplicationContextInitializer(ApplicationContext parent) {
473+
ParentContextApplicationContextInitializer(@Nullable ApplicationContext parent) {
468474
this.parent = parent;
469475
}
470476

@@ -507,15 +513,16 @@ private static class ContextLoaderHook implements SpringApplicationHook {
507513

508514
private final Mode mode;
509515

510-
private final ApplicationContextInitializer<ConfigurableApplicationContext> initializer;
516+
private final @Nullable ApplicationContextInitializer<ConfigurableApplicationContext> initializer;
511517

512518
private final Consumer<SpringApplication> configurer;
513519

514520
private final List<ApplicationContext> contexts = Collections.synchronizedList(new ArrayList<>());
515521

516522
private final List<ApplicationContext> failedContexts = Collections.synchronizedList(new ArrayList<>());
517523

518-
ContextLoaderHook(Mode mode, ApplicationContextInitializer<ConfigurableApplicationContext> initializer,
524+
ContextLoaderHook(Mode mode,
525+
@Nullable ApplicationContextInitializer<ConfigurableApplicationContext> initializer,
519526
Consumer<SpringApplication> configurer) {
520527
this.mode = mode;
521528
this.initializer = initializer;
@@ -530,6 +537,7 @@ public SpringApplicationRunListener getRunListener(SpringApplication application
530537
public void starting(ConfigurableBootstrapContext bootstrapContext) {
531538
ContextLoaderHook.this.configurer.accept(application);
532539
if (ContextLoaderHook.this.mode == Mode.AOT_RUNTIME) {
540+
Assert.state(ContextLoaderHook.this.initializer != null, "'initializer' must not be null");
533541
application.addInitializers(
534542
(AotApplicationContextInitializer<?>) ContextLoaderHook.this.initializer::initialize);
535543
}
@@ -544,24 +552,24 @@ public void contextLoaded(ConfigurableApplicationContext context) {
544552
}
545553

546554
@Override
547-
public void failed(ConfigurableApplicationContext context, Throwable exception) {
555+
public void failed(@Nullable ConfigurableApplicationContext context, Throwable exception) {
548556
ContextLoaderHook.this.failedContexts.add(context);
549557
}
550558

551559
};
552560
}
553561

554-
private <T> ApplicationContext runMain(Runnable action) throws Exception {
562+
private ApplicationContext runMain(Runnable action) throws Exception {
555563
return run(() -> {
556564
action.run();
557-
return null;
565+
return NONE;
558566
});
559567
}
560568

561-
private ApplicationContext run(ThrowingSupplier<ConfigurableApplicationContext> action) throws Exception {
569+
private ApplicationContext run(ThrowingSupplier<?> action) throws Exception {
562570
try {
563-
ConfigurableApplicationContext context = SpringApplication.withHook(this, action);
564-
if (context != null) {
571+
Object result = SpringApplication.withHook(this, action);
572+
if (result instanceof ApplicationContext context) {
565573
return context;
566574
}
567575
}

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestAnnotation.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import java.util.Arrays;
2020
import java.util.Objects;
2121

22+
import org.jspecify.annotations.Nullable;
23+
2224
import org.springframework.boot.test.context.SpringBootTest.UseMainMethod;
2325
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
2426
import org.springframework.context.ConfigurableApplicationContext;
@@ -52,7 +54,7 @@ class SpringBootTestAnnotation implements ContextCustomizer {
5254
this(TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class));
5355
}
5456

55-
private SpringBootTestAnnotation(SpringBootTest annotation) {
57+
private SpringBootTestAnnotation(@Nullable SpringBootTest annotation) {
5658
this.args = (annotation != null) ? annotation.args() : NO_ARGS;
5759
this.webEnvironment = (annotation != null) ? annotation.webEnvironment() : WebEnvironment.NONE;
5860
this.useMainMethod = (annotation != null) ? annotation.useMainMethod() : UseMainMethod.NEVER;

core/spring-boot-test/src/main/java/org/springframework/boot/test/context/SpringBootTestContextBootstrapper.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525

2626
import org.apache.commons.logging.Log;
2727
import org.apache.commons.logging.LogFactory;
28+
import org.jspecify.annotations.Nullable;
2829

2930
import org.springframework.boot.SpringBootConfiguration;
3031
import org.springframework.boot.WebApplicationType;
@@ -287,7 +288,7 @@ private List<String> getAndProcessPropertySourceProperties(MergedContextConfigur
287288
* the bootstrapper class as a property.
288289
* @return the differentiator or {@code null}
289290
*/
290-
protected String getDifferentiatorPropertySourceProperty() {
291+
protected @Nullable String getDifferentiatorPropertySourceProperty() {
291292
return getClass().getName() + "=true";
292293
}
293294

@@ -320,22 +321,22 @@ else if (webEnvironment == WebEnvironment.NONE) {
320321
* @param testClass the source test class
321322
* @return the {@link WebEnvironment} or {@code null}
322323
*/
323-
protected WebEnvironment getWebEnvironment(Class<?> testClass) {
324+
protected @Nullable WebEnvironment getWebEnvironment(Class<?> testClass) {
324325
SpringBootTest annotation = getAnnotation(testClass);
325326
return (annotation != null) ? annotation.webEnvironment() : null;
326327
}
327328

328-
protected Class<?>[] getClasses(Class<?> testClass) {
329+
protected Class<?> @Nullable [] getClasses(Class<?> testClass) {
329330
SpringBootTest annotation = getAnnotation(testClass);
330331
return (annotation != null) ? annotation.classes() : null;
331332
}
332333

333-
protected String[] getProperties(Class<?> testClass) {
334+
protected String @Nullable [] getProperties(Class<?> testClass) {
334335
SpringBootTest annotation = getAnnotation(testClass);
335336
return (annotation != null) ? annotation.properties() : null;
336337
}
337338

338-
protected SpringBootTest getAnnotation(Class<?> testClass) {
339+
protected @Nullable SpringBootTest getAnnotation(Class<?> testClass) {
339340
return TestContextAnnotationUtils.findMergedAnnotation(testClass, SpringBootTest.class);
340341
}
341342

0 commit comments

Comments
 (0)