From e1db7e3d42ad71b0ff1bd474e340ef40f73d3ad4 Mon Sep 17 00:00:00 2001 From: Andreas Turban Date: Wed, 25 Sep 2024 16:11:40 +0200 Subject: [PATCH] Support ContextClassLoader when loading optional classes (#1995) ReflectionUtil now also uses the ContextClassLoader to load classes, which enables env like OSGi to resolve classes. --- docs/release_notes.adoc | 2 + .../runtime/ExtensionClassesLoader.java | 7 +- .../spockframework/util/ReflectionUtil.java | 73 +++++++++++++++++-- .../util/ReflectionUtilSpec.groovy | 62 ++++++++++++++++ 4 files changed, 135 insertions(+), 9 deletions(-) diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index 692c2432eb..1eb6109a28 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -16,6 +16,8 @@ include::include.adoc[] * Add new <> 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 diff --git a/spock-core/src/main/java/org/spockframework/runtime/ExtensionClassesLoader.java b/spock-core/src/main/java/org/spockframework/runtime/ExtensionClassesLoader.java index 86f3c4fdf6..0c28a1e8a0 100644 --- a/spock-core/src/main/java/org/spockframework/runtime/ExtensionClassesLoader.java +++ b/spock-core/src/main/java/org/spockframework/runtime/ExtensionClassesLoader.java @@ -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.*; @@ -53,9 +54,9 @@ public List> loadClasses(String descriptorPath, Class return extClasses; } - private List locateDescriptors(String descriptorPath) { + private Collection 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); } @@ -80,7 +81,7 @@ private List readDescriptor(URL url) { @SuppressWarnings("unchecked") private Class loadExtensionClass(String className, Class 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()); diff --git a/spock-core/src/main/java/org/spockframework/util/ReflectionUtil.java b/spock-core/src/main/java/org/spockframework/util/ReflectionUtil.java index d387cf5344..9ba817b38f 100644 --- a/spock-core/src/main/java/org/spockframework/util/ReflectionUtil.java +++ b/spock-core/src/main/java/org/spockframework/util/ReflectionUtil.java @@ -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.*; @@ -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: + *
    + *
  • Spock {@link ClassLoader}
  • + *
  • {@code Thread.currentThread().getContextClassLoader()}
  • + *
+ * + * @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: + *
    + *
  • Spock {@link ClassLoader}
  • + *
  • {@code Thread.currentThread().getContextClassLoader()}
  • + *
+ * + * @param resourcePath the path of the resource + * @return the list of resources + * @throws IOException if the resources can't be loaded + */ + public static Collection 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 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) { @@ -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()); } diff --git a/spock-specs/src/test/groovy/org/spockframework/util/ReflectionUtilSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/util/ReflectionUtilSpec.groovy index f8fc2d1b81..b85872d301 100644 --- a/spock-specs/src/test/groovy/org/spockframework/util/ReflectionUtilSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/util/ReflectionUtilSpec.groovy @@ -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"() { @@ -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 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")