Springboot 应用启动原理分析

梦想与她 提交于 2020-01-20 10:54:18

在 spring boot 中,很吸引人的一个特性是可以直接把应用打包称为一个 jar/war,这个 jar/war 是可以直接启动的,不需要额外配置 web server。

疑问

  • Spring boot 如何启动?
  • Spring boot 内置的 embed tomcat 是如何工作的?静态文件,jsp,网页模板是如何加载到的?

打包为单个 jar 包时,spring boot 的启动方式

maven 打包之后,会生成两个 jar 文件:

demo-0.0.1-SNAPSHOT.jar
demo-0.0.1-SNAPSHOT.jar.original

其中 demo-0.0.1-SNAPSHOT.jar.original 是默认的 maven-jar-plugin 生成的包。demo-0.0.1-SNAPSHOT.jar 是 spring boot maven 插件生成的 jar 包,里面包含了应用的依赖,以及 spring boot 相关的类。

先来看看 spring boot 打包好的目录结构:

├── META-INF
│   ├── MANIFEST.MF
├── application.properties
├── com
│   └── example
│       └── SpringBootDemoApplication.class
├── lib
│   ├── aopalliance-1.0.jar
│   ├── spring-beans-4.2.3.RELEASE.jar
│   ├── ...
└── org
    └── springframework
        └── boot
            └── loader
                ├── ExecutableArchiveLauncher.class
                ├── JarLauncher.class
                ├── JavaAgentDetector.class
                ├── LaunchedURLClassLoader.class
                ├── Launcher.class
                ├── MainMethodRunner.class
                ├── ...                

MANIFEST.MF

Manifest-Version: 1.0
Start-Class: com.example.SpringBootDemoApplication
Implementation-Vendor-Id: com.example
Spring-Boot-Version: 1.3.0.RELEASE
Created-By: Apache Maven 3.3.3
Build-Jdk: 1.8.0_60
Implementation-Vendor: Pivotal Software, Inc.
Main-Class: org.springframework.boot.loader.JarLauncher
  1. 可以看到有 Main-Class 是 org.springframework.boot.loader.JarLauncher,这个是 jar 启动的 Main 函数。
  2. 还有一个 Start-Class 是 com.example.SpringBootDemoApplication,这个是我们应用自己的 Main 函数。

com/example 目录

这里面放的是应用的 .class 文件。

lib 目录

这里存放的是应用的 Maven 依赖的 jar 包文件。比如 spring-beans,spring-mvc 等 jar 包。

org.springframework.boot.loader

这里面存放的是 spring boot loader 的 .class 文件。

Archive 的概念

  • archive 是归档文件,这个概念在 linux 下比较常见。
  • 通常就是一个 tar/zip 格式的压缩包。
  • jar 是 zip 格式。

在 spring boot 中,抽象除了 Archive 的概念。一个 archive 可以是一个 jar(JarFileArchive),也可以是一个文件目录(ExploadedArchive)。可以理解为 spring boot 抽象出来的统一访问资源的层。

上面的 demo-0.0.1-SNAPSHOT.jar 是要给 Archive,demo-0.0.1-SNAPSHOT.jar 里面的 /lib 目录下的每一个 jar 包都是一个 Archive。

