栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

spring Boot启动流程&源码解析

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

spring Boot启动流程&源码解析

目录
      • spring boot是如何通过Java -jar 启动的
          • java -jar 做了什么
          • Jar包的打包插件及核心方法
          • Jar包目录结构
          • meta-INF内容
          • Archive的概念
          • JarLauncher
          • URLStreamHandler
        • spring boot的jar应用启动流程总结
          • 在IDE/开放目录启动Spring boot应用
      • Spring Boot是如何启动spring容器源码
        • 创建Spring Application
        • 启动
      • Spring Boot启动内嵌servlet容器

spring boot是如何通过Java -jar 启动的

得益于Spring Boot的封装,我们可以通过 java -jar 一行命令就可以启动一个web项目。再也不用搭建tomcat等相关容器。那么你是否关心过和探究过Spring Boot是如何封装达到这一效果的呢?本片文章带大家聊一聊,一探究竟。


我们都知道spring boot项目,当我们打包的时候会在pom文件中引入如下插件才能使用java -jar命令启动,这个插件帮我们生成了把我们所有依赖的jar包和我们的启动类打成jar包并指定Start-Class:启动类路径,难道指定这两个东西就能启动了吗?其实就算这样我们还是不能直接启动,因为Java没有提供任何标准的方式来加载嵌套的jar文件。在MANIFEAST文件中还执行了一个非常重要的配置就是Main-Class: org.springframework.boot.loader.JarLauncher(Spring Boot自定义出来的类加载器)来加载我们jar文件中的jar

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

java -jar 做了什么

先要弄清楚java -jar命令做了什么,在Oracle官网找到了这句描述。

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中的manifest,再找到真正的启动类

这有个疑惑:在MANIFEST.MF文件中有这么一行内容

Start-Class: com.example.SpringbootMybatisApplication

前面Java官方文档中,只提到过main-class,并没有提到Start-class;
Start-Class的值 com.example.SpringbootMybatisApplication,正是我们Java代码中的唯一类,所在的地址和类名称。
所以问题就来了::理论上执行java -jar 命令时Main-Class: org.springframework.boot.loader.JarLauncher类会被执行,但是实际上Start-Class: com.example.SpringbootMybatisApplication被执行了,这两个之间有什么联系?

  • 因为Java没有提供任何标准的方式来加载嵌套jar文件(即,它们本身包含在jar中的jar文件)。
Jar包的打包插件及核心方法

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

执行maven clean package之后,会生成两个文件

spring-boot-maven-plugin项目存在于spring-boot-tools目录中。spring-boot-maven-plugin默认有5个:goals、repackage、run、start、build-info。在打包的时候默认使用的是repackage。
spring-boot-maven-plugin的repackage能够将mvn package生成的软件包,再次打包可执行的软件包,并将mvn package生成的软件包重命名为*.original。
spring-boot-maven-plugin在代码层调用了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 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包目录结构

首先来看看jar的目录结构,都包含哪些目录和文件,解压jar包可以看到如下结构:

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

在上述目录结构中,meta-INF记录了相关jar包的基础信息,包括入口程序等。

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: springboot-mybatis
Implementation-Version: 0.0.1-SNAPSHOT
Start-Class: com.example.SpringbootMybatisApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.3.12.RELEASE
Created-By: Maven Jar Plugin 3.2.0
Main-Class: org.springframework.boot.loader.JarLauncher

可以看到有Main-Class是org.springframework.boot.loader.JarLauncher ,这个是jar启动的Main函数。
还有一个Start-Class是com.example.SpringbootMybatisApplication,这个是我们应用自己的Main函数。

Archive的概念

再继续了解底层和原理之前,我们先来了解一下Archive的概念:

  • archive即归档文件,这个概念在Linux下比较常见
  • 通常就是一个tar/zip格式的压缩包
  • jar是zip格式
    Spring Boot抽象了Archive的概念,一个Archive可以是jar(Jar FileArchive),也可以是一个文件目录(ExplodeArchive),可以抽象为统一访问资源的逻辑是:关于Spring Boot中Archive的源码如下:
public interface Archive extends Iterable {
    // 获取该归档的url
    URL getUrl() throws MalformedURLException;
    // 获取jar!/meta-INF/MANIFEST.MF或[ArchiveDir]/meta-INF/MANIFEST.MF
    Manifest getManifest() throws IOException;
    // 获取jar!/BOOT-INF/lib
            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;

从代码里可以看到,实际上是调用了url.openConnection()。这样完整的链条就可以连接起来了。

在IDE/开放目录启动Spring boot应用

在上面只提到在一个fat jar里启动SpringBoot应用的过程,那么IDE里Spring boot是如何启动的呢?
在IDE里,直接运行的Main函数是应用的Main函数:

@SpringBootApplication
public class SpringBootDemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(SpringBootDemoApplication.class, args);
    }
}

