Skip to content

Spring Boot 启动流程

本文介绍Spring Boot的启动流程。

1. 概述

Spring Boot程序启动只需要下面一行代码:

java
SpringApplication.run(BootApplication.class, args);

在这行代码之后,主要分为两步:

  • 第一步:创建SpringApplication对象
    • 确定Bean的主来源配置类,即加了@Configuration注解的类;
    • 推断应用类型;
    • 获取BootstrapRegistryInitializer
    • 获取ApplicationContextInitializer
    • 获取ApplicationListener
    • 推断主类,即包含main方法的类;
  • 第二步:调用SpringApplication对象的run()方法,主要做五件事
    • 准备环境Environment,就是各种配置参数;
    • 打印Banner,也就是在控制台打印Spring字样标志;
    • 准备容器ApplicationContext
    • 调用Runner接口,又分为CommandLineRunnerApplicationRunner;
    • 在做上面四件事之间,穿插着发布消息;

2. 创建SpringApplication对象

本小节具体介绍创建SpringApplication对象过程中作了哪些事,以及有什么作用。

创建SpringApplication的构造方法如下:

java
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.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());
  this.bootstrapRegistryInitializers = new ArrayList<>(
      getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  this.mainApplicationClass = deduceMainApplicationClass();
}

请先查看附录1-SpringFactoriesLoader。

2.1 确定Bean配置类

在第4行,将Bean的配置类保存起来,也就是之后可以从这些类中获取Bean定义:

java
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

除了在构造方法中传入配置类,我们也可以通过加载XML的方式:

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();
    // 设置XML文件为加载Bean的来源
    springApplication.setSources(Set.of("classpath:bean.xml"));

    ConfigurableApplicationContext applicationContext = springApplication.run(args);
    for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
        System.out.println(beanDefinitionName);
    }

}
xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="bean01" class="org.example.bean.Bean01"></bean>

</beans>

可以在结果中看到bean01,说明设置成功。

除了从XML中加载Bean,Spring还可以采用类路径扫描的方式加载Bean,使用的类是ClassPathBeanDefinitionScanner

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();
    // 设置XML文件为加载Bean的来源
    springApplication.setSources(Set.of("classpath:bean.xml"));

    ConfigurableApplicationContext applicationContext = springApplication.run(args);
    // 采用类路径扫描的方式加载Bean
    ClassPathBeanDefinitionScanner scanner =
            new ClassPathBeanDefinitionScanner((BeanDefinitionRegistry) applicationContext);
    scanner.scan("org.example.entity");

    for (String beanDefinitionName : applicationContext.getBeanDefinitionNames()) {
        System.out.println(beanDefinitionName);
    }

}

注意,要加入容器的Bean类上需要标注 @Component, @Repository, @Service, 或 @Controller 注解,否则无法加入容器。

2.2 推断应用类型

在第5行,推断应用类型:

java
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());

主要的逻辑是WebApplicationType.deduceFromClasspath(),源码如下:

java
public enum WebApplicationType {
  
	NONE,
	SERVLET,
	REACTIVE;

	private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.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";

	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;
	}
}

主要逻辑如下:

  • 首先判断类路径下是否存在WebFlux相关的类,并且不存在WebMvc相关的类,那么推断应用类型为REACTIVE;
  • 然后判断类路径是否存在Servlet相关的类,如果还不存在,则推断为普通程序,即NONE;
  • 最后,推断应用程序为SERVLET;

总结就是根据类路径下是否存在某些类判断程序类型,通过程序类型,在后面可以创建不同的容器;

2.3 BootstrapRegistryInitializer

设置bootstrapRegistryInitializers

