SpringBoot 核心模块原理剖析

微服务始终一个相对热门的话题,SpringBoot 则以其轻量级、内嵌 Web 容器、一键启动、方便调试等特点被越来越多的微服务实践者所采用。

知其然还要知其所以然,你了解 SpringBoot 中三大核心模块的实现原理吗?

三大核心模块

spring-boot-load 模块

正常情况下一个类加载器只能找到加载路径的 jar 包里当前目录或者文件类里面的 *.class 文件,SpringBoot 允许我们使用 java -jar archive.jar 运行包含嵌套依赖 jar 的 jar 或者 war 文件。

spring-boot-autoconfigure 模块

Spring的出现给我们管理 Bean 的依赖注入提供了便捷,但是当我们需要使用通过 pom 引入的 jar 里面的一个 Bean 时候,还是需要手动在 XML 配置文件里面配置。Springboot 则可以依据 classpath 里面的依赖内容自动配置 Bean 到 Spring 容器。

spring-boot 模块

提供了一些特性用来支持 SpringBoot 中其它模块,本文会讲解到该模块都提供了哪些功能以及实现原理。

spring-boot-loader 模块

Java 原生类加载器局限及改进思路

Java 中每种 ClassLoader 都会去自己规定的路径下查找字节码文件并加载到内存(可以参考《Java 类加载器揭秘》这场 Chat)。这里需要补充的是 Classloader 只能加载扫描路径当前目录或者当前目录文件夹下的 .class 文件,或当前目录文件夹下 jar 文件里面的.class文件。如果这个 jar 里面又嵌套了其他 jar 包文件,那么这些嵌套 jar 里面的 *.class 文件是不会被 ClassLoader 加载的。

如上图,假设类加载器 cl 扫描字节码文件路径 /Users/zhuizhumengxiang,那么 cl 可以加载到 a.class、b.class 和 c.jar 里面的 c1.class 文件,但是加载不到 c2.jar 和 c3.jar 里面的 .class 文件,因为 c2.jar 和 c3.jar 是嵌套 jar。

为了能够加载嵌套 jar 里面的资源,之前的做法都是把嵌套 jar 里面的 class 文件和应用的 class 文件打包为一个 jar,这样就不存在嵌套 jar 了,但是这样做就不能很清晰的知道哪些是应用自己的,哪些是应用依赖的,另外多个嵌套 jar 里面的 class 文件可能内容不一样但是文件名却一样时候又会引发新的问题。

spring-boot-loader 模块则允许我们使用 java -jar archive.jar 方式运行包含嵌套依赖 jar 的 jar 或者 war 文件,它提供了三种类启动器(JarLauncher、 WarLauncher 和 PropertiesLauncher),这些类启动器的目的都是为了能够加载嵌套在 jar 里面的资源(比如 class 文件、配置文件等)。JarLauncher、WarLauncher 固定去查找当前 jar 的 lib 目录里面的嵌套 jar 文件里面的资源。本文则只介绍 jar 文件。

那么我们可以先思考下,如果让我们自己做一个可以加载嵌套 jar 里面的资源的工具模块,我们会怎么做呢?

我们知道 Java 中的 AppClassLoader 和 ExtClassLoader 都是继承自 URLClassLoader 并通过构造函数传递自定义的扫描路径,那么我们是不是也可以继承 URLClassLoader,然后把嵌套 jar 里面的多个 jar 的路径作为一个 URLClassLoader 的扫描路径呢?这样该 URLClassLoader 就可以找到嵌套 jar 里面的资源了。

URLClassLoader 的构造函数会传递一个 URL[] urls 作为该加载器的类扫描路径,那么针对上图中嵌套的 jar,我们可以创建一个 URLClassLoader,它的 urls 路径内容为:

/Users/zhuizhumengxiang/

/Users/zhuizhumengxiang/c.jar/c2.jar