javapublic abstract class Archive { public abstract URL getUrl(); public String getMainClass(); public abstract Collection<Entry> getEntries(); public abstract List<Archive> getNestedArchives(EntryFilter filter);

每个 Archive 都有一个自己的 URL,比如:

jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/

还有一个 getNestedArchives 函数,这个实际返回的是 demo-0.0.1-SNAPSHOT.jar/lib 下面的 jar 的 Archive 列表。它们的 URL 是:

jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/aopalliance-1.0.jar
jar:file:/tmp/target/demo-0.0.1-SNAPSHOT.jar!/lib/spring-beans-4.2.3.RELEASE.jar

JarLauncher

从 MANIFEST.MF 可以看到 Jar 启动的 Main 函数是 JarLauncher,下面来分析它的工作流程。

class JarLauncher extends ExecutableArchiveLauncher
class ExecutableArchiveLauncher extends Launcher
  1. 以 demo-0.01-SNAPSHOT.jar 创建一个 Archive:

    JarLauncher 先找到自己所在的 jar,即 demo-0.01-SNAPSHOT.jar 的路径,然后创建一个 Archive。

  2. 获取 /lib 下面的 jar,并创建一个 LaunchedURLClassLoader:

    JarLauncher 创建好 Archive 之后,通过 getNestedArchives 函数来获取 demo-0.0.1-SNAPSHOT.jar/lib 下面所有的 jar 文件,并创建为 List。获取到这些 Archive 的 URL 之后,也就获得了 URL[] 的数组,用来构造一个自定义的 ClassLoader: LaunchedURLClassLoader。

    创建好 ClassLoader 之后,再从 MANIFEST.MF 里读取到 start-class,即 com.example.SpringBootDemoApplication,然后创建一个新的线程来启动 Main 函数。

     /**
      * Launch the application given the archive file and a fully configured classloader.
      */
     protected void launch(String[] args, String mainClass, ClassLoader classLoader)
             throws Exception {
         Runnable runner = createMainMethodRunner(mainClass, args, classLoader);
         Thread runnerThread = new Thread(runner);
         runnerThread.setContextClassLoader(classLoader);
         runnerThread.setName(Thread.currentThread().getName());
         runnerThread.start();
     }
    
     /**
      * Create the {@code MainMethodRunner} used to launch the application.
      */
     protected Runnable createMainMethodRunner(String mainClass, String[] args,
             ClassLoader classLoader) throws Exception {
         Class<?> runnerClass = classLoader.loadClass(RUNNER_CLASS);
         Constructor<?> constructor = runnerClass.getConstructor(String.class,
                 String[].class);
         return (Runnable) constructor.newInstance(mainClass, args);
     }
  3. LaunchedURLClassLoader

    LaunchedURLClassLoader 和普通的 URLClassLoader 的不同之处在于,它提供了从 Archive 加载 .class 的能力。

Spring boot 应用启动流程总结

  1. spring boot 应用打包之后,生成了一个 jar 包,里面包含了应用依赖的 jar 包,还有 spring boot loader 相关的类。
  2. Jar 包启动的 Main 函数是 JarLauncher,它负责创建一个 LaunchedURLClassLoader 来加载 /lib 下面的 jar,并以一个新线程启动应用的 Main 函数。

Embed Tomcat 的启动流程

  1. 判断是否在 web 环境

    spring boot 启动时,先通过一个简单的查找 Servlet 类的方式来判断是不是在 web 环境:

    private static final String[] WEB_ENVIRONMENT_CLASSES = { "javax.servlet.Servlet",
        "org.springframework.web.context.ConfigurableWebApplicationContext" };
    
    private boolean deduceWebEnvironment() {
        for (String className : WEB_ENVIRONMENT_CLASSES) {
            if (!ClassUtils.isPresent(className, null)) {
                return false;
            }
        }
        return true;
    }
    
  2. 如果是 web 环境,则会创建 AnnotationConfigEmbeddedWebApplicationContext,否则 spring context 就是 AnnotationConfigApplicationContext。

    //org.springframework.boot.SpringApplication
     protected ConfigurableApplicationContext createApplicationContext() {
         Class<?> contextClass = this.applicationContextClass;
         if (contextClass == null) {
             try {
                 contextClass = Class.forName(this.webEnvironment
                         ? DEFAULT_WEB_CONTEXT_CLASS : DEFAULT_CONTEXT_CLASS);
             }
             catch (ClassNotFoundException ex) {
                 throw new IllegalStateException(
                         "Unable create a default ApplicationContext, "
                                 + "please specify an ApplicationContextClass",
                         ex);
             }
         }
         return (ConfigurableApplicationContext) BeanUtils.instantiate(contextClass);
     }
    
  3. 获取 EmbeddedServletContainerFactory 的实现类

    spring boot 通过获取 EmbeddedServletContainerFactory 来启动对应的 web 服务器。常用的两个实现类是 TomcatEmbeddedServletContainerFactory 和 JettyEmbeddedServletContainerFactory。

    //TomcatEmbeddedServletContainerFactory
    @Override
    public EmbeddedServletContainer getEmbeddedServletContainer(
            ServletContextInitializer... initializers) {
        Tomcat tomcat = new Tomcat();
        File baseDir = (this.baseDirectory != null ? this.baseDirectory
                : createTempDir("tomcat"));
        tomcat.setBaseDir(baseDir.getAbsolutePath());
        Connector connector = new Connector(this.protocol);
        tomcat.getService().addConnector(connector);
        customizeConnector(connector);
        tomcat.setConnector(connector);
        tomcat.getHost().setAutoDeploy(false);
        tomcat.getEngine().setBackgroundProcessorDelay(-1);
        for (Connector additionalConnector : this.additionalTomcatConnectors) {
            tomcat.getService().addConnector(additionalConnector);
        }
        prepareContext(tomcat.getHost(), initializers);
        return getTomcatEmbeddedServletContainer(tomcat);
    }
    

    会为 tomcat 创建一个临时文件目录,如:/tmp/tomcat.2233614112516545210,作为 tomcat 的 basedir。里面会存放 tomcat 的临时文件,比如 work 目录。

    还会初始化 Tomcat 的一些 Servlet,比如比较中要的 default/jsp servlet:

    private void addDefaultServlet(Context context) {
        Wrapper defaultServlet = context.createWrapper();
        defaultServlet.setName("default");
        defaultServlet.setServletClass("org.apache.catalina.servlets.DefaultServlet");
        defaultServlet.addInitParameter("debug", "0");
        defaultServlet.addInitParameter("listings", "false");
        defaultServlet.setLoadOnStartup(1);
        // Otherwise the default location of a Spring DispatcherServlet cannot be set
        defaultServlet.setOverridable(true);
        context.addChild(defaultServlet);
        context.addServletMapping("/", "default");
    }
    
    private void addJspServlet(Context context) {
        Wrapper jspServlet = context.createWrapper();
        jspServlet.setName("jsp");
        jspServlet.setServletClass(getJspServletClassName());
        jspServlet.addInitParameter("fork", "false");
        jspServlet.setLoadOnStartup(3);
        context.addChild(jspServlet);
        context.addServletMapping("*.jsp", "jsp");
        context.addServletMapping("*.jspx", "jsp");
    }
    

Spring Boot 的 web 应用访问 Resource

当 spring boot 应用被打包为一个 jar 时,是如何访问到 web resource 的?

实际上是通过 Archive 提供的 URL,然后通过 Classloader 提供的访问 classpath resource 的能力来实现的。

index.html

比如需要配置一个 index.html,这个可以直接放在代码里的 src/main/resources/static 目录下。

对于 index.html 欢迎页,spring boot 在初始化时,就会创建一个 ViewController 来处理。

//ResourceProperties
public class ResourceProperties implements ResourceLoaderAware {

    private static final String[] SERVLET_RESOURCE_LOCATIONS = { "/" };

    private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
            "classpath:/META-INF/resources/", "classpath:/resources/",
            "classpath:/static/", "classpath:/public/" };
            
//WebMvcAutoConfigurationAdapter
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            Resource page = this.resourceProperties.getWelcomePage();
            if (page != null) {
                logger.info("Adding welcome page: " + page);
                registry.addViewController("/").setViewName("forward:index.html");
            }
        }