java
this.bootstrapRegistryInitializers = new ArrayList<>(
      getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

在创建应用容器(ApplicationContext)之前,有可能需要进行一些初始化配置,在Spring中,提供了BootstrapContext(初始上下文),BootstrapRegistryInitializer则是对初始上下文创建后进行初始化。

当创建应用容器过程中,准备好环境Environment后,会发出EnvironmentPrepared事件,该事件监听器可以拿到初始上下文,从而可以扩展环境。

因此,BootstrapRegistryInitializer主要用于Spring Cloud环境中,用于初始化与 Spring Cloud Config Server 的连接,加载远程配置属性,然后加载进环境Environment中。

我们可以使用如下方法使用BootstrapRegistryInitializer

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();

    // 设置XML文件为加载Bean的来源
    springApplication.setSources(Set.of("classpath:bean.xml"));

    springApplication.addBootstrapRegistryInitializer(registry -> {
        // 假设下面代码是从远程获取配置
        Map<String, Object> properties = new HashMap<>();
        properties.put("custom.config.key", "from spring cloud config server");
        MapPropertySource propertySource = new MapPropertySource("custom-bootstrap", properties);

        // 注册到 BootstrapRegistry,
        registry.register(MapPropertySource.class, context -> propertySource);
    });


    springApplication.addListeners(new ApplicationListener<ApplicationEnvironmentPreparedEvent>() {

        @Override
        public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
            // 从初始化容器中获取配置信息
            ConfigurableBootstrapContext bootstrapContext = event.getBootstrapContext();
            MapPropertySource mapPropertySource = bootstrapContext.getRegisteredInstanceSupplier(MapPropertySource.class).get(bootstrapContext);
            // 将获取到的配置信息加入到环境中
            ConfigurableEnvironment environment = event.getEnvironment();
            environment.getPropertySources().addLast(mapPropertySource);
        }
    });

    ConfigurableApplicationContext applicationContext = springApplication.run(args);

    ConfigurableEnvironment environment = applicationContext.getEnvironment();
    for (PropertySource<?> propertySource : environment.getPropertySources()) {
        System.out.println(propertySource);
    }

    System.out.println(environment.getProperty("custom.config.key"));
}

结果如下:

txt
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
MapPropertySource {name='custom-bootstrap'}
ApplicationInfoPropertySource {name='applicationInfo'}
from spring cloud config server

可以发现,远程获取的配置已经成功加载进环境中。

2.4 ApplicationContextInitializer

设置ApplicationContextInitializer:

java
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

这是在应用容器(ApplicationContext)创建完成后,对容器进行初始化,用法如下:

java
public class Main {
    public static void main(String[] args) {
        SpringApplication springApplication = new SpringApplication();

        // 设置XML文件为加载Bean的来源
        springApplication.setSources(Set.of("classpath:bean.xml"));

        // 设置容器初始化器
        springApplication.addInitializers(new ApplicationContextInitializer<ConfigurableApplicationContext>() {
            @Override
            public void initialize(ConfigurableApplicationContext applicationContext) {
                System.out.println("容器初始化...");
                // 可以在这里注册组件
                if (applicationContext instanceof GenericApplicationContext genericApplicationContext) {
                    genericApplicationContext.registerBean(MyBean.class);
                }
            }
        });

        ConfigurableApplicationContext applicationContext = springApplication.run(args);

        for (String name : applicationContext.getBeanDefinitionNames()) {
            System.out.println(name);
        }
    }
}

class MyBean{

}

结果发现,容器中的组件包含MyBean

2.5 ApplicationListener

java
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

在容器准备、启动过程中,会发布很多事件,我们可以添加事件订阅器,从而对某些事件作出反应:

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();

    // 设置XML文件为加载Bean的来源
    springApplication.setSources(Set.of("classpath:bean.xml"));

    // 添加事件监听器
    springApplication.addListeners(new ApplicationListener<ApplicationEvent>() {
        @Override
        public void onApplicationEvent(ApplicationEvent event) {
            System.out.println("事件为:" + event.getClass().getName());
        }
    });

    springApplication.run(args);
}

结果为:

txt
事件为:org.springframework.boot.context.event.ApplicationStartingEvent
事件为:org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent
事件为:org.springframework.boot.context.event.ApplicationContextInitializedEvent
事件为:org.springframework.boot.context.event.ApplicationPreparedEvent
事件为:org.springframework.context.event.ContextRefreshedEvent
事件为:org.springframework.boot.context.event.ApplicationStartedEvent
事件为:org.springframework.boot.availability.AvailabilityChangeEvent
事件为:org.springframework.boot.context.event.ApplicationReadyEvent
事件为:org.springframework.boot.availability.AvailabilityChangeEvent
事件为:org.springframework.context.event.ContextClosedEvent

因此,针对不同事件,我们可以做出不同的调整。

