博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
深入源码大白话理解SpringBoot 究竟是如何跑起来的?
阅读量:5876 次
发布时间:2019-06-19

本文共 10445 字,大约阅读时间需要 34 分钟。

hot3.png

前言背景

    SprintBoot是当今最流行的项目构建框架,但是大多数人会像我一样开始只会用,怎么搭建项目,怎么让项目跑起来,但是有没有真正去研究一个简简单单的Hello World程序是如何运行起来的,我本来只想研究一下 SpringBoot 是如何从 main 方法一步一步跑起来的,但是这却是一个相当深的坑。不得不说 SpringBoot 太复杂了,经过了一番痛苦的折磨,我还是把 SpringBoot 的运行原理摸清楚了,这里分享给大家。

源码分析

    1、首先我们看看 SpringBoot 简单的 Hello World 代码,就两个文件 HelloControll.java 和 Application.java,运行 Application.java 就可以跑起来一个简单的 RESTFul Web 服务器了。

    efa609c0114fb369e299be24c772c1ad855.jpg

    4a0f831a1069351eea19c06ad90257aee01.jpg

// HelloController.javapackage hello;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.bind.annotation.RequestMapping;@RestControllerpublic class HelloController {    @RequestMapping("/")    public String index() {        return "Greetings from Spring Boot!";    }}// Application.javapackage hello;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}

    当我打开浏览器看到服务器正常地将输出呈现在浏览器的时候,我不禁大呼 —— SpringBoot 真是太简单了,想必大家都是这种感觉。

    2702c2f4abf65fe69cf5bd6422f2ab5c66c.jpg

    但是问题来了,在 Application 的 main 方法里我压根没有任何地方引用 HelloController 类,那么它的代码又是如何被服务器调用起来的呢?这就需要深入到 SpringApplication.run() 方法中看个究竟了。

    不过即使不看代码,我们也很容易有这样的猜想,SpringBoot 肯定是在某个地方扫描了当前的 package,将带有 RestController 注解的类作为 MVC 层的 Controller 自动注册进了 Tomcat Server。

    还有一个让人不爽的地方是 SpringBoot 启动太慢了,一个简单的 Hello World 启动居然还需要长达 5 秒,要是再复杂一些的项目这样龟漫的启动速度那真是不好想象了。

    再抱怨一下,这个简单的 HelloWorld 虽然 pom 里只配置了一个 maven 依赖,但是传递下去,它一共依赖了 36 个 jar 包,其中以 spring 开头的 jar 包有 15 个。说这是依赖地狱真一点不为过。

    2890609f5539eb67c37844f9ba21aa4f968.jpg

    批评到这里就差不多了,下面就要正是进入主题了,看看 SpringBoot 的 main 方法到底是如何跑起来的。

    2、SpringBoot的堆栈

        了解 SpringBoot 运行的最简单的方法就是看它的调用堆栈,下面这个启动调用堆栈还不是太深,我没什么可抱怨的。

        b78af2158d9d281afcc4cfb13a9ff987298.jpg

public class TomcatServer {  @Override  public void start() throws WebServerException {  ...  }}

        接下来再看看运行时堆栈,看看一个 HTTP 请求的调用栈有多深。不看不知道一看吓了一大跳!

    40f2dd821d04f19f62e51deb3195fd1c3d1.jpg

        我通过将 IDE 窗口全屏化,并将其它的控制台窗口源码窗口统统最小化,总算勉强一个屏幕装下了整个调用堆栈。

        不过转念一想,这也不怪 SpringBoot,绝大多数都是 Tomcat 的调用堆栈,跟 SpringBoot 相关的只有不到 10 层。

    3、探索 ClassLoader

        SpringBoot 还有一个特色的地方在于打包时它使用了 FatJar 技术将所有的依赖 jar 包一起放进了最终的 jar 包中的 BOOT-INF/lib 目录中,当前项目的 class 被统一放到了 BOOT-INF/classes 目录中。

