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基础原理,这样才能做到事半功倍。

本文给大家讲解的内容是Spring Boot的ClassLoader加载机制