2.6 推断主类

java
this.mainApplicationClass = deduceMainApplicationClass();

就是通过堆栈信息,判断哪个类包含main方法。

3. 调用run()方法

当创建了SpringApplication对象后,我们就可以调用run()方法了:

java
public ConfigurableApplicationContext run(String... args) {
  // 记录启动过程的耗时
  Startup startup = Startup.create();
  if (this.properties.isRegisterShutdownHook()) {
    SpringApplication.shutdownHook.enableShutdownHookAddition();
  }
  // 创建初始上下文环境
  DefaultBootstrapContext bootstrapContext = createBootstrapContext();
  ConfigurableApplicationContext context = null;
  configureHeadlessProperty();
  // SpringApplicationRunListeners 可以看成是事件发布器!!!
  SpringApplicationRunListeners listeners = getRunListeners(args);
  // 发布starting事件
  listeners.starting(bootstrapContext, this.mainApplicationClass);
  try {
    // 封装启动参数args
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // 准备环境
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    // 打印Spring标志
    Banner printedBanner = printBanner(environment);
    // 创建容器
    context = createApplicationContext();
    context.setApplicationStartup(this.applicationStartup);
    // 准备容器,包括设置环境、注册组件
    prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    // 刷新容器,BeanFactory后处理器这时开始处理
    refreshContext(context);
    // 没用的方法
    afterRefresh(context, applicationArguments);
    // 记录容器已启动
    startup.started();
    if (this.properties.isLogStartupInfo()) {
      new StartupInfoLogger(this.mainApplicationClass, environment).logStarted(getApplicationLog(), startup);
    }
    // 发布容器已启动事件
    listeners.started(context, startup.timeTakenToStarted());
    // 调用Runner接口,包括CommandLineRunner和ApplicationRunner
    callRunners(context, applicationArguments);
  }
  catch (Throwable ex) {
    // 如果启动过程中出现错误,处理错误
    throw handleRunFailure(context, ex, listeners);
  }
  try {
    if (context.isRunning()) {
      // 发布容器已准备好事件
      listeners.ready(context, startup.ready());
    }
  }
  catch (Throwable ex) {
    throw handleRunFailure(context, ex, null);
  }
  return context;
}

3.1 准备Environment

环境准备主要是这一行代码:

java
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

在Spring Boot应用中,创建的环境是ApplicationEnvironment,默认情况下,里面配置来源只有两个:JVM环境变量和操作系统环境变量:

TIP

注意,ApplicationEnvironment是非public的,测试代码需要在org.springframework.boot包下,可以自己创建一个同名包。

java
package org.springframework.boot;

import org.springframework.core.env.PropertySource;

public class Main {
    public static void main(String[] args) {
        ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();

        for (PropertySource<?> propertySource : applicationEnvironment.getPropertySources()) {
            System.out.println(propertySource);
        }
    }
}

结果如下:

java
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}

但是,完整的配置来源如下:

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();
    springApplication.setSources(Set.of("classpath:bean.xml"));

    ConfigurableApplicationContext applicationContext = springApplication.run(args);
    for (PropertySource<?> propertySource : applicationContext.getEnvironment().getPropertySources()) {
        System.out.println(propertySource);
    }
}
txt
ConfigurationPropertySourcesPropertySource {name='configurationProperties'}
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
ApplicationInfoPropertySource {name='applicationInfo'}

注意,启动程序时需要加上命令行参数,否则第二行SimpleCommandLinePropertySource没有。

首先看SimpleCommandLinePropertySourceApplicationInfoPropertySource

  • SimpleCommandLinePropertySource:配置来源于命令行参数,具有最高优先级,所以排在前面;
  • ApplicationInfoPropertySource:其中保存了Java程序的进程ID,如{spring.application.pid=6089}

这是在方法configurePropertySources()中添加的,添加方法如下:

java
public static void main(String[] args) {
    ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();
    MutablePropertySources propertySources = applicationEnvironment.getPropertySources();

    System.out.println("----------前-------------------");
    for (PropertySource<?> propertySource : propertySources) {
        System.out.println(propertySource);
    }

    propertySources.addFirst(new SimpleCommandLinePropertySource(args));
    propertySources.addLast(new ApplicationInfoPropertySource(Main.class));

    System.out.println("----------后-------------------");
    for (PropertySource<?> propertySource : propertySources) {
        System.out.println(propertySource);
    }
  
  	// 查看程序信息
    System.out.println(propertySources.get("applicationInfo").getSource());
}
txt
----------前-------------------
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
----------后-------------------
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
ApplicationInfoPropertySource {name='applicationInfo'}
{spring.application.pid=6089}

