这个触及了Java的底层运行时机制。
这个“类加载器奥秘”。
Spring Boot并没有使用任何黑魔法,它完全遵循Java标准的双亲委派模型,但巧妙地利用了“SPI机制”和“线程上下文类加载器”这两个关键点。
把这个奥秘拆解为三个关键谜题,看完会豁然开朗。
谜题一:为什么一个普通的 main 方法能加载整个 Web 环境?
在Java中,当你执行java -jar myapp.jar时,JVM会启动一个AppClassLoader(系统类加载器),它负责加载classpath下的所有类。
关键点来了:Spring Boot打成的Fat Jar(胖包)里,BOOT-INF/classes是你的业务代码,BOOT-INF/lib是所有依赖的第三方Jar包。这个目录结构并不是JVM默认能识别的普通classpath。
Spring Boot的解法:它没有修改JVM的类加载器,而是自定义了一个LaunchedURLClassLoader。
-
在MANIFEST.MF中指定了Main-Class: org.springframework.boot.loader.JarLauncher。
-
当JVM启动时,先启动JarLauncher,它创建了一个LaunchedURLClassLoader。
-
这个自定义类加载器知道如何读取BOOT-INF/classes和BOOT-INF/lib下的嵌套Jar包,并将它们纳入自己的加载范围。
-
随后,这个类加载器去加载你的@SpringBootApplication主类,并调用其main方法。
一句话总结:不是main方法神通广大,而是启动器(Launcher)先创建了一个“识货”的类加载器,再由它去执行你的main方法。
谜题二:打破双亲委派的“线程上下文类加载器”(核心!)
这是整个流程中最精妙的地方。我们知道,Spring框架本身是一个第三方库,它是由AppClassLoader加载的。
但问题来了:当Spring容器在初始化时,需要动态加载你的业务类(比如@Service、@Controller),而此时Spring的代码属于AppClassLoader,你的业务类也在AppClassLoader里,好像没毛病?
麻烦出在“SPI(服务提供者接口)”上。
比如JNDI、JDBC、XML解析等Java标准库(由BootstrapClassLoader加载)中的接口,需要调用第三方实现(比如MySQL驱动,由AppClassLoader加载)。按照双亲委派模型,父类加载器(Bootstrap)无法反向委派给子类加载器(App)去加载类,这会导致ClassNotFoundException。
Spring Boot的解法(也是Java的官方救场方案):
线程上下文类加载器(Thread Context ClassLoader,简称TCCL)。
-
在JarLauncher启动时,会执行Thread.currentThread().setContextClassLoader(自定义类加载器)。
-
整个Spring容器的刷新(refresh)过程,都运行在这个线程中。
-
当Java标准库中的SPI代码需要加载实现类时,会调用Thread.currentThread().getContextClassLoader().loadClass(),从而成功拿到由LaunchedURLClassLoader加载的MySQL驱动或你的业务类。
这就是真相:类加载的“父委托”模型在这里被打破了,父类加载器通过TCCL这个“后门”反向获取了子类加载器的能力。
谜题三:为什么Tomcat还要自己再搞一套类加载器?
当你启动内嵌Tomcat时,你可能会好奇:Tomcat已经由AppClassLoader加载了,为什么它还要在自己的WebApp中再创建独立的WebappClassLoader?
原因有二,这也是防止“类冲突”的防火墙:
隔离多个应用:虽然我们通常只部署一个应用,但Tomcat的设计初衷是支持多应用。WebappClassLoader确保不同应用之间的类(即使包名完全相同)也不会互相干扰。
支持热部署:当你修改代码重启应用时(DevTools),Tomcat会丢弃旧的WebappClassLoader,新建一个新的,从而卸载所有旧类,避免永久代/元空间内存溢出。
流程串联:
LaunchedURLClassLoader(加载所有Jar) -> 启动main -> 创建Tomcat -> Tomcat内部创建WebappClassLoader(作为LaunchedURLClassLoader的子加载器) -> WebappClassLoader加载你的DispatcherServlet和@Controller。
完整类加载器层级关系图

面试官最想听的“一句话总结”
Spring Boot启动的奥秘在于:利用自定义LaunchedURLClassLoader解决了嵌套Jar的读取问题;利用线程上下文类加载器(TCCL)打破了双亲委派模型的方向性限制,让父类加载器能反向获取子类加载器中的SPI实现;最后,内嵌Tomcat再创建独立的WebappClassLoader,实现应用间的类隔离。
如果你对这个机制意犹未尽,我们可以继续往下挖两个方向:
既然TCCL打破了双亲委派,那为什么Tomcat的WebappClassLoader默认还要遵循“先找子类加载器,再找父类”的反向委托?(这是Tomcat特有的优化)
如果我自己写一个类,包名和JDK的一样(比如java.lang),Spring Boot能加载成功吗?为什么会报错?(安全机制的底层实现)
网硕互联帮助中心

评论前必须登录!
注册