Skip to content

Commit

Permalink
Implement OSGi Service Loader Mediator Specification
Browse files Browse the repository at this point in the history
Signed-off-by: Hannes Wellmann <[email protected]>
  • Loading branch information
HannesWell committed Jul 31, 2023
1 parent 3bd3dbd commit a9391e4
Show file tree
Hide file tree
Showing 3 changed files with 174 additions and 1 deletion.
4 changes: 3 additions & 1 deletion bundles/org.eclipse.osgi/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,9 @@ Provide-Capability: osgi.service; objectClass:List<String>="org.osgi.service.log
osgi.service; objectClass:List<String>="org.eclipse.osgi.signedcontent.SignedContentFactory"; uses:="org.eclipse.osgi.signedcontent",
osgi.service; objectClass:List<String>="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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Optional<Class<?>>> 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<Stream<?>, Optional<Class<?>>> function = frames -> frames.<Class<?>>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<Class<?>>) 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).<Class<?>>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<String> latestServiceName = new ThreadLocal<>();

private final Supplier<Optional<Class<?>>> callerClass = getCallerClassComputer();

@Override
public Enumeration<URL> preFindResources(String name, ModuleClassLoader classLoader) throws FileNotFoundException {
if (name.startsWith(SERVICE_NAME_PREFIX)) {
Stream<URL> serviceProviderFiles = searchProviders(name, classLoader, cl -> {
try {
Enumeration<URL> resources = cl.getResources(name);
return Collections.list(resources).stream();
} catch (IOException e) {
return Stream.empty();
}
});
if (serviceProviderFiles != null) {
latestServiceName.set(name);
List<URL> 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<Class<?>> services = searchProviders(serviceName, classLoader, cl -> {
try {
return Stream.of(cl.loadClass(name));
} catch (ClassNotFoundException | NoClassDefFoundError e) { // ignore
return Stream.empty();
}
});
if (services != null) {
Optional<Class<?>> findFirst = services.findFirst();
return findFirst.orElse(null);
}
}
return null;
}

private <T> Stream<T> searchProviders(String name, ModuleClassLoader classLoader,
Function<ClassLoader, Stream<T>> 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<BundleWire> 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<Class<?>> caller = callerClass.get();
// TODO: If Java-9+ is required one day, just use the following code
// ClassLoader thisLoader = ServiceLoaderMediatorHook.class.getClassLoader();
// Optional<Class<?>> caller = WALKER.walk(frames -> frames.<Class<?>>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<ClassLoader> providerClassLoaders(String name, BundleWiring wiring) {
List<BundleWire> 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();
}

}

0 comments on commit a9391e4

Please sign in to comment.