• Springboot 应用启动原理分析


    在 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。

    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 里。

  • 相关阅读:
    广播通信
    IP多播
    套接字选项
    IO处理线程
    单源最短路径
    活动安排问题贪心算法
    贪心算法
    IO控制命令
    [转]qsort详解
    2012下半年学习目录
  • 原文地址:https://www.cnblogs.com/paulwang92115/p/12216697.html
Copyright © 2020-2023  润新知