org.springframework.boot
spring-boot-maven-plugin

    这不同于我们平时经常使用的 maven shade 插件,将所有的依赖 jar 包中的 class 文件解包出来后再密密麻麻的塞进统一的 jar 包中。下面我们将 springboot 打包的 jar 包解压出来看看它的目录结构。

├── BOOT-INF│   ├── classes│   │   └── hello│   └── lib│       ├── classmate-1.3.4.jar│       ├── hibernate-validator-6.0.12.Final.jar│       ├── jackson-annotations-2.9.0.jar│       ├── jackson-core-2.9.6.jar│       ├── jackson-databind-2.9.6.jar│       ├── jackson-datatype-jdk8-2.9.6.jar│       ├── jackson-datatype-jsr310-2.9.6.jar│       ├── jackson-module-parameter-names-2.9.6.jar│       ├── javax.annotation-api-1.3.2.jar│       ├── jboss-logging-3.3.2.Final.jar│       ├── jul-to-slf4j-1.7.25.jar│       ├── log4j-api-2.10.0.jar│       ├── log4j-to-slf4j-2.10.0.jar│       ├── logback-classic-1.2.3.jar│       ├── logback-core-1.2.3.jar│       ├── slf4j-api-1.7.25.jar│       ├── snakeyaml-1.19.jar│       ├── spring-aop-5.0.9.RELEASE.jar│       ├── spring-beans-5.0.9.RELEASE.jar│       ├── spring-boot-2.0.5.RELEASE.jar│       ├── spring-boot-autoconfigure-2.0.5.RELEASE.jar│       ├── spring-boot-starter-2.0.5.RELEASE.jar│       ├── spring-boot-starter-json-2.0.5.RELEASE.jar│       ├── spring-boot-starter-logging-2.0.5.RELEASE.jar│       ├── spring-boot-starter-tomcat-2.0.5.RELEASE.jar│       ├── spring-boot-starter-web-2.0.5.RELEASE.jar│       ├── spring-context-5.0.9.RELEASE.jar│       ├── spring-core-5.0.9.RELEASE.jar│       ├── spring-expression-5.0.9.RELEASE.jar│       ├── spring-jcl-5.0.9.RELEASE.jar│       ├── spring-web-5.0.9.RELEASE.jar│       ├── spring-webmvc-5.0.9.RELEASE.jar│       ├── tomcat-embed-core-8.5.34.jar│       ├── tomcat-embed-el-8.5.34.jar│       ├── tomcat-embed-websocket-8.5.34.jar│       └── validation-api-2.0.1.Final.jar├── META-INF│   ├── MANIFEST.MF│   └── maven│       └── org.springframework└── org    └── springframework        └── boot

    这种打包方式的优势在于最终的 jar 包结构很清晰,所有的依赖一目了然。如果使用 maven shade 会将所有的 class 文件混乱堆积在一起,是无法看清其中的依赖。而最终生成的 jar 包在体积上两也者几乎是相等的。在运行机制上,使用 FatJar 技术运行程序是需要对 jar 包进行改造的,它还需要自定义自己的 ClassLoader 来加载 jar 包里面 lib 目录中嵌套的 jar 包中的类。我们可以对比一下两者的 MANIFEST 文件就可以看出明显差异。

// Generated by Maven Shade PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkCreated-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-bootMain-Class: hello.Application// Generated by SpringBootLoader PluginManifest-Version: 1.0Implementation-Title: gs-spring-bootImplementation-Version: 0.1.0Built-By: qianwpImplementation-Vendor-Id: org.springframeworkSpring-Boot-Version: 2.0.5.RELEASEMain-Class: org.springframework.boot.loader.JarLauncherStart-Class: hello.ApplicationSpring-Boot-Classes: BOOT-INF/classes/Spring-Boot-Lib: BOOT-INF/lib/Created-By: Apache Maven 3.5.4Build-Jdk: 1.8.0_191Implementation-URL: https://projects.spring.io/spring-boot/#/spring-bo ot-starter-parent/gs-spring-boot

    SpringBoot 将 jar 包中的 Main-Class 进行了替换,换成了 JarLauncher。还增加了一个 Start-Class 参数,这个参数对应的类才是真正的业务 main 方法入口。我们再看看这个 JarLaucher 具体干了什么。