RandomValuePropertySourceOriginTrackedMapPropertySource是由EnvironmentPostProcessor(环境后处理器)添加的,实现类分别为RandomValuePropertySourceEnvironmentPostProcessorConfigDataEnvironmentPostProcessor,而环境后处理器是由EnvironmentPostProcessorApplicationListener进行调用的,这是一个事件监听器,用于在环境准备好后执行。

  • RandomValuePropertySource:可以用来产生一些随机值,例如random.intrandom.longrandom.uuidrandom.xx(x x为任意值,产生一个随机字节数组,长度为16);
  • OriginTrackedMapPropertySource:用来从类路径下的配置文件application.propertiesapplication.yml加载配置信息;

添加逻辑如下:

java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();
    springApplication.setSources(Set.of("classpath:bean.xml"));

    ApplicationEnvironment applicationEnvironment = new ApplicationEnvironment();
    MutablePropertySources propertySources = applicationEnvironment.getPropertySources();

    System.out.println("----------前-------------------");
    for (PropertySource<?> propertySource : propertySources) {
        System.out.println(propertySource);
    }

    propertySources.addFirst(new SimpleCommandLinePropertySource(args));
    propertySources.addLast(new ApplicationInfoPropertySource(Main.class));

    RandomValuePropertySourceEnvironmentPostProcessor environmentPostProcessor1 =
            new RandomValuePropertySourceEnvironmentPostProcessor(new DeferredLogs());
    environmentPostProcessor1.postProcessEnvironment(applicationEnvironment, springApplication);

    ConfigDataEnvironmentPostProcessor environmentPostProcessor2 =
            new ConfigDataEnvironmentPostProcessor(new DeferredLogs(), new DefaultBootstrapContext());
    environmentPostProcessor2.postProcessEnvironment(applicationEnvironment, springApplication);

    System.out.println("----------后-------------------");
    for (PropertySource<?> propertySource : propertySources) {
        System.out.println(propertySource);
    }
}

结果如下:

java
-----------------------------
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
-----------------------------
SimpleCommandLinePropertySource {name='commandLineArgs'}
PropertiesPropertySource {name='systemProperties'}
SystemEnvironmentPropertySource {name='systemEnvironment'}
RandomValuePropertySource {name='random'}
ApplicationInfoPropertySource {name='applicationInfo'}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.yml]' via location 'optional:classpath:/''}

使用Environment产生随机值:

java
System.out.println(applicationEnvironment.getProperty("random.int"));
System.out.println(applicationEnvironment.getProperty("random.long"));
System.out.println(applicationEnvironment.getProperty("random.uuid"));
System.out.println(applicationEnvironment.getProperty("random.abc"));

3.2 打印Banner

在介绍Banner之前,请先参考附录2:Binder,并且,在准备Environment之后,会调用如下方法:

java
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
  try {
    Binder.get(environment).bind("spring.main", Bindable.ofInstance(this.properties));
  }
  catch (Exception ex) {
    throw new IllegalStateException("Cannot bind to SpringApplication", ex);
  }
}

将配置文件中以 spring.main 开头的属性绑定到SpringApplication中的properties属性上。

与这一步相关的配置是:

txt
spring.main.banner-mode

取值有三个:

  • OFF:不打印Banner;
  • CONSOLE:将Banner打印到控制台;
  • LOG:将Banner打印到日志文件;

打印Banner调用的方法如下:

java
private Banner printBanner(ConfigurableEnvironment environment) {
  // 不打印Banner
  if (this.properties.getBannerMode(environment) == Banner.Mode.OFF) {
    return null;
  }
  ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
      : new DefaultResourceLoader(null);
  // 创建Banner打印器,这里会加载Banner
  SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);
  // 打印到日志文件
  if (this.properties.getBannerMode(environment) == Mode.LOG) {
    return bannerPrinter.print(environment, this.mainApplicationClass, logger);
  }
  // 打印到控制台
  return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
}

