| Artifact | io.github.microsphere-projects:microsphere-java-core (0.3.20-SNAPSHOT, release branch) |
| Package | io.microsphere.classloading — 16 main-source types |
| JDK level | Compiled at Java 8; CI-tested on JDK 8 / 11 / 17 / 21 / 25 |
Depends on io.microsphere.util |
ClassLoaderUtils (findAllClassPathURLs, getClassResource, getClassLoader, removeClassPathURL), ClassPathUtils.getBootstrapClassPaths(), ServiceLoaderUtils.loadServicesList, ObjectUtils, StringUtils, ArrayUtils, Assert, util.jar.JarUtils |
Other core packages used |
io.microsphere.net.URLUtils (archive/base-path parsing), io.microsphere.reflect (field + method access), io.microsphere.io / io.scanner, io.microsphere.logging, io.microsphere.constants, io.microsphere.lang.Prioritized, io.microsphere.annotation.* (from microsphere-java-annotations) |
| External dependencies | none — detection is pure JDK + SPI |
This package answers one runtime question — which artifact (groupId / artifactId / version) does this class, resource
or classpath entry come from? — and adds the machinery to remove a classpath entry when an artifact must not be
present (BannedArtifactClassLoadingExecutor).
ArtifactDetector never parses java.class.path itself. It calls
ClassLoaderUtils.findAllClassPathURLs(classLoader), which walks the class-loader parent chain and delegates each
loader to the single, statically-held ServiceLoadingURLClassPathHandle
(microsphere-java-core/src/main/java/io/microsphere/util/ClassLoaderUtils.java:94, :1550-1564). That facade picks
one registered URLClassPathHandle — the first whose supports() returns true after priority sorting
(classloading/ServiceLoadingURLClassPathHandle.java:36-43).
| JDK 8 | JDK 11 | JDK 17 | JDK 21 | JDK 25 | |
|---|---|---|---|---|---|
| System loader type | sun.misc.Launcher$AppClassLoader (a URLClassLoader) |
jdk.internal.loader.ClassLoaders$AppClassLoader |
same | same | same |
| Handle that wins | ClassicURLClassPathHandle (sun.misc.URLClassPath) |
ModernURLClassPathHandle (jdk.internal.loader.URLClassPath) |
Modern | Modern | Modern |
| URL field read | urls |
unopenedUrls |
unopenedUrls |
unopenedUrls |
unopenedUrls |
| Entry source | classpath URL parsing + URLClassPath#getURLs() reflection |
URLClassPath#getURLs() reflection; jrt: / module-path entries surface as URLs from the loader, not from JPMS module reading |
same | same | same |
removeURL(...) target |
URLClassLoader.ucp |
BuiltinClassLoader.ucp (needs an open jdk.internal.loader) |
same | same | same |
| If neither handle resolves its class | — | NoOpURLClassPathHandle → getURLs() returns the shared empty array, removeURL() returns false |
same | same | same |
| Winning resolver order | Maven(1) → Manifest(5) → ArchiveFile(9) | identical | identical | identical | identical |
Two facts worth pinning down:
ClassicURLClassPathHandle.supports()is exactly "sun.misc.URLClassPathis loadable" andModernURLClassPathHandle.supports()is "jdk.internal.loader.URLClassPathis loadable" (AbstractURLClassPathHandle.java:114-117,:178-186). The test suite states the same rule from the outside:assertEquals(CURRENT_JAVA_VERSION.le(JAVA_VERSION_8), handle.supports())(.../test/java/io/microsphere/classloading/ClassicURLClassPathHandleTest.java:44-47) andassertEquals(CURRENT_JAVA_VERSION.ge(JAVA_VERSION_9), handle.supports())(ModernURLClassPathHandleTest.java:41-43).- No type in this package reads JPMS module descriptors. On JDK 9+ everything still flows through the loader's
URLClassPath, so module-path andjrt:entries are visible only insofar as the loader exposes them as URLs. JDK-library filtering therefore degrades on 9+:ArtifactDetector.removeJdkClassPathURLs(...)(ArtifactDetector.java:164-175) drops entries contained inClassPathUtils.getBootstrapClassPaths()or containingjava.home. On JDK 9+RuntimeMXBean.isBootClassPathSupported()isfalse, so the boot set is empty (ClassPathUtils.java:68-70,:111-112) and only theJAVA_HOMEcontainment check remains effective.
| Name | Read by | Default | Notes |
|---|---|---|---|
microsphere.artifact-id.manifest-attribute-names |
ManifestArtifactResourceResolver (:128-134) |
Bundle-Name,Automatic-Module-Name,Implementation-Title |
comma-separated, first non-null attribute wins |
microsphere.artifact-version.manifest-attribute-names |
ManifestArtifactResourceResolver (:147-153) |
Bundle-Version,Implementation-Version |
comma-separated, first non-null wins |
microsphere.service-loader.cached |
ServiceLoaderUtils (:104-111), affects URLClassPathHandle discovery |
false |
ArtifactDetector requests its resolvers with cached = true explicitly (ArtifactDetector.java:92) |
java.home |
ArtifactDetector.JAVA_HOME_PATH (:26, :80) |
— | non-JDK filter; captured once per JVM |
file.encoding |
BannedArtifactClassLoadingExecutor.ENCODING (:21, :69) |
— | charset used to read META-INF/banned-artifacts |
Important
The manifest attribute lists and JAVA_HOME are resolved in static initializers
(ManifestArtifactResourceResolver.java:162-164, ArtifactDetector.java:80). Set them with -D on the command
line; changing them later with System.setProperty has no effect on an already-loaded class.
java -Dmicrosphere.artifact-version.manifest-attribute-names=Implementation-Version,Specification-Version \
-jar app.jarFourteen of the sixteen types record Javadoc @since 1.0.0 (all Artifact*, *URLClassPathHandle,
BannedArtifactClassLoadingExecutor, StreamArtifactResourceResolver). Artifact and ArtifactResourceResolver
record no version at all in their source, and the package uses no @Since annotation, so for those two the first
released version cannot be established from the code. release-notes.md records two later, behaviour-visible
changes: the ArtifactDetector detection API was refactored in 0.2.7 (release-notes.md:5-15), and
StreamArtifactResourceResolver was refactored together with JarUtils in 0.3.4 (release-notes.md:135-152).
| Item | Value | Evidence |
|---|---|---|
| Documented line | release, ${revision} = 0.3.20-SNAPSHOT |
root pom.xml:54 |
| Newest published release | 0.3.20, tagged 2026-10-16 |
the 0.3.20 tag resolves to chore: merge main into release [skip ci], committed 2026-10-16T03:43:20Z |
| Previous release | 0.3.19, tagged 2026-08-30 |
the 0.3.19 tag, committed 2026-08-30T12:51:37Z |
| Changelog source | release-notes.md at the repository root |
The project publishes no CHANGELOG.md; the v0.3.16–v0.3.20 blocks of release-notes.md read Release notes generation failed and carry raw commit lists, so every entry below is quoted as a commit subject and checked against the release sources. |
0.3.20 adds no new API surface for this area — the 0.3.19...0.3.20 range is a hardening release. What it does change, and how far that reaches the sources this page documents:
- Nullability contracts. 9 of 16 main sources in the scope of this page declare
@Nonnull/@Nullable. Subjects in the range that name this area:4b409125 Add nullability annotations in classloading/9d1f4a78 Add nullability annotations to URL classpath handle/e3a84c77 Add nullability annotations to resolvers/e78c5585 Restrict resolver constructor visibility. - Null-input and behaviour fixes.
bc5cb8ea Harden class loader handling in resolver base/9e9f33f3 Fix null-safe artifact matching/f95fd45f Strengthen ClassLoaderUtils tests/973a4f13 Relax class loader test assertions.
Note
Reading the commit list: This is the densest area of the release: four of the nullability subjects land here, and e78c5585 Restrict resolver constructor visibility is a signature-level change — check it against your own subclasses of the resolver base.
Signatures below are copied from the sources on release.
// Artifact.java — immutable value object; "*" = wildcard, "?" = UNKNOWN
public class Artifact {
public static final String WILDCARD; // "*"
public static final String UNKNOWN; // "?"
public Artifact(@Nonnull String artifactId, @Nullable String version, @Nullable URL location)
public static Artifact create(@Nonnull String artifactId) // version = UNKNOWN
public static Artifact create(@Nonnull String artifactId, @Nullable String version)
public static Artifact create(@Nonnull String artifactId, @Nullable String version, @Nullable URL location)
@Nonnull public String getArtifactId()
@Nullable public String getVersion()
@Nullable public URL getLocation()
public boolean matches(Artifact artifact) // artifactId AND version
protected boolean matchesArtifactId(Artifact artifact)
protected boolean matchesVersion(Artifact artifact)
protected boolean matches(Artifact artifact, Function<Artifact, String> getterFunction)
}
// MavenArtifact.java — adds groupId to identity and matching
public class MavenArtifact extends Artifact {
public MavenArtifact(@Nonnull String groupId, @Nonnull String artifactId,
@Nullable String version, @Nullable URL location)
public static MavenArtifact create(@Nonnull String groupId, @Nonnull String artifactId) // version = UNKNOWN
public static MavenArtifact create(String groupId, String artifactId, @Nullable String version)
public static MavenArtifact create(String groupId, String artifactId, @Nullable String version, @Nullable URL location)
@Nonnull public String getGroupId()
@Override public boolean matches(Artifact artifact) // groupId AND artifactId AND version
}
// ArtifactDetector.java — the entry point; instance-based, no static shortcuts
public class ArtifactDetector {
public ArtifactDetector() // this class's class loader
public ArtifactDetector(@Nullable ClassLoader classLoader) // null -> getClassLoader(ArtifactDetector.class)
@Nonnull @Immutable public List<Artifact> detect() // detect(true): JDK libraries INCLUDED
@Nonnull @Immutable public List<Artifact> detect(boolean includedJdkLibraries)
@Nonnull @Immutable public List<Artifact> detect(@Nullable Set<URL> classPathURLs)
@Nullable public Artifact detect(@Nonnull Class<?> classInResource)
@Nullable public Artifact detect(@Nonnull URL classPathURL) // first non-null resolver wins
protected Set<URL> getClassPathURLs(boolean includedJdkLibraries)
}
// ArtifactResourceResolver.java — the SPI
public interface ArtifactResourceResolver extends Prioritized {
@Nullable Artifact resolve(@Nullable URL resourceURL);
}
public abstract class AbstractArtifactResourceResolver implements ArtifactResourceResolver {
public AbstractArtifactResourceResolver(int priority) // default class loader
public AbstractArtifactResourceResolver(ClassLoader classLoader, int priority)
@Override public final int getPriority() // final: set it via the constructor
protected final Logger logger; protected final ClassLoader classLoader; protected final int priority;
}
public abstract class StreamArtifactResourceResolver extends AbstractArtifactResourceResolver {
public StreamArtifactResourceResolver(int priority)
public StreamArtifactResourceResolver(ClassLoader classLoader, int priority) // null classLoader -> IllegalArgumentException
@Override public final Artifact resolve(URL resourceURL) // final: locates and opens the metadata stream
protected abstract boolean isArtifactMetadata(String relativePath);
protected abstract Artifact resolve(URL resourceURL, InputStream artifactMetadataData,
ClassLoader classLoader) throws IOException;
}
public class MavenArtifactResourceResolver extends StreamArtifactResourceResolver {
public static final int DEFAULT_PRIORITY = 1;
public static final String MAVEN_POM_PROPERTIES_RESOURCE_PREFIX = "META-INF/maven/";
public static final String MAVEN_POM_PROPERTIES_RESOURCE_SUFFIX = "/pom.properties";
public MavenArtifactResourceResolver() / (int priority) / (ClassLoader, int)
}
public class ManifestArtifactResourceResolver extends StreamArtifactResourceResolver {
public static final int DEFAULT_PRIORITY = 5;
public ManifestArtifactResourceResolver() / (int priority) / (ClassLoader, int)
}
public class ArchiveFileArtifactResourceResolver extends AbstractArtifactResourceResolver {
public static final int DEFAULT_PRIORITY = 9;
public ArchiveFileArtifactResourceResolver() / (int priority) / (ClassLoader, int)
@Override public Artifact resolve(URL resourceURL) // NOT final
}
// URLClassPathHandle family
public interface URLClassPathHandle extends Prioritized {
boolean supports();
@Nonnull default URL[] getURLs(@Nullable ClassLoader classLoader) // via findURLClassLoader, else empty
default boolean initializeLoaders(@Nullable ClassLoader classLoader) // findResource("just-for-initializing-loaders")
boolean removeURL(@Nullable ClassLoader classLoader, @Nullable URL url);
@Override default int getPriority() { return MIN_PRIORITY; }
}
public abstract class AbstractURLClassPathHandle implements URLClassPathHandle, Prioritized {
public static final int DEFAULT_PRIORITY = MAX_PRIORITY + 99999; // Integer.MIN_VALUE + 99999: highest precedence
@Override public boolean supports() // URLClassPath class resolvable
@Override public URL[] getURLs(ClassLoader classLoader) // ucp.getURLs() reflectively
@Override public final boolean removeURL(ClassLoader classLoader, URL url) // synchronized on urls; false only for null args (see pitfall 6)
public final void setPriority(int priority); public final int getPriority();
protected abstract String getURLClassPathClassName();
protected abstract String getUrlsFieldName();
}
public class ClassicURLClassPathHandle extends AbstractURLClassPathHandle {} // sun.misc.URLClassPath / "urls"
public class ModernURLClassPathHandle extends AbstractURLClassPathHandle {} // jdk.internal.loader.URLClassPath / "unopenedUrls"
public class NoOpURLClassPathHandle implements URLClassPathHandle {} // supports()=true, empty URLs, remove=false
public class ServiceLoadingURLClassPathHandle implements URLClassPathHandle {} // SPI facade used by ClassLoaderUtils
// BannedArtifactClassLoadingExecutor.java
public class BannedArtifactClassLoadingExecutor {
public static final String CONFIG_LOCATION = "META-INF/banned-artifacts";
public BannedArtifactClassLoadingExecutor() / (@Nullable ClassLoader classLoader)
public void execute() // detect non-JDK artifacts, remove matching classpath URLs
}microsphere-java-core/src/main/resources/META-INF/services/io.microsphere.classloading.ArtifactResourceResolver
io.microsphere.classloading.MavenArtifactResourceResolver
io.microsphere.classloading.ManifestArtifactResourceResolver
io.microsphere.classloading.ArchiveFileArtifactResourceResolver
microsphere-java-core/src/main/resources/META-INF/services/io.microsphere.classloading.URLClassPathHandle
io.microsphere.classloading.ClassicURLClassPathHandle
io.microsphere.classloading.ModernURLClassPathHandle
io.microsphere.classloading.NoOpURLClassPathHandle
StreamArtifactResourceResolver, AbstractArtifactResourceResolver and ServiceLoadingURLClassPathHandle are not
registered — the first two are bases, the third is instantiated directly by ClassLoaderUtils.
| Resolver | Priority | Reads | Produces |
|---|---|---|---|
MavenArtifactResourceResolver |
1 | the first META-INF/maven/**/pom.properties at the archive root |
MavenArtifact (real groupId) |
ManifestArtifactResourceResolver |
5 | META-INF/MANIFEST.MF main attributes |
Artifact, or null if no artifact-id attribute matches |
ArchiveFileArtifactResourceResolver |
9 | the archive file name, [artifactId]-[version].ext |
Artifact; version null when the name has no hyphen |
Detect the artifact that supplies a class — from
microsphere-java-core/src/test/java/io/microsphere/classloading/ArtifactDetectorTest.java:53-65:
ArtifactDetector detector = new ArtifactDetector();
Artifact self = detector.detect(ArtifactDetector.class); // null in an exploded target/classes dir
Artifact jsr305 = detector.detect(javax.annotation.Nonnull.class);
// test asserts: jsr305.getArtifactId() == "jsr305", jsr305.getVersion() == "3.0.2"Full classpath scan, with and without JDK entries — same file, :46-51 and :90-101:
List<Artifact> all = detector.detect(); // detect(true) — JDK entries included
List<Artifact> appOnly = detector.detect(false); // bootstrap + java.home entries removed
appOnly.forEach(a -> System.out.printf("%s %s%n", a.getArtifactId(), a.getVersion()));Resolving one archive directly — MavenArtifactResourceResolverTest.java:16-24 and
StreamArtifactResourceResolverTest.java:46-67:
ArtifactResourceResolver resolver = new MavenArtifactResourceResolver();
Artifact artifact = resolver.resolve(classResourceUrlOf(javax.annotation.Nonnull.class));
assertTrue(artifact instanceof MavenArtifact); // com.google.code.findbugs:jsr305:3.0.2File-name fallback — ArchiveFileArtifactResourceResolverTest.java:50-63:
// package-private helper exercised by the test
Artifact a = ArchiveFileArtifactResourceResolver.createArtifact("a-1.0.jar", null); // a / "1.0"
Artifact b = ArchiveFileArtifactResourceResolver.createArtifact("a.jar", null); // a / nullRemove a classpath entry — ServiceLoadingURLClassPathHandleTest.java:61-69 copies the current classpath into a
throw-away URLClassLoader and removes every entry through the handle; BaseURLClassPathHandleTest.java:58-69
removes the jmh-generator-annprocess jar from the test class loader only when handle.supports().
Ban an artifact at startup — BannedArtifactClassLoadingExecutorTest.java:38-42:
new BannedArtifactClassLoadingExecutor(classLoader).execute();# src/main/resources/META-INF/banned-artifacts — exactly three colon-separated fields per line
commons-logging:commons-logging:1.2
log4j:log4j:*
- Shaded / fat jars.
StreamArtifactResourceResolver.findArtifactMetadataEntry(...)returns the first matching entry of the archive (StreamArtifactResourceResolver.java:179-182), so a shaded jar reports whichever bundledpom.propertiestheJarFileenumeration happens to yield first — not your artifact's coordinates. Relocate the metadata or accept the Manifest/file-name fallback. - Nested jars are invisible.
MavenArtifactResourceResolver.isArtifactMetadata(...)requiresrelativePath.indexOf("META-INF/maven/") == 0(MavenArtifactResourceResolver.java:101-110). Spring Boot'sBOOT-INF/lib/*.jarentries never satisfy it, and for ajar:file:/app.jar!/BOOT-INF/lib/x.jar!/URLURLUtils.resolveArchiveFile(...)yields aFilethat does not exist, soresolveArchiveFilereturnsnullandreadArtifactMetadataDataFromResource(...)returnsnullby default (StreamArtifactResourceResolver.java:112-139). Result: no artifact for nested entries. - OSGi / custom launchers. Bundles usually expose only
MANIFEST.MF, so theBundle-Name/Bundle-Versionattributes carry the identification; if your bundles useBundle-SymbolicNameinstead, add it viamicrosphere.artifact-id.manifest-attribute-names, otherwise the resolver returnsnull(ManifestArtifactResourceResolver.java:202-214). - Exploded directories. Classes running from
target/classes,build/classesor an IDE output dir typically resolve tonull(asserted inArtifactDetectorTest.java:58-59); the file-name resolver rejects directories outright (ArchiveFileArtifactResourceResolver.java:86-89). - JDK 9+ system loaders are not
URLClassLoader.URLClassPathHandle.getURLs(...)default falls back toClassLoaderUtils.findURLClassLoader(...), which returnsnull→EMPTY_URL_ARRAY. TheAbstracthandles get around this by reading the privateucpfield, butClassLoaderUtils.removeClassPathURL(...)still refuses any loader that is not aSecureClassLoader(ClassLoaderUtils.java:1598-1603). - Strong encapsulation makes
removeURLthrow, not returnfalse. The privatejdk.internal.loaderinternals are not open by default on JDK 17/21/25. If you callremoveURL(...)(directly or throughClassLoaderUtils.removeClassPathURL(...)) against the application loader without--add-opens java.base/jdk.internal.loader=ALL-UNNAMED, the reflective read is rejected and the call propagatesIllegalStateException(wrappingIllegalAccessException,FieldUtils.java:826-841); if the loader simply has no such field,removeURLraisesNullPointerExceptionatAbstractURLClassPathHandle.java:147. Onlynullarguments produce a cleanfalse. Launch with the--add-opensflag, or catch around the call. detect(true)includes JDK entries withUNKNOWN/manifest-derived ids. This is expected output, not a bug; usedetect(false)for an application-only view.- Banned-artifacts parsing stops early. Reading halts at the first blank line
(
BannedArtifactClassLoadingExecutor.java:117-125) and any line without exactly three fields throwsIllegalArgumentException(:129-138). Comments are not supported — a# ...line will fail. execute()never reports a ban. A matched artifact is dropped throughClassLoaderUtils.removeClassPathURL(...)(:84-95) and the return value is ignored. It returnsfalse— a silent no-op — when the loader is not aSecureClassLoader, but it throws when the loader is one whose internals are encapsulated (see pitfall 6), becauseexecute()catches nothing. Verify removals in integration tests if you rely on this as a guardrail.- Wildcards are per-field.
Artifact.WILDCARDshort-circuits one field only (Artifact.java:115-122) —*:*:*bans everything,foo:*:1.0bansfooat any version.
- Utilities, constants and text —
ClassLoaderUtils,ClassPathUtils,ServiceLoaderUtils - Networking and URL protocols —
URLUtils.resolveArchiveFile/resolveBasePath - Reflection, types and beans — the field access the
Abstract*URLClassPathHandletypes need - Language abstractions —
Prioritized, which orders both SPI families - Developer-facing deep dive: Class Loading and Artifact Detection — Developer Guide