SpringBoot是如何通过jar包启动的

jar -jar xx.jar就可以启动一个web应用。再也不用操心搭建tomcat等相关web容器,底层原理是怎么实现的呢?

前提学习

java -jar的官网介绍?

If the -jar option is specified, its argument is the name of the JAR file containing class and resource files for the application. The startup class must be indicated by the Main-Class manifest header in its source code.

大致意思是:

使用-jar参数时,后面的参数是的jar文件名(本例中是springbootstarterdemo-0.0.1-SNAPSHOT.jar);

该jar文件中包含的是class和资源文件;

在manifest文件中有Main-Class的定义;

Main-Class的源码中指定了整个应用的启动类;(in its source code)

通俗话:虚拟机通过java -jar 不能直接加载jar里面的jar文件,如果想加载jar包里面所依赖的jar包需要自己去扩展实现。

总结:java -jar会去找jar中的manifest.mf文件,在那里面找到真正的启动类;

Archive的概念

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

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。关于Spring Boot中Archive的源码如下:

public interface Archive extends Iterable<Archive.Entry> {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/META-INF/MANIFEST.MF或[ArchiveDir]/META-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib/*.jar或[ArchiveDir]/BOOT-INF/lib/*.jar
    List<Archive> getNestedArchives(EntryFilter filter) throws IOException;
}

SpringBoot定义了一个接口用于描述资源,也就是org.springframework.boot.loader.archive.Archive。该接口有两个实现,分别是org.springframework.boot.loader.archive.ExplodedArchive和org.springframework.boot.loader.archive.JarFileArchive。前者用于在文件夹目录下寻找资源,后者用于在jar包环境下寻找资源。而在SpringBoot打包的fatJar中,则是使用后者。

JarFile:对jar包的封装,每个JarFileArchive都会对应一个JarFile。JarFile被构造的时候会解析内部结构,去获取jar包里的各个文件或文件夹,这些文件或文件夹会被封装到Entry中,也存储在JarFileArchive中。如果Entry是个jar,会解析成JarFileArchive。

比如一个JarFileArchive对应的URL为:

jar:file:/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar!/

对应的JarFile为:

/Users/format/Develop/gitrepository/springboot-analysis/springboot-executable-jar/target/executable-jar-1.0-SNAPSHOT.jar

Jar包的打包插件及核心方法

maven打包插件

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

mvn clean package执行后生成包结构

spring-learn-0.0.1-SNAPSHOT.jar

spring-learn-0.0.1-SNAPSHOT.jar.original

spring-boot-maven-plugin项目存在于spring-boot-tools目录中。spring-boot-maven-plugin默认有5个goals:repackage、run、start、stop、build-info。在打包的时候默认使用的是repackage。

spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,命名为*.original。(表示原始的),再次打包为可执行的软件*.jar(表示包含依赖可以直接执行的)。

spring-boot-maven-plugin的repackage在代码层面调用了RepackageMojo的execute方法,而在该方法中又调用了repackage方法。repackage方法代码及操作解析如下:

private void repackage() throws MojoExecutionException {
   // maven生成的jar,最终的命名将加上.original后缀
   Artifact source = getSourceArtifact();
   // 最终为可执行jar,即fat jar
   File target = getTargetFile();
   // 获取重新打包器,将maven生成的jar重新打包成可执行jar
   Repackager repackager = getRepackager(source.getFile());
   // 查找并过滤项目运行时依赖的jar
   Set<Artifact> artifacts = filterDependencies(this.project.getArtifacts(),
         getFilters(getAdditionalFilters()));
   // 将artifacts转换成libraries
   Libraries libraries = new ArtifactsLibraries(artifacts, this.requiresUnpack,
         getLog());
   try {
      // 获得Spring Boot启动脚本
      LaunchScript launchScript = getLaunchScript();
      // 执行重新打包,生成fat jar
      repackager.repackage(target, libraries, launchScript);
   }catch (IOException ex) {
      throw new MojoExecutionException(ex.getMessage(), ex);
   }
   // 将maven生成的jar更新成.original文件
   updateArtifact(source, target, repackager.getBackupFile());
}

生成的jar包目录解压出来的结构

spring-boot-learn-0.0.1-SNAPSHOT
├── META-INF
│   └── MANIFEST.MF
├── BOOT-INF
│   ├── classes
│   │   └── 应用程序类
│   └── lib
│       └── 第三方依赖jar
└── org
    └── springframework
        └── boot
            └── loader
                └── springboot启动程序

我们接下来关心的META-INF内容

Manifest-Version: 1.0
Implementation-Title: spring-learn
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.hello.Application
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.1.5.RELEASE
Created-By: Maven Archiver 3.4.0
Main-Class: org.springframework.boot.loader.JarLauncher

自此java -jar的入口找到

Main-Class: org.springframework.boot.loader.JarLauncher

JarLauncher

按照定义,JarLauncher可以加载内部/BOOT-INF/lib下的jar及/BOOT-INF/classes下的应用class,其实JarLauncher实现很简单:

public class JarLauncher extends ExecutableArchiveLauncher {
    public JarLauncher() {}
    public static void main(String[] args) throws Exception {
        new JarLauncher().launch(args);
    }
}

Launcher

protected void launch(String[] args) throws Exception {
		JarFile.registerUrlProtocolHandler();
		//以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。
		ClassLoader classLoader = createClassLoader(getClassPathArchives());
		//以上面构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main。进而启动整个程序。
		launch(args, getMainClass(), classLoader);
	}

JarLauncher继承于org.springframework.boot.loader.ExecutableArchiveLauncher。该类的无参构造方法最主要的功能就是构建了当前main方法所在的FatJar的JarFileArchive对象。下面来看launch方法。该方法主要是做了2个事情:

(1)以FatJar为file作为入参,构造JarFileArchive对象。获取其中所有的资源目标,取得其Url,将这些URL作为参数,构建了一个URLClassLoader。

(2)以第一步构建的ClassLoader加载MANIFEST.MF文件中Start-Class指向的业务类,并且执行静态方法main。进而启动整个程序。

public abstract class ExecutableArchiveLauncher extends Launcher {
    private final Archive archive;
    public ExecutableArchiveLauncher() {
        try {
            // 找到自己所在的jar,并创建Archive
            this.archive = createArchive();
        }
        catch (Exception ex) {
            throw new IllegalStateException(ex);
        }
    }
}

protected List<Archive> getClassPathArchives() throws Exception {
		// archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,
		// 通过这些archives的url生成LaunchedURLClassLoader
		List<Archive> archives = new ArrayList<>(this.archive.getNestedArchives(this::isNestedArchive));
		postProcessClassPathArchives(archives);
		return archives;
}

 
 
public abstract class Launcher {
    protected final Archive createArchive() throws Exception {
        ProtectionDomain protectionDomain = getClass().getProtectionDomain();
        CodeSource codeSource = protectionDomain.getCodeSource();
        URI location = (codeSource == null ? null : codeSource.getLocation().toURI());
        String path = (location == null ? null : location.getSchemeSpecificPart());
        if (path == null) {
            throw new IllegalStateException("Unable to determine code source archive");
        }
        File root = new File(path);
        if (!root.exists()) {
            throw new IllegalStateException(
                    "Unable to determine code source archive from " + root);
        }
        return (root.isDirectory() ? new ExplodedArchive(root)
                : new JarFileArchive(root));
    }
}

通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用。

jar启动流程总结

(1)Spring Boot应用打包之后,生成一个jar包,包含了应用依赖的jar包和Spring Boot loader相关的类。

(2)jar包的启动Main函数是JarLauncher,它负责创建一个LaunchedURLClassLoader来加载/lib下面的jar,并以一个新线程启动应用的Main函数。ClassLoader是如何读取到Resource?

ClassLoader读取到Resource

SpringBoot构造LaunchedURLClassLoader时,传递了一个URL[]数组。数组里是lib目录下面的jar的URL。

对于一个URL,JDK或者ClassLoader如何知道怎么读取到里面的内容的?流程如下:

  • LaunchedURLClassLoader.loadClass
  • URL.getContent()
  • URL.openConnection()
  • Handler.openConnection(URL)

最终调用的是JarURLConnection的getInputStream()函数。

public InputStream getInputStream() throws IOException {
   connect();
   if (this.jarEntryName.isEmpty()) {
     throw new IOException("no entry name specified");
   }
   return this.jarEntryData.getInputStream();
 }

从一个URL,到最终读取到URL里的内容,整个过程是比较复杂的,总结下:

  • Spring boot注册了一个Handler来处理”jar:”这种协议的URL。
  • Spring boot扩展了JarFile和JarURLConnection,内部处理jar in jar的情况。
  • 在处理多重jar in jar的URL时,Spring Boot会循环处理,并缓存已经加载到的JarFile。
  • 对于多重jar in jar,实际上是解压到了临时目录来处理,可以参考JarFileArchive里的代码。
  • 在获取URL的InputStream时,最终获取到的是JarFile里的JarEntryData。

细节很多,上面只列出比较重要的步骤。最后,URLClassLoader是如何getResource的呢?URLClassLoader在构造时,有URL[]数组参数,它内部会用这个数组来构造一个URLClassPath:

URLClassPath ucp = new URLClassPath(urls);

在URLClassPath内部会为这些URLS都构造一个Loader,然后在getResource时,会从这些Loader里一个个去尝试获取。如果获取成功的话,就像下面那样包装为一个Resource。

Resource getResource(final String name, boolean check) {
    final URL url;
    try {
        url = new URL(base, ParseUtil.encodePath(name, false));
    } catch (MalformedURLException e) {
        throw new IllegalArgumentException("name");
    }
    final URLConnection uc;
    try {
        if (check) {
            URLClassPath.check(url);
        }
        uc = url.openConnection();
        InputStream in = uc.getInputStream();
        if (uc instanceof JarURLConnection) {
            /* Need to remember the jar file so it can be closed
             * in a hurry.
             */
            JarURLConnection juc = (JarURLConnection)uc;
            jarfile = JarLoader.checkJar(juc.getJarFile());
        }
    } catch (Exception e) {
        return null;
    }
    return new Resource() {
        public String getName() { return name; }
        public URL getURL() { return url; }
        public URL getCodeSourceURL() { return base; }
        public InputStream getInputStream() throws IOException {
            return uc.getInputStream();
        }
        public int getContentLength() throws IOException {
            return uc.getContentLength();
        }
    };
}
            JarURLConnection juc = (JarURLConnection)uc;