template

像页面模板文件可以放在 src/main/resources/template 目录下,这个是需要模板实现类自己处理的,比如 ThymeleafProperties 类里的:

public static final String DEFAULT_PREFIX = "classpath:/templates/";

jsp

jsp 页面和 template 类似。实际上是通过 springmvc 内置的 JstlView 来处理的。

可以通过配置 spring.view.prefix 来设定 jsp 页面的目录。

spring.view.prefix: /WEB-INF/jsp/

统一错误页面处理

对于错误页面,Spring boot 也是通过创建一个 BasicErrorController 来统一处理的。

@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController 

对应的页面是一个简单的 html。

    @Configuration
    @ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
    @Conditional(ErrorTemplateMissingCondition.class)
    protected static class WhitelabelErrorViewConfiguration {

        private final SpelView defaultErrorView = new SpelView(
                "<html><body><h1>Whitelabel Error Page</h1>"
                        + "<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>"
                        + "<div id='created'>${timestamp}</div>"
                        + "<div>There was an unexpected error (type=${error}, status=${status}).</div>"
                        + "<div>${message}</div></body></html>");

        @Bean(name = "error")
        @ConditionalOnMissingBean(name = "error")
        public View defaultErrorView() {
            return this.defaultErrorView;
        }

Spring boot 应用的 maven 打包过程

先通过 maven-shade-plugin 生成一个包含依赖的 jar,再通过 spring-boot-maven-plugin 插件把 spring boot loader 相关的类,还有 MANIFEST.MF 打包到 jar 里。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!