From a9391e416c525a55abc71a512f318fd4a8e0e502 Mon Sep 17 00:00:00 2001 From: Hannes Wellmann Date: Wed, 5 Jul 2023 00:42:43 +0200 Subject: [PATCH] Implement OSGi Service Loader Mediator Specification Signed-off-by: Hannes Wellmann --- bundles/org.eclipse.osgi/META-INF/MANIFEST.MF | 4 +- .../internal/hookregistry/HookRegistry.java | 2 + .../hooks/ServiceLoaderMediatorHook.java | 169 ++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/ServiceLoaderMediatorHook.java diff --git a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF index 87b466c6f51..c77f80e518f 100644 --- a/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF +++ b/bundles/org.eclipse.osgi/META-INF/MANIFEST.MF @@ -100,7 +100,9 @@ Provide-Capability: osgi.service; objectClass:List="org.osgi.service.log osgi.service; objectClass:List="org.eclipse.osgi.signedcontent.SignedContentFactory"; uses:="org.eclipse.osgi.signedcontent", osgi.service; objectClass:List="org.osgi.service.condition.Condition"; osgi.condition.id="true"; uses:="org.osgi.service.condition", osgi.serviceloader;osgi.serviceloader="org.osgi.framework.connect.ConnectFrameworkFactory";register:="org.eclipse.osgi.launch.EquinoxFactory", - osgi.serviceloader;osgi.serviceloader="org.osgi.framework.launch.FrameworkFactory";register:="org.eclipse.osgi.launch.EquinoxFactory" + osgi.serviceloader;osgi.serviceloader="org.osgi.framework.launch.FrameworkFactory";register:="org.eclipse.osgi.launch.EquinoxFactory", + osgi.extender;osgi.extender="osgi.serviceloader.registrar";version:Version="1.0", + osgi.extender;osgi.extender="osgi.serviceloader.processor";version:Version="1.0" Bundle-Name: %systemBundle Bundle-SymbolicName: org.eclipse.osgi; singleton:=true Bundle-Activator: org.eclipse.osgi.internal.framework.SystemBundleActivator diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hookregistry/HookRegistry.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hookregistry/HookRegistry.java index 5990f4ced4a..da06cb5b918 100644 --- a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hookregistry/HookRegistry.java +++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hookregistry/HookRegistry.java @@ -29,6 +29,7 @@ import org.eclipse.osgi.internal.framework.EquinoxContainer; import org.eclipse.osgi.internal.hooks.DevClassLoadingHook; import org.eclipse.osgi.internal.hooks.EclipseLazyStarter; +import org.eclipse.osgi.internal.hooks.ServiceLoaderMediatorHook; import org.eclipse.osgi.internal.signedcontent.SignedBundleHook; import org.eclipse.osgi.internal.weaving.WeavingHookConfigurator; import org.eclipse.osgi.util.ManifestElement; @@ -113,6 +114,7 @@ public void initialize() { addClassLoaderHook(new DevClassLoadingHook(container.getConfiguration())); addClassLoaderHook(new EclipseLazyStarter(container)); addClassLoaderHook(new WeavingHookConfigurator(container)); + addClassLoaderHook(new ServiceLoaderMediatorHook()); configurators.add(SignedBundleHook.class.getName()); configurators.add(CDSHookConfigurator.class.getName()); loadConfigurators(configurators, errors); diff --git a/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/ServiceLoaderMediatorHook.java b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/ServiceLoaderMediatorHook.java new file mode 100644 index 00000000000..fa388696567 --- /dev/null +++ b/bundles/org.eclipse.osgi/container/src/org/eclipse/osgi/internal/hooks/ServiceLoaderMediatorHook.java @@ -0,0 +1,169 @@ +package org.eclipse.osgi.internal.hooks; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.Arrays; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.ServiceLoader; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.eclipse.osgi.internal.hookregistry.ClassLoaderHook; +import org.eclipse.osgi.internal.loader.ModuleClassLoader; +import org.osgi.framework.Constants; +import org.osgi.framework.wiring.BundleRequirement; +import org.osgi.framework.wiring.BundleWire; +import org.osgi.framework.wiring.BundleWiring; + +public class ServiceLoaderMediatorHook extends ClassLoaderHook { + + @SuppressWarnings({ "unchecked", "rawtypes" }) + private Supplier>> getCallerClassComputer() { + try { // The Java-9+ way + Class stackWalkerClass = Class.forName("java.lang.StackWalker"); //$NON-NLS-1$ + Class stackWalkerOption = Class.forName("java.lang.StackWalker$Option"); //$NON-NLS-1$ + Enum retainClassReference = Enum.valueOf(stackWalkerOption, "RETAIN_CLASS_REFERENCE"); //$NON-NLS-1$ + Object stackWalker = stackWalkerClass.getMethod("getInstance", Set.class, int.class).invoke(null, //$NON-NLS-1$ + new HashSet<>(Arrays.asList(retainClassReference)), 15); + + Method stackWalkerWalk = stackWalkerClass.getMethod("walk", Function.class); //$NON-NLS-1$ + Class stackFrameClass = Class.forName("java.lang.StackWalker$StackFrame"); //$NON-NLS-1$ + Method stackFrameGetDeclaringClass = stackFrameClass.getMethod("getDeclaringClass"); //$NON-NLS-1$ + + Function, Optional>> function = frames -> frames.>map(f -> { + try { + return (Class) stackFrameGetDeclaringClass.invoke(f); + } catch (ReflectiveOperationException e) { + return null; + } + }).filter(Objects::nonNull).limit(15) + .filter(c -> c == ServiceLoader.class || c.getEnclosingClass() == ServiceLoader.class).findFirst(); + + return () -> { + try { + return (Optional>) stackWalkerWalk.invoke(stackWalker, function); + } catch (ReflectiveOperationException e) { + return Optional.empty(); + } + }; + } catch (ReflectiveOperationException e) { // ignore + } + try { // Try the Java-1.8 way + Class reflectionClass = Class.forName("sun.reflect.Reflection"); //$NON-NLS-1$ + Method getCallerClass = Objects.requireNonNull(reflectionClass.getMethod("getCallerClass", int.class)); //$NON-NLS-1$ + return () -> IntStream.range(0, 15).>mapToObj(i -> { + try { + return (Class) getCallerClass.invoke(null, i); + } catch (ReflectiveOperationException e) { + return null; + } + }).filter(Objects::nonNull) + .filter(c -> c == ServiceLoader.class || c.getEnclosingClass() == ServiceLoader.class).findFirst(); + + } catch (ReflectiveOperationException e) { // ignore an try Java-8 way + } + throw new AssertionError("Neither the Java-8 nor the Java-9+ way to obtain the caller class is available"); //$NON-NLS-1$ + } + + private static final String SERVICE_NAME_PREFIX = "META-INF/services/"; //$NON-NLS-1$ + + private final ThreadLocal latestServiceName = new ThreadLocal<>(); + + private final Supplier>> callerClass = getCallerClassComputer(); + + @Override + public Enumeration preFindResources(String name, ModuleClassLoader classLoader) throws FileNotFoundException { + if (name.startsWith(SERVICE_NAME_PREFIX)) { + Stream serviceProviderFiles = searchProviders(name, classLoader, cl -> { + try { + Enumeration resources = cl.getResources(name); + return Collections.list(resources).stream(); + } catch (IOException e) { + return Stream.empty(); + } + }); + if (serviceProviderFiles != null) { + latestServiceName.set(name); + List services = serviceProviderFiles.collect(Collectors.toList()); + return Collections.enumeration(services); + } + } + return null; + } + + @Override + public Class preFindClass(String name, ModuleClassLoader classLoader) throws ClassNotFoundException { + String serviceName = latestServiceName.get(); // This cannot be removed if subsequent class load other + // providers for the same service + if (serviceName != null) { + Stream> services = searchProviders(serviceName, classLoader, cl -> { + try { + return Stream.of(cl.loadClass(name)); + } catch (ClassNotFoundException | NoClassDefFoundError e) { // ignore + return Stream.empty(); + } + }); + if (services != null) { + Optional> findFirst = services.findFirst(); + return findFirst.orElse(null); + } + } + return null; + } + + private Stream searchProviders(String name, ModuleClassLoader classLoader, + Function> mapper) { + BundleWiring wiring = classLoader.getBundle().adapt(BundleWiring.class); + if (requiresServiceLoaderProcessor(wiring) && isCalledFromServiceLoader()) { + return Stream.concat(Stream.of(classLoader), providerClassLoaders(name, wiring)).flatMap(mapper); + } + return null; + } + + private boolean requiresServiceLoaderProcessor(BundleWiring wiring) { + List requiredWires = wiring.getRequiredWires("osgi.extender"); //$NON-NLS-1$ + if (requiredWires.isEmpty()) { + return false; + } + return requiredWires.stream().anyMatch(w -> w.getRequirement().getDirectives().getOrDefault("filter", "") //$NON-NLS-1$//$NON-NLS-2$ + .contains("(osgi.extender=osgi.serviceloader.processor)") //$NON-NLS-1$ + && w.getProvider().getBundle().getBundleId() == Constants.SYSTEM_BUNDLE_ID); + } + + private boolean isCalledFromServiceLoader() { + Optional> caller = callerClass.get(); + // TODO: If Java-9+ is required one day, just use the following code +// ClassLoader thisLoader = ServiceLoaderMediatorHook.class.getClassLoader(); +// Optional> caller = WALKER.walk(frames -> frames.>map(f -> f.getDeclaringClass()) +// .dropWhile(c -> c.getName().startsWith("org.eclipse.osgi") && c.getClassLoader() == thisLoader) //$NON-NLS-1$ +// .findFirst()); + return caller.isPresent() && caller.get().getEnclosingClass() == ServiceLoader.class; + } + + private static Stream providerClassLoaders(String name, BundleWiring wiring) { + List requiredWires = wiring.getRequiredWires("osgi.serviceloader"); //$NON-NLS-1$ + if (!requiredWires.isEmpty()) { + String serviceName = name.substring(SERVICE_NAME_PREFIX.length()); + return requiredWires.stream().filter(wire -> { + BundleRequirement requirement = wire.getRequirement(); + String filterDirective = requirement.getDirectives().get("filter"); //$NON-NLS-1$ + Object serviceLoaderAttribute = wire.getCapability().getAttributes().get("osgi.serviceloader"); //$NON-NLS-1$ + return serviceName.equals(serviceLoaderAttribute) + && ("(osgi.serviceloader=" + serviceName + ")").equals(filterDirective); //$NON-NLS-1$ //$NON-NLS-2$ + }).map(wire -> wire.getProviderWiring().getClassLoader()); + + } + return Stream.empty(); + } + +}