草庐IT

为啥一个 Main 方法就能启动项目

刘水镜 2023-03-28 原文

在 Spring Boot 出现之前,我们要运行一个 Java Web 应用,首先需要有一个 Web 容器(例如 Tomcat 或 Jetty),然后将我们的 Web 应用打包后放到容器的相应目录下,最后再启动容器。

在 IDE 中也需要对 Web 容器进行一些配置,才能够运行或者 Debug。而使用 Spring Boot 我们只需要像运行普通 JavaSE 程序一样,run 一下 main() 方法就可以启动一个 Web 应用了。这是怎么做到的呢?今天我们就一探究竟,分析一下 Spring Boot 的启动流程。

概览

回看我们写的第一个 Spring Boot 示例,我们发现,只需要下面几行代码我们就可以跑起一个 Web 服务器:

@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
去掉类的声明和方法定义这些样板代码,核心代码就只有一个 @SpringBootApplication 注解和 SpringApplication.run(HelloApplication.class, args) 了。而我们知道注解相当于是一种配置,那么这个 run() 方法必然就是 Spring Boot 的启动入口了。

接下来,我们沿着 run() 方法来顺藤摸瓜。进入 SpringApplication 类,来看看 run() 方法的具体实现:

public class SpringApplication {
......
public ConfigurableApplicationContext run(String... args) {
// 1 应用启动计时开始
StopWatch stopWatch = new StopWatch();
stopWatch.start();

// 2 声明上下文
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;

// 3 设置 java.awt.headless 属性
configureHeadlessProperty();

// 4 启动监听器
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 5 初始化默认应用参数
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

// 6 准备应用环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);

// 7 打印 Banner(Spring Boot 的 LOGO)
Banner printedBanner = printBanner(environment);

// 8 创建上下文实例
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);

// 9 构建上下文
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

// 10 刷新上下文
refreshContext(context);

// 11 刷新上下文后处理
afterRefresh(context, applicationArguments);

// 12 应用启动计时结束
stopWatch.stop();
if (this.logStartupInfo) {
// 13 打印启动时间日志
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

// 14 发布上下文启动完成事件
listeners.started(context);

// 15 调用 runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
// 16 应用启动发生异常后的处理
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}


try {
// 17 发布上下文就绪事件
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
......
}
Spring Boot 启动时做的所有操作都这这个方法里面,当然在调用上面这个 run() 方法之前,还创建了一个 SpringApplication 的实例对象。因为上面这个 run() 方法并不是一个静态方法,所以需要一个对象实例才能被调用。

可以看到,方法的返回值类型为
ConfigurableApplicationContext,这是一个接口,我们真正得到的是 AnnotationConfigServletWebServerApplicationContext 的实例。通过类名我们可以知道,这是一个基于注解的 Servlet Web 应用上下文(我们知道上下文(context)是 Spring 中的核心概念)。

上面对于 run() 方法中的每一个步骤都做了简单的注释,接下来我们选择几个比较有代表性的来详细分析。

应用启动计时

在 Spring Boot 应用启动完成时,我们经常会看到类似下面内容的一条日志:

Started AopApplication in 2.732 seconds (JVM running for 3.734)
应用启动后,会将本次启动所花费的时间打印出来,让我们对于启动的速度有一个大致的了解,也方便我们对其进行优化。记录启动时间的工作是 run() 方法做的第一件事,在编号 1 的位置由 stopWatch.start() 开启时间统计,具体代码如下:

public void start(String taskName) throws IllegalStateException {
if (this.currentTaskName != null) {
throw new IllegalStateException("Can't start StopWatch: it's already running");
}
// 记录启动时间
this.currentTaskName = taskName;
this.startTimeNanos = System.nanoTime();
}
然后到了 run() 方法的基本任务完成的时候,由 stopWatch.stop()(编号 12 的位置)对启动时间做了一个计算,源码也很简单:

public void stop() throws IllegalStateException {
if (this.currentTaskName == null) {
throw new IllegalStateException("Can't stop StopWatch: it's not running");
}
// 计算启动时间
long lastTime = System.nanoTime() - this.startTimeNanos;
this.totalTimeNanos += lastTime;
......
}
最后,在 run() 中的编号 13 的位置将启动时间打印出来:

if (this.logStartupInfo) {
// 打印启动时间
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}

打印 Banner

Spring Boot 每次启动是还会打印一个自己的 LOGO,如图 8-6:

图 8-6 Spring Boot Logo

这种做法很常见,像 Redis、Docker 等都会在启动的时候将自己的 LOGO 打印出来。Spring Boot 默认情况下会打印那个标志性的“树叶”和 “Spring” 的字样,下面带着当前的版本。

在 run() 中编号 7 的位置调用打印 Banner 的逻辑,最终由 SpringBootBanner 类的 printBanner() 完成。这个图案定义在一个常量数组中,代码如下:

class SpringBootBanner implements Banner {
private static final String[] BANNER = {
"",
" . ____ _ __ _ _",
" /\\\\ / ___'_ __ _ _(_)_ __ __ _ \\ \\ \\ \\",
"( ( )\\___ | '_ | '_| | '_ \\/ _` | \\ \\ \\ \\",
" \\\\/ ___)| |_)| | | | | || (_| | ) ) ) )",
" ' |____| .__|_| |_|_| |_\\__, | / / / /",
" =========|_|==============|___/=/_/_/_/"
};
......

public void printBanner(Environment environment, Class<?> sourceClass, PrintStream printStream) {
for (String line : BANNER) {
printStream.println(line);
}
......
}
}
手工格式化了一下 BANNER 的字符串,轮廓已经清晰可见了。真正打印的逻辑就是 printBanner() 方法里面的那个 for 循环。

记录启动时间和打印 Banner 代码都非常的简单,而且都有很明显的视觉反馈,可以清晰的看到结果。拿出来咱们做个热身,配合断点去 Debug 会有更加直观的感受,尤其是打印 Banner 的时候,可以看到整个内容被一行一行打印出来,让我想起了早些年用那些配置极低的电脑(还是 CRT 显示器)运行着 Win98,经常会看到屏幕内容一行一行加载显示。

创建上下文实例

下面我们来到 run() 方法中编号 8 的位置,这里调用了一个 createApplicationContext() 方法,该方法最终会调用 ApplicationContextFactory 接口的代码:

ApplicationContextFactory DEFAULT = (webApplicationType) -> {
try {
switch (webApplicationType) {
case SERVLET:
return new AnnotationConfigServletWebServerApplicationContext();
case REACTIVE:
return new AnnotationConfigReactiveWebServerApplicationContext();
default:
return new AnnotationConfigApplicationContext();
}
}
catch (Exception ex) {
throw new IllegalStateException("Unable create a default ApplicationContext instance, "
+ "you may need a custom ApplicationContextFactory", ex);
}
};
这个方法就是根据 SpringBootApplication 的 webApplicationType 属性的值,利用反射来创建不同类型的应用上下文(context)。而属性 webApplicationType 的值是在前面执行构造方法的时候由
WebApplicationType.deduceFromClasspath() 获得的。通过方法名很容易看出来,就是根据 classpath 中的类来推断当前的应用类型。

我们这里是一个普通的 Web 应用,所以最终返回的类型为 SERVLET。所以会返回一个
AnnotationConfigServletWebServerApplicationContext 实例。

构建容器上下文

接着我们来到 run() 方法编号 9 的 prepareContext() 方法。通过方法名,我们也能猜到它是为 context 做上台前的准备工作的。

private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
......
// 加载资源
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
在这个方法中,会做一些准备工作,包括初始化容器上下文、设置环境、加载资源等。

加载资源

上面的代码中,又调用了一个很关键的方法——load()。这个 load() 方法真正的作用是去调用 BeanDefinitionLoader 类的 load() 方法。源码如下:

class BeanDefinitionLoader {
......
void load() {
for (Object source : this.sources) {
load(source);
}
}
private void load(Object source) {
Assert.notNull(source, "Source must not be null");
if (source instanceof Class<?>) {
load((Class<?>) source);
return;
}
if (source instanceof Resource) {
load((Resource) source);
return;
}
if (source instanceof Package) {
load((Package) source);
return;
}
if (source instanceof CharSequence) {
load((CharSequence) source);
return;
}
throw new IllegalArgumentException("Invalid source type " + source.getClass());
}
......
}
可以看到,load() 方法在加载 Spring 中各种资源。其中我们最熟悉的就是 load((Class<?>) source) 和 load((Package) source) 了。一个用来加载类,一个用来加载扫描的包。

load((Class<?>) source) 中会通过调用 isComponent() 方法来判断资源是否为 Spring 容器管理的组件。isComponent() 方法通过资源是否包含 @Component 注解(@Controller、@Service、@Repository 等都包含在内)来区分是否为 Spring 容器管理的组件。

而 load((Package) source) 方法则是用来加载 @ComponentScan 注解定义的包路径。

刷新上下文

run() 方法编号10 的 refreshContext() 方法是整个启动过程比较核心的地方。像我们熟悉的 BeanFactory 就是在这个阶段构建的,所有非懒加载的 Spring Bean(@Controller、@Service 等)也是在这个阶段被创建的,还有 Spring Boot 内嵌的 Web 容器要是在这个时候启动的。

跟踪源码你会发现内部调用的是
ConfigurableApplicationContext.refresh(),ConfigurableApplicationContext 是一个接口,真正实现这个方法的有三个类:AbstractApplicationContext、ReactiveWebServerApplicationContext 和 ServletWebServerApplicationContext。

AbstractApplicationContext 为后面两个的父类,两个子类的实现比较简单,主要是调用父类实现,比如 ServletWebServerApplicationContext 中的实现是这样的:

public final void refresh() throws BeansException, IllegalStateException {
try {
super.refresh();
}
catch (RuntimeException ex) {
WebServer webServer = this.webServer;
if (webServer != null) {
webServer.stop();
}
throw ex;
}
}
主要的逻辑都在AbstractApplicationContext 中:

@Override
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// 1 准备将要刷新的上下文
prepareRefresh();
// 2 (告诉子类,如:ServletWebServerApplicationContext)刷新内部 bean 工厂
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// 3 为上下文准备 bean 工厂
prepareBeanFactory(beanFactory);
try {
// 4 允许在子类中对 bean 工厂进行后处理
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// 5 调用注册为 bean 的工厂处理器
invokeBeanFactoryPostProcessors(beanFactory);
// 6 注册拦截器创建的 bean 处理器
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// 7 初始化国际化相关资源
initMessageSource();
// 8 初始化事件广播器
initApplicationEventMulticaster();
// 9 为具体的上下文子类初始化特定的 bean
onRefresh();
// 10 注册监听器
registerListeners();
// 11 实例化所有非懒加载的单例 bean
finishBeanFactoryInitialization(beanFactory);
// 12 完成刷新发布相应的事件(Tomcat 就是在这里启动的)
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// 遇到异常销毁已经创建的单例 bean
destroyBeans();
// 充值 active 标识
cancelRefresh(ex);
// 将异常向上抛出
throw ex;
} finally {
// 重置公共缓存,结束刷新
resetCommonCaches();
contextRefresh.end();
}
}
}
简单说一下编号 9 处的 onRefresh() 方法,该方法父类未给出具体实现,需要子类自己实现,ServletWebServerApplicationContext 中的实现如下:

protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
......
if (webServer == null && servletContext == null) {
......
// 根据配置获取一个 web server(Tomcat、Jetty 或 Undertow)
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
......
}
......
}
factory.getWebServer(getSelfInitializer()) 会根据项目配置得到一个 Web Server 实例,这里跟下一篇将要谈到的自动配置有点关系。

有关为啥一个 Main 方法就能启动项目的更多相关文章

  1. ruby - 如何使用 Nokogiri 的 xpath 和 at_xpath 方法 - 2

    我正在学习如何使用Nokogiri,根据这段代码我遇到了一些问题:require'rubygems'require'mechanize'post_agent=WWW::Mechanize.newpost_page=post_agent.get('http://www.vbulletin.org/forum/showthread.php?t=230708')puts"\nabsolutepathwithtbodygivesnil"putspost_page.parser.xpath('/html/body/div/div/div/div/div/table/tbody/tr/td/div

  2. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  3. ruby - 为什么我可以在 Ruby 中使用 Object#send 访问私有(private)/ protected 方法? - 2

    类classAprivatedeffooputs:fooendpublicdefbarputs:barendprivatedefzimputs:zimendprotecteddefdibputs:dibendendA的实例a=A.new测试a.foorescueputs:faila.barrescueputs:faila.zimrescueputs:faila.dibrescueputs:faila.gazrescueputs:fail测试输出failbarfailfailfail.发送测试[:foo,:bar,:zim,:dib,:gaz].each{|m|a.send(m)resc

  4. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  5. Ruby 方法() 方法 - 2

    我想了解Ruby方法methods()是如何工作的。我尝试使用“ruby方法”在Google上搜索,但这不是我需要的。我也看过ruby​​-doc.org,但我没有找到这种方法。你能详细解释一下它是如何工作的或者给我一个链接吗?更新我用methods()方法做了实验,得到了这样的结果:'labrat'代码classFirstdeffirst_instance_mymethodenddefself.first_class_mymethodendendclassSecond使用类#returnsavailablemethodslistforclassandancestorsputsSeco

  6. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  7. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  8. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  9. ruby-on-rails - 渲染另一个 Controller 的 View - 2

    我想要做的是有2个不同的Controller,client和test_client。客户端Controller已经构建,我想创建一个test_clientController,我可以使用它来玩弄客户端的UI并根据需要进行调整。我主要是想绕过我在客户端中内置的验证及其对加载数据的管理Controller的依赖。所以我希望test_clientController加载示例数据集,然后呈现客户端Controller的索引View,以便我可以调整客户端UI。就是这样。我在test_clients索引方法中试过这个:classTestClientdefindexrender:template=>

  10. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

随机推荐