From a5c83d00eeade6a84d2b620e9aaaea03ca3fc16d Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 13 Aug 2024 18:26:59 +0200 Subject: [PATCH 1/6] Create a lossy index of resource -> ClassPathElement mapping The size of ClassLoaderState can be extremely problematic so the idea of this patch is to have a lossy index instead and try to find a good compromise between speed and memory usage. --- .../io/quarkus/deployment/CodeGenerator.java | 2 +- .../GeneratedClassGizmoAdaptor.java | 11 +- .../deployment/dev/IsolatedDevModeMain.java | 12 +- .../deployment/dev/testing/TestSupport.java | 2 +- .../dev/testing/TestTracingProcessor.java | 10 +- .../io/quarkus/paths/ArchivePathTree.java | 10 + .../io/quarkus/paths/DirectoryPathTree.java | 5 + .../java/io/quarkus/paths/EmptyPathTree.java | 5 + .../java/io/quarkus/paths/FilePathTree.java | 5 + .../io/quarkus/paths/FilteredPathTree.java | 5 + .../io/quarkus/paths/MultiRootPathTree.java | 14 + .../main/java/io/quarkus/paths/PathTree.java | 7 + .../quarkus/paths/SharedArchivePathTree.java | 5 + .../classloading/ClassLoaderHelper.java | 20 +- .../bootstrap/app/CuratedApplication.java | 29 +- .../classloading/ClassPathElement.java | 14 +- .../classloading/ClassPathResourceIndex.java | 261 ++++++++++++ .../FilteredClassPathElement.java | 16 +- .../classloading/MemoryClassPathElement.java | 5 + .../PathTreeClassPathElement.java | 5 + .../classloading/QuarkusClassLoader.java | 384 +++++++++--------- .../ClassLoadingInterruptTestCase.java | 2 +- ...assLoadingPathTreeResourceUrlTestCase.java | 6 +- .../ClassLoadingResourceUrlTestCase.java | 8 +- .../classloader/MultiReleaseJarTestCase.java | 2 +- .../java/io/quarkus/test/QuarkusUnitTest.java | 9 +- .../test/junit/QuarkusTestExtension.java | 10 +- 27 files changed, 595 insertions(+), 269 deletions(-) create mode 100644 independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java diff --git a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java index 36dae8b38f8f3..8e5d12873a19b 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/CodeGenerator.java @@ -340,7 +340,7 @@ public static T readConfig(ApplicationModel appModel, LaunchMode launchMode, final QuarkusClassLoader.Builder configClBuilder = QuarkusClassLoader.builder("CodeGenerator Config ClassLoader", deploymentClassLoader, false); if (!allowedConfigServices.isEmpty()) { - configClBuilder.addElement(new MemoryClassPathElement(allowedConfigServices, true)); + configClBuilder.addNormalPriorityElement(new MemoryClassPathElement(allowedConfigServices, true)); } if (!bannedConfigServices.isEmpty()) { configClBuilder.addBannedElement(new MemoryClassPathElement(bannedConfigServices, true)); diff --git a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java index 1d34bdc33380e..f8cfaf031dfb2 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/GeneratedClassGizmoAdaptor.java @@ -1,17 +1,13 @@ package io.quarkus.deployment; -import static io.quarkus.commons.classloading.ClassLoaderHelper.fromClassNameToResourceName; - import java.io.StringWriter; import java.io.Writer; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; import java.util.function.Predicate; import io.quarkus.bootstrap.BootstrapDebug; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.builditem.GeneratedClassBuildItem; @@ -75,12 +71,7 @@ public Writer getSourceWriter(String className) { } public static boolean isApplicationClass(String className) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(fromClassNameToResourceName(className), true); - return !res.isEmpty(); + return QuarkusClassLoader.isApplicationClass(className); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java index d6864a09457fa..890a586828c5e 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/IsolatedDevModeMain.java @@ -30,14 +30,12 @@ import io.quarkus.bootstrap.app.CuratedApplication; import io.quarkus.bootstrap.app.RunningQuarkusApplication; import io.quarkus.bootstrap.app.StartupAction; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.bootstrap.runner.Timing; import io.quarkus.builder.BuildChainBuilder; import io.quarkus.builder.BuildContext; import io.quarkus.builder.BuildStep; -import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.deployment.builditem.ApplicationClassPredicateBuildItem; import io.quarkus.deployment.console.ConsoleCommand; import io.quarkus.deployment.console.ConsoleStateManager; @@ -478,14 +476,8 @@ public void execute(BuildContext context) { //we need to make sure all hot reloadable classes are application classes context.produce(new ApplicationClassPredicateBuildItem(new Predicate() { @Override - public boolean test(String s) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - String resourceName = ClassLoaderHelper.fromClassNameToResourceName(s); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(resourceName, true); - return !res.isEmpty(); + public boolean test(String className) { + return QuarkusClassLoader.isApplicationClass(className); } })); } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java index a71dcc3f09323..b27ba0042b125 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestSupport.java @@ -221,7 +221,7 @@ public void init() { + curatedApplication.getClassLoaderNameSuffix(), getClass().getClassLoader().getParent(), false); } - clBuilder.addElement(ClassPathElement.fromDependency(d)); + clBuilder.addNormalPriorityElement(ClassPathElement.fromDependency(d)); } } diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java index 8ff20a9bcf92d..fead5a43bbdf0 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/TestTracingProcessor.java @@ -25,7 +25,6 @@ import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.deployment.IsDevelopment; import io.quarkus.deployment.IsNormal; @@ -155,13 +154,8 @@ public ServiceStartBuildItem searchForTags(CombinedIndexBuildItem combinedIndexB return null; } - public boolean isAppClass(String theClassName) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(theClassName.replace(".", "/") + ".class", true); - return !res.isEmpty(); + public boolean isAppClass(String className) { + return QuarkusClassLoader.isApplicationClass(className); } public static class TracingClassVisitor extends ClassVisitor { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java index ff9054e17f5c5..1fc1ed20c3de8 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java @@ -86,6 +86,11 @@ static ArchivePathTree forPath(Path path, PathFilter filter, boolean manifestEna this.pathFilter = pathFilter; } + @Override + public boolean providesLocalResources() { + return false; + } + @Override public Collection getRoots() { return List.of(archive); @@ -240,6 +245,11 @@ protected OpenArchivePathTree(FileSystem fs) { this.rootPath = fs.getPath("/"); } + @Override + public boolean providesLocalResources() { + return false; + } + @Override protected Path getContainerPath() { return ArchivePathTree.this.archive; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java index b73d8acc1613d..4fbd953651824 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java @@ -36,6 +36,11 @@ protected DirectoryPathTree(Path dir, PathFilter pathFilter, PathTreeWithManifes this.dir = dir; } + @Override + public boolean providesLocalResources() { + return true; + } + @Override protected Path getRootPath() { return dir; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java index 4f31e89c6189d..9b0dbb7fbfcd1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java @@ -15,6 +15,11 @@ public static EmptyPathTree getInstance() { return INSTANCE; } + @Override + public boolean providesLocalResources() { + return false; + } + @Override public Collection getRoots() { return Collections.emptyList(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java index cb80e315b5976..25c2afab2f2d1 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java @@ -22,6 +22,11 @@ class FilePathTree implements OpenPathTree { this.pathFilter = pathFilter; } + @Override + public boolean providesLocalResources() { + return true; + } + @Override public Collection getRoots() { return Collections.singletonList(file); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java index 25de79a0eba9a..84a0ec1c67348 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java @@ -17,6 +17,11 @@ public FilteredPathTree(PathTree tree, PathFilter filter) { this.filter = Objects.requireNonNull(filter, "filter is null"); } + @Override + public boolean providesLocalResources() { + return original.providesLocalResources(); + } + @Override public Collection getRoots() { return original.getRoots(); diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java index f94b0bc9cc4f3..5c69c02ccc3ef 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java @@ -25,6 +25,20 @@ public MultiRootPathTree(PathTree... trees) { roots = tmp; } + /** + * If at least one of the PathTrees contains local resources, we return true. + */ + @Override + public boolean providesLocalResources() { + for (PathTree tree : trees) { + if (tree.providesLocalResources()) { + return true; + } + } + + return false; + } + @Override public Collection getRoots() { return roots; diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index 57dcafeda3a54..0768e189bfea9 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -80,6 +80,13 @@ static PathTree ofArchive(Path archive, PathFilter filter) { return ArchivePathTree.forPath(archive, filter); } + /** + * Whether the PathTree provides local resources. + *

+ * For instance a directory or a file provides local resources. A jar does not. + */ + boolean providesLocalResources(); + /** * The roots of the path tree. *

diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index 0ea0048549c8b..e4bc62718293c 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -122,6 +122,11 @@ private CallerOpenPathTree(SharedOpenArchivePathTree delegate) { this.delegate = delegate; } + @Override + public boolean providesLocalResources() { + return delegate.providesLocalResources(); + } + @Override public PathTree getOriginalTree() { return delegate.getOriginalTree(); diff --git a/independent-projects/bootstrap/classloader-commons/src/main/java/io/quarkus/commons/classloading/ClassLoaderHelper.java b/independent-projects/bootstrap/classloader-commons/src/main/java/io/quarkus/commons/classloading/ClassLoaderHelper.java index 92fc1c3bb4082..46a15a4c48625 100644 --- a/independent-projects/bootstrap/classloader-commons/src/main/java/io/quarkus/commons/classloading/ClassLoaderHelper.java +++ b/independent-projects/bootstrap/classloader-commons/src/main/java/io/quarkus/commons/classloading/ClassLoaderHelper.java @@ -6,6 +6,8 @@ public final class ClassLoaderHelper { private static final String JDK_INTERNAL = "jdk.internal."; private static final String SUN_MISC = "sun.misc."; + private static final String CLASS_SUFFIX = ".class"; + private ClassLoaderHelper() { //Not meant to be instantiated } @@ -19,7 +21,23 @@ private ClassLoaderHelper() { */ public static String fromClassNameToResourceName(final String className) { //Important: avoid indy! - return className.replace('.', '/').concat(".class"); + return className.replace('.', '/').concat(CLASS_SUFFIX); + } + + /** + * Helper method to convert a resource name into the corresponding class name: + * replace all "/" with "." and remove the ".class" postfix. + * + * @param resourceName + * @return the name of the respective class + */ + public static String fromResourceNameToClassName(final String resourceName) { + if (!resourceName.endsWith(CLASS_SUFFIX)) { + throw new IllegalArgumentException( + String.format("%s is not a valid resource name as it doesn't end with .class", resourceName)); + } + + return resourceName.substring(0, resourceName.length() - CLASS_SUFFIX.length()).replace('/', '.'); } public static boolean isInJdkPackage(String name) { diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 89d54ad91fb0c..38aeb072775cd 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -183,10 +183,12 @@ private void addCpElement(QuarkusClassLoader.Builder builder, ResolvedDependency //we always load this from the parent if it is available, as this acts as a bridge between the running //app and the dev mode code builder.addParentFirstElement(element); + builder.addNormalPriorityElement(element); } else if (dep.isFlagSet(DependencyFlags.CLASSLOADER_LESSER_PRIORITY)) { builder.addLesserPriorityElement(element); + } else { + builder.addNormalPriorityElement(element); } - builder.addElement(element); } public synchronized QuarkusClassLoader getOrCreateAugmentClassLoader() { @@ -212,7 +214,7 @@ public synchronized QuarkusClassLoader getOrCreateAugmentClassLoader() { } for (Path i : quarkusBootstrap.getAdditionalDeploymentArchives()) { - builder.addElement(ClassPathElement.fromPath(i, false)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(i, false)); } Map banned = new HashMap<>(); for (Collection i : configuredClassLoading.getRemovedResources().values()) { @@ -256,7 +258,7 @@ public synchronized QuarkusClassLoader getOrCreateBaseRuntimeClassLoader() { //there is no need to restart so there is no need for an additional CL for (Path root : quarkusBootstrap.getApplicationRoot()) { - builder.addElement(ClassPathElement.fromPath(root, true)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(root, true)); } } else { for (Path root : quarkusBootstrap.getApplicationRoot()) { @@ -269,7 +271,7 @@ public synchronized QuarkusClassLoader getOrCreateBaseRuntimeClassLoader() { for (AdditionalDependency i : quarkusBootstrap.getAdditionalApplicationArchives()) { if (!i.isHotReloadable()) { for (Path root : i.getResolvedPaths()) { - builder.addElement(ClassPathElement.fromPath(root, true)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(root, true)); } } else { for (Path root : i.getResolvedPaths()) { @@ -339,7 +341,7 @@ public QuarkusClassLoader createDeploymentClassLoader() { .setAggregateParentResources(true); for (Path root : quarkusBootstrap.getApplicationRoot()) { - builder.addElement(ClassPathElement.fromPath(root, true)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(root, true)); } builder.setResettableElement(new MemoryClassPathElement(Collections.emptyMap(), false)); @@ -347,7 +349,7 @@ public QuarkusClassLoader createDeploymentClassLoader() { //additional user class path elements first for (AdditionalDependency i : quarkusBootstrap.getAdditionalApplicationArchives()) { for (Path root : i.getResolvedPaths()) { - builder.addElement(ClassPathElement.fromPath(root, true)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(root, true)); } } for (ResolvedDependency dependency : appModel.getDependencies()) { @@ -361,7 +363,7 @@ public QuarkusClassLoader createDeploymentClassLoader() { } } for (Path root : configuredClassLoading.getAdditionalClasspathElements()) { - builder.addElement(ClassPathElement.fromPath(root, true)); + builder.addNormalPriorityElement(ClassPathElement.fromPath(root, true)); } return builder.build(); } @@ -387,15 +389,15 @@ public QuarkusClassLoader createRuntimeClassLoader(ClassLoader base, Map getProvidedResources() { return delegate.getProvidedResources().stream().filter(s -> s.endsWith(".class")).collect(Collectors.toSet()); } + @Override + public boolean providesLocalResources() { + return delegate.providesLocalResources(); + } + @Override public ProtectionDomain getProtectionDomain() { return delegate.getProtectionDomain(); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java index 96c6689a4816d..4d0dad1967ffe 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java @@ -3,7 +3,6 @@ import java.io.Closeable; import java.nio.file.Path; import java.security.ProtectionDomain; -import java.util.Collections; import java.util.List; import java.util.Set; import java.util.function.Function; @@ -74,6 +73,11 @@ default ResolvedDependency getResolvedDependency() { */ Set getProvidedResources(); + /** + * Whether this class path element contains local resources. + */ + boolean providesLocalResources(); + /** * * @return The protection domain that should be used to define classes from this element @@ -102,6 +106,7 @@ static ClassPathElement fromDependency(ResolvedDependency dep) { } static ClassPathElement EMPTY = new ClassPathElement() { + @Override public Path getRoot() { return null; @@ -124,7 +129,12 @@ public ClassPathResource getResource(String name) { @Override public Set getProvidedResources() { - return Collections.emptySet(); + return Set.of(); + } + + @Override + public boolean providesLocalResources() { + return false; } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java new file mode 100644 index 0000000000000..a1319064cae17 --- /dev/null +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java @@ -0,0 +1,261 @@ +package io.quarkus.bootstrap.classloading; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.function.BiConsumer; + +/** + * We used to have a full index of the resources present in the classpath stored in ClassLoaderState of QuarkusClassLoader. + *

+ * However it could lead to some major memory consumption, the index being more than 13 MB on real-life projects. + * This was a problem especially since we also keep a list of the resources in each ClassPathElement. + *

+ * Going through the whole list of ClassPathElements for each lookup is not a good option so we choose an intermediary approach + * of having a lossy index referencing the start of the resource path to reduce the number of ClassPathElements to go through. + * This index still returns exact references as we check the ClassPathElements themselves for the presence of the resource. + *

+ * In the particular example above, we went down to less than 3 MB for the index, storing ~600 entries instead of 86000+ in the + * mapping. + *

+ * We try to be clever as to how we build the resource key to reduce the number of misses. It might need further tuning in the + * future. + *

+ * The general idea is to have this index: + *

    + *
  • store a mapping between the prefix of the resource and the {@code ClassPathElement}s that contain resources starting with + * this prefix.
  • + *
  • generate a prefix that is smart: we want to reduce the size of the index but we also want to reduce the impact on + * performances (e.g. for {@code io.quarkus}, we keep one more segment, for versioned classes, we keep 3 more, we make sure + * {@code META-INF/services/} files are fully indexed...)
  • + *
  • store some additional information such as the transformed classes, banned resources, the classes that are considered + * "local" to this class loader (excluding the jars). For these elements, the information is NOT lossy.
  • + *
+ * When interrogating the index, we get the candidate {@code ClassPathElement}s and we then check if the resource is actually in + * a given {@code ClassPathElement} before actually considering it. + *

+ * In most cases, the information that is in the index is that a resource with this prefix is in these {@code ClassPathElement} + * but it might not be the precise resource we are looking for. + */ +public class ClassPathResourceIndex { + + private static final String IO_QUARKUS = "io/quarkus/"; + private static final String META_INF_MAVEN = "META-INF/maven/"; + private static final String META_INF_SERVICES = "META-INF/services/"; + private static final String META_INF_VERSIONS = "META-INF/versions/"; + + private static final int MAX_SEGMENTS_DEFAULT = 3; + private static final int MAX_SEGMENTS_IO_QUARKUS = 4; + // let's go with default max segments + 3 for the META-INF/versions/ part + private static final int MAX_SEGMENTS_META_INF_VERSIONS = MAX_SEGMENTS_DEFAULT + 3; + + /** + * This map is mapped by prefixes. + */ + private final Map resourceMapping; + private final Map transformedClasses; + + private final Set localClasses; + private final Set parentFirstResources; + private final Set bannedResources; + + private ClassPathResourceIndex(Map resourceMapping, + Map transformedClasses, + Set localClasses, + Set parentFirstResources, + Set bannedResources) { + this.resourceMapping = resourceMapping.isEmpty() ? Map.of() : Collections.unmodifiableMap(resourceMapping); + this.transformedClasses = transformedClasses.isEmpty() ? Map.of() : transformedClasses; + this.localClasses = localClasses.isEmpty() ? Set.of() : Collections.unmodifiableSet(localClasses); + this.parentFirstResources = parentFirstResources.isEmpty() ? Set.of() + : Collections.unmodifiableSet(parentFirstResources); + this.bannedResources = bannedResources.isEmpty() ? Set.of() : Collections.unmodifiableSet(bannedResources); + } + + public Set getLocalClassResourceNames() { + return localClasses; + } + + public boolean isParentFirst(String resource) { + return parentFirstResources.contains(resource); + } + + public boolean isBanned(String resource) { + return bannedResources.contains(resource); + } + + // it's tempting to use an Optional here but let's avoid the additional allocation + public ClassPathElement getFirstClassPathElement(String resource) { + ClassPathElement transformedClassClassPathElement = transformedClasses.get(resource); + if (transformedClassClassPathElement != null) { + return transformedClassClassPathElement; + } + + ClassPathElement[] candidates = resourceMapping.get(getResourceKey(resource)); + if (candidates == null) { + return null; + } + + for (int i = 0; i < candidates.length; i++) { + if (candidates[i].getProvidedResources().contains(resource)) { + return candidates[i]; + } + } + + return null; + } + + public List getClassPathElements(String resource) { + ClassPathElement transformedClassClassPathElement = transformedClasses.get(resource); + if (transformedClassClassPathElement != null) { + return List.of(transformedClassClassPathElement); + } + + ClassPathElement[] candidates = resourceMapping.get(getResourceKey(resource)); + if (candidates == null) { + return List.of(); + } + + if (candidates.length == 1) { + if (candidates[0].getProvidedResources().contains(resource)) { + return List.of(candidates[0]); + } + + return List.of(); + } + + List classPathElements = new ArrayList<>(candidates.length); + for (int i = 0; i < candidates.length; i++) { + if (candidates[i].getProvidedResources().contains(resource)) { + classPathElements.add(candidates[i]); + } + } + return classPathElements; + } + + public static Builder builder() { + return new Builder(); + } + + /** + * Returns a key that tries to find a good compromise between reducing the size of the index and providing good + * performances. + *

+ * Probably something we will have to tweak for corner cases but let's try to keep it fast. + */ + private static String getResourceKey(String resource) { + // we don't really care about this part, it can be slower + if (resource.startsWith(META_INF_MAVEN)) { + return META_INF_MAVEN; + } + if (resource.startsWith(META_INF_SERVICES)) { + // for services, we want to reference the full path + return resource; + } + StringBuilder prefix = new StringBuilder(); + + int maxSegments; + if (resource.startsWith(IO_QUARKUS)) { + maxSegments = MAX_SEGMENTS_IO_QUARKUS; + } else if (resource.startsWith(META_INF_VERSIONS)) { + maxSegments = MAX_SEGMENTS_META_INF_VERSIONS; + } else { + maxSegments = MAX_SEGMENTS_DEFAULT; + } + + char[] charArray = resource.toCharArray(); + int segmentCount = 0; + for (char c : charArray) { + if (c == '/') { + segmentCount++; + if (segmentCount == maxSegments) { + return prefix.toString(); + } + } + prefix.append(c); + } + + // let's drop the file name if we haven't truncated anything and we detect it's a file + if (segmentCount > 0 && segmentCount < maxSegments) { + int lastSlash = prefix.lastIndexOf("/"); + if (prefix.substring(lastSlash).contains(".")) { + prefix.setLength(lastSlash); + } + } + + return prefix.toString(); + } + + public static class Builder { + + private static final String CLASS_SUFFIX = ".class"; + + private final Map transformedClassCandidates = new HashMap<>(); + private final Map transformedClasses = new HashMap<>(); + private final Map> resourceMapping = new HashMap<>(); + + private final Set localClasses = new HashSet<>(); + private final Set parentFirstResources = new HashSet<>(); + private final Set bannedResources = new HashSet<>(); + + public void scanClassPathElement(ClassPathElement classPathElement, + BiConsumer consumer) { + for (String resource : classPathElement.getProvidedResources()) { + consumer.accept(classPathElement, resource); + } + } + + public void addTranformedClassCandidate(ClassPathElement classPathElement, String resource) { + transformedClassCandidates.put(resource, classPathElement); + } + + public void addResourceMapping(ClassPathElement classPathElement, String resource) { + if (classPathElement.providesLocalResources() && resource.endsWith(CLASS_SUFFIX)) { + localClasses.add(resource); + } + + ClassPathElement transformedClassClassPathElement = transformedClassCandidates.get(resource); + if (transformedClassClassPathElement != null) { + transformedClasses.put(resource, transformedClassClassPathElement); + return; + } + + String resourcePrefix = getResourceKey(resource); + + List classPathElements = resourceMapping.get(resourcePrefix); + if (classPathElements == null) { + // default initial capacity of 10 is way too large + classPathElements = new ArrayList<>(2); + resourceMapping.put(resourcePrefix, classPathElements); + } + + if (!classPathElements.contains(classPathElement)) { + classPathElements.add(classPathElement); + } + } + + public void addParentFirstResource(ClassPathElement classPathElement, String resource) { + parentFirstResources.add(resource); + } + + public void addBannedResource(ClassPathElement classPathElement, String resource) { + bannedResources.add(resource); + } + + public ClassPathResourceIndex build() { + Map compactedResourceMapping = new HashMap<>(resourceMapping.size()); + for (Entry> resourceMappingEntry : resourceMapping.entrySet()) { + compactedResourceMapping.put(resourceMappingEntry.getKey(), + resourceMappingEntry.getValue().toArray(new ClassPathElement[resourceMappingEntry.getValue().size()])); + } + + return new ClassPathResourceIndex(compactedResourceMapping, transformedClasses, + localClasses, parentFirstResources, bannedResources); + } + } +} diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java index a8dbbd2d06ec7..d2c8f42c36295 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java @@ -14,12 +14,15 @@ public class FilteredClassPathElement implements ClassPathElement { - final ClassPathElement delegate; - final Set removed; + private final ClassPathElement delegate; + private final Set removed; + private final Set resources; public FilteredClassPathElement(ClassPathElement delegate, Collection removed) { this.delegate = delegate; this.removed = new HashSet<>(removed); + this.resources = new HashSet<>(delegate.getProvidedResources()); + this.resources.removeAll(this.removed); } @Override @@ -52,9 +55,12 @@ public ClassPathResource getResource(String name) { @Override public Set getProvidedResources() { - Set ret = new HashSet<>(delegate.getProvidedResources()); - ret.removeAll(removed); - return ret; + return resources; + } + + @Override + public boolean providesLocalResources() { + return delegate.providesLocalResources(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java index d6c3864673c55..54b449f933f36 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java @@ -113,6 +113,11 @@ public Set getProvidedResources() { return resources.keySet(); } + @Override + public boolean providesLocalResources() { + return true; + } + @Override public ProtectionDomain getProtectionDomain() { // we used to include the class bytes in the ProtectionDomain diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index 8126c264dbb43..5c6cbf25d19c9 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -175,6 +175,11 @@ public void visitPath(PathVisit visit) { return resources; } + @Override + public boolean providesLocalResources() { + return pathTree.providesLocalResources(); + } + @Override protected ManifestAttributes readManifest() { return apply(OpenPathTree::getManifestAttributes); diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index e48f65016950c..9bc9e9ba7d02f 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -13,11 +13,8 @@ import java.security.ProtectionDomain; import java.sql.Driver; import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; import java.util.Collections; import java.util.Enumeration; -import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; @@ -29,6 +26,7 @@ import org.jboss.logging.Logger; +import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.paths.ManifestAttributes; /** @@ -59,15 +57,6 @@ public static List getElements(String resourceName, boolean on return ((QuarkusClassLoader) ccl).getElementsWithResource(resourceName, onlyFromCurrentClassLoader); } - public List getAllElements(boolean onlyFromCurrentClassLoader) { - List ret = new ArrayList<>(); - if (parent instanceof QuarkusClassLoader && !onlyFromCurrentClassLoader) { - ret.addAll(((QuarkusClassLoader) parent).getAllElements(onlyFromCurrentClassLoader)); - } - ret.addAll(elements); - return ret; - } - /** * Indicates if a given class is present at runtime. * @@ -78,6 +67,15 @@ public static boolean isClassPresentAtRuntime(String className) { return isResourcePresentAtRuntime(resourceName); } + /** + * Indicates if a given class is considered an application class. + */ + public static boolean isApplicationClass(String className) { + String resourceName = fromClassNameToResourceName(className); + List res = getElements(resourceName, true); + return !res.isEmpty(); + } + /** * Indicates if a given resource is present at runtime. * Can also be used to check if a class is present as a class is just a regular resource. @@ -86,8 +84,9 @@ public static boolean isClassPresentAtRuntime(String className) { * or {@code my/package/MyClass.class} for a class. */ public static boolean isResourcePresentAtRuntime(String resourcePath) { - for (ClassPathElement cpe : QuarkusClassLoader.getElements(resourcePath, false)) { - if (cpe.isRuntime()) { + List classPathElements = QuarkusClassLoader.getElements(resourcePath, false); + for (int i = 0; i < classPathElements.size(); i++) { + if (classPathElements.get(i).isRuntime()) { return true; } } @@ -96,7 +95,11 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) { } private final String name; - private final List elements; + // the ClassPathElements to consider are normalPriorityElements + lesserPriorityElements + private final List normalPriorityElements; + private final List lesserPriorityElements; + private final List bannedElements; + private final List parentFirstElements; private final ConcurrentMap protectionDomains = new ConcurrentHashMap<>(); private final ConcurrentMap definedPackages = new ConcurrentHashMap<>(); private final ClassLoader parent; @@ -105,9 +108,6 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) { */ private final boolean parentFirst; private final boolean aggregateParentResources; - private final List bannedElements; - private final List parentFirstElements; - private final List lesserPriorityElements; private final List classLoaderEventListeners; /** @@ -124,7 +124,7 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) { */ private volatile MemoryClassPathElement resettableElement; private volatile MemoryClassPathElement transformedClasses; - private volatile ClassLoaderState state; + private volatile ClassPathResourceIndex classPathResourceIndex; private final List closeTasks = new ArrayList<>(); static final ClassLoader PLATFORM_CLASS_LOADER; @@ -148,7 +148,7 @@ private QuarkusClassLoader(Builder builder) { super(builder.parent); this.name = builder.name; this.status = STATUS_OPEN; - this.elements = builder.elements; + this.normalPriorityElements = builder.normalPriorityElements; this.bannedElements = builder.bannedElements; this.parentFirstElements = builder.parentFirstElements; this.lesserPriorityElements = builder.lesserPriorityElements; @@ -180,8 +180,8 @@ private String sanitizeName(String name) { return name; } - private boolean parentFirst(String name, ClassLoaderState state) { - return parentFirst || state.parentFirstResources.contains(name); + private boolean parentFirst(String name, ClassPathResourceIndex classPathResourceIndex) { + return parentFirst || classPathResourceIndex.isParentFirst(name); } public void reset(Map generatedResources, Map transformedClasses) { @@ -193,7 +193,7 @@ public void reset(Map generatedResources, Map tr synchronized (this) { this.transformedClasses = new MemoryClassPathElement(transformedClasses, true); resettableElement.reset(generatedResources); - state = null; + classPathResourceIndex = null; } } @@ -207,14 +207,14 @@ public Enumeration getResources(String unsanitisedName) throws IOException public Enumeration getResources(String unsanitisedName, boolean parentAlreadyFoundResources) throws IOException { ensureOpen(unsanitisedName); - for (ClassLoaderEventListener l : classLoaderEventListeners) { - l.enumeratingResourceURLs(unsanitisedName, this.name); + for (int i = 0; i < classLoaderEventListeners.size(); i++) { + classLoaderEventListeners.get(i).enumeratingResourceURLs(unsanitisedName, this.name); } - ClassLoaderState state = getState(); + ClassPathResourceIndex classPathResourceIndex = getClassPathResourceIndex(); String name = sanitizeName(unsanitisedName); //for resources banned means that we don't delegate to the parent, as there can be multiple resources //for single resources we still respect this - boolean banned = state.bannedResources.contains(name); + boolean banned = classPathResourceIndex.isBanned(name); //this is a big of a hack, but is necessary to prevent service leakage //in some situations (looking at you gradle) the parent can contain the same @@ -239,15 +239,16 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea //TODO: in theory resources could have been added in dev mode //but I don't think this really matters for this code path Set resources = new LinkedHashSet<>(); - ClassPathElement[] providers = state.loadableResources.get(name); - if (providers != null) { + List classPathElements = classPathResourceIndex.getClassPathElements(name); + if (!classPathElements.isEmpty()) { boolean endsWithTrailingSlash = unsanitisedName.endsWith("/"); - for (ClassPathElement element : providers) { - Collection resList = element.getResources(name); + for (int i = 0; i < classPathElements.size(); i++) { + List resList = classPathElements.get(i).getResources(name); //if the requested name ends with a trailing / we make sure //that the resource is a directory, and return a URL that ends with a / //this matches the behaviour of URLClassLoader - for (var res : resList) { + for (int j = 0; j < resList.size(); j++) { + var res = resList.get(j); if (endsWithTrailingSlash) { if (res.isDirectory()) { try { @@ -262,8 +263,8 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea } } } else if (name.isEmpty()) { - for (ClassPathElement i : elements) { - List resList = i.getResources(""); + for (int i = 0; i < normalPriorityElements.size(); i++) { + List resList = normalPriorityElements.get(i).getResources(""); for (var res : resList) { if (res != null) { resources.add(res.getUrl()); @@ -287,64 +288,42 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea return Collections.enumeration(resources); } - private ClassLoaderState getState() { - ClassLoaderState state = this.state; - if (state == null) { + private ClassPathResourceIndex getClassPathResourceIndex() { + ClassPathResourceIndex classPathResourceIndex = this.classPathResourceIndex; + if (classPathResourceIndex == null) { synchronized (this) { - state = this.state; - if (state == null) { - Map> elementMap = new HashMap<>(); - for (ClassPathElement element : elements) { - for (String i : element.getProvidedResources()) { - if (i.startsWith("/")) { - throw new RuntimeException( - "Resources cannot start with /, " + i + " is incorrect provided by " + element); - } - if (transformedClasses.getResource(i) != null) { - elementMap.put(i, Collections.singletonList(transformedClasses)); - } else { - List list = elementMap.get(i); - if (list == null) { - elementMap.put(i, list = new ArrayList<>(2)); //default initial capacity of 10 is way too large - } - list.add(element); - } - } + classPathResourceIndex = this.classPathResourceIndex; + if (classPathResourceIndex == null) { + ClassPathResourceIndex.Builder classPathResourceIndexBuilder = ClassPathResourceIndex.builder(); + + classPathResourceIndexBuilder.scanClassPathElement(transformedClasses, + classPathResourceIndexBuilder::addTranformedClassCandidate); + + for (ClassPathElement element : normalPriorityElements) { + classPathResourceIndexBuilder.scanClassPathElement(element, + classPathResourceIndexBuilder::addResourceMapping); } - Map finalElements = new HashMap<>(); - for (Map.Entry> i : elementMap.entrySet()) { - List entryClassPathElements = i.getValue(); - if (!lesserPriorityElements.isEmpty() && (entryClassPathElements.size() > 1)) { - List entryNormalPriorityElements = new ArrayList<>(entryClassPathElements.size()); - List entryLesserPriorityElements = new ArrayList<>(entryClassPathElements.size()); - for (ClassPathElement classPathElement : entryClassPathElements) { - if (lesserPriorityElements.contains(classPathElement)) { - entryLesserPriorityElements.add(classPathElement); - } else { - entryNormalPriorityElements.add(classPathElement); - } - } - // ensure the lesser priority elements are added later - entryClassPathElements = new ArrayList<>(entryClassPathElements.size()); - entryClassPathElements.addAll(entryNormalPriorityElements); - entryClassPathElements.addAll(entryLesserPriorityElements); - } - finalElements.put(i.getKey(), - entryClassPathElements.toArray(new ClassPathElement[entryClassPathElements.size()])); + + for (ClassPathElement lesserPriorityElement : lesserPriorityElements) { + classPathResourceIndexBuilder.scanClassPathElement(lesserPriorityElement, + classPathResourceIndexBuilder::addResourceMapping); } - Set banned = new HashSet<>(); - for (ClassPathElement i : bannedElements) { - banned.addAll(i.getProvidedResources()); + + for (ClassPathElement bannedElement : bannedElements) { + classPathResourceIndexBuilder.scanClassPathElement(bannedElement, + classPathResourceIndexBuilder::addBannedResource); } - Set parentFirstResources = new HashSet<>(); - for (ClassPathElement i : parentFirstElements) { - parentFirstResources.addAll(i.getProvidedResources()); + + for (ClassPathElement parentFirstElement : parentFirstElements) { + classPathResourceIndexBuilder.scanClassPathElement(parentFirstElement, + classPathResourceIndexBuilder::addParentFirstResource); } - return this.state = new ClassLoaderState(finalElements, banned, parentFirstResources); + + return this.classPathResourceIndex = classPathResourceIndexBuilder.build(); } } } - return state; + return classPathResourceIndex; } @Override @@ -355,8 +334,8 @@ public URL getResource(String unsanitisedName) { l.gettingURLFromResource(unsanitisedName, this.name); } String name = sanitizeName(unsanitisedName); - ClassLoaderState state = getState(); - if (state.bannedResources.contains(name)) { + ClassPathResourceIndex classPathResourceIndex = getClassPathResourceIndex(); + if (classPathResourceIndex.isBanned(name)) { return null; } //TODO: because of dev mode we iterate, to see if any resources were added @@ -364,82 +343,111 @@ public URL getResource(String unsanitisedName) { //this is very important for bytebuddy performance boolean endsWithTrailingSlash = unsanitisedName.endsWith("/"); if (name.endsWith(".class") && !endsWithTrailingSlash) { - ClassPathElement[] providers = state.loadableResources.get(name); - if (providers != null) { - final ClassPathResource resource = providers[0].getResource(name); + ClassPathElement classPathElement = classPathResourceIndex.getFirstClassPathElement(name); + if (classPathElement != null) { + final ClassPathResource resource = classPathElement.getResource(name); if (resource == null) { - throw new IllegalStateException(providers[0] + " from " + getName() + " (closed=" + this.isClosed() - + ") was expected to provide " + name + " but failed"); + throw new IllegalStateException( + classPathElement + " from " + getName() + " (closed=" + this.isClosed() + + ") was expected to provide " + name + " but failed"); } return resource.getUrl(); } } else { - for (ClassPathElement i : elements) { - ClassPathResource res = i.getResource(name); - if (res != null) { - //if the requested name ends with a trailing / we make sure - //that the resource is a directory, and return a URL that ends with a / - //this matches the behaviour of URLClassLoader - if (endsWithTrailingSlash) { - if (res.isDirectory()) { - try { - return new URL(res.getUrl().toString() + "/"); - } catch (MalformedURLException e) { - throw new RuntimeException(e); - } + URL url = getClassPathElementResourceUrl(normalPriorityElements, name, endsWithTrailingSlash); + if (url != null) { + return url; + } + url = getClassPathElementResourceUrl(lesserPriorityElements, name, endsWithTrailingSlash); + if (url != null) { + return url; + } + } + return parent.getResource(unsanitisedName); + } + + private static URL getClassPathElementResourceUrl(List classPathElements, String name, + boolean endsWithTrailingSlash) { + for (int i = 0; i < classPathElements.size(); i++) { + ClassPathResource res = classPathElements.get(i).getResource(name); + if (res != null) { + //if the requested name ends with a trailing / we make sure + //that the resource is a directory, and return a URL that ends with a / + //this matches the behaviour of URLClassLoader + if (endsWithTrailingSlash) { + if (res.isDirectory()) { + try { + return new URL(res.getUrl().toString() + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException(e); } - } else { - return res.getUrl(); } + } else { + return res.getUrl(); } } } - return parent.getResource(unsanitisedName); + + return null; } @Override public InputStream getResourceAsStream(String unsanitisedName) { ensureOpen(unsanitisedName); - for (ClassLoaderEventListener l : classLoaderEventListeners) { - l.openResourceStream(unsanitisedName, this.name); + for (int i = 0; i < classLoaderEventListeners.size(); i++) { + classLoaderEventListeners.get(i).openResourceStream(unsanitisedName, this.name); } String name = sanitizeName(unsanitisedName); - ClassLoaderState state = getState(); - if (state.bannedResources.contains(name)) { + ClassPathResourceIndex classPathResourceIndex = getClassPathResourceIndex(); + if (classPathResourceIndex.isBanned(name)) { return null; } //dev mode may have added some files, so we iterate to check, but not for classes if (name.endsWith(".class")) { - ClassPathElement[] providers = state.loadableResources.get(name); - if (providers != null) { - final ClassPathResource resource = providers[0].getResource(name); + ClassPathElement classPathElement = classPathResourceIndex.getFirstClassPathElement(name); + if (classPathElement != null) { + final ClassPathResource resource = classPathElement.getResource(name); if (resource == null) { - throw new IllegalStateException(providers[0] + " from " + getName() + " (closed=" + this.isClosed() - + ") was expected to provide " + name + " but failed"); + throw new IllegalStateException( + classPathElement + " from " + getName() + " (closed=" + this.isClosed() + + ") was expected to provide " + name + " but failed"); } return new ByteArrayInputStream(resource.getData()); } } else { - for (ClassPathElement i : elements) { - ClassPathResource res = i.getResource(name); - if (res != null) { - if (res.isDirectory()) { - try { - return res.getUrl().openStream(); - } catch (IOException e) { - log.debug("Ignoring exception that occurred while opening a stream for resource " + unsanitisedName, - e); - // behave like how java.lang.ClassLoader#getResourceAsStream() behaves - // and don't propagate the exception - continue; - } + InputStream inputStream = getClassPathElementResourceInputStream(normalPriorityElements, name); + if (inputStream != null) { + return inputStream; + } + inputStream = getClassPathElementResourceInputStream(lesserPriorityElements, name); + if (inputStream != null) { + return inputStream; + } + } + return parent.getResourceAsStream(unsanitisedName); + } + + private static InputStream getClassPathElementResourceInputStream(List classPathElements, String name) { + for (ClassPathElement classPathElement : classPathElements) { + ClassPathResource res = classPathElement.getResource(name); + if (res != null) { + if (res.isDirectory()) { + try { + return res.getUrl().openStream(); + } catch (IOException e) { + log.debug("Ignoring exception that occurred while opening a stream for resource " + name, + e); + // behave like how java.lang.ClassLoader#getResourceAsStream() behaves + // and don't propagate the exception + continue; } - return new ByteArrayInputStream(res.getData()); } + return new ByteArrayInputStream(res.getData()); } } - return parent.getResourceAsStream(unsanitisedName); + + return null; } /** @@ -495,17 +503,17 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE //if the interrupt bit is set then we clear it and restore it at the end boolean interrupted = Thread.interrupted(); try { - ClassLoaderState state = getState(); + ClassPathResourceIndex classPathResourceIndex = getClassPathResourceIndex(); synchronized (getClassLoadingLock(name)) { Class c = findLoadedClass(name); if (c != null) { return c; } String resourceName = fromClassNameToResourceName(name); - if (state.bannedResources.contains(resourceName)) { + if (classPathResourceIndex.isBanned(resourceName)) { throw new ClassNotFoundException(name); } - boolean parentFirst = parentFirst(resourceName, state); + boolean parentFirst = parentFirst(resourceName, classPathResourceIndex); if (parentFirst) { try { return parent.loadClass(name); @@ -513,15 +521,15 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE log.tracef("Class %s not found in parent first load from %s", name, parent); } } - ClassPathElement[] resource = state.loadableResources.get(resourceName); - if (resource != null) { - ClassPathElement classPathElement = resource[0]; - ClassPathResource classPathElementResource = classPathElement.getResource(resourceName); + ClassPathElement classPathElement = classPathResourceIndex.getFirstClassPathElement(resourceName); + if (classPathElement != null) { + final ClassPathResource classPathElementResource = classPathElement.getResource(resourceName); if (classPathElementResource != null) { //can happen if the class loader was closed byte[] data = classPathElementResource.getData(); definePackage(name, classPathElement); Class cl = defineClass(name, data, 0, data.length, - protectionDomains.computeIfAbsent(classPathElement, ClassPathElement::getProtectionDomain)); + protectionDomains.computeIfAbsent(classPathElement, + ClassPathElement::getProtectionDomain)); if (Driver.class.isAssignableFrom(cl)) { driverLoaded = true; } @@ -532,6 +540,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE if (!parentFirst) { return parent.loadClass(name); } + throw new ClassNotFoundException(name); } @@ -592,23 +601,20 @@ public List getElementsWithResource(String name, boolean local if (parent instanceof QuarkusClassLoader && !localOnly) { ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); } - ClassPathElement[] classPathElements = getState().loadableResources.get(name); - if (classPathElements == null) { + List classPathElements = getClassPathResourceIndex().getClassPathElements(name); + if (classPathElements.isEmpty()) { return ret; } - ret.addAll(Arrays.asList(classPathElements)); + ret.addAll(classPathElements); return ret; } - public List getLocalClassNames() { + public Set getLocalClassNames() { ensureOpen(); - List ret = new ArrayList<>(); - for (String name : getState().loadableResources.keySet()) { - if (name.endsWith(".class")) { - //TODO: clients of this method actually need the non-transformed variant and are transforming it back !? - ret.add(name.substring(0, name.length() - 6).replace('/', '.')); - } + Set ret = new HashSet<>(); + for (String resourceName : getClassPathResourceIndex().getLocalClassResourceNames()) { + ret.add(ClassLoaderHelper.fromResourceNameToClassName(resourceName)); } return ret; } @@ -663,17 +669,19 @@ public void close() { log.debug("Failed to clean up DB drivers"); } } - for (ClassPathElement element : elements) { - //note that this is a 'soft' close - //all resources are closed, however the CL can still be used - //but after close no resources will be held past the scope of an operation - try (ClassPathElement ignored = element) { - //the close() operation is implied by the try-with syntax - } catch (Exception e) { - log.error("Failed to close " + element, e); - } - } - for (ClassPathElement element : bannedElements) { + + closeClassPathElements(normalPriorityElements); + // parentFirstElements are part of elements so no need to close them + closeClassPathElements(lesserPriorityElements); + closeClassPathElements(bannedElements); + + ResourceBundle.clearCache(this); + + status = STATUS_CLOSED; + } + + private static void closeClassPathElements(List classPathElements) { + for (ClassPathElement element : classPathElements) { //note that this is a 'soft' close //all resources are closed, however the CL can still be used //but after close no resources will be held past the scope of an operation @@ -683,9 +691,6 @@ public void close() { log.error("Failed to close " + element, e); } } - ResourceBundle.clearCache(this); - - status = STATUS_CLOSED; } public boolean isClosed() { @@ -717,7 +722,7 @@ public String toString() { public static class Builder { final String name; final ClassLoader parent; - final List elements = new ArrayList<>(); + final List normalPriorityElements = new ArrayList<>(); final List bannedElements = new ArrayList<>(); final List parentFirstElements = new ArrayList<>(); final List lesserPriorityElements = new ArrayList<>(); @@ -745,9 +750,23 @@ public Builder(String name, ClassLoader parent, boolean parentFirst) { * @param element The element to add * @return This builder */ - public Builder addElement(ClassPathElement element) { - log.debugf("Adding elements %s to QuarkusClassLoader %s", element, name); - elements.add(element); + public Builder addNormalPriorityElement(ClassPathElement element) { + log.debugf("Adding normal priority element %s to QuarkusClassLoader %s", element, name); + normalPriorityElements.add(element); + return this; + } + + /** + * Adds an element which will only be used to load a class or resource if no normal priority + * element containing that class or resource exists. + * This is used in order control the order of elements when multiple contain the same classes + * + * @param element The element to add + * @return This builder + */ + public Builder addLesserPriorityElement(ClassPathElement element) { + log.debugf("Adding lesser priority element %s to QuarkusClassLoader %s", element, name); + lesserPriorityElements.add(element); return this; } @@ -807,19 +826,6 @@ public Builder addBannedElement(ClassPathElement element) { return this; } - /** - * Adds an element which will only be used to load a class or resource if no normal - * element containing that class or resource exists. - * This is used in order control the order of elements when multiple contain the same classes - * - * @param element The element to add - * @return This builder - */ - public Builder addLesserPriorityElement(ClassPathElement element) { - lesserPriorityElements.add(element); - return this; - } - /** * If this is true then a getResources call will always include the parent resources. *

@@ -854,8 +860,8 @@ public Builder addClassLoaderEventListeners(List class */ public QuarkusClassLoader build() { if (resettableElement != null) { - if (!elements.contains(resettableElement)) { - elements.add(0, resettableElement); + if (!normalPriorityElements.contains(resettableElement)) { + normalPriorityElements.add(0, resettableElement); } } this.classLoaderEventListeners.trimToSize(); @@ -868,20 +874,6 @@ public ClassLoader parent() { return parent; } - static final class ClassLoaderState { - - final Map loadableResources; - final Set bannedResources; - final Set parentFirstResources; - - ClassLoaderState(Map loadableResources, Set bannedResources, - Set parentFirstResources) { - this.loadableResources = loadableResources; - this.bannedResources = bannedResources; - this.parentFirstResources = parentFirstResources; - } - } - @Override public String getName() { return name; diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java index 459d5857d4567..59e2c208a4152 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingInterruptTestCase.java @@ -24,7 +24,7 @@ public void testClassLoaderWhenThreadInterrupted() throws Exception { jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp"); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) + .addNormalPriorityElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) .build(); Class c = cl.loadClass(InterruptClass.class.getName()); Assertions.assertNotEquals(c, InterruptClass.class); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingPathTreeResourceUrlTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingPathTreeResourceUrlTestCase.java index 74fc4fc2effe4..9749a6bc68455 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingPathTreeResourceUrlTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingPathTreeResourceUrlTestCase.java @@ -37,7 +37,7 @@ public void testUrlReturnedFromClassLoaderDirectory(String testPath) throws Exce jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp"); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) + .addNormalPriorityElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); @@ -75,7 +75,7 @@ public void testResourceAsStreamForDirectory(String testPath) throws Exception { try { jar.as(ExplodedExporter.class).exportExploded(tmpDir.toFile(), "tmpcltest"); final ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true)) + .addNormalPriorityElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true)) .build(); try (final InputStream is = cl.getResourceAsStream("b/")) { @@ -103,7 +103,7 @@ public void testUrlReturnedFromClassLoaderJarFile(String testPath) throws Except jar.as(ZipExporter.class).exportTo(path.toFile(), true); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(path, true)) + .addNormalPriorityElement(ClassPathElement.fromPath(path, true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java index 66b4c658f018f..191ac1a48190f 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/ClassLoadingResourceUrlTestCase.java @@ -42,7 +42,7 @@ public void testUrlReturnedFromClassLoaderDirectory(String testPath) throws Exce jar.as(ExplodedExporter.class).exportExploded(path.toFile(), "tmp"); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) + .addNormalPriorityElement(ClassPathElement.fromPath(path.resolve("tmp"), true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); @@ -80,7 +80,7 @@ public void testResourceAsStreamForDirectory(String testPath) throws Exception { try { jar.as(ExplodedExporter.class).exportExploded(tmpDir.toFile(), "tmpcltest"); final ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true)) + .addNormalPriorityElement(ClassPathElement.fromPath(tmpDir.resolve("tmpcltest"), true)) .build(); try (final InputStream is = cl.getResourceAsStream("b/")) { @@ -108,7 +108,7 @@ public void testUrlReturnedFromClassLoaderJarFile(String testPath) throws Except jar.as(ZipExporter.class).exportTo(path.toFile(), true); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(path, true)) + .addNormalPriorityElement(ClassPathElement.fromPath(path, true)) .build(); URL res = cl.getResource("a.txt"); Assertions.assertNotNull(res); @@ -135,7 +135,7 @@ public void testMemoryUrlConnections() throws Exception { Thread.sleep(2); ClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement( + .addNormalPriorityElement( new MemoryClassPathElement(Collections.singletonMap("a.txt", "hello".getBytes(StandardCharsets.UTF_8)), true)) .build(); diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java index 2885f7db5cc21..9fd4207904578 100644 --- a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloader/MultiReleaseJarTestCase.java @@ -52,7 +52,7 @@ void setUp(@TempDir Path tempDirectory) throws IOException { @Test public void shouldLoadMultiReleaseJarOnJDK9Plus() throws IOException { try (QuarkusClassLoader cl = QuarkusClassLoader.builder("test", getClass().getClassLoader(), false) - .addElement(ClassPathElement.fromPath(jarPath, true)) + .addNormalPriorityElement(ClassPathElement.fromPath(jarPath, true)) .build()) { URL resource = cl.getResource("foo.txt"); assertNotNull(resource, "foo.txt was not found in generated JAR"); diff --git a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java index cf96eab2d6c90..3e0b9bb4d5747 100644 --- a/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java +++ b/test-framework/junit5-internal/src/main/java/io/quarkus/test/QuarkusUnitTest.java @@ -595,13 +595,8 @@ public void execute(BuildContext context) { //we need to make sure all hot reloadable classes are application classes context.produce(new ApplicationClassPredicateBuildItem(new Predicate() { @Override - public boolean test(String s) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(s.replace(".", "/") + ".class", true); - return !res.isEmpty(); + public boolean test(String className) { + return QuarkusClassLoader.isApplicationClass(className); } })); } diff --git a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java index 0f881f19022ce..2558f437c6dab 100644 --- a/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java +++ b/test-framework/junit5/src/main/java/io/quarkus/test/junit/QuarkusTestExtension.java @@ -70,7 +70,6 @@ import io.quarkus.bootstrap.app.AugmentAction; import io.quarkus.bootstrap.app.RunningQuarkusApplication; import io.quarkus.bootstrap.app.StartupAction; -import io.quarkus.bootstrap.classloading.ClassPathElement; import io.quarkus.bootstrap.classloading.QuarkusClassLoader; import io.quarkus.bootstrap.logging.InitialConfigurator; import io.quarkus.builder.BuildChainBuilder; @@ -1244,13 +1243,8 @@ public void execute(BuildContext context) { //we need to make sure all hot reloadable classes are application classes context.produce(new ApplicationClassPredicateBuildItem(new Predicate() { @Override - public boolean test(String s) { - QuarkusClassLoader cl = (QuarkusClassLoader) Thread.currentThread() - .getContextClassLoader(); - //if the class file is present in this (and not the parent) CL then it is an application class - List res = cl - .getElementsWithResource(s.replace(".", "/") + ".class", true); - return !res.isEmpty(); + public boolean test(String className) { + return QuarkusClassLoader.isApplicationClass(className); } })); } From 6f0e8022d020f7ff2c6804ab159111ae922b74bc Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 15 Aug 2024 15:57:44 +0200 Subject: [PATCH 2/6] Make impl of ClassPathResourceIndex#getResourceKey() faster Previous implementation: Warmup Iteration 7: 63202.300 ops/ms Warmup Iteration 8: 64074.043 ops/ms Warmup Iteration 9: 63297.222 ops/ms Warmup Iteration 10: 63905.908 ops/ms New implementation: Warmup Iteration 6: 234089.005 ops/ms Warmup Iteration 7: 234372.742 ops/ms Warmup Iteration 8: 235722.737 ops/ms Warmup Iteration 9: 233265.148 ops/ms Also added a test to make sure we don't break it. --- .../classloading/ClassPathResourceIndex.java | 34 ++++++++----------- .../ClassPathResourceIndexTestCase.java | 24 +++++++++++++ 2 files changed, 39 insertions(+), 19 deletions(-) create mode 100644 independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndexTestCase.java diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java index a1319064cae17..3e91a4b8cdce6 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java @@ -54,6 +54,9 @@ public class ClassPathResourceIndex { // let's go with default max segments + 3 for the META-INF/versions/ part private static final int MAX_SEGMENTS_META_INF_VERSIONS = MAX_SEGMENTS_DEFAULT + 3; + private static final char SLASH = '/'; + private static final char DOT = '.'; + /** * This map is mapped by prefixes. */ @@ -148,7 +151,7 @@ public static Builder builder() { *

* Probably something we will have to tweak for corner cases but let's try to keep it fast. */ - private static String getResourceKey(String resource) { + static String getResourceKey(String resource) { // we don't really care about this part, it can be slower if (resource.startsWith(META_INF_MAVEN)) { return META_INF_MAVEN; @@ -157,7 +160,6 @@ private static String getResourceKey(String resource) { // for services, we want to reference the full path return resource; } - StringBuilder prefix = new StringBuilder(); int maxSegments; if (resource.startsWith(IO_QUARKUS)) { @@ -168,27 +170,21 @@ private static String getResourceKey(String resource) { maxSegments = MAX_SEGMENTS_DEFAULT; } - char[] charArray = resource.toCharArray(); - int segmentCount = 0; - for (char c : charArray) { - if (c == '/') { - segmentCount++; - if (segmentCount == maxSegments) { - return prefix.toString(); + int position = 0; + for (int i = 0; i < maxSegments; i++) { + int slashPosition = resource.indexOf(SLASH, position); + if (slashPosition > 0) { + position = slashPosition + 1; + } else { + if (i > 0 && resource.substring(position).indexOf(DOT) >= 0) { + break; + } else { + return resource; } } - prefix.append(c); - } - - // let's drop the file name if we haven't truncated anything and we detect it's a file - if (segmentCount > 0 && segmentCount < maxSegments) { - int lastSlash = prefix.lastIndexOf("/"); - if (prefix.substring(lastSlash).contains(".")) { - prefix.setLength(lastSlash); - } } - return prefix.toString(); + return resource.substring(0, position - 1); } public static class Builder { diff --git a/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndexTestCase.java b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndexTestCase.java new file mode 100644 index 0000000000000..91d3024a35024 --- /dev/null +++ b/independent-projects/bootstrap/core/src/test/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndexTestCase.java @@ -0,0 +1,24 @@ +package io.quarkus.bootstrap.classloading; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +public class ClassPathResourceIndexTestCase { + + @Test + public void testGetResourceKey() { + assertEquals("io/quarkus/core/deployment", + ClassPathResourceIndex.getResourceKey("io/quarkus/core/deployment/MyClass.class")); + assertEquals("io/quarkus/core/deployment", + ClassPathResourceIndex.getResourceKey("io/quarkus/core/deployment/package/MyClass.class")); + assertEquals("org/apache/commons", + ClassPathResourceIndex.getResourceKey("org/apache/commons/codec/MyClass.class")); + assertEquals("test.properties", ClassPathResourceIndex.getResourceKey("test.properties")); + assertEquals("META-INF/maven/", ClassPathResourceIndex.getResourceKey("META-INF/maven/commons-codec/file.properties")); + assertEquals("io/quarkus", ClassPathResourceIndex.getResourceKey("io/quarkus/MyClass.class")); + assertEquals("META-INF/services/my-service", ClassPathResourceIndex.getResourceKey("META-INF/services/my-service")); + assertEquals("META-INF/versions/17/io/quarkus/core", + ClassPathResourceIndex.getResourceKey("META-INF/versions/17/io/quarkus/core/deployment")); + } +} From ccb140f4cf13b104b11439a274a5e8057cf778d4 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 16 Aug 2024 11:21:14 +0200 Subject: [PATCH 3/6] Further simplify QuarkusClassLoader#isApplicationClass() --- .../classloading/QuarkusClassLoader.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index 9bc9e9ba7d02f..b1e9a4167f95e 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -49,12 +49,13 @@ public class QuarkusClassLoader extends ClassLoader implements Closeable { } public static List getElements(String resourceName, boolean onlyFromCurrentClassLoader) { - final ClassLoader ccl = Thread.currentThread().getContextClassLoader(); - if (!(ccl instanceof QuarkusClassLoader)) { - throw new IllegalStateException("The current classloader is not an instance of " - + QuarkusClassLoader.class.getName() + " but " + ccl.getClass().getName()); + if (Thread.currentThread().getContextClassLoader() instanceof QuarkusClassLoader classLoader) { + return classLoader.getElementsWithResource(resourceName, onlyFromCurrentClassLoader); } - return ((QuarkusClassLoader) ccl).getElementsWithResource(resourceName, onlyFromCurrentClassLoader); + + throw new IllegalStateException("The current classloader is not an instance of " + + QuarkusClassLoader.class.getName() + " but " + + Thread.currentThread().getContextClassLoader().getClass().getName()); } /** @@ -71,9 +72,16 @@ public static boolean isClassPresentAtRuntime(String className) { * Indicates if a given class is considered an application class. */ public static boolean isApplicationClass(String className) { - String resourceName = fromClassNameToResourceName(className); - List res = getElements(resourceName, true); - return !res.isEmpty(); + if (Thread.currentThread().getContextClassLoader() instanceof QuarkusClassLoader classLoader) { + String resourceName = fromClassNameToResourceName(className); + ClassPathResourceIndex classPathResourceIndex = classLoader.getClassPathResourceIndex(); + + return classPathResourceIndex.getFirstClassPathElement(resourceName) != null; + } + + throw new IllegalStateException("The current classloader is not an instance of " + + QuarkusClassLoader.class.getName() + " but " + + Thread.currentThread().getContextClassLoader().getClass().getName()); } /** From 87543ec1ebba0637210255426f3e59962456cc46 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Fri, 16 Aug 2024 11:25:41 +0200 Subject: [PATCH 4/6] Honor parentFirst in QuarkusClassLoader#getElementsWithResource() For whatever reason, we weren't honoring parentFirst in this method which looks like an oversight and un undesirable behavior. --- .../bootstrap/classloading/QuarkusClassLoader.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index b1e9a4167f95e..fd7c59ad37ca1 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -605,15 +605,21 @@ public List getElementsWithResource(String name) { public List getElementsWithResource(String name, boolean localOnly) { ensureOpen(name); + boolean parentFirst = parentFirst(name, getClassPathResourceIndex()); + List ret = new ArrayList<>(); - if (parent instanceof QuarkusClassLoader && !localOnly) { + + if (parentFirst && !localOnly && parent instanceof QuarkusClassLoader) { ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); } + List classPathElements = getClassPathResourceIndex().getClassPathElements(name); - if (classPathElements.isEmpty()) { - return ret; - } ret.addAll(classPathElements); + + if (!parentFirst && !localOnly && parent instanceof QuarkusClassLoader) { + ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); + } + return ret; } From 32c605e0d626c260f2bf13bc496dae3517a15903 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Mon, 26 Aug 2024 17:26:23 +0200 Subject: [PATCH 5/6] Remove a hack introduced for Liquibase that is no longer needed. With the new index, we don't need that anymore. I tested that the test introduced in this commit is still working fine (LiquibaseExtensionMigrateAtStartDirectoryChangeLogTest). Note that I fixed it as I forgot to consider the less priority elements there and I went for fixing it and then was wondering why this would be even useful. I also introduced a shortcut for empty resource. --- .../bootstrap/classloading/ClassPathResourceIndex.java | 4 ++++ .../bootstrap/classloading/QuarkusClassLoader.java | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java index 3e91a4b8cdce6..843c2397b1bd2 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java @@ -152,6 +152,10 @@ public static Builder builder() { * Probably something we will have to tweak for corner cases but let's try to keep it fast. */ static String getResourceKey(String resource) { + if (resource.isEmpty()) { + return resource; + } + // we don't really care about this part, it can be slower if (resource.startsWith(META_INF_MAVEN)) { return META_INF_MAVEN; diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index fd7c59ad37ca1..246bed37c2486 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -270,15 +270,6 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea } } } - } else if (name.isEmpty()) { - for (int i = 0; i < normalPriorityElements.size(); i++) { - List resList = normalPriorityElements.get(i).getResources(""); - for (var res : resList) { - if (res != null) { - resources.add(res.getUrl()); - } - } - } } if (!banned) { if ((resources.isEmpty() && !parentAlreadyFoundResources) || aggregateParentResources) { From dfa14559691b7496238f02c71a07f28183b397ce Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Thu, 29 Aug 2024 17:51:26 +0200 Subject: [PATCH 6/6] Clarify the API around reloadable classes Make sure we use the proper vocabulary at each level and make sure the class loader API is consistently using reloadable instead of local. Per discussion with Alexey. --- .../dev/testing/JunitTestRunner.java | 2 +- .../java/io/quarkus/paths/ArchivePathTree.java | 8 ++++---- .../io/quarkus/paths/DirectoryPathTree.java | 4 ++-- .../java/io/quarkus/paths/EmptyPathTree.java | 2 +- .../java/io/quarkus/paths/FilePathTree.java | 4 ++-- .../io/quarkus/paths/FilteredPathTree.java | 4 ++-- .../io/quarkus/paths/MultiRootPathTree.java | 10 +++++----- .../main/java/io/quarkus/paths/PathTree.java | 6 +++--- .../quarkus/paths/SharedArchivePathTree.java | 4 ++-- .../bootstrap/app/CuratedApplication.java | 4 ++-- .../classloading/ClassPathElement.java | 6 +++--- .../classloading/ClassPathResourceIndex.java | 18 +++++++++--------- .../classloading/FilteredClassPathElement.java | 4 ++-- .../classloading/MemoryClassPathElement.java | 2 +- .../classloading/PathTreeClassPathElement.java | 4 ++-- .../classloading/QuarkusClassLoader.java | 4 ++-- 16 files changed, 43 insertions(+), 43 deletions(-) diff --git a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java index f33f204c47bb0..27b73b210bbef 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/dev/testing/JunitTestRunner.java @@ -652,7 +652,7 @@ public String apply(Class aClass) { //this is a lot more complex //we need to transform the classes to make the tracing magic work QuarkusClassLoader deploymentClassLoader = (QuarkusClassLoader) Thread.currentThread().getContextClassLoader(); - Set classesToTransform = new HashSet<>(deploymentClassLoader.getLocalClassNames()); + Set classesToTransform = new HashSet<>(deploymentClassLoader.getReloadableClassNames()); Map transformedClasses = new HashMap<>(); for (String i : classesToTransform) { try { diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java index 1fc1ed20c3de8..4cfb78ce96ce4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/ArchivePathTree.java @@ -87,8 +87,8 @@ static ArchivePathTree forPath(Path path, PathFilter filter, boolean manifestEna } @Override - public boolean providesLocalResources() { - return false; + public boolean isArchiveOrigin() { + return true; } @Override @@ -246,8 +246,8 @@ protected OpenArchivePathTree(FileSystem fs) { } @Override - public boolean providesLocalResources() { - return false; + public boolean isArchiveOrigin() { + return true; } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java index 4fbd953651824..fea32a71fd2c4 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/DirectoryPathTree.java @@ -37,8 +37,8 @@ protected DirectoryPathTree(Path dir, PathFilter pathFilter, PathTreeWithManifes } @Override - public boolean providesLocalResources() { - return true; + public boolean isArchiveOrigin() { + return false; } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java index 9b0dbb7fbfcd1..847e4978cd0f3 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/EmptyPathTree.java @@ -16,7 +16,7 @@ public static EmptyPathTree getInstance() { } @Override - public boolean providesLocalResources() { + public boolean isArchiveOrigin() { return false; } diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java index 25c2afab2f2d1..e9db0a9321148 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilePathTree.java @@ -23,8 +23,8 @@ class FilePathTree implements OpenPathTree { } @Override - public boolean providesLocalResources() { - return true; + public boolean isArchiveOrigin() { + return false; } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java index 84a0ec1c67348..9e501a0fcc810 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/FilteredPathTree.java @@ -18,8 +18,8 @@ public FilteredPathTree(PathTree tree, PathFilter filter) { } @Override - public boolean providesLocalResources() { - return original.providesLocalResources(); + public boolean isArchiveOrigin() { + return original.isArchiveOrigin(); } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java index 5c69c02ccc3ef..0c9b21bcbd803 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/MultiRootPathTree.java @@ -26,17 +26,17 @@ public MultiRootPathTree(PathTree... trees) { } /** - * If at least one of the PathTrees contains local resources, we return true. + * If at least one of the PathTrees is not an archive, we return false. */ @Override - public boolean providesLocalResources() { + public boolean isArchiveOrigin() { for (PathTree tree : trees) { - if (tree.providesLocalResources()) { - return true; + if (!tree.isArchiveOrigin()) { + return false; } } - return false; + return true; } @Override diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java index 0768e189bfea9..c0c40b85a6837 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/PathTree.java @@ -81,11 +81,11 @@ static PathTree ofArchive(Path archive, PathFilter filter) { } /** - * Whether the PathTree provides local resources. + * Whether the content of this tree comes from an archive or not. *

- * For instance a directory or a file provides local resources. A jar does not. + * This is useful for instance when you want to determine if the resources can be updated in dev mode. */ - boolean providesLocalResources(); + boolean isArchiveOrigin(); /** * The roots of the path tree. diff --git a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java index e4bc62718293c..4546526403849 100644 --- a/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java +++ b/independent-projects/bootstrap/app-model/src/main/java/io/quarkus/paths/SharedArchivePathTree.java @@ -123,8 +123,8 @@ private CallerOpenPathTree(SharedOpenArchivePathTree delegate) { } @Override - public boolean providesLocalResources() { - return delegate.providesLocalResources(); + public boolean isArchiveOrigin() { + return delegate.isArchiveOrigin(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java index 38aeb072775cd..852e3c9147f89 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/app/CuratedApplication.java @@ -496,8 +496,8 @@ public Set getProvidedResources() { } @Override - public boolean providesLocalResources() { - return delegate.providesLocalResources(); + public boolean containsReloadableResources() { + return delegate.containsReloadableResources(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java index 4d0dad1967ffe..2edc3f09d1cf0 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathElement.java @@ -74,9 +74,9 @@ default ResolvedDependency getResolvedDependency() { Set getProvidedResources(); /** - * Whether this class path element contains local resources. + * Whether this class path element contains resources that can be reloaded in dev mode. */ - boolean providesLocalResources(); + boolean containsReloadableResources(); /** * @@ -133,7 +133,7 @@ public Set getProvidedResources() { } @Override - public boolean providesLocalResources() { + public boolean containsReloadableResources() { return false; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java index 843c2397b1bd2..5d6c9d1518048 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java @@ -63,25 +63,25 @@ public class ClassPathResourceIndex { private final Map resourceMapping; private final Map transformedClasses; - private final Set localClasses; + private final Set relodableClasses; private final Set parentFirstResources; private final Set bannedResources; private ClassPathResourceIndex(Map resourceMapping, Map transformedClasses, - Set localClasses, + Set reloadableClasses, Set parentFirstResources, Set bannedResources) { this.resourceMapping = resourceMapping.isEmpty() ? Map.of() : Collections.unmodifiableMap(resourceMapping); this.transformedClasses = transformedClasses.isEmpty() ? Map.of() : transformedClasses; - this.localClasses = localClasses.isEmpty() ? Set.of() : Collections.unmodifiableSet(localClasses); + this.relodableClasses = reloadableClasses.isEmpty() ? Set.of() : Collections.unmodifiableSet(reloadableClasses); this.parentFirstResources = parentFirstResources.isEmpty() ? Set.of() : Collections.unmodifiableSet(parentFirstResources); this.bannedResources = bannedResources.isEmpty() ? Set.of() : Collections.unmodifiableSet(bannedResources); } - public Set getLocalClassResourceNames() { - return localClasses; + public Set getReloadableClasses() { + return relodableClasses; } public boolean isParentFirst(String resource) { @@ -199,7 +199,7 @@ public static class Builder { private final Map transformedClasses = new HashMap<>(); private final Map> resourceMapping = new HashMap<>(); - private final Set localClasses = new HashSet<>(); + private final Set reloadableClasses = new HashSet<>(); private final Set parentFirstResources = new HashSet<>(); private final Set bannedResources = new HashSet<>(); @@ -215,8 +215,8 @@ public void addTranformedClassCandidate(ClassPathElement classPathElement, Strin } public void addResourceMapping(ClassPathElement classPathElement, String resource) { - if (classPathElement.providesLocalResources() && resource.endsWith(CLASS_SUFFIX)) { - localClasses.add(resource); + if (classPathElement.containsReloadableResources() && resource.endsWith(CLASS_SUFFIX)) { + reloadableClasses.add(resource); } ClassPathElement transformedClassClassPathElement = transformedClassCandidates.get(resource); @@ -255,7 +255,7 @@ public ClassPathResourceIndex build() { } return new ClassPathResourceIndex(compactedResourceMapping, transformedClasses, - localClasses, parentFirstResources, bannedResources); + reloadableClasses, parentFirstResources, bannedResources); } } } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java index d2c8f42c36295..153cf63369b43 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/FilteredClassPathElement.java @@ -59,8 +59,8 @@ public Set getProvidedResources() { } @Override - public boolean providesLocalResources() { - return delegate.providesLocalResources(); + public boolean containsReloadableResources() { + return delegate.containsReloadableResources(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java index 54b449f933f36..3a38c59df55e1 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/MemoryClassPathElement.java @@ -114,7 +114,7 @@ public Set getProvidedResources() { } @Override - public boolean providesLocalResources() { + public boolean containsReloadableResources() { return true; } diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java index 5c6cbf25d19c9..776ad705a0751 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/PathTreeClassPathElement.java @@ -176,8 +176,8 @@ public void visitPath(PathVisit visit) { } @Override - public boolean providesLocalResources() { - return pathTree.providesLocalResources(); + public boolean containsReloadableResources() { + return !pathTree.isArchiveOrigin(); } @Override diff --git a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java index 246bed37c2486..0229da5591c61 100644 --- a/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/QuarkusClassLoader.java @@ -614,11 +614,11 @@ public List getElementsWithResource(String name, boolean local return ret; } - public Set getLocalClassNames() { + public Set getReloadableClassNames() { ensureOpen(); Set ret = new HashSet<>(); - for (String resourceName : getClassPathResourceIndex().getLocalClassResourceNames()) { + for (String resourceName : getClassPathResourceIndex().getReloadableClasses()) { ret.add(ClassLoaderHelper.fromResourceNameToClassName(resourceName)); } return ret;