public class JarLauncher{    ...  static void main(String[] args) {    new JarLauncher().launch(args);  }  protected void launch(String[] args) {    try {      JarFile.registerUrlProtocolHandler();      ClassLoader cl = createClassLoader(getClassPathArchives());      launch(args, getMainClass(), cl);    }    catch (Exception ex) {        ex.printStackTrace();        System.exit(1);    }  }  protected void launch(String[] args, String mcls, ClassLoader cl) {        Runnable runner = createMainMethodRunner(mcls, args, cl);        Thread runnerThread = new Thread(runner);        runnerThread.setContextClassLoader(classLoader);        runnerThread.setName(Thread.currentThread().getName());        runnerThread.start();  }}class MainMethodRunner {  @Override  public void run() {    try {      Thread th = Thread.currentThread();      ClassLoader cl = th.getContextClassLoader();      Class
mc = cl.loadClass(this.mainClassName); Method mm = mc.getDeclaredMethod("main", String[].class); if (mm == null) { throw new IllegalStateException(this.mainClassName + " does not have a main method"); } mm.invoke(null, new Object[] { this.args }); } catch (Exception ex) { ex.printStackTrace(); System.exit(1); } }}

    从源码中可以看出 JarLaucher 创建了一个特殊的 ClassLoader,然后由这个 ClassLoader 来另启一个单独的线程来加载 MainClass 并运行。

    又一个问题来了,当 JVM 遇到一个不认识的类,BOOT-INF/lib 目录里又有那么多 jar 包,它是如何知道去哪个 jar 包里加载呢?我们继续看这个特别的 ClassLoader 的源码。

class LaunchedURLClassLoader extends URLClassLoader {  ...  private Class
doLoadClass(String name) { if (this.rootClassLoader != null) { return this.rootClassLoader.loadClass(name); } findPackage(name); Class
cls = findClass(name); return cls; }}

    这里的 rootClassLoader 就是双亲委派模型里的 ExtensionClassLoader ,JVM 内置的类会优先使用它来加载。如果不是内置的就去查找这个类对应的 Package。

