深入学习 Spring Web 开发 —— 启动日志

无分号教派
• 阅读 779

上一篇文章,我们紧紧围绕 @SpringBootApplication 引入的注解和类,对 Spring Boot 项目的启动过程做了一次分析。在实际的开发过程中,项目的代码毫无疑问是与我们最为相关的,另外,我们也不可忽视项目日志在我们日常开发中所起的作用。因此,本文将围绕项目的启动日志,对项目的启动过程再做一次分析,以便于我们更好地理解项目的运行逻辑。本系列文章,笔者将会使用较多的笔墨展示代码与日志的相互关联,希望通过这样的方式,可以慢慢让读者培养起一种代码与日志彼此贯穿的思路,以帮助读者在实际开发过程中,更好地解决所遇到的问题。

下面是项目的一次启动日志:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.2)

2022-09-08 08:28:22.964  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : Starting HelloWorldApplication using Java 11.0.12 on susamludeMac.local with PID 54995 (/Users/susamlu/code/java/spring-web/spring-web-helloworld/target/classes started by susamlu in /Users/susamlu/code/java/spring-web)
2022-09-08 08:28:22.968  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : No active profile set, falling back to 1 default profile: "default"
2022-09-08 08:28:24.900  INFO 54995 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-09-08 08:28:24.912  INFO 54995 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-09-08 08:28:24.913  INFO 54995 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-09-08 08:28:25.090  INFO 54995 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-09-08 08:28:25.091  INFO 54995 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1910 ms
2022-09-08 08:28:25.857  INFO 54995 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-09-08 08:28:25.873  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : Started HelloWorldApplication in 3.735 seconds (JVM running for 4.41)

SpringApplication 的静态 run() 方法,最终会调用到自身的实例 run() 方法。实例 run() 方法的内容会相对比较复杂,为了简化其中的逻辑,我们重点关注 printBanner()、prepareContext()、refreshContext() 几个方法。

public class SpringApplication {

    // ...

    public ConfigurableApplicationContext run(String... args) {
        // ...
        Banner printedBanner = printBanner(environment);
        // ...
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        // ...
    }

    // ...

}

printBanner

printBanner() 见名知意,是用来打印 Spring Boot 项目的 Banner 的:


  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v2.7.2)

SpringApplication 调用 SpringApplicationBannerPrinter 的 print() 方法打印 Banner。

public class SpringApplication {

    // ...

    private Banner printBanner(ConfigurableEnvironment environment) {
        // ...
        SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
        // ...
        return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    }

    // ...

}

SpringApplicationBannerPrinter 优先打印 ImageBanner 或 TextBanner,如果没有设置 ImageBanner 和 TextBanner,则打印 Spring Boot 默认的 Banner。

class SpringApplicationBannerPrinter {

    // ...

    // TextBanner 的文件名
    static final String DEFAULT_BANNER_LOCATION = "banner.txt";

    // ImageBanner 支持的后缀格式
    static final String[] IMAGE_EXTENSION = {"gif", "jpg", "png"};

    // 默认的 Banner
    private static final Banner DEFAULT_BANNER = new SpringBootBanner();

    // ...

    Banner print(Environment environment, Class<?> sourceClass, PrintStream out) {
        Banner banner = getBanner(environment);
        banner.printBanner(environment, sourceClass, out);
        return new PrintedBanner(banner, sourceClass);
    }

    // ...

    private Banner getBanner(Environment environment) {
        Banners banners = new Banners();
          // 优先打印 ImageBanner
        banners.addIfNotNull(getImageBanner(environment));
        // 没有设置 ImageBanner,则优先打印 TextBanner
        banners.addIfNotNull(getTextBanner(environment));
        if (banners.hasAtLeastOneBanner()) {
            return banners;
        }
        // ...
        // ImageBanner、TextBanner 都没有设置,则打印 Spring Boot 默认的 Banner
        return DEFAULT_BANNER;
    }

    // ...

}

默认的 Banner 值的主要内容由 SpringBootBanner 类的一个常量值定义:

class SpringBootBanner implements Banner {

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

    // ...

}

如果我们想自定义 Banner,我们可以在项目的 resources 目录下放置 banner.txt 文件,从而改变 Banner 的打印。如我的 banner.txt 文件的内容为:

                     _    __               
    ____   ____ _   (_)  / /_   _____  ___ 
   / __ \ / __ `/  / /  / __/  / ___/ / _ \
  / /_/ // /_/ /  / /  / /_   (__  ) /  __/
 / .___/ \__,_/  /_/   \__/  /____/  \___/ 
/_/                                        

应用启动后打印的 Banner 信息被修改为:

                     _    __               
    ____   ____ _   (_)  / /_   _____  ___ 
   / __ \ / __ `/  / /  / __/  / ___/ / _ \
  / /_/ // /_/ /  / /  / /_   (__  ) /  __/
 / .___/ \__,_/  /_/   \__/  /____/  \___/ 