其实在IDE里启动SpringBoot应用是最简单的一种情况,因为依赖的Jar都让IDE放到classpath里了,所以Spring boot直接启动就完事了。
还有一种情况是在一个开放目录下启动SpringBoot启动。所谓的开放目录就是把fat jar解压,然后直接启动应用。
这时,Spring boot会判断当前是否在一个目录里,如果是的,则构造一个ExplodedArchive(前面在jar里时是JarFileArchive),后面的启动流程类似fat jar的。

总结
JarLauncher通过加载BOOT-INF/classes目录及BOOT-INF/lib目录下jar文件,实现了fat jar的启动。
SpringBoot通过扩展JarFile、JarURLConnection及URLStreamHandler,实现了jar in jar中资源的加载。
SpringBoot通过扩展URLClassLoader–LauncherURLClassLoader,实现了jar in jar中class文件的加载。
WarLauncher通过加载WEB-INF/classes目录及WEB-INF/lib和WEB-INF/lib-provided目录下的jar文件,实现了war文件的直接启动及web容器中的启动。

Spring Boot是如何启动spring容器源码

1、调用SpringApplication.run方法启动spring boot应用

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args); //调用run方法
    }
}

2、 使用自定义SpringApplication进行启动

public static ConfigurableApplicationContext run(Class[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}
创建Spring Application
public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		//将启动类放入primarySources,因为我们的启动类是一个配置类,配置了ComponentScan,在spring中启动也是传入一个配置类
		this.primarySources = new linkedHashSet<>(Arrays.asList(primarySources));
		//根据class Path下的类,推算当前web应用类型(webFlux,servlet)
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		//就是去spring.factroies中去获取所有key:org.springframework.context.ApplicationContextInitializer
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		//就是去spring.factories 中去获取所有key: org.springframework.context.ApplicationListener
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 根据main方法推算出mainApplicationClass
		this.mainApplicationClass = deduceMainApplicationClass();
	}

总结:

  1. 获取启动类:根据启动类加载ioc容器
  2. 获取web应用类型
  3. spring.factories读取了对外扩展的ApplicationContextInitializer,ApplicationListener主要为了:对外扩展,对内解耦(比如全局配置文件、热部署文件)
  4. 根据main推算出所在的类
启动

在创建Spring Application之后调用run方法