private void findPackage(final String name) {    int lastDot = name.lastIndexOf('.');    if (lastDot != -1) {        String packageName = name.substring(0, lastDot);        if (getPackage(packageName) == null) {            try {                definePackage(name, packageName);            } catch (Exception ex) {                // Swallow and continue            }        }    }}private final HashMap
packages = new HashMap<>();protected Package getPackage(String name) { Package pkg; synchronized (packages) { pkg = packages.get(name); } if (pkg == null) { if (parent != null) { pkg = parent.getPackage(name); } else { pkg = Package.getSystemPackage(name); } if (pkg != null) { synchronized (packages) { Package pkg2 = packages.get(name); if (pkg2 == null) { packages.put(name, pkg); } else { pkg = pkg2; } } } } return pkg;}private void definePackage(String name, String packageName) { String path = name.replace('.', '/').concat(".class"); for (URL url : getURLs()) { try { if (url.getContent() instanceof JarFile) { JarFile jf= (JarFile) url.getContent(); if (jf.getJarEntryData(path) != null && jf.getManifest() != null) { definePackage(packageName, jf.getManifest(), url); return null; } } } catch (IOException ex) { // Ignore } } return null;}

    ClassLoader 会在本地缓存包名和 jar包路径的映射关系,如果缓存中找不到对应的包名,就必须去 jar 包中挨个遍历搜寻,这个就比较缓慢了。不过同一个包名只会搜寻一次,下一次就可以直接从缓存中得到对应的内嵌 jar 包路径。深层 jar 包的内嵌 class 的 URL 路径长下面这样,使用感叹号 ! 分割。

jar:file:/workspace/springboot-demo/target/application.jar!/BOOT-INF/lib/snakeyaml-1.19.jar!/org/yaml/snakeyaml/Yaml.class

    不过这个定制的 ClassLoader 只会用于打包运行时,在 IDE 开发环境中 main 方法还是直接使用系统类加载器加载运行的。

    不得不说,SpringbootLoader 的设计还是很有意思的,它本身很轻量级,代码逻辑很独立没有其它依赖,它也是 SpringBoot 值得欣赏的点之一。

    4、HelloController 自动注册

        还剩下最后一个问题,那就是 HelloController 没有被代码引用,它是如何注册到 Tomcat 服务中去的?它靠的是注解传递机制。

        a57e5c494055de61f7810a05e3d730830b2.jpg

        SpringBoot 深度依赖注解来完成配置的自动装配工作,它自己发明了几十个注解,确实严重增加了开发者的心智负担,你需要仔细阅读文档才能知道它是用来干嘛的。Java 注解的形式和功能是分离的,它不同于 Python 的装饰器是功能性的,Java 的注解就好比代码注释,本身只有属性,没有逻辑,注解相应的功能由散落在其它地方的代码来完成,需要分析被注解的类结构才可以得到相应注解的属性。

        那注解是又是如何传递的呢?

        

@SpringBootApplicationpublic class Application {    public static void main(String[] args) {        SpringApplication.run(Application.class, args);    }}@ComponentScanpublic @interface SpringBootApplication {...}public @interface ComponentScan {    String[] basePackages() default {};}

        首先 main 方法可以看到的注解是 SpringBootApplication,这个注解又是由ComponentScan 注解来定义的,ComponentScan 注解会定义一个被扫描的包名称,如果没有显示定义那就是当前的包路径。SpringBoot 在遇到 ComponentScan 注解时会扫描对应包路径下面的所有 Class,根据这些 Class 上标注的其它注解继续进行后续处理。当它扫到 HelloController 类时发现它标注了 RestController 注解。

@RestControllerpublic class HelloController {...}@Controllerpublic @interface RestController {}

    而 RestController 注解又标注了 Controller 注解。SpringBoot 对 Controller 注解进行了特殊处理,它会将 Controller 注解的类当成 URL 处理器注册到 Servlet 的请求处理器中,在创建 Tomcat Server 时,会将请求处理器传递进去。HelloController 就是如此被自动装配进 Tomcat 的。

扫描处理注解是一个非常繁琐肮脏的活计,特别是这种用注解来注解注解(绕口)的高级使用方法,这种方法要少用慎用。SpringBoot 中有大量的注解相关代码,企图理解这些代码是乏味无趣的没有必要的,它只会把你的本来清醒的脑袋搞晕。

    总结

    以上就是对SpringBoot怎么运行的总结,大致过程就是这样的,但是有些细节还没说到,有待进一步深入了解,有兴趣的话可以一起讨论。

转载于:https://my.oschina.net/u/3492343/blog/2999242

你可能感兴趣的文章
[LeetCode] Reverse Lists
查看>>
前台页面之<base>标签
查看>>
angular分页插件tm.pagination 解决触发二次请求的问题
查看>>
day08-文件操作
查看>>
教学-45 对象的相等
查看>>
贪食蛇
查看>>
关于Spring 中的事务
查看>>
为什么现在都用面向对象开发,为什么现在都用分层开发结构?
查看>>
【离散数学】 SDUT OJ 偏序关系
查看>>
写给学弟学妹的产品入门建议(持续更新)
查看>>
view视图总结
查看>>
C# 知识点随手学习网站推荐
查看>>
深入剖析tomcat之一个简单的web服务器
查看>>
记一次数据库查询语句的优化
查看>>
poj2365
查看>>
SQL学习笔记三select语句的各种形式
查看>>
Android cts all pass 全攻略
查看>>
数学 SCU 4436 Easy Math
查看>>
Django form组件
查看>>
1006 等差数列
查看>>