bannerPrinter.print()方法中,逻辑如下:

  • 首先获取Banner,如果在类路径下没有找到文件banner.txt,那么将使用默认的Banner:SpringBootBanner;我们可以在配置文件中修改Banner的位置:spring.banner.location(默认值banner.txt);
  • 然后打印Banner;

可以在如下网站创建自定义的Banner:https://devops.datenkollektiv.de/banner.txt/index.html

在类路径下添加文件banner.txt,里面的内容是自定义的banner,就会打印自定义的banner。

3.3 准备容器

准备容器ApplicationContext主要分为以下内容:

  • 创建容器:根据应用类型,创建不同容器;
  • 初始化容器:为容器设置环境Environment、应用ApplicationContextInitializer、从Bean来源加载Bean定义;
  • 刷新容器:调用容器的refresh()方法,此时Bean工厂后处理器、Bean后处理器(对于单例Bean)起作用;

主要代码如下:

java
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);

由于之前在容器已经介绍过容器相关知识,此处略过。

3.4 调用Runner

在准备好容器后,就会调用Runner:

java
callRunners(context, applicationArguments);

Runner主要分为两类:

  • CommandLineRunner

    java
    public interface CommandLineRunner extends Runner {
    	void run(String... args) throws Exception;
    }
  • ApplicationRunner

    java
    public interface ApplicationRunner extends Runner {
    	void run(ApplicationArguments args) throws Exception;
    }

两者最大的不同是参数的不同。调用Runner的具体方法如下:

java
private void callRunner(Runner runner, ApplicationArguments args) {
  if (runner instanceof ApplicationRunner) {
    callRunner(ApplicationRunner.class, runner, (applicationRunner) -> applicationRunner.run(args));
  }
  if (runner instanceof CommandLineRunner) {
    callRunner(CommandLineRunner.class, runner,
        (commandLineRunner) -> commandLineRunner.run(args.getSourceArgs()));
  }
}

ApplicationArguments是对args的封装:

java
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

ApplicationArguments会将args分为两类:

  • OptionArgs:以--开头的参数,例如--server.port
  • NonOptionArgs():除了OptionArgs,其他都是;
java
public static void main(String[] args) {
    String[] testArgs = new String[]{
            "--server.port=8080",
            "--host",
            "--ip=",
            "debug",
            "name=zs",
            "-age=20"
    };
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(testArgs);

    System.out.println(applicationArguments.getNonOptionArgs());
    System.out.println(applicationArguments.getOptionNames());
    for (String optionName : applicationArguments.getOptionNames()) {
        System.out.println(applicationArguments.getOptionValues(optionName));
    }
}

结果如下:

txt
[debug, name=zs, -age=20]  // NonOptionArgs
[ip, server.port, host]    // OptionNames
[]
[8080]
[]

如何定义Runner呢?我们只需要使组件继承CommandLineRunnerApplicationRunner接口即可:

java
public class Bean01 implements CommandLineRunner {
    @Override
    public void run(String... args) throws Exception {
        System.out.println("command line runner");
    }
}
java
public static void main(String[] args) {
    SpringApplication springApplication = new SpringApplication();
    springApplication.setSources(Set.of("classpath:bean.xml"));  // bean.xml包含Bean01定义
    springApplication.run(args);
}

运行上面代码,会发现"command line runner"成功打印。

3.5 事件发布器

在准备环境、创建容器过程中的某些时间点,会发布特定的事件,这个工作由SpringApplicationRunListeners完成:

jade
SpringApplicationRunListeners listeners = getRunListeners(args);

其中聚合了SpringApplicationRunListener

TIP

SpringApplicationRunListener,看名字像监听器,但其实是事件发布器,

Details
java
public interface SpringApplicationRunListener {

	default void starting(ConfigurableBootstrapContext bootstrapContext) {
	}

	default void environmentPrepared(ConfigurableBootstrapContext bootstrapContext,
			ConfigurableEnvironment environment) {
	}

	default void contextPrepared(ConfigurableApplicationContext context) {
	}

	default void contextLoaded(ConfigurableApplicationContext context) {
	}

	default void started(ConfigurableApplicationContext context, Duration timeTaken) {
	}

