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/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/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..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 @@ -86,6 +86,11 @@ static ArchivePathTree forPath(Path path, PathFilter filter, boolean manifestEna this.pathFilter = pathFilter; } + @Override + public boolean isArchiveOrigin() { + return true; + } + @Override public Collection getRoots() { return List.of(archive); @@ -240,6 +245,11 @@ protected OpenArchivePathTree(FileSystem fs) { this.rootPath = fs.getPath("/"); } + @Override + public boolean isArchiveOrigin() { + return true; + } + @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..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 @@ -36,6 +36,11 @@ protected DirectoryPathTree(Path dir, PathFilter pathFilter, PathTreeWithManifes this.dir = dir; } + @Override + public boolean isArchiveOrigin() { + return false; + } + @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..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 @@ -15,6 +15,11 @@ public static EmptyPathTree getInstance() { return INSTANCE; } + @Override + public boolean isArchiveOrigin() { + 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..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 @@ -22,6 +22,11 @@ class FilePathTree implements OpenPathTree { this.pathFilter = pathFilter; } + @Override + public boolean isArchiveOrigin() { + return false; + } + @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..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 @@ -17,6 +17,11 @@ public FilteredPathTree(PathTree tree, PathFilter filter) { this.filter = Objects.requireNonNull(filter, "filter is null"); } + @Override + public boolean isArchiveOrigin() { + return original.isArchiveOrigin(); + } + @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..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 @@ -25,6 +25,20 @@ public MultiRootPathTree(PathTree... trees) { roots = tmp; } + /** + * If at least one of the PathTrees is not an archive, we return false. + */ + @Override + public boolean isArchiveOrigin() { + for (PathTree tree : trees) { + if (!tree.isArchiveOrigin()) { + return false; + } + } + + return true; + } + @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..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 @@ -80,6 +80,13 @@ static PathTree ofArchive(Path archive, PathFilter filter) { return ArchivePathTree.forPath(archive, filter); } + /** + * Whether the content of this tree comes from an archive or not. + *

+ * This is useful for instance when you want to determine if the resources can be updated in dev mode. + */ + 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 0ea0048549c8b..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 @@ -122,6 +122,11 @@ private CallerOpenPathTree(SharedOpenArchivePathTree delegate) { this.delegate = delegate; } + @Override + public boolean isArchiveOrigin() { + return delegate.isArchiveOrigin(); + } + @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..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 @@ -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 containsReloadableResources() { + return delegate.containsReloadableResources(); + } + @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..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 @@ -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 resources that can be reloaded in dev mode. + */ + boolean containsReloadableResources(); + /** * * @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 containsReloadableResources() { + 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..5d6c9d1518048 --- /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; + + private static final char SLASH = '/'; + private static final char DOT = '.'; + + /** + * This map is mapped by prefixes. + */ + private final Map resourceMapping; + private final Map transformedClasses; + + private final Set relodableClasses; + private final Set parentFirstResources; + private final Set bannedResources; + + private ClassPathResourceIndex(Map resourceMapping, + Map transformedClasses, + Set reloadableClasses, + Set parentFirstResources, + Set bannedResources) { + this.resourceMapping = resourceMapping.isEmpty() ? Map.of() : Collections.unmodifiableMap(resourceMapping); + this.transformedClasses = transformedClasses.isEmpty() ? Map.of() : transformedClasses; + 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 getReloadableClasses() { + return relodableClasses; + } + + 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. + */ + 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; + } + if (resource.startsWith(META_INF_SERVICES)) { + // for services, we want to reference the full path + return resource; + } + + 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; + } + + 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; + } + } + } + + return resource.substring(0, position - 1); + } + + 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 reloadableClasses = 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.containsReloadableResources() && resource.endsWith(CLASS_SUFFIX)) { + reloadableClasses.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, + 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 a8dbbd2d06ec7..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 @@ -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 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 d6c3864673c55..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 @@ -113,6 +113,11 @@ public Set getProvidedResources() { return resources.keySet(); } + @Override + public boolean containsReloadableResources() { + 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..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 @@ -175,6 +175,11 @@ public void visitPath(PathVisit visit) { return resources; } + @Override + public boolean containsReloadableResources() { + return !pathTree.isArchiveOrigin(); + } + @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..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 @@ -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; /** @@ -51,21 +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); - } - 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; + throw new IllegalStateException("The current classloader is not an instance of " + + QuarkusClassLoader.class.getName() + " but " + + Thread.currentThread().getContextClassLoader().getClass().getName()); } /** @@ -78,6 +68,22 @@ 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) { + 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()); + } + /** * 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 +92,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 +103,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 +116,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 +132,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 +156,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 +188,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 +201,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 +215,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 +247,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 { @@ -261,15 +270,6 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea } } } - } else if (name.isEmpty()) { - for (ClassPathElement i : elements) { - List resList = i.getResources(""); - for (var res : resList) { - if (res != null) { - resources.add(res.getUrl()); - } - } - } } if (!banned) { if ((resources.isEmpty() && !parentAlreadyFoundResources) || aggregateParentResources) { @@ -287,64 +287,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 +333,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 +342,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 +502,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 +520,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 +539,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE if (!parentFirst) { return parent.loadClass(name); } + throw new ClassNotFoundException(name); } @@ -588,27 +596,30 @@ 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)); } - ClassPathElement[] classPathElements = getState().loadableResources.get(name); - if (classPathElements == null) { - return ret; + + List classPathElements = getClassPathResourceIndex().getClassPathElements(name); + ret.addAll(classPathElements); + + if (!parentFirst && !localOnly && parent instanceof QuarkusClassLoader) { + ret.addAll(((QuarkusClassLoader) parent).getElementsWithResource(name)); } - ret.addAll(Arrays.asList(classPathElements)); + return ret; } - public List getLocalClassNames() { + public Set getReloadableClassNames() { 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().getReloadableClasses()) { + ret.add(ClassLoaderHelper.fromResourceNameToClassName(resourceName)); } return ret; } @@ -663,17 +674,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 +696,6 @@ public void close() { log.error("Failed to close " + element, e); } } - ResourceBundle.clearCache(this); - - status = STATUS_CLOSED; } public boolean isClosed() { @@ -717,7 +727,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 +755,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 +831,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 +865,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 +879,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/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")); + } +} 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); } })); }