/Users/zhuizhumengxiang/c.jar/c3.jar

  • 根据第一个路径 URLClassLoader 加载器可以查找到 a.class、b.class 和 c.jar 里面的 c1.class 文件。
  • 根据第二个路径可以加载到 c2.jar 里面的 .class 文件。
  • 根据第三个路径可以加载到 c3.jar 里面的 .class 文件。

这是一个可以解决嵌套 jar 的思路,但是还有一个问题需要解决,就是默认情况下我们启动 main 函数所在的类时候用的类加载器是 AppClassLoader,而它的加载路径是 classpath。那么我们自定义的 URLClassLoader 什么时候使用呢?

为了使用这个自定义 URLClassLoader,可以想办法让我们自定义的 URLClassLoader 来加载我们的 main 函数,但是一个逃离不了的现实是当使用 Java 命令启动 main 函数所在类时候使用的总是 AppClassLoader,那么现在只有在中间加一层来解决这个问题。

具体来说是使用 Java 命令启动时候启动一个中间类的 main 函数,这个中间类里面自定义 URLClassLoader,然后使用自定义 URLClassLoader 来加载我们真正的 main 函数。

如上图 Application 假设为含有 main 函数的类,之前是直接使用 AppClassLoader 进行加载,那么现在我们先使用 APPClassLoader 加载 Launcher 类,该类内部在创建一个 URLClassLoader 用来加载我们的 Application 类。下面具体介绍下 spring-boot-loader是如何解决嵌套 jar 问题的。

spring-boot-loader 模块提供的 jar 目录结构

为了解决嵌套 jar 问题,Springboot 中 jar 文件格式规定如下。

archive.jar

|

+-META-INF(1)

| +-MANIFEST.MF

+-org(2)

| +-springframework

| +-boot

| +-loader

| +-<spring boot loader classes>

+-com(3)

| +-mycompany

| +project

| +-YouClasses.class

+-lib(4)

+-dependency1.jar

+-dependency2.jar

  • 结构(1)是 jar 文件中 MANIFEST.MF 文件的存放处。
  • 结构(2)是 Spring-boot-loader 本身需要的 class 放置处。
  • 结构(3)是应用本身的文件资源放置处。
  • 结构(4)是应用依赖的 jar 固定放置处,即 lib 目录。

那么 spring-boot 是如何去创建这个结构并且按照这个结构加载资源呢?

首先在打包时候会使用 spring-boot-maven-plugin 插件重写打成的 jar 文件,会设置META-INF/MANIFEST.MF 中的 Main-Class:org.springframework.boot.loader.JarLauncher、Start-Class: com.mycompany.project.MyApplication,并拷贝 spring-boot-loader包里面的 class 文件到结构(2),应用依赖的 jar 拷贝到(4),应用本身的类拷贝到(3)。

接着,运行 java -jar archive.jar ,Launcher 会加载 JarLauncher 类并执行其中的 main 函数,JarLauncher 主要关心构造一个合适的 URLClassLoader 加载器用来调用我们应用程序(MyApplication)的 main 方法。

SpringBoot 的这种格式可以明确地让我们知道应用本身包含哪些类,应用依赖了哪些类。

spring-boot-maven-plugin 插件打包流程分析

SpringBoot 应用打包时候需要引入如下 Maven 插件才会生成上面介绍的结构的 jar。

<plugin>

<groupId>org.springframework.boot</groupId>

<artifactId>spring-boot-maven-plugin</artifactId>

<version>1.5.9.RELEASE</version>

<executions>

<execution>

<goals>

<goal>repackage</goal>

</goals>

</execution>

</executions>

</plugin>

本文使用 Springboot 版本为 1.3.5.RELEASE,Maven 插件版本为 1.5.9.RELEASE。

当我们执行 mvn clean package 进行打包生成 jar 文件后,spring-boot-maven-plugin 插件会对 jar 文件进行重写,具体重写步骤,请见下面的时序图。

  • 步骤(1)是 Maven 插件执行的入口类。
  • 步骤(2)设置是否从 jar 本节里面排除掉 spring-boot-devtools 的 jar 包,默认是不排除。这个可以在引入插件的地方配置,如下:

