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/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 f8e4280b4f463..9a7f23bad69a5 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..ae6efe1dde43b 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.addElement(element); } else if (dep.isFlagSet(DependencyFlags.CLASSLOADER_LESSER_PRIORITY)) { builder.addLesserPriorityElement(element); + } else { + builder.addElement(element); } - builder.addElement(element); } public synchronized QuarkusClassLoader getOrCreateAugmentClassLoader() { @@ -493,6 +495,11 @@ public Set 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..3c09c9fb55358 --- /dev/null +++ b/independent-projects/bootstrap/core/src/main/java/io/quarkus/bootstrap/classloading/ClassPathResourceIndex.java @@ -0,0 +1,229 @@ +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; + +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 (ClassPathElement candidate : candidates) { + if (candidate.getProvidedResources().contains(resource)) { + return candidate; + } + } + + 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 (ClassPathElement candidate : candidates) { + if (candidate.getProvidedResources().contains(resource)) { + classPathElements.add(candidate); + } + } + return Collections.unmodifiableList(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..5a99249ebd75d 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,9 @@ 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 +27,7 @@ import org.jboss.logging.Logger; +import io.quarkus.commons.classloading.ClassLoaderHelper; import io.quarkus.paths.ManifestAttributes; /** @@ -59,15 +58,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 +68,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. @@ -96,7 +95,11 @@ public static boolean isResourcePresentAtRuntime(String resourcePath) { } private final String name; + // the ClassPathElement to consider are elements + lesserPriorityElements private final List elements; + 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; @@ -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; } } @@ -210,11 +210,11 @@ public Enumeration getResources(String unsanitisedName, boolean parentAlrea for (ClassLoaderEventListener l : classLoaderEventListeners) { l.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,8 +239,8 @@ 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 providers = classPathResourceIndex.getClassPathElements(name); + if (!providers.isEmpty()) { boolean endsWithTrailingSlash = unsanitisedName.endsWith("/"); for (ClassPathElement element : providers) { Collection resList = element.getResources(name); @@ -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<>(); + classPathResourceIndex = this.classPathResourceIndex; + if (classPathResourceIndex == null) { + ClassPathResourceIndex.Builder classPathResourceIndexBuilder = ClassPathResourceIndex.builder(); + + classPathResourceIndexBuilder.scanClassPathElement(transformedClasses, + classPathResourceIndexBuilder::addTranformedClassCandidate); + 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); - } - } + 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,37 +342,52 @@ 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(elements, 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 (ClassPathElement classPathElement : classPathElements) { + ClassPathResource res = classPathElement.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 @@ -405,41 +398,55 @@ public InputStream getResourceAsStream(String unsanitisedName) { l.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(elements, 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); } @@ -592,23 +600,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 +668,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(elements); + // 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 +690,6 @@ public void close() { log.error("Failed to close " + element, e); } } - ResourceBundle.clearCache(this); - - status = STATUS_CLOSED; } public boolean isClosed() { @@ -868,20 +872,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/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 915f33f98eda1..b436ad12fb561 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; @@ -1253,13 +1252,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); } })); }