SpringBoot项目启动过程源码终于整体捋了一遍(四)

上篇分析了初始化SpringApplication时是如何推断应用类型的,并且问题依然遗留着,即初始化SpringApplication的时候resourceLoader为null怎么拿类加载器。这篇带着这个问题继续往下撸源码,还是把SpringApplication的构造方法贴一下:

继续往下看:

 

 setInitializers()方法即设置初始化器,ApplicationContextInitializer即应用上下文的初始化器,ApplicationContextInitializer.class只是个类路径,想设置至少得先拿到初始化器的对象吧,不出意料的话这个getSpringFactoriesInstances()方法应该是通过这个类路径去拿初始化器,看看这个方法:

 直接看下面那个重载的方法吧,这里参数type即ApplicationContextInitializer.class,另外两个参数都是空数组,这个方法第一行就是去拿类加载器,好家伙终于知道拿类加载器了,之前遗留的问题似乎可以去找答案了,看一下这个getClassLoader()方法:

 原来resourceLoader为null也不要紧,还留了一手ClassUtils.getDefaultClassLoader(),意思是还有默认类加载器?看一下这个getDefaultClassLoader()方法:

 这里可以简单介绍一下类加载机制,java的类加载器大类分三种,即启动类加载器(BootstrapClassLoader)、扩展类加载器(ExtClassLoader)和系统类加载器(AppClassLoader),也可以说有第四种用户自定义类加载器,不过这个也属于系统类加载器,java的类加载机制双亲委派模型可以去了解一下,之前也写过相关博客(https://blog.csdn.net/weixin_42447959/article/details/81265888)。在这个方法里,我们可以看到类加载器优先拿的是Thread.currentThread().getContextClassLoader(),如果拿不到那就拿加载当前类即ClassUtils的类加载器,如果还拿不到那就拿ClassLoader.getSystemClassLoader(),这个就是系统类加载器AppClassLoader。

这里重点看一下Thread.currentThread().getContextClassLoader(),这个是去拿线程上下文加载器,线程创建者在创建线程之后用对应的setContextClassLoader()方法将适合的类加载器设置到线程中,那么线程中的代码就可以通过getContextClassLoader()获取到这个类加载器来加载类或者资源,如果不设置默认是系统类加载器AppClassLoader。

那这里的线程上下文加载器是哪个呢?这得看项目是怎么启动的。如果项目是在IDEA或者其他编辑工具中启动,这时候的tomcat可能是编辑器自带的也可能直接就是本地的tomcat,它们又不认识springboot不boot的,才不会专门给线程设置上下文加载器,所以最后线程上下文加载器就是AppClassLoader。但是如果是在服务器中java -jar启动就不一样了,说明这时候项目已经被打成了fat jar,都知道fat jar里面是内置tomcat的,这也是它为什么可直接执行,这个内置的tomcat可是自己人,它创建线程的时候还真去给线程设置了一个上下文加载器,要知道具体是哪种类加载器先去看看这个fat jar是怎么启动的,反编译了一个可执行jar包,目录结构如下:

这个MANIFEST.MF文件是jar包的描述文件,可知main-class是JarLauncher ,那就从这里梳理一下fat jar的执行过程,看能不能发现什么,点进这个类果然有个main()方法:

构造了JarLauncher对象然后调用这个launcher方法,继续看这个方法:

只关注类加载器相关的,可以看到第二行是去拿类加载器了,几个嵌套方法调用点进去最后可以看到:

 原来这个的classLoader是这个LaunchedURLClassLoader,继续往下看构造好类加载器后紧接着调用了launch()方法,看一下这个方法:

果然,原来是这里给线程设置了上下文加载器LaunchedURLClassLoader,子线程里也都有这个上下文加载器了。

至于为什么要专门设置一个LaunchedURLClassLoader作为上下文加载器呢,其实LaunchedURLClassLoader和默认的AppClassLoader都是继承了URLClassLoader,只不过构造LaunchedURLClassLoader的时候指定的urls很特殊,这个和JarLauncher中这两个常量有关:

我们都知道spring boot打包需要特定的打包插件 spring-boot-maven-plugin,这个打包插件其实就是在maven原来的基础上二次打包,把项目依赖的jar包也打进去,所有可以直接运行,这两个常量就是告诉LaunchedURLClassLoader这个类加载去classer路径下找项目class文件,去lib路径下去找外部jar包,所有fat jar的内部结构都是固定的,这个类加载器自然也是量身定制的。

所以java -jar启动项目和在本地编辑器中启动项目,线程的上下文加载器是不同的,这也解释了为什么在代码中如果想用类加载器,开发调试的时候还好好的,打成jar包后一部署就完了,因为他们的类加载器可能不一样。所以不建议在代码中用XXX.class.getClassLoader(),这种方法拿到的只能是AppClassLoader。而建议用Thread.currentThread().getContextClassLoader(),这拿到的类加载器可不一定,但是肯定都可以加载项目资源文件。

了解这个线程上下文加载器的过程中,还发现它的来源其实挺有意思。还记得java类加载的双亲委派模型不?jdk1.2的时候引入了双亲委派模型的类加载机制,但是在jdk1.3的时候又引入了JNDI服务,JNDI目的就是对资源进行集中管理和查找,JDNI是一堆接口,需要由独立的厂商去实现这些接口并部署在应用程序的ClassPath下,这堆接口的类是jdk自己的东西由启动类加载器去加载,但启动类加载器可不认识各厂商的实现类,别人的东西启动类加载器不能加载。为了解决这个问题,Java设计团队只好引入了一个线程上下文类加载器的设计。这个类加载器可以通过Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是AppClassLoader。有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的厂商实现类代码。也就是在父类加载器加载的类中请求子类加载器去继续完成类加载的动作,这种行为实际上就是打破了双亲委派模型。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

刚刚提到了java中的SPI,全称是服务发现机制。当服务的提供者提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。应用场景直白点说就是:我(JDK)提供了一组服务接口(数据库驱动、日志等),但是这个服务具体怎么干活你可以自己去实现这个接口,前提是你要遵循约定(把类名写在/META-INF里)。这个接口是我自己的类加载器加载的,但是我自己的类加载器不能加载你的实现类,所以我需要借助线程上下文类加载器。

本来想继续看初始化SpringApplication的时候如何设置初始化器的,这又扯远了,这就是为什么当时决定连载下去的原因,我也不知道需要连载几篇,这篇就到这里,例常总结一下:

这篇解决了之前的遗留问题,resourceLoader为null时会有获取默认类加载器的一套逻辑。然后也是总结了java的类加载相关,下篇再继续看getSpringFactoriesInstances()方法,继续看初始化SpringApplication的时候如何设置初始化器的。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值