Add nullability annotations to core/spring-boot-docker-compose

See gh-46587
This commit is contained in:
Moritz Halbritter 2025-07-22 13:36:51 +02:00
parent b6e4533296
commit cb2a26ceec
27 changed files with 150 additions and 85 deletions

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.List;
import org.jspecify.annotations.Nullable;
/**
* Provides access to the ports that can be used to connect to a {@link RunningService}.
*
@ -52,6 +54,6 @@ public interface ConnectionPorts {
* all host ports
* @return a list of all host ports using the given protocol
*/
List<Integer> getAll(String protocol);
List<Integer> getAll(@Nullable String protocol);
}

View File

@ -22,6 +22,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.Config;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostConfig;
import org.springframework.boot.docker.compose.core.DockerCliInspectResponse.HostPort;
@ -57,7 +59,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
return (config != null) && "host".equals(config.networkMode());
}
private Map<ContainerPort, Integer> buildMappingsForNetworkSettings(NetworkSettings networkSettings) {
private Map<ContainerPort, Integer> buildMappingsForNetworkSettings(@Nullable NetworkSettings networkSettings) {
if (networkSettings == null || CollectionUtils.isEmpty(networkSettings.ports())) {
return Collections.emptyMap();
}
@ -73,7 +75,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
return Collections.unmodifiableMap(mappings);
}
private boolean isIpV4(HostPort hostPort) {
private boolean isIpV4(@Nullable HostPort hostPort) {
String ip = (hostPort != null) ? hostPort.hostIp() : null;
return !StringUtils.hasLength(ip) || ip.contains(".");
}
@ -108,7 +110,7 @@ class DefaultConnectionPorts implements ConnectionPorts {
}
@Override
public List<Integer> getAll(String protocol) {
public List<Integer> getAll(@Nullable String protocol) {
List<Integer> hostPorts = new ArrayList<>();
this.mappings.forEach((containerPort, hostPort) -> {
if (protocol == null || protocol.equalsIgnoreCase(containerPort.protocol())) {

View File

@ -25,6 +25,8 @@ import java.util.Map.Entry;
import java.util.function.Function;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.logging.LogLevel;
import org.springframework.util.Assert;
@ -41,7 +43,7 @@ class DefaultDockerCompose implements DockerCompose {
private final DockerHost hostname;
DefaultDockerCompose(DockerCli cli, String host) {
DefaultDockerCompose(DockerCli cli, @Nullable String host) {
this.cli = cli;
this.hostname = DockerHost.get(host, () -> cli.run(new DockerCliCommand.Context()));
}
@ -114,7 +116,8 @@ class DefaultDockerCompose implements DockerCompose {
return inspectResponses.stream().collect(Collectors.toMap(DockerCliInspectResponse::id, Function.identity()));
}
private DockerCliInspectResponse inspectContainer(String id, Map<String, DockerCliInspectResponse> inspected) {
private @Nullable DockerCliInspectResponse inspectContainer(String id,
Map<String, DockerCliInspectResponse> inspected) {
DockerCliInspectResponse inspect = inspected.get(id);
if (inspect != null) {
return inspect;

View File

@ -19,6 +19,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.Collections;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.origin.Origin;
import org.springframework.boot.origin.OriginProvider;
@ -45,10 +47,10 @@ class DefaultRunningService implements RunningService, OriginProvider {
private final DockerEnv env;
private final DockerComposeFile composeFile;
private final @Nullable DockerComposeFile composeFile;
DefaultRunningService(DockerHost host, DockerComposeFile composeFile, DockerCliComposePsResponse composePsResponse,
DockerCliInspectResponse inspectResponse) {
DefaultRunningService(DockerHost host, @Nullable DockerComposeFile composeFile,
DockerCliComposePsResponse composePsResponse, DockerCliInspectResponse inspectResponse) {
this.origin = new DockerComposeOrigin(composeFile, composePsResponse.name());
this.name = composePsResponse.name();
this.image = ImageReference
@ -101,7 +103,7 @@ class DefaultRunningService implements RunningService, OriginProvider {
}
@Override
public DockerComposeFile composeFile() {
public @Nullable DockerComposeFile composeFile() {
return this.composeFile;
}

View File

@ -27,6 +27,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
import org.springframework.boot.docker.compose.core.DockerCliCommand.Type;
@ -60,7 +61,7 @@ class DockerCli {
* @param workingDirectory the working directory or {@code null}
* @param dockerComposeOptions the Docker Compose options to use or {@code null}.
*/
DockerCli(File workingDirectory, DockerComposeOptions dockerComposeOptions) {
DockerCli(@Nullable File workingDirectory, @Nullable DockerComposeOptions dockerComposeOptions) {
this.processRunner = new ProcessRunner(workingDirectory);
this.dockerCommands = dockerCommandsCache.computeIfAbsent(workingDirectory,
(key) -> new DockerCommands(this.processRunner));
@ -82,7 +83,7 @@ class DockerCli {
return dockerCommand.deserialize(json);
}
private Consumer<String> createOutputConsumer(LogLevel logLevel) {
private @Nullable Consumer<String> createOutputConsumer(@Nullable LogLevel logLevel) {
if (logLevel == null || logLevel == LogLevel.OFF) {
return null;
}
@ -123,7 +124,7 @@ class DockerCli {
* Return the {@link DockerComposeFile} being used by this CLI instance.
* @return the Docker Compose file
*/
DockerComposeFile getDockerComposeFile() {
@Nullable DockerComposeFile getDockerComposeFile() {
return this.dockerComposeOptions.composeFile();
}
@ -205,11 +206,14 @@ class DockerCli {
* @param activeProfiles the profiles to activate
* @param arguments the arguments to pass to Docker Compose
*/
record DockerComposeOptions(DockerComposeFile composeFile, Set<String> activeProfiles, List<String> arguments) {
record DockerComposeOptions(@Nullable DockerComposeFile composeFile, Set<String> activeProfiles,
List<String> arguments) {
DockerComposeOptions {
activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
arguments = (arguments != null) ? arguments : Collections.emptyList();
DockerComposeOptions(@Nullable DockerComposeFile composeFile, @Nullable Set<String> activeProfiles,
@Nullable List<String> arguments) {
this.composeFile = composeFile;
this.activeProfiles = (activeProfiles != null) ? activeProfiles : Collections.emptySet();
this.arguments = (arguments != null) ? arguments : Collections.emptyList();
}
static DockerComposeOptions none() {

View File

@ -78,8 +78,8 @@ abstract sealed class DockerCliCommand<R> {
@SuppressWarnings("unchecked")
R deserialize(String json) {
if (this.responseType == Void.class) {
return null;
if (this.responseType == None.class) {
return (R) None.INSTANCE;
}
return (R) ((!this.listResponse) ? DockerJson.deserialize(json, this.responseType)
: DockerJson.deserializeToList(json, this.responseType));
@ -175,10 +175,10 @@ abstract sealed class DockerCliCommand<R> {
/**
* The {@code docker compose up} command.
*/
static final class ComposeUp extends DockerCliCommand<Void> {
static final class ComposeUp extends DockerCliCommand<None> {
ComposeUp(LogLevel logLevel, List<String> arguments) {
super(Type.DOCKER_COMPOSE, logLevel, Void.class, false, getCommand(arguments));
super(Type.DOCKER_COMPOSE, logLevel, None.class, false, getCommand(arguments));
}
private static String[] getCommand(List<String> arguments) {
@ -196,10 +196,10 @@ abstract sealed class DockerCliCommand<R> {
/**
* The {@code docker compose down} command.
*/
static final class ComposeDown extends DockerCliCommand<Void> {
static final class ComposeDown extends DockerCliCommand<None> {
ComposeDown(Duration timeout, List<String> arguments) {
super(Type.DOCKER_COMPOSE, Void.class, false, getCommand(timeout, arguments));
super(Type.DOCKER_COMPOSE, None.class, false, getCommand(timeout, arguments));
}
private static String[] getCommand(Duration timeout, List<String> arguments) {
@ -216,10 +216,10 @@ abstract sealed class DockerCliCommand<R> {
/**
* The {@code docker compose start} command.
*/
static final class ComposeStart extends DockerCliCommand<Void> {
static final class ComposeStart extends DockerCliCommand<None> {
ComposeStart(LogLevel logLevel, List<String> arguments) {
super(Type.DOCKER_COMPOSE, logLevel, Void.class, false, getCommand(arguments));
super(Type.DOCKER_COMPOSE, logLevel, None.class, false, getCommand(arguments));
}
private static String[] getCommand(List<String> arguments) {
@ -234,10 +234,10 @@ abstract sealed class DockerCliCommand<R> {
/**
* The {@code docker compose stop} command.
*/
static final class ComposeStop extends DockerCliCommand<Void> {
static final class ComposeStop extends DockerCliCommand<None> {
ComposeStop(Duration timeout, List<String> arguments) {
super(Type.DOCKER_COMPOSE, Void.class, false, getCommand(timeout, arguments));
super(Type.DOCKER_COMPOSE, None.class, false, getCommand(timeout, arguments));
}
private static String[] getCommand(Duration timeout, List<String> arguments) {
@ -268,6 +268,15 @@ abstract sealed class DockerCliCommand<R> {
}
static final class None {
public static final None INSTANCE = new None();
private None() {
}
}
/**
* Docker compose version.
*

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
/**
* Response from {@link DockerCliCommand.ComposePs docker compose ps}.
*
@ -27,6 +29,6 @@ package org.springframework.boot.docker.compose.core;
* @author Andy Wilkinson
* @author Phillip Webb
*/
record DockerCliComposePsResponse(String id, String name, String image, String state) {
record DockerCliComposePsResponse(String id, String name, @Nullable String image, String state) {
}

View File

@ -19,6 +19,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
/**
* Response from {@link DockerCliCommand.Inspect docker inspect}.
*
@ -31,7 +33,8 @@ import java.util.Map;
* @author Phillip Webb
*/
record DockerCliInspectResponse(String id, DockerCliInspectResponse.Config config,
DockerCliInspectResponse.NetworkSettings networkSettings, DockerCliInspectResponse.HostConfig hostConfig) {
DockerCliInspectResponse.NetworkSettings networkSettings,
DockerCliInspectResponse.@Nullable HostConfig hostConfig) {
/**
* Configuration for the container that is portable between hosts.

View File

@ -21,6 +21,8 @@ import java.util.Collections;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.DockerCli.DockerComposeOptions;
import org.springframework.boot.logging.LogLevel;
@ -126,7 +128,7 @@ public interface DockerCompose {
* @param activeProfiles a set of the profiles that should be activated
* @return a {@link DockerCompose} instance
*/
static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles) {
static DockerCompose get(DockerComposeFile file, @Nullable String hostname, Set<String> activeProfiles) {
return get(file, hostname, activeProfiles, Collections.emptyList());
}
@ -140,7 +142,7 @@ public interface DockerCompose {
* @return a {@link DockerCompose} instance
* @since 3.4.0
*/
static DockerCompose get(DockerComposeFile file, String hostname, Set<String> activeProfiles,
static DockerCompose get(DockerComposeFile file, @Nullable String hostname, Set<String> activeProfiles,
List<String> arguments) {
DockerCli cli = new DockerCli(null, new DockerComposeOptions(file, activeProfiles, arguments));
return new DefaultDockerCompose(cli, hostname);

View File

@ -26,6 +26,8 @@ import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -107,7 +109,7 @@ public final class DockerComposeFile {
* current directory
* @return the located file or {@code null} if no Docker Compose file can be found
*/
public static DockerComposeFile find(File workingDirectory) {
public static @Nullable DockerComposeFile find(@Nullable File workingDirectory) {
File base = (workingDirectory != null) ? workingDirectory : new File(".");
if (!base.exists()) {
return null;

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.origin.Origin;
/**
@ -27,7 +29,7 @@ import org.springframework.boot.origin.Origin;
* @author Andy Wilkinson
* @since 3.1.0
*/
public record DockerComposeOrigin(DockerComposeFile composeFile, String serviceName) implements Origin {
public record DockerComposeOrigin(@Nullable DockerComposeFile composeFile, String serviceName) implements Origin {
@Override
public String toString() {

View File

@ -21,6 +21,8 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.jspecify.annotations.Nullable;
import org.springframework.util.CollectionUtils;
/**
@ -69,7 +71,7 @@ class DockerEnv {
return this.map;
}
private record Entry(String key, String value) {
private record Entry(String key, @Nullable String value) {
}

View File

@ -21,6 +21,8 @@ import java.util.List;
import java.util.function.Function;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.springframework.util.StringUtils;
/**
@ -69,7 +71,7 @@ final class DockerHost {
* {@link DockerCliContextResponse}
* @return a new docker host instance
*/
static DockerHost get(String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) {
static DockerHost get(@Nullable String host, Supplier<List<DockerCliContextResponse>> contextsSupplier) {
return get(host, System::getenv, contextsSupplier);
}
@ -81,7 +83,7 @@ final class DockerHost {
* {@link DockerCliContextResponse}
* @return a new docker host instance
*/
static DockerHost get(String host, Function<String, String> systemEnv,
static DockerHost get(@Nullable String host, Function<String, String> systemEnv,
Supplier<List<DockerCliContextResponse>> contextsSupplier) {
host = (StringUtils.hasText(host)) ? host : fromServicesHostEnv(systemEnv);
host = (StringUtils.hasText(host)) ? host : fromDockerHostEnv(systemEnv);
@ -94,24 +96,24 @@ final class DockerHost {
return systemEnv.apply("SERVICES_HOST");
}
private static String fromDockerHostEnv(Function<String, String> systemEnv) {
private static @Nullable String fromDockerHostEnv(Function<String, String> systemEnv) {
return fromEndpoint(systemEnv.apply("DOCKER_HOST"));
}
private static String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) {
private static @Nullable String fromCurrentContext(Supplier<List<DockerCliContextResponse>> contextsSupplier) {
DockerCliContextResponse current = getCurrentContext(contextsSupplier.get());
return (current != null) ? fromEndpoint(current.dockerEndpoint()) : null;
}
private static DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) {
private static @Nullable DockerCliContextResponse getCurrentContext(List<DockerCliContextResponse> candidates) {
return candidates.stream().filter(DockerCliContextResponse::current).findFirst().orElse(null);
}
private static String fromEndpoint(String endpoint) {
private static @Nullable String fromEndpoint(@Nullable String endpoint) {
return (StringUtils.hasLength(endpoint)) ? fromUri(URI.create(endpoint)) : null;
}
private static String fromUri(URI uri) {
private static @Nullable String fromUri(URI uri) {
try {
return switch (uri.getScheme()) {
case "http", "https", "tcp" -> uri.getHost();

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
@ -38,7 +40,7 @@ class ImageName {
private final String string;
ImageName(String domain, String path) {
ImageName(@Nullable String domain, String path) {
Assert.hasText(path, "'path' must not be empty");
this.domain = getDomainOrDefault(domain);
this.name = getNameWithDefaultPath(this.domain, path);
@ -90,7 +92,7 @@ class ImageName {
return this.string;
}
private String getDomainOrDefault(String domain) {
private String getDomainOrDefault(@Nullable String domain) {
if (domain == null || LEGACY_DOMAIN.equals(domain)) {
return DEFAULT_DOMAIN;
}
@ -104,7 +106,7 @@ class ImageName {
return name;
}
static String parseDomain(String value) {
static @Nullable String parseDomain(String value) {
int firstSlash = value.indexOf('/');
String candidate = (firstSlash != -1) ? value.substring(0, firstSlash) : null;
if (candidate != null && Regex.DOMAIN.matcher(candidate).matches()) {

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.regex.Matcher;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ObjectUtils;
@ -32,13 +34,13 @@ public final class ImageReference {
private final ImageName name;
private final String tag;
private final @Nullable String tag;
private final String digest;
private final @Nullable String digest;
private final String string;
private ImageReference(ImageName name, String tag, String digest) {
private ImageReference(ImageName name, @Nullable String tag, @Nullable String digest) {
Assert.notNull(name, "'name' must not be null");
this.name = name;
this.tag = tag;
@ -68,7 +70,7 @@ public final class ImageReference {
* Return the tag from the reference or {@code null}.
* @return the referenced tag
*/
public String getTag() {
public @Nullable String getTag() {
return this.tag;
}
@ -76,7 +78,7 @@ public final class ImageReference {
* Return the digest from the reference or {@code null}.
* @return the referenced digest
*/
public String getDigest() {
public @Nullable String getDigest() {
return this.digest;
}
@ -111,7 +113,7 @@ public final class ImageReference {
return this.string;
}
private String buildString(String name, String tag, String digest) {
private String buildString(String name, @Nullable String tag, @Nullable String digest) {
StringBuilder string = new StringBuilder(name);
if (tag != null) {
string.append(":").append(tag);

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.Nullable;
/**
* Exception thrown by {@link ProcessRunner} when the process exits with a non-zero code.
*
@ -37,7 +39,7 @@ class ProcessExitException extends RuntimeException {
this(exitCode, command, stdOut, stdErr, null);
}
ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, Throwable cause) {
ProcessExitException(int exitCode, String[] command, String stdOut, String stdErr, @Nullable Throwable cause) {
super(buildMessage(exitCode, command, stdOut, stdErr), cause);
this.exitCode = exitCode;
this.command = command;

View File

@ -29,6 +29,7 @@ import java.util.function.Consumer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogMessage;
@ -47,7 +48,7 @@ class ProcessRunner {
private static final Log logger = LogFactory.getLog(ProcessRunner.class);
private final File workingDirectory;
private final @Nullable File workingDirectory;
/**
* Create a new {@link ProcessRunner} instance.
@ -60,7 +61,7 @@ class ProcessRunner {
* Create a new {@link ProcessRunner} instance.
* @param workingDirectory the working directory for the process
*/
ProcessRunner(File workingDirectory) {
ProcessRunner(@Nullable File workingDirectory) {
this.workingDirectory = workingDirectory;
}
@ -83,7 +84,7 @@ class ProcessRunner {
* @return the output of the command
* @throws ProcessExitException if execution failed
*/
String run(Consumer<String> outputConsumer, String... command) {
String run(@Nullable Consumer<String> outputConsumer, String... command) {
logger.trace(LogMessage.of(() -> "Running '%s'".formatted(String.join(" ", command))));
Process process = startProcess(command);
ReaderThread stdOutReader = new ReaderThread(process.getInputStream(), "stdout", outputConsumer);
@ -133,13 +134,13 @@ class ProcessRunner {
private final InputStream source;
private final Consumer<String> outputConsumer;
private final @Nullable Consumer<String> outputConsumer;
private final StringBuilder output = new StringBuilder();
private final CountDownLatch latch = new CountDownLatch(1);
ReaderThread(InputStream source, String name, Consumer<String> outputConsumer) {
ReaderThread(InputStream source, String name, @Nullable Consumer<String> outputConsumer) {
this.source = source;
this.outputConsumer = outputConsumer;
setName("OutputReader-" + name);
@ -174,7 +175,7 @@ class ProcessRunner {
return this.output.toString();
}
catch (InterruptedException ex) {
return null;
return "";
}
}

View File

@ -18,6 +18,8 @@ package org.springframework.boot.docker.compose.core;
import java.util.Map;
import org.jspecify.annotations.Nullable;
/**
* Provides details of a running Docker Compose service.
*
@ -69,7 +71,7 @@ public interface RunningService {
* @return the Docker Compose file
* @since 3.5.0
*/
default DockerComposeFile composeFile() {
default @Nullable DockerComposeFile composeFile() {
return null;
}

View File

@ -17,4 +17,7 @@
/**
* Core interfaces and classes for working with Docker Compose.
*/
@NullMarked
package org.springframework.boot.docker.compose.core;
import org.jspecify.annotations.NullMarked;

View File

@ -23,6 +23,7 @@ import java.util.Set;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jspecify.annotations.Nullable;
import org.springframework.aot.AotDetector;
import org.springframework.boot.SpringApplicationShutdownHandlers;
@ -57,11 +58,11 @@ class DockerComposeLifecycleManager {
private static final String IGNORE_LABEL = "org.springframework.boot.ignore";
private final File workingDirectory;
private final @Nullable File workingDirectory;
private final ApplicationContext applicationContext;
private final ClassLoader classLoader;
private final @Nullable ClassLoader classLoader;
private final SpringApplicationShutdownHandlers shutdownHandlers;
@ -80,10 +81,10 @@ class DockerComposeLifecycleManager {
new DockerComposeSkipCheck(), null);
}
DockerComposeLifecycleManager(File workingDirectory, ApplicationContext applicationContext, Binder binder,
DockerComposeLifecycleManager(@Nullable File workingDirectory, ApplicationContext applicationContext, Binder binder,
SpringApplicationShutdownHandlers shutdownHandlers, DockerComposeProperties properties,
Set<ApplicationListener<?>> eventListeners, DockerComposeSkipCheck skipCheck,
ServiceReadinessChecks serviceReadinessChecks) {
@Nullable ServiceReadinessChecks serviceReadinessChecks) {
this.workingDirectory = workingDirectory;
this.applicationContext = applicationContext;
this.classLoader = applicationContext.getClassLoader();

View File

@ -23,6 +23,8 @@ import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.Binder;
import org.springframework.boot.docker.compose.core.RunningService;
@ -64,7 +66,7 @@ public class DockerComposeProperties {
/**
* Hostname or IP of the machine where the docker containers are started.
*/
private String host;
private @Nullable String host;
/**
* Start configuration.
@ -109,11 +111,11 @@ public class DockerComposeProperties {
this.lifecycleManagement = lifecycleManagement;
}
public String getHost() {
public @Nullable String getHost() {
return this.host;
}
public void setHost(String host) {
public void setHost(@Nullable String host) {
this.host = host;
}

View File

@ -20,6 +20,8 @@ import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.SpringApplicationAotProcessor;
import org.springframework.util.ClassUtils;
@ -44,7 +46,7 @@ class DockerComposeSkipCheck {
SKIPPED_STACK_ELEMENTS = Collections.unmodifiableSet(skipped);
}
boolean shouldSkip(ClassLoader classLoader, DockerComposeProperties.Skip properties) {
boolean shouldSkip(@Nullable ClassLoader classLoader, DockerComposeProperties.Skip properties) {
if (properties.isInTests() && hasAtLeastOneRequiredClass(classLoader)) {
Thread thread = Thread.currentThread();
for (StackTraceElement element : thread.getStackTrace()) {
@ -56,7 +58,7 @@ class DockerComposeSkipCheck {
return false;
}
private boolean hasAtLeastOneRequiredClass(ClassLoader classLoader) {
private boolean hasAtLeastOneRequiredClass(@Nullable ClassLoader classLoader) {
for (String requiredClass : REQUIRED_CLASSES) {
if (ClassUtils.isPresent(requiredClass, classLoader)) {
return true;

View File

@ -16,6 +16,8 @@
package org.springframework.boot.docker.compose.lifecycle;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.docker.compose.core.RunningService;
/**
@ -33,7 +35,7 @@ class ServiceNotReadyException extends RuntimeException {
this(service, message, null);
}
ServiceNotReadyException(RunningService service, String message, Throwable cause) {
ServiceNotReadyException(RunningService service, String message, @Nullable Throwable cause) {
super(message, cause);
this.service = service;
}

View File

@ -17,4 +17,7 @@
/**
* Lifecycle management for Docker Compose with the context of a Spring application.
*/
@NullMarked
package org.springframework.boot.docker.compose.lifecycle;
import org.jspecify.annotations.NullMarked;

View File

@ -21,6 +21,8 @@ import java.util.Arrays;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactory;
import org.springframework.boot.docker.compose.core.DockerComposeFile;
@ -92,7 +94,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
}
@Override
public final D getConnectionDetails(DockerComposeConnectionSource source) {
public final @Nullable D getConnectionDetails(DockerComposeConnectionSource source) {
return (!accept(source)) ? null : getDockerComposeConnectionDetails(source);
}
@ -112,7 +114,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
* @param source the source
* @return the service connection or {@code null}.
*/
protected abstract D getDockerComposeConnectionDetails(DockerComposeConnectionSource source);
protected abstract @Nullable D getDockerComposeConnectionDetails(DockerComposeConnectionSource source);
/**
* Convenient base class for {@link ConnectionDetails} results that are backed by a
@ -120,9 +122,9 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
*/
protected static class DockerComposeConnectionDetails implements ConnectionDetails, OriginProvider {
private final Origin origin;
private final @Nullable Origin origin;
private volatile SslBundle sslBundle;
private volatile @Nullable SslBundle sslBundle;
/**
* Create a new {@link DockerComposeConnectionDetails} instance.
@ -134,11 +136,11 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
}
@Override
public Origin getOrigin() {
public @Nullable Origin getOrigin() {
return this.origin;
}
protected SslBundle getSslBundle(RunningService service) {
protected @Nullable SslBundle getSslBundle(RunningService service) {
if (this.sslBundle != null) {
return this.sslBundle;
}
@ -155,7 +157,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return sslBundle;
}
private SslBundle getJksSslBundle(RunningService service) {
private @Nullable SslBundle getJksSslBundle(RunningService service) {
JksSslStoreDetails keyStoreDetails = getJksSslStoreDetails(service, "keystore");
JksSslStoreDetails trustStoreDetails = getJksSslStoreDetails(service, "truststore");
if (keyStoreDetails == null && trustStoreDetails == null) {
@ -173,13 +175,13 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
options, protocol);
}
private ResourceLoader getResourceLoader(Path workingDirectory) {
private ResourceLoader getResourceLoader(@Nullable Path workingDirectory) {
ClassLoader classLoader = ApplicationResourceLoader.get().getClassLoader();
return ApplicationResourceLoader.get(classLoader,
SpringFactoriesLoader.forDefaultResourceLocation(classLoader), workingDirectory);
}
private JksSslStoreDetails getJksSslStoreDetails(RunningService service, String storeType) {
private @Nullable JksSslStoreDetails getJksSslStoreDetails(RunningService service, String storeType) {
String type = service.labels().get("org.springframework.boot.sslbundle.jks.%s.type".formatted(storeType));
String provider = service.labels()
.get("org.springframework.boot.sslbundle.jks.%s.provider".formatted(storeType));
@ -193,7 +195,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return new JksSslStoreDetails(type, provider, location, password);
}
private Path getWorkingDirectory(RunningService runningService) {
private @Nullable Path getWorkingDirectory(RunningService runningService) {
DockerComposeFile composeFile = runningService.composeFile();
if (composeFile == null || CollectionUtils.isEmpty(composeFile.getFiles())) {
return Path.of(".");
@ -201,7 +203,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return composeFile.getFiles().get(0).toPath().getParent();
}
private SslOptions createSslOptions(String ciphers, String enabledProtocols) {
private SslOptions createSslOptions(@Nullable String ciphers, @Nullable String enabledProtocols) {
Set<String> ciphersSet = null;
if (StringUtils.hasLength(ciphers)) {
ciphersSet = StringUtils.commaDelimitedListToSet(ciphers);
@ -213,7 +215,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
return SslOptions.of(ciphersSet, enabledProtocolsSet);
}
private SslBundle getPemSslBundle(RunningService service) {
private @Nullable SslBundle getPemSslBundle(RunningService service) {
PemSslStoreDetails keyStoreDetails = getPemSslStoreDetails(service, "keystore");
PemSslStoreDetails trustStoreDetails = getPemSslStoreDetails(service, "truststore");
if (keyStoreDetails == null && trustStoreDetails == null) {
@ -231,7 +233,7 @@ public abstract class DockerComposeConnectionDetailsFactory<D extends Connection
PemSslStore.load(trustStoreDetails, resourceLoader)), key, options, protocol);
}
private PemSslStoreDetails getPemSslStoreDetails(RunningService service, String storeType) {
private @Nullable PemSslStoreDetails getPemSslStoreDetails(RunningService service, String storeType) {
String type = service.labels().get("org.springframework.boot.sslbundle.pem.%s.type".formatted(storeType));
String certificate = service.labels()
.get("org.springframework.boot.sslbundle.pem.%s.certificate".formatted(storeType));

View File

@ -17,4 +17,7 @@
/**
* Service connection support for Docker Compose.
*/
@NullMarked
package org.springframework.boot.docker.compose.service.connection;
import org.jspecify.annotations.NullMarked;

View File

@ -22,6 +22,7 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.boot.docker.compose.core.DockerCliCommand.ComposeVersion;
import org.springframework.boot.docker.compose.core.DockerCliCommand.None;
import org.springframework.boot.logging.LogLevel;
import static org.assertj.core.api.Assertions.assertThat;
@ -85,7 +86,7 @@ class DockerCliCommandTests {
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("up", "--no-color", "--detach", "--wait",
"--renew-anon-volumes");
assertThat(command.deserialize("[]")).isNull();
assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
}
@Test
@ -94,7 +95,7 @@ class DockerCliCommandTests {
List.of("--remove-orphans"));
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("down", "--timeout", "1", "--remove-orphans");
assertThat(command.deserialize("[]")).isNull();
assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
}
@Test
@ -103,7 +104,7 @@ class DockerCliCommandTests {
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getLogLevel()).isEqualTo(LogLevel.INFO);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("start", "--dry-run");
assertThat(command.deserialize("[]")).isNull();
assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
}
@Test
@ -111,7 +112,7 @@ class DockerCliCommandTests {
DockerCliCommand<?> command = new DockerCliCommand.ComposeStop(Duration.ofSeconds(1), List.of("--dry-run"));
assertThat(command.getType()).isEqualTo(DockerCliCommand.Type.DOCKER_COMPOSE);
assertThat(command.getCommand(COMPOSE_VERSION)).containsExactly("stop", "--timeout", "1", "--dry-run");
assertThat(command.deserialize("[]")).isNull();
assertThat(command.deserialize("[]")).isSameAs(None.INSTANCE);
}
@Test