springboot项目的启动探秘

本文探讨了SpringBoot项目的启动过程,从main方法开始,详细分析了如何创建ApplicationContext,加载配置,初始化数据库连接池,以及如何判断项目类型。在启动过程中,banner的打印、接口的接收机制也进行了揭秘。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


本文持续更新中。

springboot版本 2.1.0

让我们带着以下问题:

  • 一个springboot项目的banner是什么时候打印的?
  • 数据库连接池这些对数据库进行设置的操作在哪里进行的?
  • 一个项目启动后是怎样接收接口访问的?

开始我们的探秘之路?

一、探秘之路

  1. 首先,从springboot 的main方法着手
@SpringBootApplication
@ImportResource({"classpath:applicationContext-all.xml"})
@EnableAspectJAutoProxy (exposeProxy = true)
@EnableTransactionManagement
@Component
@EnableOAuthServer
public class Application 
{
    public static void main( String[] args )
    {
    	if(args != null && args.length>0) {
    		for(String arg:args) {
    			if(arg.startsWith("setuFilePath:")) {
    				String setuFilePath = arg.substring(13);
    				SetuSystemUtil.setuFilePath = setuFilePath;
    				System.out.println("使用外部配置文件:"+setuFilePath);
    			}
    		}
    	}
    	
    	
    	SetuSystemUtil.applicationName="founder_gateway";
    	SetuSystemUtil.sessionBeanClazz=SetuSession.class;
        SpringApplication.run(Application.class, args);
    }

}

为了指定默认启动类和默认设置,我们调用SpringApplication.run(Application.class, args)启动项目。

  1. ?,现在,进入org.springframework.boot.SpringApplication#run(java.lang.Class<?>, java.lang.String…)方法。
/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified source using default settings.
	 * @param primarySource the primary source to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
	public static ConfigurableApplicationContext run(Class<?> primarySource,
			String... args) {
		return run(new Class<?>[] { primarySource }, args);
	}

该方法可以运行一个SpringApplication。run方法接收的第一个参数是一个Class<?>类型的数组。

  1. 继续。进入org.springframework.boot.SpringApplication#run(java.lang.Class<?>[], java.lang.String[])
/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified sources using default settings and user supplied arguments.
	 * @param primarySources the primary sources to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
	public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

该方法中,创建了一个启动类SpringApplication对象。该对象是用来启动Spring 应用的。默认情况下,该对象会按照下面步骤启动应用:

  • 创建一个合适的ApplicationContext实例(取决于你的路径)
  • 注册一个CommandLinePropertySource来像Spring properties那样暴露命令行参数
  • 刷新应用上下文,加载所有的singleton beans
  • 触发CommandLineRunner beans

对象创建好之后,会执行run方法。不过,在查看run方法之前,先看看SpringApplication对象是怎样产生的?。

  1. 进入SpringApplication的构造函数
/**
	 * Create a new {@link SpringApplication} instance. The application context will load
	 * beans from the specified primary sources (see {@link SpringApplication class-level}
	 * documentation for details. The instance can be customized before calling
	 * {@link #run(String...)}.
	 * @param primarySources the primary bean sources
	 * @see #run(Class, String[])
	 * @see #SpringApplication(ResourceLoader, Class...)
	 * @see #setSources(Set)
	 */
public SpringApplication(Class<?>... primarySources) {
		this(null, primarySources);
	}

该方法创建一个SpringApplication实例。这个application context 将从指定的主类中加载beans。

  1. 进入this的。也就是org.springframework.boot.SpringApplication#SpringApplication(org.springframework.core.io.ResourceLoader, java.lang.Class<?>…)。
/**
	 * Create a new {@link SpringApplication} instance. The application context will load
	 * beans from the specified primary sources (see {@link SpringApplication class-level}
	 * documentation for details. The instance can be customized before calling
	 * {@link #run(String...)}.
	 * @param resourceLoader the resource loader to use
	 * @param primarySources the primary bean sources
	 * @see #run(Class, String[])
	 * @see #setSources(Set)
	 */
	@SuppressWarnings({ "unchecked", "rawtypes" })
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		//判断当前项目类型。具体源码见下面
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		//设置初始化器
		setInitializers((Collection) getSpringFactoriesInstances(
				ApplicationContextInitializer.class));
		//设置监听器。
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		//判断主类。具体源码见下面
		this.mainApplicationClass = deduceMainApplicationClass();
	}

