我真不信,这年头还有人能懂 SpringBoot 的 ClassLoader 加载机制
SpringBoot 的 ClassLoader 加载机制
在 Spring Boot 的嵌入式 Web 容器原理一节中,我们已经介绍了 Spring Boot 对 Tomcat 容器的加载过程,本节我们进一步讲解 SpringBoot 的 ClassLoader 加载机制。
熟悉 Tomcat 工作原理的人应该知道,Tomcat 内部实现了自定义的类加载器,打破了 Java 的双亲委派机制,下面我们先看看什么是双亲委派机制。
双亲委派机制
双亲委派机制是指 Java 的类加载器收到一个类加载请求时,该类加载器首先会把请求委派给父类加载器。每个类加载器都是如此,只有当父类加载器在自己的搜索范围内找不到指定类时,子类加载器才会尝试自己去加载。Java 类加载机制如下图所示。
我们通常将类加载器分为下面的三种类型。
● 启 动 类 加 载 器 ( Bootstrap ClassLoader ) : 加 载 jre/lib/rt.jar。
● 扩 展 类 加 载 器 ( Extension ClassLoader ) : 加 载 jre/lib/ext/*.jar。
● 应 用 程 序 类 加 载 器 ( Application ClassLoader ) : 加 载 classpath 上指定的类库。
如果使用 JDK 默认的双亲委派模式,Tomcat 的类加载器可以加载吗?我们思考一下 Tomcat 作为一个 Web 容器的使用场景。
在 Web 容器中,可能同时需要部署两个以上的应用程序。一个典型的场景是不同的应用程序会依赖同一个第三方类库的不同版本,不能要求同一个类库在同一个服务器中只有一份,因此要保证每个应用程序的类库都是独立的,保证相互隔离。
Tomcat 如果使用默认类加载器,是无法加载两个相同类库的不同版本的。所以 Tomcat 团队设计了自己独特的类加载机制,解决上面的应用 jar 包冲突等问题,通过自定义的类加载机制可以完美地解决 Tomcat 容器中不同应用的隔离问题。下面我们看看 Tomcat 的类加载机制图和 JDK 默认的加载机制图的区别,如下图所示。
其中:
● Common ClassLoader:Tomcat 最基本的类加载器,加载路径中的 Class 可以被 Tomcat 容器本身及各个 WebApp 访问。
● Catalina ClassLoader:Tomcat 容器私有的类加载器,加载路径中的 Class 对于 WebApp 不可见。
● Shared ClassLoader:各个 WebApp 共享的类加载器,加载路径中的 Class 对所有 WebApp 可见,但是对于 Tomcat 容器不可见。
● WebApp ClassLoader:各个 WebApp 私有的类加载器,加载路径中的 Class 只对当前 WebApp 可见,各个项目就是通过各自的 WebApp ClassLoader 加载进入 Tomcat 容器的。探索 Spring Boot 的 ClassLoaderSpring Boot 的内置 Tomcat 是如何加载到我们的项目中的呢?我们还是从 SpringApplication 的 run 方法开始追溯 Tomcat 启动 Web Server 的过程,ApplicationContext 执行刷新操作并创建嵌入式容器,源码如下:
然后,进入 EmbeddedServletContainer 的 getEmbeddedServletContainer 方法,它会初始化 Tomcat 实例并准备 Context。
最后,跟进 prepareContext 方法,我们就可以看到嵌入式 Tomcat 的类加载方式,源码如下:
可 见 , Spring Boot 以 启 动 线 程 的 Context ClassLoader 作 为 Tomcat 的 WebApp ClassLoader 的父类加载器,而 Tomcat 的 WebApp 类加载器使用 TomcatEmbeddedWebAppClassLoader。所以整个项目的 jar 包的加载都是由 Spring Boot 的主线程 Context ClassLoader 完成的,于是 Context ClassLoader 就可以访问我们的 Web 容器下的所有资源了。
需要说明的是,Spring Boot 使用了 FatJar 技术将所有依赖放在一个最终的 jar 包文件 BOOT-INF/lib 中,它可以把当前项目的 Class 全部放在 BOOT-INF/classes 目录中。你可以在 Spring Boot 的工程项目中看到,在 pom.xml 文件中引入了如下依赖:
jar 包目录结构如下:
从这个目录结构中,你可以看到 Tomcat 的启动包(tomcat-embedcore-8.5.29.jar)就在 Lib 目录下。而 FatJar 的启动 Main 函数就是 JarLauncher,它负责创建 LaunchedURLClassLoader 来加载/lib 下面的所有 jar 包。下面是 Spring Boot 应用的 Manifest 文件内容。
这里的 Main-Class 是 org.springframework.boot.loader.JarLauncher,它是这个 jar 包启动的 Main 函数。
还有一个 Start-Class:
com.test.demo.SpringbootDemoApplication,它是应用自己的 Main 函数 。 Spring Boot 将 jar 包 中 的 Main-Class 进 行 了 替 换 , 换 成 了 JarLauncher,并增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 Main 函数入口。我们再看看这个 JarLaucher 具体干了什么,源码如下:
launch 方法分为三步:(1)注册 URL 协议并清除应用缓存。
(2)设置类加载路径。
(3)执行 main 方法。
这里面,Spring Boot 自定义的 ClassLoader 能够识别 FatJar 中的资源,包括:在指定目录下的项目编译 Class、在指定目录下的项目依赖 jar 包。Spring Boot 支持多个!/分隔符,通过自行实现的 ZipFile 解析器实现了对 URL 插入的定制化 Handler,将获取的 URL 数据作为参数传递给自定义的 URLClassLoader,最终实现资源的获取和解析。
综上,在传统的以 Tomcat 容器部署 War 包项目中,我们的 Web 项目其实是一个被加载对象。Tomcat 容器作为主线程的父类加载器来加载不同的应用,Tomcat 独特的 WebApp ClassLoader 各自加载不同目录下的 War 包应用,应用之间使用 ClassLoader 实现了很好的隔离。
Spring Boot 主要通过实例化 SpringApplication 来启动应用,内置的 Tomcat 容器实现相关 Web 环境及初始化资源准备,并将 Tomcat 内嵌的 WebApp ClassLoader 作为子 ClassLoader 挂载到 Spring Boot 的主线程 Context ClassLoader 。 同 时 , Spring Boot 中 的 @Controller 、@RequestMapping 等 Web 服 务 资 源 通 过 自 动 装 配 机 制 , 在 SpringApplication 启动过程中通过扫描将资源对象加载到 Spring IoC 容器中。最后 Spring Boot 使用 FatJar 自定义的 jar 包压缩和加载机制,规范了 Spring Boot 项目的包及目录结构。
小结
目前,基于脚手架(基底)模式进行软件构建已经成为微服务架构落地的主流开发方式,可以显著提升开发人员的工作效率。SpringBoot 本身基于 Spring 框架,继承了 Spring 强大的技术特性。本章我们对 Spring Boot 框架的核心模块和机制进行了剖析,详细讲解了 SpringBoot 的自动化配置原理、Starter 机制和自定义 Starter 的工作原理,固化了“约定优于配置”和“开箱即用”等简洁的开发理念和高效开发方式。同时,本章也是后续 Spring Cloud 微服务治理的基础,在开始技术进阶之前,务必掌握 Spring Boot 基础原理,这样才能做到事半功倍。
评论