SpringBoot启动Spring容器原理

SpringApplication启动类

public ConfigurableApplicationContext run(String... args) {
		// 用来记录当前springboot启动耗时
		StopWatch stopWatch = new StopWatch();
		// 就是记录了启动开始时间
		stopWatch.start();
		// 它是任何spring上下文的接口, 所以可以接收任何ApplicationContext实现
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		// 开启了Headless模式
		configureHeadlessProperty();
		//从META-INF/spring.factories中获取监听器SpringApplicationRunListeners(spring-boot模块下)
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布1.ApplicationStartingEvent事件,在运行开始时发布.
		listeners.starting();
		try {
			//根据命令行参数实例化一个ApplicationArguments
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 预初始化环境:读取环境变量,读取配置文件信息(基于监听器)
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			// 忽略beaninfo的bean
			configureIgnoreBeanInfo(environment);
			// 打印Banner 横幅
			Banner printedBanner = printBanner(environment);
			//根据webApplicationType创建Spring上下文
			context = createApplicationContext();
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			//预初始化spring上下文
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 加载spring ioc 容器   **相当重要   由于是使用AnnotationConfigServletWebServerApplicationContext 启动的spring容器所以springboot对它做了扩展:
			//  加载自动配置类:invokeBeanFactoryPostProcessors,  创建servlet容器onRefresh
			refreshContext(context);
			afterRefresh(context, applicationArguments);
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

准备环境prepareEnvironment基于监听器

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// 根据webApplicationType创建Environment 创建就会读取: java环境变量和系统环境变量
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		// 将命令行参数读取环境变量中
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		// 将@PropertieSource的配置信息放在第一位,因为读取配置文件@PropertieSource优先级是最低的
		ConfigurationPropertySources.attach(environment);
		// 发布了ApplicationEnvironmentPreparedEvent的事件 ,读取了全局配置文件
		listeners.environmentPrepared(environment);
		// 将所有spring.main开头的配置信息绑定SpringApplication
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		// 更新PropertySources
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

创建不同类型的容器createApplicationContext,比如基于servlet类型的web容器


AnnotationConfigServletWebServerApplicationContext


protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET:
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, " + "please specify an ApplicationContextClass",
						ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);

启动spring容器的核心代码refreshContext(context)

protected void refresh(ApplicationContext applicationContext) {
		Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
		((AbstractApplicationContext) applicationContext).refresh();
	}

外置tomcat如何启动springboot应用?

tomcat不会主动去启动springboot应用 ,所以tomcat启动的时候调用了SpringBootServletInitializer的SpringApplicationBuilder ,

什么是SPI

SPI,全称为 Service Provider Interface(服务提供者接口),是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。

当servlet容器启动时候 就会去META-INF/services 文件夹中找到javax.servlet.ServletContainerInitializer,  当servlet容器启动时候就会通过spi机制去该文件中找到ServletContainerInitializer的实现类,从而创建它的实例调用onstartUp启动springboot应用。


SpringServletContainerInitializer


public void onStartup(@Nullable Set<Class<?>> webAppInitializerClasses, ServletContext servletContext)
			throws ServletException {

		List<WebApplicationInitializer> initializers = new LinkedList<>();

		if (webAppInitializerClasses != null) {
			for (Class<?> waiClass : webAppInitializerClasses) {
				// Be defensive: Some servlet containers provide us with invalid classes,
				// no matter what @HandlesTypes says...
				if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) &&
						WebApplicationInitializer.class.isAssignableFrom(waiClass)) {
					try {
						initializers.add((WebApplicationInitializer)
								ReflectionUtils.accessibleConstructor(waiClass).newInstance());
					}
					catch (Throwable ex) {
						throw new ServletException("Failed to instantiate WebApplicationInitializer class", ex);
					}
				}
			}
		}

		if (initializers.isEmpty()) {
			servletContext.log("No Spring WebApplicationInitializer types detected on classpath");
			return;
		}

		servletContext.log(initializers.size() + " Spring WebApplicationInitializers detected on classpath");
		AnnotationAwareOrderComparator.sort(initializers);
		for (WebApplicationInitializer initializer : initializers) {
			initializer.onStartup(servletContext);
		}
	}

 

            initializer.onStartup(servletContext);

