Module: io.github.microsphere-projects:microsphere-java-core · 0.3.20-SNAPSHOT (release branch)
Packages: io.microsphere.util (34) · io.microsphere.util.jar (1) · io.microsphere.constants (8) ·
io.microsphere.text (1) · io.microsphere.misc (1) — 45 main types · Java 8 baseline, CI on JDK 8 / 11 / 17 / 21 / 25
Path prefix used below: microsphere-java-core/src/main/java/io/microsphere/ (sources),
microsphere-java-core/src/main/resources/ (SPI files) and microsphere-java-core/src/test/java/io/microsphere/
(tests). Full paths are given once, then shortened to the last two segments (e.g. util/ClassLoaderUtils.java).
This group is the Foundation layer: no type in it depends on any other io.microsphere.* package except
io.microsphere.constants, io.microsphere.util (intra-package), and the annotations module. Everything above —
collection, filter, invoke, lang, reflect, net, classloading, event — imports down into it.
graph TD
ANN["annotations<br/>@Nonnull @Nullable @Immutable @ConfigurationProperty"] --> U
ANN --> C["io.microsphere.constants<br/>Symbol / Separator / Path / Protocol / File / Property / Resource"]
U["io.microsphere.util<br/>ClassLoaderUtils · ServiceLoaderUtils · ClassPathUtils<br/>Version · VersionUtils · ArrayUtils · StringUtils · Assert · SystemUtils · NumberUtils · Utils"]
C --> U
TXT["io.microsphere.text<br/>FormatUtils"] --> U
U --> JAR["io.microsphere.util.jar<br/>JarUtils"]
U --> MISC["io.microsphere.misc<br/>UnsafeUtils"]
U --> X["every other core package:<br/>collection · filter · invoke · lang · reflect · net · classloading"]
LANG["lang.Prioritized<br/>(ordering contract)"] --> U
Two inbound dependencies worth naming precisely:
lang.Prioritized—ServiceLoaderUtilssorts every provider list withPrioritized.COMPARATOR(util/ServiceLoaderUtils.java:890); the comparator is imported fromio.microsphere.lang. This is the one cross-package contract the slice leans on for behaviour, soloadServices*results depend onPrioritizedliving inlang(documented in Language Abstractions).text.FormatUtilsis used byServiceLoaderUtilsto build its "no provider" message (util/ServiceLoaderUtils.java:884) and byVersion(util/Version.java), sotextis a leaf theutilpackage calls into — it must stay free of upward imports.
Consumption runs outward too: classloading.ArtifactDetector is driven by ClassLoaderUtils.findAllClassPathURLs
(util/ClassLoaderUtils.java:1550-1564), and Version.ofVersion(Class) builds an ArtifactDetector to resolve a
library version (util/Version.java:626-640) — see Class Loading and Artifacts.
| Type | Kind | Responsibility | Key hooks |
|---|---|---|---|
Utils |
marker interface | identifies a utility class by convention | util/Utils.java (empty) |
ClassLoaderUtils |
utility | class + resource resolution over a loader hierarchy; classpath mutation | loadClass (:547), getResource (:783), ResourceType (:1931) |
ServiceLoaderUtils |
utility | ServiceLoader facade: discovery, Prioritized sort, optional cache |
loadServicesList (:468), loadServicesAsList (:843, :873) |
ClassPathUtils |
utility | boot/app classpath snapshots, runtime class location | getBootstrapClassPaths (:127), getRuntimeClassLocation (:207) |
Version |
value object | immutable, comparable semver-ish model | ofVersion(String) (:595), Operator (:671), compareTo (:372) |
VersionUtils |
utility | JDK version constants + comparison helpers | CURRENT_JAVA_VERSION (:44), detectJavaMajorVersion (:284) |
ArrayUtils |
utility | typed empty arrays, factory, search, reverse | of (:211), contains (:1887, :1911, :2096), reverse (:2162) |
StringUtils |
utility | blank checks, split, contains, index/replace |
isBlank (:81), split (:137, :163) |
Assert |
utility (throws) | precondition guards; not a Utils |
assertNotNull (:131), assertNoNullElements (:348) |
SystemUtils |
utility | OS/JDK flags, system-property access, encoding constants | IS_JAVA_8 (:357), getSystemProperty (:605) |
NumberUtils |
utility (empty) | reserved placeholder — no public methods | util/NumberUtils.java |
text.FormatUtils |
utility | {} placeholder substitution |
format (text/FormatUtils.java:38) |
util.jar.JarUtils |
utility | URL→JarFile, entry filtering, manifest path |
toJarFile (:86), filter (:193), MANIFEST_RESOURCE_PATH (:63) |
misc.UnsafeUtils |
utility | sun.misc.Unsafe handle + field/array offsets |
getUnsafe (:564), offsetCache (:559) |
constants.* |
constant interfaces | shared literals, aggregated by Constants |
constants/Constants.java:13-14 |
The base primitive is nullSafeClassLoader(classLoader) (util/ClassLoaderUtils.java:218): a null loader never
throws here — it is replaced by the system class loader. On top of it, getDefaultClassLoader() (:244) tries, in
order: the thread-context loader → the loader that loaded ClassLoaderUtils itself → the default. getClassLoader (loadedClass) (:298) returns a class's own loader, or (for a bootstrap-loaded class, where getClassLoader() is
null) falls back through the caller frames.
The full ancestor chain is built by getInheritableClassLoaders(classLoader) (:1069), which delegates to
doGetInheritableClassLoaders (:1092): it seeds a LinkedHashSet with the loader, then follows getParent() until
null — so the set is ordered child → … → bootstrap and is returned unmodifiable. findLoadedClass (:497) and
loadClass (:547) use that ordering to look for an already-defined class before attempting a fresh load.
flowchart TD
A["loadClass(cl, name)"] --> NS["nullSafeClassLoader(cl)<br/>null → system loader (:218)"]
NS --> PR{"name blank?"}
PR -- yes --> NUL["return null (:620)"]
PR -- no --> CACHE{"cached?"}
CACHE -- "no" --> LOAD["doLoadClass on this loader"]
CACHE -- "yes" --> KEY["buildCacheKey = name + loader.hashCode() (:1902)"]
KEY --> CHM["loadedClassesCache.computeIfAbsent (:589, :92)"]
CHM --> LOAD
LOAD --> P["Class.forName via loader"]
P --> OK{"resolved?"}
OK -- yes --> RET["Class"]
OK -- "Throwable (caught, trace-logged)" --> PRIM["resolvePrimitiveClassForName"]
PRIM --> RETN["Class or null"]
The catch is deliberately a Throwable (not Exception) and only logged at trace (:620): a NoClassDefFoundError
from a half-linked class degrades to null just like a genuine miss. The consequence (pitfall 9 in the user guide) is
that null is ambiguous — it means "not here", not "does not exist".
getResource(classLoader, resourceName) (:783) is the untyped entry: it iterates ResourceType.values() in
declaration order — DEFAULT, CLASS, PACKAGE (:1931) — and returns the first loader hit. The typed variant
getResource(classLoader, resourceType, resourceName) (:843) skips that scan and pins one ResourceType, after
resourceType.resolve(name) normalises the key and nullSafeClassLoader fixes the loader. getResources(...)
(:669, :707) is the plural form and is the only one that declares IOException (it calls loader.getResources).
ResourceType is a per-constant strategy: each value answers supports(name) and normalize(name):
| Constant | supports |
normalize |
Evidence |
|---|---|---|---|
DEFAULT |
always true |
identity | util/ClassLoaderUtils.java:1945 |
CLASS |
dotted class name or *.class |
dots → slashes, keeps .class |
:1983 |
PACKAGE |
no / or \, not *.class |
dotted package form | :2030 |
resolve(name) |
returns null for blank / unsupported |
normalize → normalizePath → strip leading / |
:2086 |
Because resolve returns null when a type does not apply, the typed getResource (:843) simply yields null
rather than searching under a malformed name — that null-check is the type guard, and it is why CLASS never matches
a package path and PACKAGE never matches a .class file.
getClassResource (:1035) is a thin specialisation: it appends .class to the binary name and forces
ResourceType.CLASS, so getClassResource(cl, Foo.class) resolves exactly one loader-visible Foo.class.
loadServicesList overloads funnel to one place. The public entry
loadServicesList(serviceType, classLoader, cached) (:468) wraps its result in unmodifiableList and delegates to
the package-private loadServicesAsList(serviceType, classLoader, cached) (:843):
flowchart TD
S["loadServicesList(type, cl, cached) (:468)"] --> U["unmodifiableList(...)"]
U --> ASL["loadServicesAsList(type, cl, cached) (:843)"]
ASL --> C{"cached?"}
C -- "true" --> CIA["servicesCache.computeIfAbsent(type, → loadServicesAsList(type,cl)) (:846)"]
C -- "false" --> RAW["loadServicesAsList(type, cl) (:873)"]
CIA --> RAW
RAW --> NCL{"classLoader == null?"}
NCL -- yes --> GC["classLoader = getClassLoader(type) (:874)"]
NCL -- no --> SL
GC --> SL["ServiceLoader.load(type, cl) (:878)"]
SL --> HAS{"iterator.hasNext()?"}
HAS -- "no" --> THROW["IllegalArgumentException — NO provider (:882-886)"]
HAS -- yes --> LINK["newLinkedList(iterator) (:888)"]
LINK --> SORT["sort(list, Prioritized.COMPARATOR) (:890)"]
SORT --> OUT["sorted List"]
Two branch-level facts the tests lock down:
- Empty is a throw, not an empty list.
loadServicesAsList(type, cl)explicitly checks!iterator.hasNext()and throwsIllegalArgumentExceptionwith a formatted message built viaFormatUtils.format(:882-886,:884). The defaultcachedvalue isSERVICE_LOADER_CACHED(:411), itself read once at class-init from-Dmicrosphere.service-loader.cached(:112,:138, default"false"at:107). - Ordering is
Prioritized, never file order.sort(serviceList, COMPARATOR)(:890) re-sorts after load; theServiceLoaderconfiguration-file order is irrelevant.loadFirstService/loadLastService(:602,:719) then returnListUtils.first/lastof that sorted list, andloadService(:828) picks first-vs-last by a boolean.
sequenceDiagram
participant App as Caller
participant SLU as ServiceLoaderUtils
participant Cache as servicesCache (ConcurrentMap)
participant CL as ClassLoaderUtils
participant JDK as java.util.ServiceLoader
participant CFG as META-INF/services/<type>
App->>SLU: loadServicesList(type, cl, true)
SLU->>SLU: loadServicesAsList(type, cl, true) (:843)
SLU->>Cache: computeIfAbsent(type) (:846)
alt cache miss
Cache->>CL: getClassLoader(type) when cl == null (:874)
Cache->>JDK: ServiceLoader.load(type, cl) (:878)
JDK->>CL: getResources(cl, "META-INF/services/"+name) (:669/:707)
CL-->>JDK: URL(s) of the config file
JDK->>CFG: parse provider class names
CFG-->>JDK: provider classes
JDK-->>Cache: iterator (instances)
Cache->>Cache: throw IllegalArgumentException if empty (:882)
Cache->>Cache: newLinkedList + sort(COMPARATOR) (:888, :890)
end
Cache-->>SLU: sorted List
SLU-->>App: unmodifiableList (first = highest priority)
Note the cache key is the service type alone (:846); the classLoader is captured by the lambda but not mixed
into the key, so two different loaders that request the same type share one cached list. That is the exact behaviour
recorded as pitfall 2 in the user guide.
Version.ofVersion(String) (util/Version.java:595) runs two splits:
split(version.trim(), HYPHEN)→[0]is the numeric core,[1](if length > 1) is the pre-release tag.split(core, DOT)→major = getValue[0];minor = size > 1 ? getValue[1] : 0;patch = size > 2 ? getValue[2] : 0; any segments beyond three are ignored.
getValue(String) (:661) trims and parseInts; a NumberFormatException is rethrown as
IllegalArgumentException("contains the non-number part"). The constructor (:165) asserts each part ≥ 0 and
major+minor+patch > 0, so Version.of(0,0,0) is rejected (IllegalArgumentException). Comparison is compareTo
(:372) — major, then minor, then patch, then comparePreRelease (:403), where a null pre-release ranks after
any non-null tag (1.2.3 > 1.2.3-alpha > 1.2.3-beta only insofar as beta/alpha compare lexically). Operator.of(symbol)
(:766) maps the five string operators "=" < "<=" > ">=" (backed by SymbolConstants) to the EQ/LT/LE/GT/GE
enum (:671); anything else throws IllegalArgumentException. ofVersion(Class) (:626-640) defers to the running
JDK (VersionUtils.CURRENT_JAVA_VERSION) when the class's loader is null (bootstrap), and throws
IllegalArgumentException when the detected artifact carries no version.
flowchart TD
IN["ofVersion(String) (:595)"] --> NA["assertNotNull + assertNotBlank (:595)"]
NA --> SP1["split(trim, HYPHEN)"]
SP1 --> CORE["core = [0]"]
SP1 --> PRE{"length > 1?"}
PRE -- yes --> PRT["preRelease = [1]"]
PRE -- no --> PRTN["preRelease = null"]
CORE --> SP2["split(core, DOT)"]
SP2 --> MAJ["major = getValue[0] (:661)"]
SP2 --> MIN["minor = size>1 ? getValue[1] : 0"]
SP2 --> PAT["patch = size>2 ? getValue[2] : 0"]
MAJ --> CT["new Version(major,minor,patch,preRelease) (:165)"]
MIN --> CT
PAT --> CT
PRT --> CT
PRTN --> CT
CT --> AS{"all >= 0 and sum > 0?"}
AS -- no --> IAE["IllegalArgumentException"]
AS -- yes --> OK["Version"]
The numeric core and the pre-release tag are split by the first hyphen, so 7.8.9-RC1 keeps RC1 intact
(util/VersionTest.java:331). Beyond-three dot segments are silently dropped, which makes ofVersion tolerant of
Maven-style qualifiers that carry extra dots — the qualifier text simply never reaches getValue.
ClassPathUtils computes two static final snapshots once, at class-init. initBootstrapClassPaths (:68) calls
resolveClassPaths(runtimeMXBean.isBootClassPathSupported(), this::getBootClassPath); on JDK 9+ the boolean is
false, so resolveClassPaths(boolean, Supplier) (:94) short-circuits to emptySet() and only the java.home
containment heuristic (used upstream in classloading) still filters JDK libraries. initClassPaths (:73) always
splits getClassPath() on PATH_SEPARATOR via resolveClassPaths(String) (:103).
On the archive side, JarUtils.toJarFile(URL) (util/jar/JarUtils.java:86) first
resolveJarAbsolutePath(url) (:168) — which resolves the containing file through net.URLUtils — then opens a
JarFile; a missing archive logs the IOException and returns null, whereas a null URL throws
NullPointerException and a non-jar/file protocol throws IllegalArgumentException at
assertJarURLProtocol (:111). filter(jarFile, JarEntryFilter) (:193) returns a shared emptyList() for a null
jar. The manifest itself is addressed by the MANIFEST_RESOURCE_PATH = "META-INF/MANIFEST.MF" constant (:63) that the
classloading resolvers read through this utility.
SystemUtils reads JDK identity from java.specification.version via matchesJavaVersion(prefix) (:638), so
IS_JAVA_8 = matchesJavaVersion("1.8") (:357) is independent of VersionUtils.CURRENT_JAVA_VERSION, which parses
java.version (util/VersionUtils.java:266). getSystemProperty(key, default) (:605) consults the cached copy
first, then System.getProperty, and returns the default after logging a warning if a SecurityException is thrown —
the property access never propagates a security failure.
The slice's central design rule is that lookups degrade (return null, an empty/immutable collection, or a neutral
boolean) rather than throw — but that rule has a documented, enumerable set of exceptions. Getting this boundary right
is the main risk for callers, so it is catalogued here per method:
| Method | Absent / invalid input → | Actually throws on | Evidence |
|---|---|---|---|
ClassLoaderUtils.doLoadClass |
null |
— (catches Throwable, trace-logs) |
util/ClassLoaderUtils.java:620 |
ClassLoaderUtils.getResource |
null |
— | :783, :843 |
ClassLoaderUtils.getResources |
empty Set |
declared IOException / NullPointerException |
:669, :707 |
ClassLoaderUtils.newURLClassLoader |
— | IllegalArgumentException (null URL element) |
:1667 |
ServiceLoaderUtils.loadServices* |
throws | IllegalArgumentException when zero providers |
:882-886 |
ServiceLoaderUtils.getServiceClasses(…, true) |
throws | IllegalStateException (unloadable / non-assignable) |
:262, :270, :276 |
ClassPathUtils.getBootstrapClassPaths |
empty Set (JDK 9+) |
— | :68, :94 |
StringUtils.split |
EMPTY_STRING_ARRAY / single-element |
— | :163 |
ArrayUtils.contains(int[],…) |
false for null array |
— (but wrong on unsorted input) | :1911 |
Version.ofVersion(String) / getValue / ofVersion(Class) |
— | IllegalArgumentException |
util/Version.java:595, :661, :626-640 |
Assert.* |
— | IllegalArgumentException / ArrayIndexOutOfBoundsException |
util/Assert.java:60-449 |
FormatUtils.format(null, …) |
null |
— | text/FormatUtils.java:62 |
JarUtils.toJarFile(missing) |
null |
NullPointerException (null URL) / IllegalArgumentException (bad protocol) |
util/jar/JarUtils.java:86, :111 |
UnsafeUtils static init |
— | UnsupportedOperationException → ExceptionInInitializerError |
misc/UnsafeUtils.java:564, :570-572 |
The pattern is consistent: resolution and lookup APIs degrade; construction, parsing, and assertion APIs throw. The
loadServices* family is the one surprising case — a loading API that throws — because an empty META-INF/services
file is treated as a misconfiguration, not as a legitimate "no providers" state.
| Pattern | Implementing class(es) | Evidence |
|---|---|---|
| Marker interface + static facade | Utils adopted by ClassLoaderUtils, ServiceLoaderUtils, ClassPathUtils, ArrayUtils, StringUtils, SystemUtils, VersionUtils, text.FormatUtils, util.jar.JarUtils, misc.UnsafeUtils |
util/Utils.java (empty); each is abstract + private ctor |
| Facade (with delegation) | ClassLoaderUtils presents classpath read/mutate while delegating to the URLClassPathHandle SPI |
static urlClassPathHandle (util/ClassLoaderUtils.java:94), used at :1550-1564, :1598-1603 |
| Lazy holder / benign-race cache | loadedClassesCache (:92), servicesCache (:140 → computeIfAbsent :846), UnsafeUtils.offsetCache (:559) |
ConcurrentHashMap computeIfAbsent — duplicate computation possible, no corruption |
| Null object / neutral-value degradation | doLoadClass returns null on any Throwable (:620); getResourceAsString returns null for a missing resource (:875); NoOp-style empties in ClassPathUtils on JDK 9+ (:94) |
util/ClassLoaderUtils.java:620, :875; util/ClassPathUtils.java:94 |
| Strategy (per resource kind) | ResourceType.DEFAULT / CLASS / PACKAGE each implement supports + normalize, selected by the getResource overload |
util/ClassLoaderUtils.java:1931, :1945, :1983, :2030, :2086 |
| Value object + specification | Version (@Immutable, Comparable, Serializable) with equals/hashCode (:326, :346) and Operator as a BiPredicate specification (:671) |
util/Version.java:95-102, :220-317, :671 |
| Factory method | Version.of/ofVersion(...) (:441, :595), ArrayUtils.of/ofArray (:211, :375), ClassLoaderUtils.newURLClassLoader (:1667) |
see cited lines |
| Composition of constant interfaces | Constants extends the six literal interfaces so consumers can import static Constants.* |
constants/Constants.java:13-14 |
| Template via comparison operator | Operator.EQ/LT/LE/GT/GE.test(v1, v2) centralises null and identity handling around compareTo |
util/Version.java:671 |
Note
Assert is the deliberate anti-pattern of the Utils convention (throws instead of degrading, and does not
implement Utils), and NumberUtils is the deliberate empty utility — a placeholder that keeps its implements Utils marker but exposes no behaviour (util/Assert.java, util/NumberUtils.java). Both exceptions prove the rule.
- Provide more implementations of any SPI type.
ServiceLoaderUtilsreadsMETA-INF/services/<fqcn>(SERVICES_PROVIDER_LOCATIONutil/ServiceLoaderUtils.java:119, pattern:127) throughClassLoaderUtils, so a downstream module adds a provider by dropping one line in its ownMETA-INF/servicesfile — the core library ships thirteen such registrations. To influence which provider is picked first, implementio.microsphere.lang.Prioritizedand return a smallergetPriority();sort(COMPARATOR)(:890) honours it, and file order does not matter. - Toggle service caching globally. Set
-Dmicrosphere.service-loader.cached=true(:112, read at class-init:138) to makeSERVICE_LOADER_CACHEDdefault thecachedflag on the two-argument overloads (:411). Callers that need per-loader isolation should still passcached = falseexplicitly because the cache key ignores the loader (:846). - Plug a new classpath reader.
ClassLoaderUtilsdoes not readsun.misc.URLClassPathitself; it holds theServiceLoadingURLClassPathHandlefacade (:94) which selects aURLClassPathHandleSPI implementation. Supporting a future loader is aclassloading-package change, but the seam lives here —findAllClassPathURLs(:1550-1564) andremoveClassPathURL(:1598-1603, which still requires aSecureClassLoader) are the entry points every caller funnels through. - Add literals without touching utilities. The
constantsinterfaces are pure open-for-extension-by-adding: a new separator belongs inSeparatorConstants, a protocol inProtocolConstants, and the aggregateConstants(constants/Constants.java:13-14) picks it up automatically for wildcard importers. - Resource-kind normalisation is extensible per call. Passing an explicit
ResourceTypetogetResource(cl, type, name)(:843) is the supported way to override theDEFAULT → CLASS → PACKAGEprobe order of the untyped overload (:783);resolvereturnsnull(a clean miss) when the chosen type does not fit the name (:2086).
loadedClassesCacheisnew ConcurrentHashMap<>(256)(util/ClassLoaderUtils.java:92), filled viacomputeIfAbsentinloadClass(...,true)(:589). The key isclassName + classLoader.hashCode()(:1902): benign races can only duplicate a load; correctness rests onhashCodebeing stable, and a cross-loaderhashCodecollision would wrongly share a cached class — acceptable because loaders rarely collide, but it is the reason the cache is keyed per (name, loader) pair.servicesCacheis aConcurrentMap<Class<?>, List<?>>(:140) populated bycomputeIfAbsent(:846). The value list is wrapped unmodifiable at the boundary (:469), so published lists are safe to share; the loader is not part of the key (see §3.3). Loading is O(providers) plus aPrioritizedsort each cache miss.UnsafeUtils.offsetCacheis aConcurrentHashMap<String,Long>(misc/UnsafeUtils.java:559) memoising field offsets;getUnsafe()(:564) runs once in the static initialiser, and its failure becomes anExceptionInInitializerError(:570-572) — so the very first touch of the class is the riskiest moment, not any call site.- Immutable snapshots.
ClassPathUtils.bootstrapClassPaths/classPaths(util/ClassPathUtils.java:61,:64) and allSystemUtilsIS_JAVA_*/ property-value constants (util/SystemUtils.java:347-517) arestatic finaland computed once — reading them is free, and they can never be refreshed at runtime (a flexibility tax). SystemUtils.systemPropertiesCopyis not volatile. It is a plainprivate static Map<String,String>(:528);copySystemProperties()synchronises onSystemUtils.class(:548), but readers that grab the copy outside that lock (getSystemProperty,:605) rely on the map already having been published. Mutate system properties only via the synchronised path.Versionis fully immutable (@Immutable, all-final state,:95-102) withequals/hashCode(:326,:346), making it safe to publish and to use as a map key;compareTo(:372) allocates nothing.- Array searches.
ArrayUtils.containsfor numeric primitives usesArrays.binarySearch(:1911-2069) — O(log n) but only correct on sorted data;boolean[]/Object[]overloads are O(n) linear scans (:1887,:2096).reverse(T[])(:2162) is in-place and throwsNullPointerExceptionon anullarray. - Uncached service loads allocate. Each non-cached
loadServicesAsListbuilds aLinkedListand sorts it (:888,:890); inside a loop, prefer thecached = trueoverload or hoist the result.
Location: microsphere-java-core/src/test/java/io/microsphere/{util,constants,text,misc}/. Tests are JUnit 5
(org.junit.jupiter.api.Test); those needing a logger extend LoggingTest (e.g. ServiceLoaderUtilsTest,
JarUtilsTest).
- Both
java.versionschemes are asserted, not assumed.VersionUtilsTest.testDetectJavaMajorVersionpins modern ("17.0.0"→"17") and legacy ("1.8.0"→"8") and the no-dot case ("1"→"1") (util/VersionUtilsTest.java:142-150);testTestCurrentJavaVersioncompares againstCURRENT_JAVA_VERSIONrather than a literal (:110-113) — this is what keeps the same suite green across 8/11/17/21/25. - Degrade-vs-throw boundaries are pinned explicitly.
ServiceLoaderUtilsTestasserts that a provider-less type throwsIllegalArgumentExceptionfor everyloadServicesList/loadServices/loadFirstService/loadLastServiceshape (util/ServiceLoaderUtilsTest.java:107-112,:139-143,:170-174,:200-205), and thatgetServiceClassesthrowsIllegalStateExceptionwithfailFast=truebut degrades to an empty set withfailFast=false(:59-69).ClassLoaderUtilsTestconfirms class/resource lookups degrade tonullfornull/blank/missing inputs (util/ClassLoaderUtilsTest.java:345-376,:438). Prioritizedordering is exercised through a real fixture. The suite registers twoEventListenerproviders and assertsloadFirstServiceyieldsEchoEventListener2whileloadLastServiceyieldsEchoEventListener(util/ServiceLoaderUtilsTest.java:211-232) — directly proving the sort atutil/ServiceLoaderUtils.java:890, not theMETA-INF/servicesfile order.Versionsemantics are covered edge-first.VersionTestasserts null-comparison returnsfalsenever throws (:218-221), pre-release ranking (:213-215),ofVersion(Class)returning"3.0.2"for a jsr305 class andCURRENT_JAVA_VERSIONfor a bootstrap class (:124-132), andIllegalArgumentExceptionfor a class with no version metadata (:134-137,:321-324).JarUtilsmixes returns and throws deliberately, and the test checks each.JarUtilsTestasserts a valid archive yields aJarFile, a missing one yieldsnull, a bad protocol throwsIllegalArgumentException, and anullURL throwsNullPointerException(util/jar/JarUtilsTest.java:104-125), plusfilter(null, null)==emptyList()(:99-102).StringUtils/Assert/FormatUtilscover the text primitives.StringUtilsTest.testSplitverifies null safety, single-element behaviour on anulldelimiter, and empty-token preservation, cross-checking the char and string overloads agree (util/StringUtilsTest.java:89-124);FormatUtilsTestpins the surplus/deficit placeholder cases (text/FormatUtilsTest.java:34-53).- Writing new tests here. For an SPI type, add a provider line to
src/test/resources/META-INF/services/...and implementPrioritizedto assert ordering. For degrade behaviour, assertnull/empty against a resource that does not exist; for the throw paths, wrap inassertThrows. Prefer fixtures already on the test classpath — the canonical one isjavax.annotation.Nullable→ version3.0.2(util/VersionTest.java:124-128).
nullfromClassLoaderUtilsis ambiguous. A blank name, a caughtThrowable, and a genuine miss all returnnull(util/ClassLoaderUtils.java:620); the caller cannot distinguish them without enabling trace logging.- The service cache is loader-blind.
servicesCachekeys on type only (:846), so caching is unsafe for isolated loaders presenting the same type — passcached = false. getAllLoadedClassesMapmis-attributes loaders. Its loop maps every inheritable loader to the caller-supplied loader's loaded classes (util/ClassLoaderUtils.java:1142-1144); do not treat its values as per-loader.- JDK-9+ boot classpath is always empty.
ClassPathUtils.getBootstrapClassPaths()yieldsemptySet()past JDK 8 (:68,:94), so any "is this a JDK library?" check built on it silently degrades to thejava.homeheuristic. sun.misc.Unsafeis a class-load-time risk.UnsafeUtilscan fail during static init withExceptionInInitializerError(misc/UnsafeUtils.java:570-572) on JDKs wheresun.misc.Unsafeis gone — it must be touched lazily/conditionally, never eagerly in a hot path shared across JVMs.NumberUtilsprovides nothing. It is a marked-but-empty utility (util/NumberUtils.java); do not depend on it, and do not read its existence as an API.
- Utilities, Constants and Text — User Guide — the operator-facing view of the same sources
- Class Loading and Artifacts — the
URLClassPathHandleSPI thatClassLoaderUtilsdelegates to, andVersion.ofVersion(Class)'sArtifactDetector - Collections and Filters —
Lists/Sets/Prioritizedused by the service sort - Language Abstractions —
io.microsphere.lang.Prioritized, the ordering contract