/_/                                        

prepareContext

prepareContext() 是其中非常重要的一个方法,该方法的作用在上一篇文章中就所有提及。

public class SpringApplication {

    // ...

    private void prepareContext(/* ... */) {
        // ...
        logStartupInfo(context.getParent() == null);
        logStartupProfileInfo(context);
        // ...
    }

    // ...

}

prepareContext() 通过 logStartupInfo()、logStartupProfileInfo() 两个方法,分别输出下面两句日志:

2022-09-08 08:28:22.964  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : Starting HelloWorldApplication using Java 11.0.12 on susamludeMac.local with PID 54995 (/Users/susamlu/code/java/spring-web/spring-web-helloworld/target/classes started by susamlu in /Users/susamlu/code/java/spring-web)
2022-09-08 08:28:22.968  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : No active profile set, falling back to 1 default profile: "default"

refreshContext

refreshContext() 也是其中的核心方法,Tomcat 的启动就发生在该方法的调用过程中。refreshContext() 最终会调用 AbstractApplicationContext 的 refresh() 方法,refresh() 又调用了自身的 onRefresh() 和 finishRefresh() 方法。

public abstract class AbstractApplicationContext extends DefaultResourceLoader
        implements ConfigurableApplicationContext {

    // ...

    public void refresh() throws BeansException, IllegalStateException {
        // ...
        onRefresh();
        // ...
        finishRefresh();
        // ...
    }

    // ...

}

其中 onRefresh() 实际调用的是 ServletWebServerApplicationContext 的 onRefresh() 方法,然后再通过 ServletWebServerFactory 获取 WebServer。

public class ServletWebServerApplicationContext extends GenericWebApplicationContext
        implements ConfigurableWebServerApplicationContext {

    // ...

    protected void onRefresh() {
        // ...
        createWebServer();
        // ...
    }

    // ...

    private void createWebServer() {
        // ...
        ServletWebServerFactory factory = getWebServerFactory();
        // ...
        this.webServer = factory.getWebServer(getSelfInitializer());
        // ...
    }

    // ...

}

ServletWebServerFactory 会创建 TomcatWebServer,创建 TomcatWebServer 的时候,它的 initialize() 方法会被自动调用,initialize() 会启动 Tomcat 并输出相关日志。

public class TomcatWebServer implements WebServer {

    // ...

    private void initialize() throws WebServerException {
        logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
        // ...
        this.tomcat.start();
        // ...
    }

    // ...

}
2022-09-08 08:28:24.900  INFO 54995 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2022-09-08 08:28:24.912  INFO 54995 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2022-09-08 08:28:24.913  INFO 54995 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.65]
2022-09-08 08:28:25.090  INFO 54995 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2022-09-08 08:28:25.091  INFO 54995 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1910 ms

Tomcat 启动完,onRefresh() 也接着调用完成,再接着 finishRefresh() 会调用 TomcatWebServer 的 start() 方法,输出 Tomcat 启动完成的日志。

public class TomcatWebServer implements WebServer {

    // ...

    public void start() throws WebServerException {
        // ...
        logger.info("Tomcat started on port(s): " + getPortsDescription(true) + " with context path '"
                + getContextPath() + "'");
        // ...
    }

    // ...

}
2022-09-08 08:28:25.857  INFO 54995 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''

上面的方法都调用完后,最终又回到了 SpringApplication 的 run() 方法,run() 最后输出应用启动完成的日志。

public class SpringApplication {
    
    // ...

    public ConfigurableApplicationContext run(String... args) {
        // ...
        new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
        // ...
    }

    // ...
    
}
2022-09-08 08:28:25.873  INFO 54995 --- [           main] o.s.springweb.HelloWorldApplication      : Started HelloWorldApplication in 3.735 seconds (JVM running for 4.41)

整个流程,可以总结为以下一张图:

深入学习 Spring Web 开发 —— 启动日志

回顾

上一篇文章,我们以一张图展示了项目启动的流程,本文也同样总结成了一张图,对于这两张图所涉及到的过程,它们相互之间的顺序是怎么样的呢?

当我们把这两张图拿来对比,并对照相关的源代码,不难发现,SpringFactoriesLoader 的 loadSpringFactories() 方法是最先被触发的,接着,开始输出 Spring Banner,再接着,开始执行 logStartupInfo()、logStartupProfileInfo() 两个方法。上面方法都执行完之后,接着,SpringApplication 的 load() 方法开始执行,再接着就是配置类和自动配置类的解析。这些都执行完了之后,就开始启动 Tomcat,一直到整个项目启动完成。这当中的顺序,可以用下面一张图来表示:

深入学习 Spring Web 开发 —— 启动日志

点赞
收藏
评论区
推荐文章
kenx kenx
4年前
SpringBoot包扫描之多模块多包名扫描和同类名扫描冲突解决
前言我们在开发springboot项目时候,创建好SpringBoot项目就可以通过启动类直间启动,运行一个web项目,非常方便简单,不像我们之前通过SpringSpringMvc要运行启动一个web项目还需要要配置各种包扫描和tomcat才能启动我将应用分成了parentcommoncomponentapp这种模式,1.parent是一个单纯的p
Irene181 Irene181
4年前
使用Python一键删除全盘文件自动关机并留后门
/1前言/今天我们要做的案例是怎样利用Python做一个hacker软件。众所周知,一般的Hacker对于黑操作系统一般常用手法莫过于发送木马客户端,修改系统注册表。组策略,获得开机启动权限,入侵电脑然后对电脑的文件进行修改来达到不可告人的目的。今天我们要讲的就是最基础的,怎样获得开机启动,先给大家讲最基础添加文件到系统启动项的文件夹中,当然更加高端点也可
Stella981 Stella981
3年前
Mock工具之Mockito实战
在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Moc
可莉 可莉
3年前
12个前端必会 H5 问题及解决方法
△是新朋友吗?记得先点web前端学习圈关注我哦~!(https://oscimg.oschina.net/oscnet/b4ee1ffbc0c74a72aa3eb22c26a0b25b.jpg)前言作为一个开发了多个H5项目的前端工程师,在开发过程中难免会遇到一些兼容性等爬过坑的问题。现在我将这些问题一
Stella981 Stella981
3年前
SpringBoot学习之路:02.第一个程序Hello World及项目结构介绍
      上一篇我们介绍了SpringBoot项目的环境搭建和在idea下项目的创建过程,今天要说的是SpringBoot项目的下的第一个程序HelloWorld,及SpringBoot项目结构的分析。首先打开SpringBoot初始项目:!(https://static.oschina.net/uploads/space/20
Stella981 Stella981
3年前
ShopsN开源商城系统2.3.0即将更新。开源程序也可媲美商业代码了
2018年5月,即将更新2.3.0.目前版本2.2.6. 2.3.0聚集了几个月来数个电商实战商业项目的开发精华。修复了大批bug,安全漏洞,功能优化,近百项完善之处,因此此次更新,我们将定义为中型更新。2.3.0标志着ShopsN商城系统开源免费项目将脱离程序员才能改着用的阶段,不懂程序的小白用户也可以拿来做免费商用了。这里的代码可能传的不及时
Stella981 Stella981
3年前
Android自动化页面测速在美团的实践
背景随着移动互联网的快速发展,移动应用越来越注重用户体验。美团技术团队在开发过程中也非常注重提升移动应用的整体质量,其中很重要的一项内容就是页面的加载速度。如果发生冷启动时间过长、页面渲染时间过长、网络请求过慢等现象,就会直接影响到用户的体验,所以,如何监控整个项目的加载速度就成为我们部门面临的重要挑战。对于测速这个问题,很多同学首先会想到在页面
Easter79 Easter79
3年前
SpringBoot学习之路:02.第一个程序Hello World及项目结构介绍
      上一篇我们介绍了SpringBoot项目的环境搭建和在idea下项目的创建过程,今天要说的是SpringBoot项目的下的第一个程序HelloWorld,及SpringBoot项目结构的分析。首先打开SpringBoot初始项目:!(https://static.oschina.net/uploads/space/20
Easter79 Easter79
3年前
SpringBoot开发及学习
SpringBoot是Spring新出的一个框架,他的目的一如始初简化开发。我们开发项目的时候,为了让项目运行起来,我们要考虑很多架构、配置、依赖等问题,这些问题其实每个项目都要考虑,而且每个项目的开发都有固定的模版,这些重复的工作是每个项目的样板代码,SpringBoot做的就是帮我们完成这些重复行的工作,让我们只关注业务逻辑。主要帮我们完成了以下
Stella981 Stella981
3年前
SpringBoot开发及学习
SpringBoot是Spring新出的一个框架,他的目的一如始初简化开发。我们开发项目的时候,为了让项目运行起来,我们要考虑很多架构、配置、依赖等问题,这些问题其实每个项目都要考虑,而且每个项目的开发都有固定的模版,这些重复的工作是每个项目的样板代码,SpringBoot做的就是帮我们完成这些重复行的工作,让我们只关注业务逻辑。主要帮我们完成了以下
Git 代码分支管理 | 京东云技术团队
Git代码分支的命名规范以及管理方式对项目的版本发布至关重要,为了解决实际开发过程中版本发布时代码管理混乱、冲突等比较头疼的问题,我们将在文中阐述如何更好的管理代码分支。