From 65ccfee1002c1beaa136e6424a1930e0cd7b7f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christoph=20L=C3=A4ubrich?= Date: Tue, 18 Nov 2025 12:48:00 +0100 Subject: [PATCH] [POC] OSGi Core R9 - Java SPI Support A proof of concept on how to intercept SPI loads in apache felix without using bytecode weaving according to the new preliminary spec. --- .../felix/framework/BundleWiringImpl.java | 173 ++++++++++++++++++ 1 file changed, 173 insertions(+) diff --git a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java index 809bb358c9..139a0a6a99 100644 --- a/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java +++ b/framework/src/main/java/org/apache/felix/framework/BundleWiringImpl.java @@ -53,7 +53,10 @@ import org.osgi.resource.Wire; import org.osgi.service.resolver.ResolutionException; +import java.io.BufferedReader; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; import java.lang.reflect.Constructor; import java.net.URL; import java.security.AccessController; @@ -172,6 +175,12 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE private volatile ConcurrentHashMap m_accessorLookupCache; + // Core SPI support: Maps SPI implementation class names to their provider bundles + private final Map m_spiClassMap = new ConcurrentHashMap<>(); + + // Core SPI support: Prefix for Java SPI resources + private static final String JAVA_SPI_PREFIX = "META-INF/services/"; + BundleWiringImpl( Logger logger, Map configMap, StatefulResolver resolver, BundleRevisionImpl revision, List fragments, @@ -1232,6 +1241,12 @@ private Enumeration findResourcesByDelegation(String name) } } + // Core SPI support: Check if this is a ServiceLoader resource request + if (name.startsWith(JAVA_SPI_PREFIX)) + { + aggregateSpiResources(name, completeUrlList); + } + return new CompoundEnumeration((Enumeration[]) completeUrlList.toArray(new Enumeration[completeUrlList.size()])); } @@ -1566,6 +1581,12 @@ private Object findClassOrResourceByDelegation(String name, boolean isClass) { result = searchDynamicImports(pkgName, name, isClass); } + + // Core SPI support: As a last resort for classes, check if this is an SPI implementation class + if (result == null && isClass) + { + result = loadSpiClass(name); + } } } finally @@ -1904,6 +1925,158 @@ public URL nextElement() } } + // Core SPI support methods + + /** + * Aggregates SPI resources from all wired bundles that export the service interface package. + * This method is called when META-INF/services/ resources are requested. + */ + private void aggregateSpiResources(String name, List> completeUrlList) + { + // Extract the service interface name from the resource path + String spiTypeName = name.substring(JAVA_SPI_PREFIX.length()); + + try + { + // Try to load the service interface class from the requesting bundle + Class spiClass = getClassByDelegation(spiTypeName); + + // Get the package name of the service interface + String spiPackage = Util.getClassPackage(spiTypeName); + + // Find all bundles that are wired to provide this package + List wiredProviders = findWiredProviders(spiPackage); + + // Collect SPI resources from all wired providers + for (BundleRevision providerRevision : wiredProviders) + { + if (providerRevision == m_revision) + { + // Skip ourselves, we've already searched our own resources + continue; + } + + BundleWiring providerWiring = providerRevision.getWiring(); + if (providerWiring != null) + { + // Get the resource from the provider bundle + Enumeration providerUrls = + ((BundleRevisionImpl) providerRevision).getResourcesLocal(name); + + if (providerUrls != null && providerUrls.hasMoreElements()) + { + completeUrlList.add(providerUrls); + + // Parse the SPI resource and record implementation classes + parseSpiResource(providerUrls, providerRevision); + } + } + } + } + catch (ClassNotFoundException e) + { + // If we can't load the service interface, we can't do cross-bundle discovery + // This is expected and not an error - just continue with normal resource loading + } + } + + /** + * Finds all bundle revisions that are wired to provide the specified package. + */ + private List findWiredProviders(String packageName) + { + List providers = new ArrayList<>(); + + // Check imported packages + BundleRevision importedProvider = m_importedPkgs.get(packageName); + if (importedProvider != null) + { + providers.add(importedProvider); + } + + // Check required bundles + List requiredProviders = m_requiredPkgs.get(packageName); + if (requiredProviders != null) + { + providers.addAll(requiredProviders); + } + + // Include ourselves + providers.add(m_revision); + + return providers; + } + + /** + * Parses SPI resource files and records implementation class mappings. + */ + private void parseSpiResource(Enumeration urls, BundleRevision providerRevision) + { + while (urls.hasMoreElements()) + { + URL url = urls.nextElement(); + try (InputStream is = url.openStream(); + BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"))) + { + String line; + while ((line = reader.readLine()) != null) + { + // Remove comments + int commentIndex = line.indexOf('#'); + if (commentIndex >= 0) + { + line = line.substring(0, commentIndex); + } + + // Trim whitespace + line = line.trim(); + + // Skip empty lines + if (line.isEmpty()) + { + continue; + } + + // Record the mapping from implementation class to provider bundle + m_spiClassMap.put(line, providerRevision); + } + } + catch (IOException e) + { + // If we can't read the resource, just skip it + m_logger.log(m_revision.getBundle(), Logger.LOG_DEBUG, + "Failed to parse SPI resource: " + url, e); + } + } + } + + /** + * Attempts to load an SPI implementation class from its provider bundle. + * This is called as a last resort when normal class loading fails. + */ + private Class loadSpiClass(String className) + { + BundleRevision providerRevision = m_spiClassMap.get(className); + if (providerRevision != null) + { + BundleWiring providerWiring = providerRevision.getWiring(); + if (providerWiring != null) + { + try + { + // Load the class from the provider bundle + return ((BundleWiringImpl) providerWiring).getClassByDelegation(className); + } + catch (ClassNotFoundException e) + { + // If the provider bundle can't load the class, remove the mapping + m_spiClassMap.remove(className); + } + } + } + return null; + } + public static class BundleClassLoader extends SecureClassLoader implements BundleReference { static final boolean m_isParallel;