Skip to content

Commit

Permalink
Support ContextClassLoader when loading optional classes (#1995)
Browse files Browse the repository at this point in the history
ReflectionUtil now also uses the ContextClassLoader to load classes,
which enables env like OSGi to resolve classes.
  • Loading branch information
AndreasTu authored Sep 25, 2024
1 parent 1c64881 commit e1db7e3
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 9 deletions.
2 changes: 2 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ include::include.adoc[]
* Add new <<extensions.adoc#default-value-provider,`IDefaultValueProviderExtension`>> extension point to add support for special classes in the Stub's default `EmptyOrDummyResponse` spockPull:1994[]
* Improve `@Timeout` extension will now use virtual threads if available spockPull:1986[]
* Improve mock argument matching, types constraints or arguments in interactions can now handle primitive types like `_ as int` spockIssue:1974[]
* Support ContextClassLoader when loading optional classes via `ReflectionUtil` spockPull:1995[]
** This enables loading of optional classes in e.g. OSGi environments
* `EmbeddedSpecRunner` and `EmbeddedSpecCompiler` now support the construction with a custom `ClassLoader` spockPull:1988[]
** This allows the use of these classes in an OSGi environment, where the class imports in the embedded spec are not visible to the Spock OSGi bundle ClassLoader

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package org.spockframework.runtime;

import org.spockframework.runtime.extension.*;
import org.spockframework.util.ReflectionUtil;
import spock.config.ConfigurationObject;

import java.io.*;
Expand Down Expand Up @@ -53,9 +54,9 @@ public <T> List<Class<? extends T>> loadClasses(String descriptorPath, Class<T>
return extClasses;
}

private List<URL> locateDescriptors(String descriptorPath) {
private Collection<URL> locateDescriptors(String descriptorPath) {
try {
return Collections.list(RunContext.class.getClassLoader().getResources(descriptorPath));
return ReflectionUtil.getResourcesFromClassLoaders(descriptorPath);
} catch (Exception e) {
throw new ExtensionException("Failed to locate extension descriptors", e);
}
Expand All @@ -80,7 +81,7 @@ private List<String> readDescriptor(URL url) {
@SuppressWarnings("unchecked")
private <T> Class<? extends T> loadExtensionClass(String className, Class<T> baseClass) {
try {
Class<?> loadedClass = RunContext.class.getClassLoader().loadClass(className);
Class<?> loadedClass = ReflectionUtil.loadClass(className);
if (!baseClass.isAssignableFrom(loadedClass)) {
throw new ExtensionException("Failed to load extension class '%s' as it is not assignable to '%s'")
.withArgs(className, baseClass.getName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,10 @@
import static java.util.Arrays.asList;

import java.io.File;
import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.*;
import java.net.URL;
import java.security.CodeSource;
import java.util.*;

Expand All @@ -38,30 +40,88 @@ public static String getPackageName(Class<?> clazz) {
public static Class<?> loadFirstAvailableClass(String... classNames) {
for (String className : classNames) {
Class<?> clazz = loadClassIfAvailable(className);
if (clazz != null) {return clazz;}
if (clazz != null) {
return clazz;
}
}
return null;
}

public static Class<?> loadClassIfAvailable(String className) {
try {
return ReflectionUtil.class.getClassLoader().loadClass(className);
return loadClass(className);
} catch (ClassNotFoundException e) {
return null;
}
}

/**
* Loads the className in the following order:
* <ul>
* <li>Spock {@link ClassLoader}</li>
* <li>{@code Thread.currentThread().getContextClassLoader()}</li>
* </ul>
*
* @param className the class to load
* @return the loaded class
* @throws ClassNotFoundException if the class could not be found
*/
public static Class<?> loadClass(String className) throws ClassNotFoundException {
ClassLoader spockClassLoader = ReflectionUtil.class.getClassLoader();
try {
return spockClassLoader.loadClass(className);
} catch (ClassNotFoundException outerEx) {
ClassLoader contextClassLoader;
try {
//Try ContextClassLoader to better support for runtimes like OSGi
contextClassLoader = Thread.currentThread().getContextClassLoader();
} catch (SecurityException ex) {
throw outerEx;
}
if (contextClassLoader != null && contextClassLoader != spockClassLoader) {
return contextClassLoader.loadClass(className);
}
throw outerEx;
}
}

/**
* Returns the resources from the following classloaders:
* <ul>
* <li>Spock {@link ClassLoader}</li>
* <li>{@code Thread.currentThread().getContextClassLoader()}</li>
* </ul>
*
* @param resourcePath the path of the resource
* @return the list of resources
* @throws IOException if the resources can't be loaded
*/
public static Collection<URL> getResourcesFromClassLoaders(String resourcePath) throws IOException {
ClassLoader spockClassLoader = ReflectionUtil.class.getClassLoader();
// We need to use a sorted Set here, to filter out duplicates, if the ContextClassLoader can also reach the Spock classloader
TreeSet<URL> set = new TreeSet<>(Comparator.comparing(URL::toString));
set.addAll(Collections.list(spockClassLoader.getResources(resourcePath)));
try {
//Also resolve resources via ContextClassLoader to better support for runtimes like OSGi
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null && spockClassLoader != contextClassLoader) {
set.addAll(Collections.list(contextClassLoader.getResources(resourcePath)));
}
} catch (SecurityException ignored) {
}
return set;
}

public static boolean isClassAvailable(String className) {
return loadClassIfAvailable(className) != null;
}

public static boolean isMethodAvailable(String className, String methodName) {
try {
Class clazz = ReflectionUtil.class.getClassLoader().loadClass(className);
return getMethodByName(clazz, methodName) != null;
} catch (ClassNotFoundException e) {
Class<?> clazz = loadClassIfAvailable(className);
if (clazz == null) {
return false;
}
return getMethodByName(clazz, methodName) != null;
}

public static boolean isAnnotationPresent(AnnotatedElement element, String className) {
Expand All @@ -71,6 +131,7 @@ public static boolean isAnnotationPresent(AnnotatedElement element, String class

return false;
}

public static boolean isArray(Object obj) {
return (obj != null && obj.getClass().isArray());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import spock.lang.*

import java.lang.annotation.Annotation
import java.lang.reflect.Method
import java.util.jar.JarFile

class ReflectionUtilSpec extends Specification {
def "get package name"() {
Expand All @@ -37,6 +38,67 @@ class ReflectionUtilSpec extends Specification {
ReflectionUtil.loadClassIfAvailable("not.AvailableClass") == null
}

def "load class from ContextClassloader"() {
given:
def oldLoader = Thread.currentThread().getContextClassLoader()

def className = "test.TestIf"
def cl = new ByteBuddyTestClassLoader()
cl.defineInterface(className)

expect:
ReflectionUtil.loadClassIfAvailable(className) == null

when:
Thread.currentThread().setContextClassLoader(cl)
then:
ReflectionUtil.loadClassIfAvailable(className).classLoader == cl

cleanup:
Thread.currentThread().setContextClassLoader(oldLoader)
}

def "getResourcesFromClassLoaders"() {
when:
def res = ReflectionUtil.getResourcesFromClassLoaders(JarFile.MANIFEST_NAME)
then:
res.size() >= 1
}

def "getResourcesFromClassLoaders - ContextClassLoader"() {
given:
def oldLoader = Thread.currentThread().getContextClassLoader()

def resPath = "TestResourceFileName"
def resCl = new ClassLoader() {
@Override
Enumeration<URL> getResources(String name) throws IOException {
if (name == resPath) {
def url = new URL("file:/" + resPath)
return new Vector([
//Check that we filter out duplicates
url,
url
]).elements()
}
return super.getResources(name)
}
}

expect:
ReflectionUtil.getResourcesFromClassLoaders(resPath).isEmpty()

when:
Thread.currentThread().setContextClassLoader(resCl)
def res = ReflectionUtil.getResourcesFromClassLoaders(resPath)
then:
res.size() == 1
res[0].toString().contains(resPath)

cleanup:
Thread.currentThread().setContextClassLoader(oldLoader)
}

def "check if class exists"() {
expect:
ReflectionUtil.isClassAvailable("java.util.List")
Expand Down

0 comments on commit e1db7e3

Please sign in to comment.