该方法,首先将primarySources转换为一个list(可变参数只是个语法糖,本质还是数组)。然后通过deduceFromClasspath()判断当前项目类型。

deduceFromClasspath()源码如下:

/**
	 * The application should not run as a web application and should not start an
	 * embedded web server.
	 */
	NONE,

	/**
	 * The application should run as a servlet-based web application and should start an
	 * embedded servlet web server.
	 */
	SERVLET,

	/**
	 * The application should run as a reactive web application and should start an
	 * embedded reactive web server.
	 */
	REACTIVE;

	private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
			"org.springframework.web.context.ConfigurableWebApplicationContext" };

	private static final String WEBMVC_INDICATOR_CLASS = "org.springframework."
			+ "web.servlet.DispatcherServlet";

	private static final String WEBFLUX_INDICATOR_CLASS = "org."
			+ "springframework.web.reactive.DispatcherHandler";

	private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

	private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

	private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

static WebApplicationType deduceFromClasspath() {
		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
				&& !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
			return WebApplicationType.REACTIVE;
		}
		for (String className : SERVLET_INDICATOR_CLASSES) {
			if (!ClassUtils.isPresent(className, null)) {
				return WebApplicationType.NONE;
			}
		}
		return WebApplicationType.SERVLET;
	}

该方法通过判断指定路径下的文件是否存在并可以加载来判断当前项目类型。

deduceMainApplicationClass():该方法是通过创建一个RuntimeException来获取跟踪栈,进而判断每一个栈中的方法是否和"main"相等。然后,通过classForName的方法加载main方法所在的类。

	private Class<?> deduceMainApplicationClass() {
		try {
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
			for (StackTraceElement stackTraceElement : stackTrace) {
				if ("main".equals(stackTraceElement.getMethodName())) {
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		}
		catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}
  1. 现在,springApplication对象判断好了当前项目类型且设置好了初始化器、监听器。就开始执行run方法了,
	public static ConfigurableApplicationContext run(Class<?>[] primarySources,
			String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

具体org.springframework.boot.SpringApplication#run(java.lang.String…)方法内部如下:

/**
	 * Run the Spring application, creating and refreshing a new
	 * {@link ApplicationContext}.
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return a running {@link ApplicationContext}
	 */
	public ConfigurableApplicationContext run(String... args) {
		//stopWatch相当于计时工具。最后会打印出本次启动项目所花时间
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		
		ConfigurableApplicationContext context = null;
		//创建启动错误报告对象
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		configureHeadlessProperty();
		
		//将启动main方法携带的参数args也加到监听器中。还记得之前创建SpringApplication对象时也设置了监听器吗。
		SpringApplicationRunListeners listeners = getRunListeners(args);
		//开启监听
		listeners.starting();
		try {
			//对参数进行包装。对环境进行
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(
					args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners,
					applicationArguments);
			configureIgnoreBeanInfo(environment);

			//打印出旗帜
			Banner printedBanner = printBanner(environment);
			//根据当前项目的类型(是SERVLET/REACTIVE/默认),创建应用上下文环境。
			context = createApplicationContext();
			//创建启动异常报告对象
			exceptionReporters = getSpringFactoriesInstances(
					SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			//还记得创建SpringApplication对象时设置的初始化器嘛。现在就要使用初始化器来初始化context了
			prepareContext(context, environment, listeners, applicationArguments,
					printedBanner);
			//刷新上下文,会打印出一堆日志出来。如初始化数据库连接池,设置切面、初始化tomcat、启动tomcat、过滤器映射
			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 {
			//当应用上下文被刷新了并且CommandLineRunners和ApplicationRunners被唤起后,在这个run方法结束之前立即唤起
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

当运行到Banner printedBanner = printBanner(environment);时,将打印出旗帜(当然这个旗帜是配置的,可以在resource目录下新建一个banner.txt文件):
在这里插入图片描述
prepareContext()方法将打印出第一行日志:
在这里插入图片描述

到这里,终于完了?。

二、结论

最终要的就是这行代码

new SpringApplication(primarySources).run(args);

前面是提供SpringApplication对象所需的初始化器和监听器,run方法是应用初始化器和开始监听。

初识化数据库连接池是在resfreshContext里面进行的。

判断当前项目是个什么项目(servlet),是看指定路径下是否有相应的类。

启动流程:

打印旗帜
日志设置
创建启动错误报告对象
准备上下文,打印第一行启动日志
刷新上下文,这里会进行初始数据库连接池、初始化启动tomcat、过滤器映射

三、参考文献

springboot源码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值