<configuration>

<excludeDevtools>true</excludeDevtools>

</configuration>

  • 步骤(5)(6)是主要环节,就是设置 MANIFEST.MF,Main-Class: org.springframework.boot.loader.JarLauncher、Start-Class: com.mycompany.project.MyApplication,并写入到文件,注意这里 MyApplication 代表了 SpringBoot 里面启动整体应用的包含 main 函数的那个类,也就是加了 @SpringBootApplication 注解的那个类。
  • 步骤(7)写入应用依赖的 jar 包到 lib 目录。
  • 步骤(8)拷贝 spring-boot-load 包里面的 class 文件到 jar 包的结构(2)处。

注:这里读者可以先思考下为何要拷贝本来应该放入到 lib 里 spring-boot-

loader.jar 里面的 class 到结构(2)?

JarLauncher 执行流程分析

为了解决嵌套 jar 资源加载问题,上节讲解了 Boot 提供的专用 Maven 插件用来修改 jar 包的 Main-Class: org.springframework.boot.loader.JarLauncher、Start-Class: com.mycompany.project.MyApplication,修改后的结果是当我们执行 java -jar archive.jar 时候会启动 JarLauncher 的 main 函数,而不是我们 SpringBoot 应用里 MyApplication 的 main 函数,下面看看 JarLauncher 的具体执行时序图。

全文还包括:

  • spring-boot-autoconfigure 模块
  • spring-boot 模块
  • 总结

原文发布于微信公众号 - CSDN技术头条(CSDN_Tech)

原文发表时间:2018-03-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

Web-第三十一天 WebService学习【悟空教程】

简单的网络应用使用单一语言写成,它的唯一外部程序就是它所依赖的数据库。大家想想是不是这样呢?

23540
来自专栏C/C++基础

g++入门教程

g++是GNU开发的C++编译器,是GCC(GNU Compiler Collection)GNU编译器套件的组成部分。另外,gcc是GNU的C编译器。

1.9K10
来自专栏Java帮帮-微信公众号-技术文章全总结

SpringBoot中Spring-cache与Redis整合【面试+工作】

也是在整合redis的时候偶然间发现spring-cache的。这也是一个不错的框架,与spring的事务使用类似,只要添加一些注解方法,就可以动态的去操作缓存...

48730
来自专栏坚毅的PHP

cherryPy学习

url参数映射 (搜索词cherrypy add url parameter) class Root: @cherrypy.expose def check...

42760
来自专栏北京马哥教育

29 条运维工程师必会实用 Linux 命令

虽然Linux发行版支持各种各样的饿GUI(graphical user interfaces),但在某些情况下,Linux的命令行接口(bash)仍然是简单...

32890
来自专栏JackeyGao的博客

Django 自定义管理命令

Django 提供了一组非常实用的命令, 可以通过django-admin.py和pytohn manage.py脚本调用. 关于这个Management Co...

22420
来自专栏架构师之路

一分钟实现分布式锁

一、缘起 分布式环境下,多台机器上多个进程对一个数据进行操作,如果不做互斥,就有可能出现“余额扣成负数”,或者“商品超卖”的情况,如何实现简易分布式锁,对分布式...

42060
来自专栏Eugene's Blog

php文件包含漏洞分类目录文章标签友情链接联系我们

16420
来自专栏linux、Python学习

案例+解读,来自有道大神的17个常用Linux命令深度解析

命令后带(Mac)标记的,表示该命令在Mac OSX下测试,其它的在Debian下测试。

16560
来自专栏JackeyGao的博客

终端操作(SHELL)技巧

本篇是一些小但是有用的终端操作技巧和一些快捷方式,可以让你在 linux 命令行有出奇的效率。一方面这些技巧可以让你的效率有所提高, 但有时候也会有隐患, 所以...

15100

扫码关注云+社区

领取腾讯云代金券