This is an automated email from the ASF dual-hosted git repository.
jbonofre pushed a commit to branch karaf-4.4.x
in repository https://gitbox.apache.org/repos/asf/karaf.git
The following commit(s) were added to refs/heads/karaf-4.4.x by this push:
new 033286e15e GH-2741: Clean the OSGi bundle cache on JDK version change
(#2899)
033286e15e is described below
commit 033286e15e0dfe45cb4e239f5459e45ed31ad0e5
Author: JB Onofré <[email protected]>
AuthorDate: Mon Sep 14 15:19:30 2026 +0200
GH-2741: Clean the OSGi bundle cache on JDK version change (#2899)
The bundle cache under data/cache can persist a bundle wiring that was
resolved against a different JDK. After a JDK major version
upgrade/downgrade, reusing that stale wiring can silently leave core
bundles (e.g. Configuration Admin, File Install) without expected
wires, so filesystem/config changes stop being detected with no error
reported.
Karaf now records the JDK specification version used on the previous
start in data/jdk_version. On startup, if the recorded version differs
from the current one, the bundle cache is cleaned before the framework
is created, forcing a full re-resolution. This can be disabled with
-Dkaraf.clean.cache.on.jdk.change=false.
---
.../org/apache/karaf/main/ConfigProperties.java | 10 ++
.../java/org/apache/karaf/main/util/Utils.java | 38 +++++++
.../java/org/apache/karaf/main/util/UtilsTest.java | 117 +++++++++++++++++++++
3 files changed, 165 insertions(+)
diff --git a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
index cb186d79c2..895c950356 100644
--- a/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
+++ b/main/src/main/java/org/apache/karaf/main/ConfigProperties.java
@@ -179,6 +179,12 @@ public class ConfigProperties {
*/
private static final String PROPERTY_USE_LOCK = "karaf.lock";
+ /**
+ * When true (default), the OSGi bundle cache is automatically cleaned if
a JDK version
+ * change is detected since the last start of this instance. See {@link
Utils#cleanCacheOnJdkChange}.
+ */
+ private static final String PROPERTY_CLEAN_CACHE_ON_JDK_CHANGE =
"karaf.clean.cache.on.jdk.change";
+
File karafHome;
File karafBase;
File karafData;
@@ -304,6 +310,10 @@ public class ConfigProperties {
props.setProperty(Constants.FRAMEWORK_STORAGE,
storage.getAbsolutePath());
}
+ if
(Boolean.parseBoolean(System.getProperty(PROPERTY_CLEAN_CACHE_ON_JDK_CHANGE,
"true"))) {
+ Utils.cleanCacheOnJdkChange(karafData, new
File(props.getProperty(Constants.FRAMEWORK_STORAGE)));
+ }
+
if (shutdownCommand == null || shutdownCommand.isEmpty()) {
try {
shutdownCommand = UUID.randomUUID().toString();
diff --git a/main/src/main/java/org/apache/karaf/main/util/Utils.java
b/main/src/main/java/org/apache/karaf/main/util/Utils.java
index f7003fe294..0e0dc94f26 100644
--- a/main/src/main/java/org/apache/karaf/main/util/Utils.java
+++ b/main/src/main/java/org/apache/karaf/main/util/Utils.java
@@ -24,10 +24,48 @@ import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URI;
import java.net.URL;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
import java.util.StringTokenizer;
public class Utils {
+ /**
+ * Name of the marker file, stored in the Karaf data directory, recording
the
+ * specification version of the JDK that was used for the last start of
the instance.
+ */
+ public static final String JDK_VERSION_MARKER_FILE = "jdk_version";
+
+ /**
+ * Detects a JDK (specification) version change since the last start of
this instance and,
+ * when detected, cleans the OSGi bundle cache before the framework is
created.
+ * <p>
+ * The bundle cache can persist a bundle wiring that was resolved against
a different JDK
+ * (different major version exposes a different set of packages to the
system bundle). Reusing
+ * such a stale wiring after a JDK upgrade/downgrade can silently leave
core bundles (e.g.
+ * Configuration Admin, File Install) without expected wires, without any
error being reported.
+ *
+ * @param karafData the Karaf data directory, where the marker file is
stored
+ * @param cacheDirectory the OSGi framework bundle cache directory
+ * @throws IOException if the marker file can't be read/written, or the
cache can't be cleaned
+ */
+ public static void cleanCacheOnJdkChange(File karafData, File
cacheDirectory) throws IOException {
+ String currentVersion =
System.getProperty("java.specification.version");
+ File marker = new File(karafData, JDK_VERSION_MARKER_FILE);
+ String previousVersion = null;
+ if (marker.exists()) {
+ previousVersion = new String(Files.readAllBytes(marker.toPath()),
StandardCharsets.UTF_8).trim();
+ }
+ if (previousVersion != null && !previousVersion.isEmpty() &&
!previousVersion.equals(currentVersion)) {
+ System.err.println("WARN: JDK version change detected since the
last start (" + previousVersion + " -> " + currentVersion
+ + "). Cleaning the OSGi bundle cache (" + cacheDirectory +
") to avoid reusing a bundle wiring resolved against a different JDK.");
+ deleteDirectory(cacheDirectory);
+ cacheDirectory.mkdirs();
+ }
+ karafData.mkdirs();
+ Files.write(marker.toPath(),
currentVersion.getBytes(StandardCharsets.UTF_8));
+ }
+
public static File getKarafHome(Class<?> mainClass, String
karafHomeProperty, String karafHomeEnv) throws IOException {
File rc = null;
diff --git a/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java
b/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java
new file mode 100644
index 0000000000..c459cec6d1
--- /dev/null
+++ b/main/src/test/java/org/apache/karaf/main/util/UtilsTest.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.karaf.main.util;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+public class UtilsTest {
+
+ @Rule
+ public final TemporaryFolder temporaryFolder = new TemporaryFolder();
+
+ @Test
+ public void firstStartCreatesMarkerAndKeepsCache() throws Exception {
+ File data = temporaryFolder.newFolder("data");
+ File cache = new File(data, "cache");
+ Files.createDirectories(cache.toPath());
+ Path bundleFile = cache.toPath().resolve("bundle0");
+ Files.write(bundleFile, new byte[0]);
+
+ String originalVersion =
System.getProperty("java.specification.version");
+ try {
+ System.setProperty("java.specification.version", "17");
+ Utils.cleanCacheOnJdkChange(data, cache);
+
+ assertTrue("cache content must be preserved on first start",
Files.exists(bundleFile));
+ File marker = new File(data, Utils.JDK_VERSION_MARKER_FILE);
+ assertTrue(marker.exists());
+ assertEquals("17", new String(Files.readAllBytes(marker.toPath()),
StandardCharsets.UTF_8).trim());
+ } finally {
+ restore(originalVersion);
+ }
+ }
+
+ @Test
+ public void sameJdkVersionKeepsCache() throws Exception {
+ File data = temporaryFolder.newFolder("data");
+ File cache = new File(data, "cache");
+ Files.createDirectories(cache.toPath());
+ Path bundleFile = cache.toPath().resolve("bundle0");
+ Files.write(bundleFile, new byte[0]);
+
+ String originalVersion =
System.getProperty("java.specification.version");
+ try {
+ System.setProperty("java.specification.version", "17");
+ Utils.cleanCacheOnJdkChange(data, cache);
+ // started again with the same JDK version
+ Utils.cleanCacheOnJdkChange(data, cache);
+
+ assertTrue("cache content must be preserved across restarts with
the same JDK", Files.exists(bundleFile));
+ } finally {
+ restore(originalVersion);
+ }
+ }
+
+ @Test
+ public void jdkVersionChangeCleansCache() throws Exception {
+ File data = temporaryFolder.newFolder("data");
+ File cache = new File(data, "cache");
+ Files.createDirectories(cache.toPath());
+ Path bundleFile = cache.toPath().resolve("bundle0");
+ Files.write(bundleFile, new byte[0]);
+
+ String originalVersion =
System.getProperty("java.specification.version");
+ try {
+ System.setProperty("java.specification.version", "11");
+ Utils.cleanCacheOnJdkChange(data, cache);
+ assertTrue(Files.exists(bundleFile));
+
+ // restart under a different major JDK version
+ System.setProperty("java.specification.version", "17");
+ Utils.cleanCacheOnJdkChange(data, cache);
+
+ assertFalse("stale cache from the previous JDK must be cleaned",
Files.exists(bundleFile));
+ assertTrue("cache directory must be recreated", cache.exists());
+ File marker = new File(data, Utils.JDK_VERSION_MARKER_FILE);
+ assertEquals("17", new String(Files.readAllBytes(marker.toPath()),
StandardCharsets.UTF_8).trim());
+ } finally {
+ restore(originalVersion);
+ }
+ }
+
+ private void restore(String originalVersion) {
+ if (originalVersion == null) {
+ System.clearProperty("java.specification.version");
+ } else {
+ System.setProperty("java.specification.version", originalVersion);
+ }
+ }
+
+}