GH-1192 - Migrate code base to jSpecify for nullness verification.

This commit is contained in:
Oliver Drotbohm
2025-02-17 17:10:47 +01:00
parent a136ff920b
commit 9ea2b24b53
134 changed files with 548 additions and 211 deletions

View File

@@ -17,6 +17,11 @@
<dependencies>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>spring-modulith-core</artifactId>
@@ -67,4 +72,5 @@
</dependency>
</dependencies>
</project>

View File

@@ -22,6 +22,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import org.jspecify.annotations.Nullable;
import org.springframework.data.domain.DomainEvents;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.ReflectionUtils.MethodCallback;
@@ -67,7 +68,7 @@ public class AggregateTestUtils {
*/
private static class DomainEventsMethodFinder implements MethodCallback {
Method method;
@Nullable Method method;
/*
* (non-Javadoc)

View File

@@ -26,6 +26,7 @@ import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;
import org.springframework.context.PayloadApplicationEvent;
@@ -145,7 +146,7 @@ class DefaultPublishedEvents implements PublishedEvents, ApplicationListener<App
* @see org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents#matching(java.util.function.Function, java.lang.Object)
*/
@Override
public <S> TypedPublishedEvents<T> matching(Function<T, S> mapper, S value) {
public <S> TypedPublishedEvents<T> matching(Function<T, S> mapper, @Nullable S value) {
return matching(mapper, (Predicate<S>) it -> Objects.equals(it, value));
}

View File

@@ -23,6 +23,7 @@ import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
@@ -50,7 +51,7 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
* @see org.springframework.test.context.ContextCustomizerFactory#createContextCustomizer(java.lang.Class, java.util.List)
*/
@Override
public ContextCustomizer createContextCustomizer(Class<?> testClass,
public @Nullable ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
var moduleTest = TestContextAnnotationUtils.findAnnotationDescriptor(testClass, ApplicationModuleTest.class);
@@ -239,6 +240,11 @@ class ModuleContextCustomizerFactory implements ContextCustomizerFactory {
for (String name : registry.getBeanDefinitionNames()) {
var type = factory.getType(name, false);
if (type == null) {
continue;
}
var module = modules.getModuleByType(type)
.filter(Predicate.not(ApplicationModule::isRootModule));

View File

@@ -109,15 +109,21 @@ public class ModuleTestExecution implements Iterable<ApplicationModule> {
return () -> {
var annotation = AnnotatedElementUtils.findMergedAnnotation(type, ApplicationModuleTest.class);
var packageName = type.getPackage().getName();
if (annotation == null) {
throw new IllegalStateException(
"%s not found on %s!".formatted(ApplicationModuleTest.class.getName(), type.getName()));
}
var packageName = PackageName.ofType(type).toString();
var modules = BOOTSTRAP.of(findSpringBootApplicationByClasses(annotation, type));
var moduleName = annotation.module();
var module = StringUtils.hasText(moduleName)
? modules.getModuleByName(moduleName).orElseThrow( //
() -> new IllegalStateException(String.format("Unable to find module %s!", moduleName)))
() -> new IllegalStateException("Unable to find module %s!".formatted(moduleName)))
: modules.getModuleForPackage(packageName).orElseThrow( //
() -> new IllegalStateException(String.format("Package %s is not part of any module!", packageName)));
() -> new IllegalStateException("Package %s is not part of any module!".formatted(packageName)));
return EXECUTIONS.computeIfAbsent(new Key(module.getBasePackage().getName(), annotation),
it -> new ModuleTestExecution(annotation, modules, module));

View File

@@ -20,7 +20,7 @@ import java.util.Collection;
import java.util.function.Function;
import java.util.function.Predicate;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**

View File

@@ -21,7 +21,7 @@ import java.util.function.Function;
import java.util.function.Predicate;
import org.assertj.core.api.AbstractAssert;
import org.springframework.lang.Nullable;
import org.jspecify.annotations.Nullable;
import org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents;
import org.springframework.util.Assert;

View File

@@ -17,6 +17,7 @@ package org.springframework.modulith.test;
import java.util.function.Function;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
@@ -35,8 +36,8 @@ import org.springframework.util.Assert;
*/
class PublishedEventsParameterResolver implements ParameterResolver, AfterEachCallback {
private ThreadBoundApplicationListenerAdapter listener;
private final Function<ExtensionContext, ApplicationContext> lookup;
private @Nullable ThreadBoundApplicationListenerAdapter listener;
PublishedEventsParameterResolver() {
this(ctx -> SpringExtension.getApplicationContext(ctx));
@@ -74,7 +75,10 @@ class PublishedEventsParameterResolver implements ParameterResolver, AfterEachCa
var publishedEvents = PublishedEventsFactory.createPublishedEvents();
initializeListener(extensionContext);
listener.registerDelegate(publishedEvents);
if (listener != null) {
listener.registerDelegate(publishedEvents);
}
return publishedEvents;
}

View File

@@ -30,9 +30,9 @@ import java.util.function.Supplier;
import org.awaitility.Awaitility;
import org.awaitility.core.ConditionFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.lang.CheckReturnValue;
import org.springframework.lang.Nullable;
import org.springframework.modulith.test.PublishedEvents.TypedPublishedEvents;
import org.springframework.modulith.test.PublishedEventsAssert.PublishedEventAssert;
import org.springframework.transaction.TransactionDefinition;
@@ -96,7 +96,13 @@ public class Scenario {
var definition = new DefaultTransactionDefinition(transactionTemplate);
definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
this.transactionOperations = new TransactionTemplate(transactionTemplate.getTransactionManager(), definition);
var transactionManager = transactionTemplate.getTransactionManager();
if (transactionManager == null) {
throw new IllegalStateException("No transaction manager found on TransactionTemplate!");
}
this.transactionOperations = new TransactionTemplate(transactionManager, definition);
this.publisher = publisher;
this.events = events;
this.defaultCustomizer = Function.identity();
@@ -498,7 +504,7 @@ public class Scenario {
private final Class<E> type;
private final Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filter;
private final ExecutionResult<?, T> previousResult;
private final @Nullable ExecutionResult<?, T> previousResult;
/**
* Creates a new {@link EventResult} for the given type and filter.
@@ -508,7 +514,7 @@ public class Scenario {
* @param previousResult a potentially previously calculated result.
*/
EventResult(Class<E> type, Function<TypedPublishedEvents<E>, TypedPublishedEvents<E>> filtered,
ExecutionResult<?, T> previousResult) {
@Nullable ExecutionResult<?, T> previousResult) {
Assert.notNull(type, "Event type must not be null!");

View File

@@ -1,5 +1,5 @@
/**
* Integration test support for Spring Modulith {@link org.springframework.modulith.core.ApplicationModules}.
*/
@org.springframework.lang.NonNullApi
@org.jspecify.annotations.NullMarked
package org.springframework.modulith.test;

View File

@@ -37,6 +37,7 @@ import org.springframework.modulith.test.ScenarioCustomizerIntegrationTests.Test
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionTemplate;
/**
@@ -53,7 +54,10 @@ class ScenarioCustomizerIntegrationTests {
@Bean
TransactionTemplate transactionTemplate() {
return mock(TransactionTemplate.class);
var txManager = mock(PlatformTransactionManager.class);
return new TransactionTemplate(txManager);
}
@Bean