[#]: 表示较为重要

1 Spring Boot Overview

  • SpringBoot是一个快速开发框架,快速的将一些常用的第三方依赖整合(原理:通过Maven子父工程的方式),简化XML配置,全部采用注解形式,内置Http服务器(JettyTomcat),最终以【java应用程序】进行执行。
  • 它是为了简化Spring应用的创建、运行、调试、部署等而出现的,使用它可以做到专注于Spring应用的开发,而无需过多关注XML的配置。

1.1 SpringBoot核心原理:启动流程

Step1 SpringBoot核心通过Maven继承依赖关系快速整合第三方框架

<parent>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-parent</artifactId>
	<version>2.3.12.RELEASE</version>	
</parent>

<dependencies>
	<!-- SpringBoot 整合SpringMVC -->
	<!-- 我们依赖 spring-boot-starter-web 能够帮我整合Spring环境 原理通过Maven子父工程 -->
	<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-web</artifactId>
	</dependency>
</dependencies>

springboot 通过引用spring-boot-starter-web依赖,整合SpingMVC框架。当你添加了相应的starter模块,就相当于添加了相应的所有必须的依赖包,包括spring-boot-starter(这是Spring Boot的核心启动器,包含了:自动配置、日志和YAML);spring-boot-starter-test(支持常规的测试依赖,包括JUnit、Hamcrest、Mockito以及spring-test模块);spring-boot-starter-web (支持全栈式Web开发,包括Tomcat和spring-webmvc)等相关依赖。

Step2 基于SpringMVC无配置文件(纯Java)完全注解化实现SpringBoot框架,Main函数启动

如下是一个SpringBoot项目的启动类:

//springboot中只需要有@SpringBootApplication这个注解,有了它马上就能够让整个应用跑起来。
//实际上它只是一个组合注解: @Configuration配置类,@ComponentScan类,包扫描,@EnableAutoConfiguration 根据需求自动加载相关的bean这3个注解。
@SpringBootApplication // [spring-boot]
@ComponentScan(basePackages = {"cn.johnnyzen.yy.xx"}) // [spring-context]
public class XXBizApplication {
    @Autowired
    private ApplicationContext applicationContext;

    public static void main( String[] args ) {
        // way 1
        // SpringApplication springApplication = new SpringApplication(XXBizApplication .class);
        // ApplicationContext applicationContext1 = springApplication.run(args);
        
        // way 2
        // ApplicationContext applicationContext = SpringApplication.run(XXBizApplication .class, args); 

        // way 3
        ApplicationContext applicationContext = new SpringApplicationBuilder()
                .sources(Parent.class)
                .child(DataServiceBizApplication.class)
                .run(args);

        SpringContextUtil.setApplicationContext(applicationContext);
    }
}

springboot有3种方式启动,都会在没有web.xml配置文件的情况,通过java代码控制整个SpringMVC的初始化过程,java代码最终会生成class文件,内置Tomcat就会加载这些class文件,当所有程序加载完成后,项目就可以访问了。

Step3 启动流程(简版)

启动流程如下:

  1. 初始化监听器,以及添加到SpringApplication的自定义监听器。

  2. 发布ApplicationStartedEvent事件,如果想监听ApplicationStartedEvent事件,你可以这样定义:public class ApplicationStartedListener implements ApplicationListener,然后通过SpringApplication.addListener(..)添加进去即可。

  3. 装配参数和环境,确定是web环境还是非web环境。

  4. 装配完环境后,就触发ApplicationEnvironmentPreparedEvent事件。

  5. 如果SpringApplication的showBanner属性被设置为true,则打印启动的Banner。

  6. 创建ApplicationContext,会根据是否是web环境,来决定创建什么类型的ApplicationContext。

  7. 装配Context的环境变量,注册Initializers、beanNameGenerator等。

  8. 发布ApplicationPreparedEvent事件。

  9. 注册springApplicationArguments、springBootBanner,加载资源等

  10. 遍历调用所有SpringApplicationRunListener的contextLoaded()方法。

  11. 调用ApplicationContext的refresh()方法,装配context beanfactory等非常重要的核心组件。

  12. 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。

  13. 发布ApplicationReadyEvent事件,启动完毕,表示服务已经可以开始正常提供服务了。通常我们这里会监听这个事件来打印一些监控性质的日志,表示应用正常启动了。

2 Spring Framework Core Classes

2.1 环境类接口

2.1.1 EnvironmentCapable

org.springframework.core.env.EnvironmentCapable

  • Class Module Structure
package org.springframework.core.env;

public interface EnvironmentCapable {
    Environment getEnvironment();
}

2.1.2 Environment [#]

org.springframework.core.env.Environment

  • Class Module Structure
package org.springframework.core.env;

public interface Environment extends PropertyResolver {
    String[] getActiveProfiles();

    String[] getDefaultProfiles();

    /** @deprecated */
    @Deprecated
    boolean acceptsProfiles(String... var1);

    boolean acceptsProfiles(Profiles var1);
}

2.1.3 PropertyResolver

org.springframework.core.env.PropertyResolver

  • Class Module Structure
package org.springframework.core.env;

import org.springframework.lang.Nullable;

public interface PropertyResolver {
    boolean containsProperty(String var1);

    @Nullable
    String getProperty(String var1);

    String getProperty(String var1, String var2);

    @Nullable
    <T> T getProperty(String var1, Class<T> var2);

    <T> T getProperty(String var1, Class<T> var2, T var3);

    String getRequiredProperty(String var1) throws IllegalStateException;

    <T> T getRequiredProperty(String var1, Class<T> var2) throws IllegalStateException;

    String resolvePlaceholders(String var1);

    String resolveRequiredPlaceholders(String var1) throws IllegalArgumentException;
}

2.1 ApplicationContext [#]

org.springframework.context.ApplicationContext

2.1.1 Class Module Structure

package org.springframework.context;

import org.springframework.beans.factory.HierarchicalBeanFactory;
import org.springframework.beans.factory.ListableBeanFactory;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.core.env.EnvironmentCapable;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.lang.Nullable;

public interface ApplicationContext extends EnvironmentCapable, ListableBeanFactory, HierarchicalBeanFactory, MessageSource, ApplicationEventPublisher, ResourcePatternResolver {
    @Nullable
    String getId();

    String getApplicationName();

    String getDisplayName();

    long getStartupDate();

    @Nullable
    ApplicationContext getParent();

    AutowireCapableBeanFactory getAutowireCapableBeanFactory() throws IllegalStateException;
}

2.1.2 Introduce

  • ApplicationContextSpringBeanFactory之外的另一个核心接口或容器;其
  • 允许容器通过应用程序上下文环境创建、获取、管理bean。
  • 应用程序提供配置的中央接口
  • 应用程序运行时这是只读的,但如果实现支持这一点,则可以重新加载

2.1.3 Usage And API

  • [1] 常见使用方式
//[spring boot]
ApplicationContext applicationContext = SpringApplication.run(XXBizApplication.class, args);

//等效于
SpringApplication springApplication = new SpringApplication(DataServiceBizApplication.class);
ConfigurableApplicationContext applicationContext = springApplication.run(args); // ConfigurableApplicationContext extends ApplicationContext
  • [2] API
  • 访问【环境配置信息】的能力。继承自org.springframework.core.env.EnvironmentCapable接口。

  • 访问【应用程序组件的Bean工厂】的能力。

public interface ApplicationContext extends ListableBeanFactory, HierarchicalBeanFactory
  • org.springframework.beans.factory.ListableBeanFactory继承。
package org.springframework.beans.factory;

import java.lang.annotation.Annotation;
import java.util.Map;
import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface ListableBeanFactory extends BeanFactory {
    boolean containsBeanDefinition(String var1);
    int getBeanDefinitionCount();
    String[] getBeanDefinitionNames();
    String[] getBeanNamesForType(ResolvableType var1);
    String[] getBeanNamesForType(ResolvableType var1, boolean var2, boolean var3);
    String[] getBeanNamesForType(@Nullable Class<?> var1);
    String[] getBeanNamesForType(@Nullable Class<?> var1, boolean var2, boolean var3);
    <T> Map<String, T> getBeansOfType(@Nullable Class<T> var1) throws BeansException;
    <T> Map<String, T> getBeansOfType(@Nullable Class<T> var1, boolean var2, boolean var3) throws BeansException;
    String[] getBeanNamesForAnnotation(Class<? extends Annotation> var1);
    Map<String, Object> getBeansWithAnnotation(Class<? extends Annotation> var1) throws BeansException;
    @Nullable
    <A extends Annotation> A findAnnotationOnBean(String var1, Class<A> var2) throws NoSuchBeanDefinitionException;
}
  • 继承自org.springframework.beans.factory.HierarchicalBeanFactory 接口

We can have multiple application contexts that share a parent-child relationship.A context hierarchy allows multiple child contexts to share beans which reside in the parent context. Each child context can override configuration inherited from the parent context.(我们可以有多个共享父子关系的应用程序上下文。上下文层次结构允许多个子上下文共享驻留在父上下文中的bean。每个子上下文都可以覆盖从父上下文继承的配置。)

package org.springframework.beans.factory;

import org.springframework.lang.Nullable;

public interface HierarchicalBeanFactory extends BeanFactory {
    @Nullable
    BeanFactory getParentBeanFactory();

    boolean containsLocalBean(String var1);
}

  • 以通用方式加载【文件资源】的能力。继承自org.springframe.core.io.ResourceLoader接口。
//[1]
public interface ApplicationContext extends ResourcePatternResolver

//[2]
public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    Resource[] getResources(String var1) throws IOException;
}

//[3]
public interface ResourceLoader {
    String CLASSPATH_URL_PREFIX = "classpath:";

    Resource getResource(String var1);

    @Nullable
    ClassLoader getClassLoader();
}
  • 向【注册侦听器发布事件】的能力。继承自ApplicationEventPublisher接口。
//[1]
public interface ApplicationContext extends ApplicationEventPublisher

//[2]
package org.springframework.context;

@FunctionalInterface
public interface ApplicationEventPublisher {
    default void publishEvent(ApplicationEvent event) {
        this.publishEvent((Object)event);
    }

    void publishEvent(Object var1);
}
  • 支持【国际化】、【解析消息】的能力。继承自MessageSource接口。
//[1]
public interface ApplicationContext extends MessageSource

//[2]
package org.springframework.context;

import java.util.Locale;
import org.springframework.lang.Nullable;

public interface MessageSource {
    @Nullable
    String getMessage(String var1, @Nullable Object[] var2, @Nullable String var3, Locale var4);

    String getMessage(String var1, @Nullable Object[] var2, Locale var3) throws NoSuchMessageException;

    String getMessage(MessageSourceResolvable var1, Locale var2) throws NoSuchMessageException;
}
  • 从父上下文继承。后代上下文中的定义总是优先级。例如,这意味着单个父上下文可以被整个web应用程序使用,而每个servlet都有自己独立于任何其他servlet的子上下文。其接口主要子类(接口)包括:ConfigurableApplicationContextWebApplicationContext


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

2.1.4 ConfigurableApplicationContext

  • ConfigurableApplicationContext:该接口提供了根据配置创建、获取bean的一些方法,其中主要常用的实现包括:ClassPathXmlApplicationContextFileSystemXmlApplicationContext等。提供了通过各种途径去加载实例化bean的手段。


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

2.1.5 AnnotationConfigWebApplicationContext

  • AnnotationConfigWebApplicationContextWebApplicationContext实现,它接受带注释的类作为输入—特别是@Configuration-annotated类,但也接受普通的@Component类和使用javax兼容JSR-330的类。注入注解。允许逐个注册类(指定类名作为配置位置)以及类路径扫描(指定基本包作为配置位置)。对于多个@Configuration类,后面的@Bean定义将覆盖前面加载的文件中定义的类。可以利用这一点,通过额外的配置类故意覆盖某些bean定义。提供了注册注解类和扫描注解类等操作:
    public void register(Class<?>... annotatedClasses) {---注册一个或多个要处理的带注释的类。
        Assert.notEmpty(annotatedClasses, "At least one annotated class must be specified");
        this.annotatedClasses.addAll(Arrays.asList(annotatedClasses));
    }

    public void scan(String... basePackages) {---在指定的包中扫描类
        Assert.notEmpty(basePackages, "At least one base package must be specified");
        this.basePackages.addAll(Arrays.asList(basePackages));
    }

核心方法:loadBeanDefinitions(...)


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

2.1.6 XmlWebApplicationContext

XmlWebApplicationContextWebApplicationContext实现,它从XML文档获取配置,默认情况下,配置将取自/WEB-INF/applicationContext*。xml" for the root context, and "/WEB-INF/test-servlet。对于具有名称空间“test-servlet”的上下文(类似于对于具有servlet-name“test”的DispatcherServlet实例)。

2.1.7 ClassPathXmlApplicationContext

ClassPathXmlApplicationContext:独立的XML应用程序上下文,从类路径中获取上下文定义文件,将普通路径解释为包含包路径的类路径资源名(例如,“mypackage / myresource.txt”)。
适用于测试工具以及嵌入在jar中的应用程序上下文。
配置位置的默认值可以通过getConfigLocations重写,配置位置可以表示具体的文件,比如“/myfiles/context”。
xml“或ant样式的模式,比如”/myfiles/*-context。参见org.springframework.util模式细节的AntPathMatcher javadoc。

注意:对于多个配置位置,后面的bean定义将覆盖前面加载的文件中定义的配置位置。可以利用这一点,通过额外的XML文件故意覆盖某些bean定义。提供了诸多加载xml的操作:


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

2.1.8 FileSystemXmlApplicationContext

独立的XML应用程序上下文,从文件系统url获取上下文定义文件,将普通路径解释为相对的文件系统位置(例如,“mydir / myfile.txt”)。适用于测试线束以及独立环境。注意:普通路径总是被解释为相对于当前VM工作目录,即使它们以斜杠开头。(这与Servlet容器中的语义一致。)使用显式的“file:”前缀强制执行绝对文件路径。配置位置的默认值可以通过getConfigLocations重写,配置位置可以表示具体的文件,比如“/myfiles/context”。xml“或ant样式的模式,比如”/myfiles/*-context。参见org.springframework.util。模式细节的AntPathMatcher javadoc)。注意:对于多个配置位置,后面的bean定义将覆盖前面加载的文件中定义的配置位置。可以利用这一点,通过额外的XML文件故意覆盖某些bean定义。这是一个简单的一站式应用程序上下文。考虑将GenericApplicationContext类与org.springframework.bean .factory.xml结合使用。用于更灵活的上下文。


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

2.1.9 ClassPathXmlApplicationContext

ClassPathXmlApplicationContext VS FileSystemXmlApplicationContext

  • 一个是从类路径获取配置文件并进一步实例化bean
  • 一个是从文件系统或url加载文件并进一步实例化bean。

3 Spring Boot Core Classes

3.1 SpringApplication [#]

  • org.springframework.boot.SpringApplication

3.1 Introduce

  • SpringApplicationspringboot驱动spring应用上下文的引导类;
  • 其启动方法 run 分为实例方法静态方法。使用方式如下:
@SpringBootApplication 
public class XXBizApplication {
    //方式 1
    public static void main(String[] args) {
        SpringApplication.run(XXBizApplication.class, args);
    }

    //方式 2
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(MySpringConfiguration.class);
        app.run(args);
    }
    
    //方式 3
    public static void main(String[] args) {
        new SpringApplicationBuilder()
            .sources(Parent.class)
            .child(XXBizApplication.class)
            .run(args);
    }
}

3.2 Startup Process/启动流程 : springApplicationObject.run(args)

run()实例方法启动Spring应用,实质上是为Spring应用创建并初始化Spring上下文(ApplicationContext)。

SpringApplication springApplication = new SpringApplication(XXBizApplication.class);
ConfigurableApplicationContext applicationContext = springApplication.run(args);
  1. 初始化监听器,以及添加到SpringApplication的自定义监听器。

  2. 发布ApplicationStartedEvent事件,如果想监听ApplicationStartedEvent事件,你按如下方式这样定义,然后,通过SpringApplication.addListener(..)添加进去即可:

public class ApplicationStartedListener implements ApplicationListener
  1. 装配参数和环境,确定是web环境还是非web环境

  2. 装配完环境后,就触发ApplicationEnvironmentPreparedEvent事件。

  3. 如果SpringApplicationshowBanner属性被设置为true,则:打印启动的Banner

  4. 创建ApplicationContext,会根据是否是web环境,来决定创建什么类型的ApplicationContext

  5. 装配Context的环境变量,注册InitializersbeanNameGenerator等。

  6. 发布ApplicationPreparedEvent事件。

  7. 注册springApplicationArgumentsspringBootBanner,加载资源等

  8. 遍历调用所有SpringApplicationRunListenercontextLoaded()方法。

  9. 调用ApplicationContextrefresh()方法,装配context, beanfactory等非常重要的核心组件。

  10. 查找当前ApplicationContext中是否注册有CommandLineRunner,如果有,则遍历执行它们。

  11. 发布ApplicationReadyEvent事件,启动完毕,表示服务已经可以开始正常提供服务了。通常我们这里会监听这个事件来打印一些监控性质的日志,表示应用正常启动了。

此外,SpringBoot会触发其他的一些事件,这些事件按下列顺序触发:

  • (1)ApplicationStartingEvent:项目刚启动时触发,此时除了注册监听器和初始器之外,其他所有处理都没有开始;
  • (2)ApplicationEnvironmentPreparedEvent:上下文得到环境信息之后触发,此时上下文创建还没有创建;
  • (3)ApplicationPreparedEvent:bean的定义信息加载完成之后触发,此时bean还没有初始化;
  • (4)ApplicationReadyEvent:在所有bean初始化完毕,所有回调处理完成,系统准备处理服务请求时触发;
  • (5)ApplicationFailedEvent:启动过程出现异常时候触发。

3.3 Startup Process/启动流程 : SpringApplication.run(XXBizApplication.class, args)

如果我们使用的是SpringApplication的**静态方法run(class, args)
那么,这个方法里面:1)首先, 要创建一个SpringApplication对象实例;2)然后,调用这个创建好的SpringApplication的实例方法。

本质:SpringApplication.run()的底层其实就是new了一个SpringApplication的对象,并执行run()方法。

ApplicationContext applicationContext = SpringApplication.run(DataServiceBizApplication.class, args);

SpringApplication实例初始化的时候,它会提前做几件事情:

  1. 根据classpath里面是否存在某个特征类(org.springframework.web.context.ConfigurableWebApplicationContext)来决定是否应该创建一个为Web应用使用的ApplicationContext类型。

  2. 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationContextInitializer

  3. 使用SpringFactoriesLoader在应用的classpath中查找并加载所有可用的ApplicationListener

  4. 推断并设置main方法的定义类。

它会执行以下步骤:

  1. 创建一个合适的ApplicationContext实例 (取决于classpath)。

  2. 注册一个CommandLinePropertySource,以便将命令行参数作为Spring properties

  3. 刷新application context,加载所有单例beans

  4. 激活所有CommandLineRunner beans

3.4 SpringApplicationBuilder

有时我们需要创建多层次的ApplicationContext (例如,父子关系的SpringApplicationContext[父] 和Spring MVC[子]),这时我们可以使用SpringApplicationBuilder将多个方法调用串起来,通过parent()child()来创建多层次的ApplicationContext


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

启动信息如下:

第 9 行,启动SampleController

第10行,查找active profile,无,设为default

第11行,刷新上下文。

第12行,初始化tomcat,设置端口8080,设置访问方式为http

第13行,启动tomcat服务。

第14行,启动Servlet引擎。

第15行,Spring内嵌的WebApplicationContext 初始化开始。

第16行,Spring内嵌的WebApplicationContext 初始化完成。

第17行,映射servlet,将 dispatcherServlet 映射到 [/]

第18行,映射filter,将 characterEncodingFilter 映射到 [/*]

第19行,映射filter,将 hiddenHttpMethodFilter 映射到 [/*]

第20行,映射filter,将 httpPutFormContentFilter 映射到 [/*]

第21行,映射filter,将 requestContextFilter 映射到 [/*]

第22行,查找 @ControllerAdvice

第23行,映射路径 "{[/]}" 到 cn.johnnyzen.spring.controller.SampleController.home()

第24行,映射路径 "{[/error]}" 到 org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)

第25行,映射路径 "{[/error],produces=[text/html]}" 到 org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)

第30行,tomcat启动完毕。

第31行,SampleController启动耗费的时间。

第32行,初始化 dispatcherServlet

第33行,dispatcherServlet 的初始化已启动。

第34行,dispatcherServlet 的初始化已完成。

第35行,收到shutdown关闭请求。

第36行,关闭AnnotationConfigEmbeddedWebApplicationContext

虽然SpringBootApplication的启动过程非常复杂,但只需要在main方法里调用SpringApplicatio.run()方法即可启动Spring Boot应用。

3.4 Close(Shutdown) Process/关闭流程 : 监听关闭流程/关闭钩子/关机钩子

3.4.1 关机钩子的定义

关机钩子是一种特殊的构造,允许开发人员插入一段代码,以便在JVM关闭时执行
尤其在我们需要在JVM关闭时执行特殊的垃圾数据清理、状态打印操作时,这就很方便了。

通常使用关机钩子处理的一般结构,如:
确保我们能够很快看到应用程序退出的原因,例如是由于外部原因(例如杀死请求从O / S、system.exit(0)),还是由于资源问题(内存、线程)。而shutdown钩子/关机钩子很容易解决这个问题,因为它允许我们提供一个任意的代码块,JVM在关闭时将调用这个代码块。

3.4.2 Sample Code

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.PreDestroy;


@SpringBootApplication // [spring-boot]
//@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
//@RestController // // [spring-web(mvc / webflux)]
//@RequestMapping("/") // [spring-web(mvc / webflux)]
//@ComponentScan(basePackages = {"cn.seres.bd.dataservice"}) // [spring-context]
public class XXBizApplication {
    private static final Logger logger = LoggerFactory.getLogger(XXBizApplication .class);
    @Autowired
    private ApplicationContext applicationContext;

    public static void main( String[] args ) {
        SpringApplication springApplication = new SpringApplication(DataServiceBizApplication.class);
        ConfigurableApplicationContext applicationContext = springApplication.run(args);

        // [way 1] JVM ShutdownHook 先于 Spring Framework 的 ApplicationListener<ContextClosedEvent> 执行
        // 特点:Java VM 自带,使用方便,多个钩子间是并行执行的
        Runtime.getRuntime().addShutdownHook(
            new Thread(() -> logger.info("shutdown hook, jvm demo"))
        );

        // [way 2] Spring Framework 的 ApplicationListener<ContextClosedEvent> 先于 javax.annotation.PreDestroy 执行
        applicationContext.addApplicationListener(new ApplicationListener<ContextClosedEvent>() {
            @Override
            public void onApplicationEvent(ContextClosedEvent event) {
                logger.info("shutdown ...");
            }
        });

        SpringContextUtil.setApplicationContext(applicationContext);
    }

    // [way 3] ApplicationListener<ContextClosedEvent> 先于 javax.annotation.PreDestroy 执行
    @PreDestroy
    public void onExit() {
        logger.info("###STOP ing ###");
        try {
            Thread.sleep(5 * 1000);
        } catch (InterruptedException e) {
            logger.error("", e);;
        }
        logger.info("###STOP FROM THE LIFECYCLE###");
    }
}

或者:

@Component
public class ApplicationShutdownHook implements DisposableBean, ApplicationListener<ContextClosedEvent> {
    private static final Logger logger = LoggerFactory.getLogger(ApplicationShutdownHook.class);

    //step1 JVM ShutdownHook 先于 Spring Framework 的 ApplicationListener<ContextClosedEvent> 执行
    @PostConstruct
    public void init() {
        Runtime.getRuntime().addShutdownHook(new Thread(() -> logger.info("shutdown hook, jvm runtime hook")));
    }

    //step2 Spring Framework 的 ApplicationListener<ContextClosedEvent> 先于 javax.annotation.PreDestroy 执行
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        logger.info("shutdown hook, ContextClosedEvent");

    }

    //step3 javax.annotation.PreDestroy 先于 org.springframework.beans.factory.DisposableBean#destroy 执行
    @PreDestroy
    public void preDestroy() {
        logger.info("shutdown hook, pre destroy");
    }

    //step4
    @Override
    public void destroy() throws Exception {
        logger.info("shutdown hook, disposable bean");
    }
}
2022-05-18 10:13:04.673  INFO 94365 --- [       Thread-6] c.y.s.b.s.demo.ShutdownHookDemo : shutdown hook, jvm runtime hook
2022-05-18 10:13:04.675  INFO 94365 --- [TaskExecutor-24] c.y.s.b.s.demo.ShutdownHookDemo : shutdown hook, ContextClosedEvent
2022-05-18 10:13:04.709  INFO 94365 --- [extShutdownHook] c.y.s.b.s.demo.ShutdownHookDemo : shutdown hook, pre destroy
2022-05-18 10:13:04.709  INFO 94365 --- [extShutdownHook] c.y.s.b.s.demo.ShutdownHookDemo : shutdown hook, disposable bean
Process finished with exit code 143 (interrupted by signal 15: SIGTERM)

特别强调,针对这种hook,如果是kill -9 这种强制杀死进程,是不会触发的。
所以,对于此类应用应使用kill -15温柔的关闭应用程序。(kill -9kill -15的区别)
kill -9 和 kill -15 的区别 - CSDN

3.4.3 方式1 JVM自带的shutdownHook

Runtime.getRuntime().addShutdownHook(new Thread(() -> logger.info("shutdown hook, jvm demo")));

3.4.4 方式2 监听Spring的ContextClosedEvent

关于ContextClosedEvent等事件描述,可以参照以下示例(内容来自Spring官网)

Sr.No Spring Built-in Events Description
1 ContextRefreshedEvent This event is published when the ApplicationContext is either initialized or refreshed. This can also be raised using the refresh() method on the ConfigurableApplicationContext interface.
2 ContextStartedEvent This event is published when the ApplicationContext is started using the start() method on the ConfigurableApplicationContext interface. You can poll your database or you can restart any stopped application after receiving this event.
3 ContextStoppedEvent This event is published when the ApplicationContext is stopped using the stop() method on the ConfigurableApplicationContext interface. You can do required housekeep work after receiving this event.(当使用ConfigurationApplicationContext接口上的stop()方法停止ApplicationContext时,会发布此事件。收到此活动后,您可以进行必要的内务管理工作。)
4 ContextClosedEvent This event is published when the ApplicationContext is closed using the close() method on the ConfigurableApplicationContext interface. A closed context reaches its end of life; it cannot be refreshed or restarted.(当使用ConfigurationApplicationContext接口上的close()方法关闭ApplicationContext时,会发布此事件。封闭的环境达到了生命的尽头;它无法刷新或重新启动。)
5 RequestHandledEvent This is a web-specific event telling all beans that an HTTP request has been serviced.

实现ApplicationListener接口,监听ContextClosedEvent

@Component
@Slf4j
public class ShutdownHookDemo implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent event) {
        log.info("shutdown hook, ContextClosedEvent");
    }
}

3.4.5 方式3 实现org.springframework.beans.factory.DisposableBean接口

Interface to be implemented by beans that want to release resources on destruction. A BeanFactory will invoke the destroy method on individual destruction of a scoped bean(对于那些希望在Bean销毁时需要执行资源释放的Bean,可以实现该接口。对每个单独的Bean,BeanFactory会负责调用destory方法。)

package org.springframework.beans.factory
public interface DisposableBean {
	/**
	 * Invoked by the containing {@code BeanFactory} on destruction of a bean.
	 * @throws Exception in case of shutdown errors. Exceptions will get logged
	 * but not rethrown to allow other beans to release their resources as well.
	 */
	void destroy() throws Exception;

}

Sample Code

@Component
@Slf4j
public class ShutdownHookDemo implements DisposableBean {
    @Override
    public void destroy() throws Exception {
        log.info("shutdown hook, disposable bean");
    }
}

3.4.6 方式4 使用注解*javax.annotation.PreDestory : @PreDestory

The PreDestroy annotation is used on methods as a callback notification to signal that the instance is in the process of being removed by the container. The method annotated with PreDestroy is typically used to release resources that it has been holding.(该注解的用途也是用于释放一些Bean持有的资源,当Bean的实例被容器移除时,会触发。)

不过对于该注解的使用,是有一些限制条件的,在注解的注释中也有描述

  • The method MUST NOT have any parameters except in the case of interceptors in which case it takes an InvocationContext object as defined by the Interceptors specification.(除了在interceptor情况下可以接收InvocationContext参数外,方法声明必须是无参的)
  • The method defined on an interceptor class MUST HAVE one of the following signatures:(如果作为Interceptors,必须使用以下两种方法签名)
    + void (InvocationContext)
    + Object (InvocationContext) throws Exception
  • The method defined on a non-interceptor class MUST HAVE the following signature:(如果不是作为Intercpetor,必须使用下面的方法签名)
    + void ()
  • The method on which PreDestroy is applied MAY be public, protected, package private or private. (方法访问权限可以是public/protected/private)
  • The method MUST NOT be static.(方法能声明为static)
  • The method MAY be final.(方法可以声明为final)

Sample Code

@Component
@Slf4j
public class ShutdownHookDemo {
    @PreDestroy
    public void preDestroy() {
        log.info("shutdown hook, pre destroy");
    }
}

对于@PreDestory会存在一个不生效的情况,对于Scope为prototype的bean,Spring不会调用@PreDestory标记的方法,以下是官方的一些解答(stackoverflowhttps://stackoverflow.com/questions/16373276/predestroy-method-of-a-spring-singleton-bean-not-called)。

In contrast to the other scopes, Spring does not manage the complete lifecycle of a prototype bean**: the container instantiates, configures, and otherwise assembles a prototype object, and hands it to the client, with no further record of that prototype instance.(Spring并未对Prototype类型Bean的全生命周期进行管理)

Thus, although initialization lifecycle callback methods are called on all objects regardless of scope, in the case of prototypes, configured destruction lifecycle callbacks are not called. The client code must clean up prototype-scoped objects and release expensive resources that the prototype bean(s) are holding.(对于Prototype类型  的Bean必须由使用方自己负责清理资源)

To get the Spring container to release resources held by prototype-scoped beans, try using a custom bean post-processor, which holds a reference to beans that need to be cleaned up(客户端可以借助BeanPostProcessor实现自助清理工作)

3.5 Close(Shutdown) Process/关闭流程 : 优雅地停止服务

在使用Springboot的时候,都要涉及到服务的停止和启动,当我们停止服务的时候,很多时候大家都是kill -9 直接把程序进程杀掉,这样程序不会执行优雅的关闭。而且一些没有执行完的程序就会直接退出。

我们很多时候都需要安全地将服务停止,也就是把没有处理完的工作继续处理完成。比如停止一些依赖的服务,输出一些日志,发一些信号给其他的应用系统,这个在保证系统的高可用是非常有必要的。那么咱么就来看一下几种停止springboot的方法。

3.5.1 优雅停机的定义

什么叫优雅停机?

简单的说,就是向应用进程发出停止指令之后,能保证正在执行的业务操作不受影响,直到操作运行完毕之后再停止服务。应用程序接收到停止指令之后,会进行如下操作:

  1. 停止接收新的访问请求
  2. 正在处理的请求,等待请求处理完毕;对于内部正在执行的其他任务,比如定时任务、mq 消费等等,也要等当前正在执行的任务执行完毕,并且不再启动新的任务
  3. 当应用准备关闭的时候,按需向外发出信号,告知其他应用服务准备接手,以保证服务高可用

如果暴力地关闭应用程序,比如通过kill -9 <pid>命令强制直接关闭应用程序进程,可能会导致正在执行的任务数据丢失或者错乱,也可能会导致任务所持有的全局资源等不到释放,比如当前任务持有 redis 的锁,并且没有设置过期时间,当任务突然被终止并且没有主动释放锁,会导致其他进程因无法获取锁而不能处理业务。

那么如何在不影响正在执行的业务的情况下,将应用程序安全的进行关闭呢?

3.5.2 方式1 actuator

第1种方式 : Springboot提供的actuator的功能,它可以执行shutdown, health, info等,默认情况下,actuator的shutdown是disable的,我们需要打开它。

  • Step1 引入acturator的maven依赖。
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • Step2 然后将shutdown节点打开,也将/actuator/shutdown暴露web访问也设置上,除了shutdown之外还有health, info的web访问都打开的话将management.endpoints.web.exposure.include=*就可以。将如下配置设置到application.properties里边。设置一下服务的端口号为3333。
server.port=3333
management.endpoint.shutdown.enabled=true
management.endpoints.web.exposure.include=shutdown
  • Step3 接下来,咱们创建一个springboot工程,然后设置一个bean对象,配置上PreDestroy方法。这样在停止的时候会打印语句。bean的整个生命周期分为创建、初始化、销毁,当最后关闭的时候会执行销毁操作。在销毁的方法中执行一条输出日志。
package com.hqs.springboot.shutdowndemo.bean;
 
import javax.annotation.PreDestroy;
 
/**
 * @author huangqingshi
 * @Date 2019-08-17
 */
publicclass TerminateBean {
    @PreDestroy
    public void preDestroy() {
        System.out.println("TerminalBean is destroyed");
    }

}

做一个configuration,然后提供一个获取bean的方法,这样该bean对象会被初始化。

package com.hqs.springboot.shutdowndemo.config;
 
import com.hqs.springboot.shutdowndemo.bean.TerminateBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
 
/**
 * @author huangqingshi
 * @Date 2019-08-17
 */
@Configuration
publicclass ShutDownConfig {
 
    @Bean
    public TerminateBean getTerminateBean() {
        returnnew TerminateBean();
    }
 
}

在启动类里边输出一个启动日志,当工程启动的时候,会看到启动的输出,接下来咱们执行停止命令。

curl -X POST http://localhost:3333/actuator/shutdown

以下日志可以输出启动时的日志打印和停止时的日志打印,同时程序已经停止。


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

3.5.3 方式2 ApplicationContext.close()

第2种方法 : 也较简单,获取程序启动时候的context,然后关闭主程序启动时的context。这样程序在关闭的时候也会调用PreDestroy注解。如下方法在程序启动十秒后进行关闭。

/* method 2: use ctx.close to shutdown all application context */
        ConfigurableApplicationContext applicationContext = SpringApplication.run(ShutdowndemoApplication.class, args);
 
        try {
            TimeUnit.SECONDS.sleep(10);
 
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
 
        applicationContext.close();

3.5.4 方式3 ApplicationPidFileWriter + app.pid + stop.sh

springboot启动的时候将进程号写入一个app.pid文件,生成的路径是可以指定的,可以通过命令 cat /Users/huangqingshi/app.id | xargs kill 命令直接停止服务,这个时候bean对象的PreDestroy方法也会调用的。这种方法大家使用的比较普遍。
写一个start.sh用于启动springboot程序;然后,写一个停止程序stop.sh将服务停止。

/* method 3 : generate a pid in a specified path, while use command to shutdown pid :
            'cat /Users/huangqingshi/app.pid | xargs kill' */
        SpringApplication application = new SpringApplication(ShutdowndemoApplication.class);
        application.addListeners(new ApplicationPidFileWriter("/Users/huangqingshi/app.pid"));
        application.run();

3.5.5 方式4/5 SpringApplication.exit(...) | System.exit(exitCode)

通过调用一个SpringApplication.exit()方法也可以退出程序,同时将生成一个退出码,这个退出码可以传递给所有的context
这个就是一个JVM的钩子,通过调用这个方法的话会把所有PreDestroy的方法执行并停止,并且传递给具体的退出码给所有ApplicationContext
通过调用System.exit(exitCode)可以将这个错误码也传给JVM
程序执行完后最后会输出:Process finished with exit code 0,给JVM一个SIGNAL

/* method 4: exit this application using static method */
        ConfigurableApplicationContext ctx = SpringApplication.run(ShutdowndemoApplication.class, args);
        exitApplication(ctx);
public static void exitApplication(ConfigurableApplicationContext context) {
        int exitCode = SpringApplication.exit(context, (ExitCodeGenerator) () -> 0);
 
        System.exit(exitCode);
    }


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

3.5.6 方式6 MyShutdownController

自己写一个Controller,然后将自己写好的Controller获取到程序的context,然后调用自己配置的Controller方法退出程序。通过调用自己写的/shutDownContext方法关闭程序:

curl -X POST http://localhost:3333/shutDownContext
package com.hqs.springboot.shutdowndemo.controller;
 
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
 
/**
 * @author huangqingshi
 * @Date 2019-08-17
 */
@RestController
publicclass ShutDownController implements ApplicationContextAware {
 
    private ApplicationContext context;
 
    @PostMapping("/shutDownContext")
    public String shutDownContext() {
        ConfigurableApplicationContext ctx = (ConfigurableApplicationContext) context;
        ctx.close();
        return"context is shutdown";
    }
 
    @GetMapping("/")
    public String getIndex() {
        return"OK";
    }
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
}

3.5.7 方式7 【暴力停止/强制停止】:kill -9

如何暴力停止呢,简单,直接kill -9 相应的PID即可。

不建议此操作

4 Springboot 中 bootstrap.yml 与 application.yml 的区别

相同点

  • bootstrap.ymlapplication.yml都是 SpringBoot 项目中的配置文件。

不同点

他们的区别主要有以下的几个方面:

(1) 加载顺序区别

  • bootstrap配置文件是比application配置文件优先加载的。

因为bootstrap是由spring父上下文加载,而application是由子上下文加载

bootstrap.yml 先于 application.yml。
bootstrap.yml 用于 ApplicationContext 引导阶段。由父 Spring ApplicationContext 加载。定义系统级别的参数配置,这些参数一般不会变动。

application.yml 用来定义应用级别的参数配置。搭配 spring-cloud-config 使用时 application.yml 定义的参数可以实现动态替换。
application.yml 会覆盖 bootstrap.yml 的参数配置。

(2)优先级区别

  • bootstrap加载的配置信息是不能被application的相同配置覆盖的。如果两个配置文件同时存在,也是以bootstrap为主
springboot的配置优先级:
  由里向外,外层覆盖里层。
  命令行参数 > 操作系统环境变量 > 应用外的配置文件 > 应用内的配置文件

加入springcloud配置中心后的配置优先级:
1) 配置中心 > 命令行参数 > 本地 application.yml > 本地 bootstrap.yml
2) 配置允许覆盖:
spring:
  cloud:
    config:
      allowOverride: true
      overrideNone: true
      overrideSystemProperties: false

(3)应用场景区别

bootstrap常见应用场景

  • 1.配置一些固定的,不能被覆盖的属性.用于一些系统级别的参数配置

本地的配置文件是默认不能覆盖远程的配置的

  • 2.一些需要加密/解密的场景

  • 3.当你使用了Spring Cloud Config 配置中心时,这时需要在boostrap配置文件中添加连接到配置中心的配置属性来加载外部配置中心的配置信息。

专业翻译如下


[SpringBoot]Spring Boot Framework @ Environment / ApplicationContext & SpringApplication-小白菜博客

application常见应用场景

  • 1.常用于SpringBoot项目的自动化配置

  • 2.用于一些应用级别的参数配置

在大部分情况下不用区分这两种情况,只需要使用application即可,效果基本是一致的

SpringBoot配置优先级(bootstrap和application)

X 参考文献