public static ConfigurableApplicationContext run(Class[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

run方法:

public ConfigurableApplicationContext run(String... args) {
		//用来记录当前Spring Boot启动耗时
		StopWatch stopWatch = new StopWatch();
		//记录启动开始时间
		stopWatch.start();
		//它是任何spring上下文接口,所以可以接受任何ApplicationContext实现
		ConfigurableApplicationContext context = null;
		// 开启了Headless模式:
		configureHeadlessProperty();
		// 去spring.factroies中读取了SpringApplicationRunListener 的组件,  就是用来发布事件或者运行监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 发布1.ApplicationStartingEvent事件,在运行开始时发送
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
			// 根据命令行参数 实例化一个ApplicationArguments 
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 预初始化环境: 读取环境变量,读取配置文件信息(基于监听器)
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
			// 忽略beaninfo的bean
			configureIgnoreBeanInfo(environment);
			// 打印Banner 横幅
			Banner printedBanner = printBanner(environment);
			// 根据webApplicationType创建Spring上下文  
			context = createApplicationContext();
			context.setApplicationStartup(this.applicationStartup);
			//预初始化spring上下文
			prepareContext(bootstrapContext, 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, listeners);
			throw new IllegalStateException(ex);
		}

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

总结:

  1. 初始化SpringApplication 从spring.factories 读取 listener ApplicationContextInitializer 。
    2.运行run方法
    3.读取 环境变量 配置信息…
  2. 创建springApplication上下文:ServletWebServerApplicationContext
  3. 预初始化上下文 : 读取启动类
    6.调用refresh 加载ioc容器
    加载所有的自动配置类
    创建servlet容器
    ps.在这个过程中springboot会调用很多监听器对外进行扩展
Spring Boot启动内嵌servlet容器

1、在refreshContext()方法中调用refresh方法下的onRefresh方法

 public void refresh() throws BeansException, IllegalStateException {
        synchronized(this.startupShutdownMonitor) {
            StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
            this.prepareRefresh();
            ConfigurableListableBeanFactory beanFactory = this.obtainFreshBeanFactory();
            this.prepareBeanFactory(beanFactory);

            try {
                this.postProcessBeanFactory(beanFactory);
                StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
                this.invokeBeanFactoryPostProcessors(beanFactory);
                this.registerBeanPostProcessors(beanFactory);
                beanPostProcess.end();
                this.initMessageSource();
                this.initApplicationEventMulticaster();
                this.onRefresh();//启动servlet容器
                this.registerListeners();
                this.finishBeanFactoryInitialization(beanFactory);
                this.finishRefresh();
            } catch (BeansException var10) {
                if (this.logger.isWarnEnabled()) {
                    this.logger.warn("Exception encountered during context initialization - cancelling refresh attempt: " + var10);
                }

                this.destroyBeans();
                this.cancelRefresh(var10);
                throw var10;
            } finally {
                this.resetCommonCaches();
                contextRefresh.end();
            }

        }
    }

2、进入onRefresh方法

@Override
	protected void onRefresh() {
		super.onRefresh();
		try {
			createWebServer(); //创建servlet容器
		}
		catch (Throwable ex) {
			throw new ApplicationContextException("Unable to start web server", ex);
		}
	}

3、进入创建servlet容器方法

private void createWebServer() {
		WebServer webServer = this.webServer;
		ServletContext servletContext = getServletContext();
		if (webServer == null && servletContext == null) {
			StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
			ServletWebServerFactory factory = getWebServerFactory();
			createWebServer.tag("factory", factory.getClass().toString());
			this.webServer = factory.getWebServer(getSelfInitializer()); //创建对应的servlet容器
			createWebServer.end();
			getBeanFactory().registerSingleton("webServerGracefulShutdown",
					new WebServerGracefulShutdownLifecycle(this.webServer));
			getBeanFactory().registerSingleton("webServerStartStop",
					new WebServerStartStopLifecycle(this, this.webServer));
		}
		else if (servletContext != null) {
			try {
				getSelfInitializer().onStartup(servletContext);
			}
			catch (ServletException ex) {
				throw new ApplicationContextException("Cannot initialize servlet context", ex);
			}
		}
		initPropertySources();
	}


4、这里我们以Tomcat容器为例

@Override
	public WebServer getWebServer(ServletContextInitializer... initializers) {
		if (this.disableMBeanRegistry) {
			Registry.disableRegistry();
		}
		Tomcat tomcat = new Tomcat();
		File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
		tomcat.setbaseDir(baseDir.getAbsolutePath());
		Connector connector = new Connector(this.protocol);
		connector.setThrowOnFailure(true);
		tomcat.getService().addConnector(connector);
		customizeConnector(connector);
		tomcat.setConnector(connector);
		tomcat.getHost().setAutoDeploy(false);
		configureEngine(tomcat.getEngine());
		for (Connector additionalConnector : this.additionalTomcatConnectors) {
			tomcat.getService().addConnector(additionalConnector);
		}
		prepareContext(tomcat.getHost(), initializers);
		return getTomcatWebServer(tomcat); //启动Tomcat
	}

5、进入getTomcatWebServer()——>启动Tomcat方法

public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
		Assert.notNull(tomcat, "Tomcat Server must not be null");
		this.tomcat = tomcat;
		this.autoStart = autoStart;
		this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
		initialize();//进入
	}
private void initialize() throws WebServerException {
		logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
		synchronized (this.monitor) {
			try {
				addInstanceIdToEngineName();

				Context context = findContext();
				context.addLifecycleListener((event) -> {
					if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
						// Remove service connectors so that protocol binding doesn't
						// happen when the service is started.
						removeServiceConnectors();
					}
				});

				// Start the server to trigger initialization listeners
				this.tomcat.start();//启动Tomcat方法

				// We can re-throw failure exception directly in the main thread
				rethrowDeferredStartupExceptions();

				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
				}
				catch (NamingException ex) {
					// Naming is not enabled. Continue
				}

				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();//将Tomcatawait
			}
			catch (Exception ex) {
				stopSilently();
				destroySilently();
				throw new WebServerException("Unable to start embedded Tomcat", ex);
			}
		}
	}
private void startDaemonAwaitThread() {
		Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

			@Override
			public void run() {
				TomcatWebServer.this.tomcat.getServer().await();  //将Tomcatawait
			}

		};
		awaitThread.setContextClassLoader(getClass().getClassLoader());
		awaitThread.setDaemon(false);
		awaitThread.start();
	}

最后附上一张内置与外置servlet容器启动流程图

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/439568.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号