Class ChildFirstURLClassLoader

All Implemented Interfaces:
Closeable, AutoCloseable

public class ChildFirstURLClassLoader extends URLClassLoader
A URLClassLoader that resolves Spring Boot's own bootstrap classes (org.springframework.boot.loader.*: Launcher, JarLauncher, Archive, LaunchedClassLoader, the "nested:"/"jar:" URL protocol handlers, etc.) from its own URLs before delegating to its parent. Every other class name uses standard parent-first delegation. Used to bootstrap in-process Spring Boot fat-jar services (RESTV3, MCP): the JVM's system classpath also carries spring-boot-loader.jar (required for the JDK to resolve Spring Boot's "nested:" URL protocol), so with plain parent-first delegation, Spring Boot's own Launcher/JarLauncher/Archive classes get defined by the system classloader instead of by this loader - orphaning any extra jars (e.g. lib/optional/security) added only to this loader's own URLs, since Launcher.createClassLoader() parents its internal app classloader on "this.getClass().getClassLoader()", i.e. whichever loader actually defined the Launcher class.

The child-first override is deliberately scoped to just that one package: this loader's own URLs also carry jars (e.g. lib/optional/security's spring-security-*) that RESTV3's fat jar bundles its own copies of internally. Applying child-first broadly would let this loader's copy of those classes win over the fat jar's, and since the two copies aren't necessarily the same build, a class defined by this loader can fail to link against a supertype that only exists in the fat jar's own nested dependencies (observed as NoClassDefFoundError for org.springframework.beans.factory.xml.NamespaceHandler when spring-security-config was resolved from lib/optional/security instead of the fat jar's bundled spring-beans). Standard parent-first delegation is what actually resolves lib/optional/security-only classes (e.g. a customer's own LDAP UserDetailsContextMapper) correctly: they simply aren't present on the parent chain above this loader, so delegation falls through to this loader's own URLs regardless.

org.springframework.web.servlet.* is handled the opposite way: delegation to the parent is refused outright, without even trying this loader's own (equally empty, for this package) URLs first. The distribution ships a standalone lib/optional/spring/spring-webmvc-*.jar - reachable from this loader's parent chain - for unrelated Processing-Unit/OpenSpaces Spring MVC support (see xap-dist's xap-openspaces.xml assembly and GsCommandFactory.appendSpringClassPath(), used by spaceInstance()/standalonePuInstance() command building), a real jar with real classes, not a missing one - so plain parent-first delegation does not fail-and-fall-through the way it does for e.g. org.springdoc.* (which only ever exists inside RESTV3's own fat jar): the parent chain's copy of e.g. org.springframework.web.servlet.resource.LiteWebJarsResourceResolver gets defined successfully, just from the wrong (external, PU-oriented) build. Its own "new WebJarVersionLocator()" then resolves via *its own* defining classloader - the external one, which has no visibility into RESTV3's fat jar's nested BOOT-INF/lib - so a class that is genuinely bundled (webjars-locator-lite) throws NoClassDefFoundError anyway. Refusing to ask the parent for org.springframework.web.servlet.* at all forces the loader that asked (the Spring Boot LaunchedURLClassLoader nested inside RESTV3's own fat jar, a child of this loader) to fall back to its own bundled copy instead, the same copy the rest of the fat jar's Spring context is already using.

The prefix is exactly org.springframework.web.servlet. - the entire content of the spring-webmvc jar, no more and no less (verified: spring-web-*.jar has zero classes under org/springframework/web/servlet/, and spring-webmvc-*.jar has nothing else) - not narrower and not broader. Two earlier, wrong boundaries were each fixed by a regression at the next one:

  • Blocking all of org.springframework.web.* (too broad) additionally caught org.springframework.web.util.pattern.PathPatternParser and org.springframework.web.filter. GenericFilterBean - both spring-web classes, not spring-webmvc. spring-security-web (lib/optional/security, kept external on this loader's own parent chain deliberately for LDAP customization - see above; never a descendant of this loader, so blocking a package here has no effect on its own resolution) needs those from the *same* external realm it itself loads from, for XML <http> filter-chain parsing and pathPatternRequestMatcherBuilder respectively. Forcing them onto the fat jar's copy instead just moved a two-copies conflict from NoClassDefFoundError (on webjars) to BeanNotOfRequiredTypeException / BeanDefinitionParsingException.
  • Narrowing all the way down to just org.springframework.web.servlet.resource.* (too narrow) fixed that, but then missed that other org.springframework.web.servlet.* classes still resolved externally too - e.g. org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport, which itself creates a org.springframework.web.servlet.resource.ResourceUrlProvider bean using *its own* (external) defining classloader, never asking this loader at all since WebMvcConfigurationSupport itself was never blocked. Meanwhile org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration (fat-jar-only, spring-boot- autoconfigure) expects a ResourceUrlProvider parameter type resolved *through* this loader - forced to the fat jar's copy by the block. Two distinct Class objects again, this time within the blocked package itself: NoSuchBeanDefinitionException on ResourceUrlProvider building 'welcomePageHandlerMapping' (observed via jay.dalal's ci-5603 repro).
The org.springframework.web.servlet.* boundary avoids both: it's the complete self-contained content of one jar (spring-webmvc), so every class that creates or consumes another spring-webmvc class - regardless of which spring-webmvc class started the chain - resolves consistently through this loader to the same (fat jar's) realm. Nothing in spring-web (accept/, bind/, client/, context/, cors/, filter/, jsf/, method/, multipart/, server/, service/, util/) is touched, so spring-security-web keeps its consistent external realm too.