	default void ready(ConfigurableApplicationContext context, Duration timeTaken) {
	}

	default void failed(ConfigurableApplicationContext context, Throwable exception) {
	}

}

其中一个实现类是EventPublishingRunListener,它又通过SimpleApplicationEventMulticasterApplicationListener产生关联。

附录

1. SpringFactoriesLoader

在 Spring Boot 中,SpringFactoriesLoader 是一个核心工具类,位于 Spring Core 模块(org.springframework.core.io.support包),用于实现 Spring 的自动配置和 SPI(Service Provider Interface)机制。它的主要作用是从类路径下的 META-INF/spring.factories 文件中加载和实例化配置的类,从而支持 Spring 的可扩展性和模块化。

工作原理:

  • SpringFactoriesLoader 扫描类路径下所有 META-INF/spring.factories 文件。
  • 这些文件是 properties 格式,键是接口或类的全限定名,值是实现类的全限定名列表(逗号分隔)。
  • 加载时,SpringFactoriesLoader 根据指定的接口类型返回对应的实现类列表或实例。

例如,现在有如下接口和实现类:

java
package org.example.bean;

public interface IRun {
    void run();
}

public class Cat implements IRun{
    @Override
    public void run() {
        System.out.println("cat is running");
    }
}

public class Dog implements IRun{
    @Override
    public void run() {
        System.out.println("dog is running");
    }
}

然后在META-INF/spring.factories中配置如下:

java
org.example.bean.IRun=org.example.bean.Cat,org.example.bean.Dog

最后,我们就可以使用SpringFactoriesLoader加载META-INF/spring.factories中指定接口的实现类了:

java
public static void main(String[] args) {
    List<IRun> iRuns = SpringFactoriesLoader.loadFactories(IRun.class, Main.class.getClassLoader());
    for (IRun iRun : iRuns) {
        System.out.println(iRun);
    }
}

结果如下:

txt
org.example.bean.Cat@2acf57e3
org.example.bean.Dog@506e6d5e

ConfigurationPropertySourcesPropertySource的作用是将Envirnment中的属性源适配为一个统一的入口,提供松散属性绑定,因此放在第一个,使用如下命令添加:

java
ConfigurationPropertySources.attach(environment);

例如,现在配置文件中又如下配置:

properties
user-name=zs
user_age=20
userAddress=gz

我们在程序中使用如下方式获取配置:

java
System.out.println(applicationEnvironment.getProperty("user-name"));
System.out.println(applicationEnvironment.getProperty("user-age"));
System.out.println(applicationEnvironment.getProperty("user-address"));

如果没有ConfigurationPropertySourcesPropertySource,结果如下,可以发现只有名字完全相同才可以获取成功:

txt
zs
null
null

而有了ConfigurationPropertySourcesPropertySource,则可以处理属性名称的变体(如驼峰式、连字符式、蛇形命名等),例如 server.portserverPort 会被视为等价。

2. Binder

在 Spring 框架中,Binder 是 Spring Boot 提供的一个核心工具类,位于 org.springframework.boot.context.properties.bind 包,主要用于 将外部配置(如 application.properties、application.yml、命令行参数等)绑定到 Java 对象。它为 Spring Boot 的 @ConfigurationProperties 机制提供了底层支持,负责将 PropertySource 中的配置数据映射到 Java Bean 的字段或方法上。

例如:

java
public class ManualBinderExample {
    public static void main(String[] args) {
        // 创建 Environment 和 PropertySource
        StandardEnvironment environment = new StandardEnvironment();
        Map<String, Object> properties = new HashMap<>();
        properties.put("app.name", "TestApp");
        properties.put("app.port", "9090");
        environment.getPropertySources().addFirst(new MapPropertySource("test", properties));

        // 使用 Binder 绑定
        Binder binder = Binder.get(environment);
        AppConfig config = binder.bind("app", AppConfig.class).orElse(new AppConfig());

        // 输出结果
        System.out.println("App Name: " + config.getName()); // TestApp
        System.out.println("App Port: " + config.getPort()); // 9090
    }
}

class AppConfig{
    private String name;
    private String port;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getPort() {
        return port;
    }

    public void setPort(String port) {
        this.port = port;
    }
}