spring boot logging 启动过程分析 spring boot 启动原理_spring

  • SpringBootServletInitializer
public void onStartup(ServletContext servletContext) throws ServletException {
		// Logger initialization is deferred in case an ordered
		// LogServletContextInitializer is being used
		this.logger = LogFactory.getLog(getClass());
		WebApplicationContext rootAppContext = createRootApplicationContext(servletContext);
		if (rootAppContext != null) {
			servletContext.addListener(new ContextLoaderListener(rootAppContext) {
				@Override
				public void contextInitialized(ServletContextEvent event) {
					// no-op because the application context is already initialized
				}
			});
		}
		else {
			this.logger.debug("No ContextLoaderListener registered, as " + "createRootApplicationContext() did not "
					+ "return an application context");
		}
	}

启动springboot应用


createRootApplicationContext


protected WebApplicationContext createRootApplicationContext(ServletContext servletContext) {
   SpringApplicationBuilder builder = createSpringApplicationBuilder();
   builder.main(getClass());
   ApplicationContext parent = getExistingRootWebApplicationContext(servletContext);
   if (parent != null) {
      this.logger.info("Root context already created (using as parent).");
      servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, null);
      builder.initializers(new ParentContextApplicationContextInitializer(parent));
   }
   builder.initializers(new ServletContextApplicationContextInitializer(servletContext));
   builder.contextClass(AnnotationConfigServletWebServerApplicationContext.class);
   // 调用configure
   builder = configure(builder);
   builder.listeners(new WebEnvironmentPropertySourceInitializer(servletContext));
   SpringApplication application = builder.build();
   if (application.getAllSources().isEmpty()
         && MergedAnnotations.from(getClass(), SearchStrategy.TYPE_HIERARCHY).isPresent(Configuration.class)) {
      application.addPrimarySources(Collections.singleton(getClass()));
   }
   Assert.state(!application.getAllSources().isEmpty(),
         "No SpringApplication sources have been defined. Either override the "
               + "configure method or add an @Configuration annotation");
   // Ensure error pages are registered
   if (this.registerErrorPageFilter) {
      application.addPrimarySources(Collections.singleton(ErrorPageFilterConfiguration.class));
   }
   application.setRegisterShutdownHook(false);
   return run(application);
}


run(application),和手动调用一个原理SpringApplication.run(Application.class, args);


protected WebApplicationContext run(SpringApplication application) {
		return (WebApplicationContext) application.run();
	}

内置tomcat启动原理

待定...