Fixe SonarQube Reliability-Issues S2789, S3077 und S2184

S2789 (32 Stellen): null-Checks auf Optional-Feldern entfernt bzw. durch
Objects.requireNonNullElse(field, Optional.empty()) ersetzt. Die zuvor
defensive Behandlung von null-Optionals erfolgt jetzt ueber den
Bibliotheksaufruf, sodass das Verhalten unveraendert bleibt, aber die
direkte Null-Pruefung gegen Optional entfaellt.

S3077 (5 Stellen): volatile-Felder mit Objekt-Referenzen durch
AtomicReference ersetzt (ScheduledExecutorServiceSchedulerAdapter,
BootstrapRunner.guiApplicationRunContext, PdfPreviewPane.currentDocument/
currentRenderer/currentSourceFile, SingleInstanceGuard.socket). Die
PdfPreviewPane-Felder werden auf JavaFX- bzw. Worker-Thread genutzt;
AtomicReference bietet hier konsistente atomare Publikation ohne
Verhaltensaenderung.

S2184 (3 Stellen): Integer-Division SECONDARY_SPACING / 2 durch
SECONDARY_SPACING / 2.0 ersetzt, damit das Insets-Argument als double
ohne implizite Truncierung berechnet wird.
This commit is contained in:
2026-05-07 17:11:29 +02:00
parent 11eac074ef
commit 32e32a9b27
18 changed files with 76 additions and 80 deletions
@@ -4,6 +4,7 @@ import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@@ -105,7 +106,7 @@ public final class GuiSchedulerTab {
public GuiSchedulerTab(
Optional<SchedulerControlUseCase> schedulerUseCase,
Supplier<Boolean> isConfigDirty) {
this.schedulerUseCase = schedulerUseCase == null ? Optional.empty() : schedulerUseCase;
this.schedulerUseCase = Objects.requireNonNullElse(schedulerUseCase, Optional.empty());
this.isConfigDirty = isConfigDirty != null ? isConfigDirty : () -> false;
tab.setClosable(false);
buildUi();
@@ -146,8 +146,8 @@ public record GuiStartupContext(
*/
public GuiStartupContext {
initialState = Objects.requireNonNull(initialState, "initialState must not be null");
startupNotice = startupNotice == null ? Optional.empty() : startupNotice;
applicationContextError = applicationContextError == null ? Optional.empty() : applicationContextError;
startupNotice = Objects.requireNonNullElse(startupNotice, Optional.empty());
applicationContextError = Objects.requireNonNullElse(applicationContextError, Optional.empty());
configurationFileLoader = Objects.requireNonNull(configurationFileLoader,
"configurationFileLoader must not be null");
configurationFileWriter = Objects.requireNonNull(configurationFileWriter,
@@ -191,8 +191,8 @@ public record GuiStartupContext(
"promptEditorPortFactory must not be null");
createNewDatabasePort = Objects.requireNonNull(createNewDatabasePort,
"createNewDatabasePort must not be null");
schedulerControlUseCase = schedulerControlUseCase == null ? Optional.empty() : schedulerControlUseCase;
configurationFileLockPort = configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
schedulerControlUseCase = Objects.requireNonNullElse(schedulerControlUseCase, Optional.empty());
configurationFileLockPort = Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
applicationContextInitializer = applicationContextInitializer == null
? GuiApplicationContextInitializer.noOp() : applicationContextInitializer;
}
@@ -276,7 +276,7 @@ public final class GuiBatchRunCoordinator {
this.historicalDocumentContextPort = Objects.requireNonNull(
historicalDocumentContextPort, "historicalDocumentContextPort must not be null");
this.configurationFileLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
}
/**
@@ -33,7 +33,7 @@ public record GuiBatchRunLaunchOutcome(
* Compact constructor normalising the failure message holder.
*/
public GuiBatchRunLaunchOutcome {
failureMessage = failureMessage == null ? Optional.empty() : failureMessage;
failureMessage = Objects.requireNonNullElse(failureMessage, Optional.empty());
}
/**
@@ -88,16 +88,16 @@ public record GuiBatchRunResultRow(
}
Objects.requireNonNull(fingerprint, "fingerprint must not be null");
Objects.requireNonNull(status, "status must not be null");
finalFileName = finalFileName == null ? Optional.empty() : finalFileName;
correctedFileName = correctedFileName == null ? Optional.empty() : correctedFileName;
resolvedDate = resolvedDate == null ? Optional.empty() : resolvedDate;
aiReasoning = aiReasoning == null ? Optional.empty() : aiReasoning;
aiFailureMessage = aiFailureMessage == null ? Optional.empty() : aiFailureMessage;
finalFileName = Objects.requireNonNullElse(finalFileName, Optional.empty());
correctedFileName = Objects.requireNonNullElse(correctedFileName, Optional.empty());
resolvedDate = Objects.requireNonNullElse(resolvedDate, Optional.empty());
aiReasoning = Objects.requireNonNullElse(aiReasoning, Optional.empty());
aiFailureMessage = Objects.requireNonNullElse(aiFailureMessage, Optional.empty());
Objects.requireNonNull(processingDuration, "processingDuration must not be null");
if (processingDuration.isNegative()) {
throw new IllegalArgumentException("processingDuration must not be negative");
}
historicalContext = historicalContext == null ? Optional.empty() : historicalContext;
historicalContext = Objects.requireNonNullElse(historicalContext, Optional.empty());
}
/**
@@ -302,7 +302,7 @@ public final class GuiBatchRunTab {
targetFolderSupplier, "targetFolderSupplier must not be null");
Optional<ConfigurationFileLockPort> effectiveLockPort =
configurationFileLockPort == null ? Optional.empty() : configurationFileLockPort;
Objects.requireNonNullElse(configurationFileLockPort, Optional.empty());
this.coordinator = new GuiBatchRunCoordinator(
(configPath, observer, token) ->
launcherSupplier.get().launch(configPath, observer, token),
@@ -618,7 +618,7 @@ public final class GuiBatchRunTab {
HBox selectionButtonBar = new HBox(SECONDARY_SPACING, reprocessButton, resetStatusButton);
selectionButtonBar.setAlignment(Pos.CENTER_LEFT);
selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, SECONDARY_SPACING / 2, 0));
selectionButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2.0, 0, SECONDARY_SPACING / 2.0, 0));
// Meldungsbereich unterhalb der Selektions-Buttons (linke Spalte)
messageArea.setId("batch-run-message-area");
@@ -1246,7 +1246,7 @@ public final class GuiBatchRunTab {
HBox runButtonBar = new HBox(SECONDARY_SPACING, startButton, cancelButton);
runButtonBar.setAlignment(Pos.CENTER_LEFT);
runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2, 0, 0, 0));
runButtonBar.setPadding(new Insets(SECONDARY_SPACING / 2.0, 0, 0, 0));
return runButtonBar;
}
@@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -170,18 +171,18 @@ public final class PdfPreviewPane {
/**
* Aktuell geöffnetes PDF-Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist.
* Leerer Referenzwert wenn kein Dokument geöffnet ist.
*/
private volatile PDDocument currentDocument = null;
private final AtomicReference<PDDocument> currentDocument = new AtomicReference<>();
/**
* Renderer für das aktuell geöffnete Dokument. Zugriff ausschließlich vom Worker-Thread.
* {@code null} wenn kein Dokument geöffnet ist.
* Leerer Referenzwert wenn kein Dokument geöffnet ist.
*/
private volatile PDFRenderer currentRenderer = null;
private final AtomicReference<PDFRenderer> currentRenderer = new AtomicReference<>();
/** Aktuell geladene Quelldatei; null wenn keine Selektion vorliegt. */
private volatile Path currentSourceFile = null;
/** Aktuell geladene Quelldatei; leerer Referenzwert wenn keine Selektion vorliegt. */
private final AtomicReference<Path> currentSourceFile = new AtomicReference<>();
/** Aktuell angezeigte Seite (1-basiert; 0 wenn keine Datei geladen). */
private volatile int currentPage = 0;
@@ -303,7 +304,7 @@ public final class PdfPreviewPane {
clear();
return;
}
currentSourceFile = sourceFile;
currentSourceFile.set(sourceFile);
currentPage = 0;
totalPages = -1;
pageCache.clear();
@@ -318,7 +319,7 @@ public final class PdfPreviewPane {
* Muss auf dem JavaFX Application Thread aufgerufen werden.
*/
public void clear() {
currentSourceFile = null;
currentSourceFile.set(null);
currentPage = 0;
totalPages = -1;
pageCache.clear();
@@ -472,12 +473,13 @@ public final class PdfPreviewPane {
try {
PDDocument doc = Loader.loadPDF(ioFile);
currentDocument = doc;
currentRenderer = new PDFRenderer(doc);
currentDocument.set(doc);
PDFRenderer renderer = new PDFRenderer(doc);
currentRenderer.set(renderer);
int pages = Math.max(1, doc.getNumberOfPages());
BufferedImage buffered =
currentRenderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB);
renderer.renderImageWithDPI(0, RENDER_DPI, ImageType.RGB);
Image fxImage = SwingFXUtils.toFXImage(buffered, null);
final int totalPagesFinal = pages;
@@ -513,7 +515,7 @@ public final class PdfPreviewPane {
* @param seq die Sequenznummer dieser Anforderung
*/
private void renderPageOnWorker(int page, long seq) {
PDFRenderer renderer = currentRenderer;
PDFRenderer renderer = currentRenderer.get();
if (renderer == null) {
// Dokument wurde zwischenzeitlich geschlossen nichts zu tun
return;
@@ -542,9 +544,8 @@ public final class PdfPreviewPane {
* auf dem Worker-Thread und ist idempotent.
*/
private void closeCurrentDocumentOnWorker() {
PDDocument doc = currentDocument;
currentDocument = null;
currentRenderer = null;
PDDocument doc = currentDocument.getAndSet(null);
currentRenderer.set(null);
if (doc != null) {
try {
doc.close();
@@ -824,7 +825,7 @@ public final class PdfPreviewPane {
}
private void updateNavigationButtons() {
boolean canNavigate = enabled && currentSourceFile != null && totalPages > 0;
boolean canNavigate = enabled && currentSourceFile.get() != null && totalPages > 0;
prevButton.setDisable(!canNavigate || currentPage <= 1);
nextButton.setDisable(!canNavigate || currentPage >= totalPages);
}
@@ -29,10 +29,10 @@ public record GuiConfigurationEditorState(
* @param values current editable configuration values; must not be {@code null}
*/
public GuiConfigurationEditorState {
loadedFileSnapshot = loadedFileSnapshot == null ? Optional.empty() : loadedFileSnapshot;
loadedFileSnapshot = Objects.requireNonNullElse(loadedFileSnapshot, Optional.empty());
baselineValues = Objects.requireNonNull(baselineValues, "baselineValues must not be null");
values = Objects.requireNonNull(values, "values must not be null");
pendingMigrationMessage = pendingMigrationMessage == null ? Optional.empty() : pendingMigrationMessage;
pendingMigrationMessage = Objects.requireNonNullElse(pendingMigrationMessage, Optional.empty());
}
/**
@@ -39,7 +39,7 @@ public record GuiMessageEntry(
Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(text, "text must not be null");
Objects.requireNonNull(timestamp, "timestamp must not be null");
source = source == null ? Optional.empty() : source;
source = Objects.requireNonNullElse(source, Optional.empty());
}
/**
@@ -54,7 +54,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
*/
final AtomicReference<BatchRunTrigger> currentTrigger = new AtomicReference<>();
private volatile ScheduledExecutorService executor;
private final AtomicReference<ScheduledExecutorService> executor = new AtomicReference<>();
/**
* Erstellt einen neuen Adapter.
@@ -85,7 +85,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
public void startScheduler(SchedulerConfig config, BatchRunTrigger trigger) {
Objects.requireNonNull(config, "config darf nicht null sein");
Objects.requireNonNull(trigger, "trigger darf nicht null sein");
if (executor != null) {
if (executor.get() != null) {
logger.debug("Scheduler ist bereits aktiv Start-Aufruf wird ignoriert.");
return;
}
@@ -105,7 +105,7 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
0L,
config.intervalSeconds(),
TimeUnit.SECONDS);
executor = newExecutor;
executor.set(newExecutor);
logger.info("Scheduler gestartet. Intervall: {} Sekunden.", config.intervalSeconds());
}
@@ -118,12 +118,11 @@ public class ScheduledExecutorServiceSchedulerAdapter implements SchedulerPort {
*/
@Override
public void stopScheduler() {
ScheduledExecutorService localExecutor = executor;
ScheduledExecutorService localExecutor = executor.getAndSet(null);
if (localExecutor == null) {
logger.debug("Scheduler ist bereits gestoppt Stop-Aufruf wird ignoriert.");
return;
}
executor = null;
currentTrigger.set(null);
localExecutor.shutdown();
logger.info("Scheduler angehalten.");
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.port.in;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
/**
@@ -37,9 +38,9 @@ public record HistoricalDocumentContext(
* {@code lastFailureInstant} {@code null} sind
*/
public HistoricalDocumentContext {
lastTargetFileName = lastTargetFileName == null ? Optional.empty() : lastTargetFileName;
lastSuccessInstant = lastSuccessInstant == null ? Optional.empty() : lastSuccessInstant;
lastFailureInstant = lastFailureInstant == null ? Optional.empty() : lastFailureInstant;
lastTargetFileName = Objects.requireNonNullElse(lastTargetFileName, Optional.empty());
lastSuccessInstant = Objects.requireNonNullElse(lastSuccessInstant, Optional.empty());
lastFailureInstant = Objects.requireNonNullElse(lastFailureInstant, Optional.empty());
}
/**
@@ -1,6 +1,7 @@
package de.gecheckt.pdf.umbenenner.application.port.in;
import java.time.Instant;
import java.util.Objects;
import java.util.Optional;
import de.gecheckt.pdf.umbenenner.application.port.out.RunSummary;
@@ -48,18 +49,10 @@ public record SchedulerStatus(
if (state == null) {
throw new IllegalArgumentException("state darf nicht null sein");
}
if (lastRunEndedAt == null) {
throw new IllegalArgumentException("lastRunEndedAt darf nicht null sein");
}
if (lastRunSummary == null) {
throw new IllegalArgumentException("lastRunSummary darf nicht null sein");
}
if (nextTickAt == null) {
throw new IllegalArgumentException("nextTickAt darf nicht null sein");
}
if (lastError == null) {
throw new IllegalArgumentException("lastError darf nicht null sein");
}
Objects.requireNonNull(lastRunEndedAt, "lastRunEndedAt darf nicht null sein");
Objects.requireNonNull(lastRunSummary, "lastRunSummary darf nicht null sein");
Objects.requireNonNull(nextTickAt, "nextTickAt darf nicht null sein");
Objects.requireNonNull(lastError, "lastError darf nicht null sein");
}
/**
@@ -32,7 +32,7 @@ public record EffectiveApiKeyDescriptor(
*/
public EffectiveApiKeyDescriptor {
Objects.requireNonNull(origin, "origin must not be null");
envVarName = envVarName == null ? Optional.empty() : envVarName;
envVarName = Objects.requireNonNullElse(envVarName, Optional.empty());
}
/**
@@ -45,7 +45,7 @@ public record ModelCatalogRequest(
if (timeoutSeconds <= 0) {
throw new IllegalArgumentException("timeoutSeconds must be positive, was: " + timeoutSeconds);
}
baseUrl = baseUrl == null ? Optional.empty() : baseUrl;
apiKey = apiKey == null ? Optional.empty() : apiKey;
baseUrl = Objects.requireNonNullElse(baseUrl, Optional.empty());
apiKey = Objects.requireNonNullElse(apiKey, Optional.empty());
}
}
@@ -38,7 +38,7 @@ public record EditorValidationFinding(
public EditorValidationFinding {
Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(message, "message must not be null");
fieldKey = fieldKey == null ? Optional.empty() : fieldKey;
fieldKey = Objects.requireNonNullElse(fieldKey, Optional.empty());
}
/**
@@ -89,9 +89,7 @@ public sealed interface CheckpointResult
Objects.requireNonNull(checkpointId, "checkpointId must not be null");
Objects.requireNonNull(severity, "severity must not be null");
Objects.requireNonNull(message, "message must not be null");
correctionSuggestion = correctionSuggestion == null
? Optional.empty()
: correctionSuggestion;
correctionSuggestion = Objects.requireNonNullElse(correctionSuggestion, Optional.empty());
}
/**
@@ -13,6 +13,7 @@ import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -260,10 +261,11 @@ public class BootstrapRunner {
* or via the {@link GuiApplicationContextInitializer} callback that the workspace invokes
* on a background thread after each successful file open. Read by
* {@link #launchGuiBatchRun}, {@link #launchGuiMiniBatchRun}, and
* {@link #resetDocumentStatusForGui}. {@code volatile} ensures visibility across threads
* without explicit synchronisation on the happy path.
* {@link #resetDocumentStatusForGui}. {@link AtomicReference} ensures atomic publication
* of the context across threads.
*/
private volatile Optional<ApplicationRunContext> guiApplicationRunContext = Optional.empty();
private final AtomicReference<Optional<ApplicationRunContext>> guiApplicationRunContext =
new AtomicReference<>(Optional.empty());
/**
* Der Scheduler-Use-Case, der beim GUI-Start mit einer gültigen Konfiguration
@@ -1083,7 +1085,7 @@ public class BootstrapRunner {
result -> LOG.debug("Scheduler-Tick-Ergebnis: {}",
result.getClass().getSimpleName()));
BatchRunTrigger batchRunTrigger = () -> {
Optional<ApplicationRunContext> ctxOpt = guiApplicationRunContext;
Optional<ApplicationRunContext> ctxOpt = guiApplicationRunContext.get();
if (ctxOpt.isEmpty()) {
return new BatchRunTriggerResult.Failed(
"Kein Anwendungskontext verfügbar.",
@@ -1248,28 +1250,28 @@ public class BootstrapRunner {
migrateConfigurationIfNeeded(configFilePath);
StartConfiguration config = loadAndValidateConfiguration(configFilePath);
initializeSchema(config);
guiApplicationRunContext = Optional.of(
new ApplicationRunContext(config, resolveActiveJdbcUrl(config)));
guiApplicationRunContext.set(Optional.of(
new ApplicationRunContext(config, resolveActiveJdbcUrl(config))));
LOG.info("GUI-Anwendungskontext initialisiert für Konfiguration: {}", configFilePath);
return Optional.empty();
} catch (ConfigurationLoadingException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration konnte nicht geladen werden: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
guiApplicationRunContext.set(Optional.empty());
return Optional.of(CONFIG_LOAD_FAILED_PREFIX + e.getMessage());
} catch (InvalidStartConfigurationException e) {
LOG.warn("GUI-Anwendungskontext: Konfiguration nicht lauffähig: {}", e.getMessage());
guiApplicationRunContext = Optional.empty();
guiApplicationRunContext.set(Optional.empty());
return Optional.of("Konfiguration nicht lauffähig: " + e.getMessage());
} catch (DocumentPersistenceException e) {
LOG.warn("GUI-Anwendungskontext: SQLite-Initialisierung fehlgeschlagen: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
guiApplicationRunContext.set(Optional.empty());
return Optional.of("SQLite konnte nicht initialisiert werden: " + e.getMessage());
} catch (RuntimeException e) {
LOG.warn("GUI-Anwendungskontext: Unerwarteter Fehler bei Initialisierung: {}",
e.getMessage());
guiApplicationRunContext = Optional.empty();
guiApplicationRunContext.set(Optional.empty());
return Optional.of("Unerwarteter Fehler bei der Kontextinitialisierung: "
+ (e.getMessage() == null ? e.getClass().getSimpleName() : e.getMessage()));
}
@@ -1422,7 +1424,7 @@ public class BootstrapRunner {
Objects.requireNonNull(cancellationToken, "cancellationToken must not be null");
LOG.info("GUI-Verarbeitungslauf: Startanforderung für Konfiguration {}.", configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) {
LOG.debug("GUI-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRun(ctx.get(), progressObserver, cancellationToken);
@@ -1512,7 +1514,7 @@ public class BootstrapRunner {
LOG.info("GUI-Mini-Verarbeitungslauf: Startanforderung für {} Dokument(e), Konfiguration {}.",
fingerprintFilter.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) {
LOG.debug("GUI-Mini-Verarbeitungslauf: Verwende vorbereiteten Anwendungskontext.");
return executeRunWithFilter(ctx.get(), fingerprintFilter, progressObserver, cancellationToken);
@@ -1595,7 +1597,7 @@ public class BootstrapRunner {
LOG.info("GUI-Status-Reset: {} Dokument(e) zurücksetzen, Konfiguration {}.",
fingerprints.size(), configFilePath);
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) {
LOG.debug("GUI-Status-Reset: Verwende vorbereiteten Anwendungskontext.");
return executeResetWithContext(ctx.get(), fingerprints);
@@ -2181,7 +2183,7 @@ public class BootstrapRunner {
* @return aktive JDBC-URL; nie {@code null}
*/
private String resolveJdbcUrlForGui(Path configFilePath) {
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) {
return ctx.get().jdbcUrl();
}
@@ -2204,7 +2206,7 @@ public class BootstrapRunner {
* @return validierte Konfiguration; nie {@code null}
*/
private StartConfiguration resolveStartConfigurationForGui(Path configFilePath) {
Optional<ApplicationRunContext> ctx = guiApplicationRunContext;
Optional<ApplicationRunContext> ctx = guiApplicationRunContext.get();
if (ctx.isPresent()) {
return ctx.get().startConfiguration();
}
@@ -5,6 +5,7 @@ import java.net.BindException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
/**
* Prozessweiter Einzelinstanz-Schutz auf Basis eines Loopback-ServerSocket-Binds.
@@ -44,7 +45,7 @@ public class SingleInstanceGuard implements AutoCloseable {
public static final int DEFAULT_PORT = 47832;
private final int port;
private volatile ServerSocket socket;
private final AtomicReference<ServerSocket> socket = new AtomicReference<>();
private final AtomicBoolean closed = new AtomicBoolean(false);
/**
@@ -85,7 +86,7 @@ public class SingleInstanceGuard implements AutoCloseable {
try {
InetAddress loopback = InetAddress.getLoopbackAddress();
ServerSocket serverSocket = new ServerSocket(port, 1, loopback);
this.socket = serverSocket;
this.socket.set(serverSocket);
Runtime.getRuntime().addShutdownHook(new Thread(() -> schliesseSilent(serverSocket),
"single-instance-guard-shutdown"));
} catch (BindException e) {
@@ -106,7 +107,7 @@ public class SingleInstanceGuard implements AutoCloseable {
@Override
public void close() {
if (closed.compareAndSet(false, true)) {
schliesseSilent(socket);
schliesseSilent(socket.get());
}
}