SpringBoot原理分析-1

SpringBoot原理分析

作为一个javaer,和boot打交道是很常见的吧。熟悉boot的人都会知道,启动一个springboot应用,就是用鼠标点一下启动main方法,然后等着就行了。我们来看看这个main里面。

@SpringBootApplication
public class ExampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(ExampleApplication.class, args);
    }
}

这个类很简单吧,有三个注意点。

  • ExampleApplication就是类名,为了规范起见,这个类名一般都是xxxApplication。
  • 有一个注解@SpringBootApplication。
  • 然后就一个main方法,里面使用了SpringApplication的静态方法run(),传进去了俩参数。

这就神奇地启动起来了,为什么呢?

分析的版本:SpringBoot 2.7.18

需要结合以前用xml文件来配置Spring容器的形式来对比Boot用注解的形式

1.注解&自动配置

目前只知道注解的作用即可,至于这些注解是怎么起作用的,见后续...

@SpringBootApplication

这一节来详细聊一聊这个注解。@SpringBootApplication 是 Spring Boot 框架中的一个核心注解,用于简化 Spring Boot 应用的配置和启动

它的作用

  • @SpringBootApplication 标注在应用的主类上,用于启动 Spring Boot 应用。
  • 它启用了 Spring Boot 的自动配置机制,根据项目的依赖自动配置 Spring 应用。
  • 组件扫描:它启用了组件扫描,自动发现并注册带有 @Component@Service@Repository@Controller 等注解的类。
  • 配置类:它标记该类为 Spring 的配置类,相当于 @Configuration 注解。

点进去看看,该注解的结构如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited // 四个元注解
@SpringBootConfiguration
@EnableAutoConfiguration //启用SpringBoot的自动配置机制。SpringBoot会根据类路径中的依赖自动配置应用。例如,如果类路径中有spring-boot-starter-web,SpringBoot会自动配置Tomcat和SpringMVC。

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) }) // 启用组件扫描,并排除一些特定的过滤器。默认是注解标注的类所在包及其子包。实际上是不是就是主启动类所在包及其子包!!!!!
public @interface SpringBootApplication {
..............
}

// 发现这是一个组合注解

@SpringBootConfiguration 注解:主要作用是标记一个类为 Spring Boot 的配置类与 @Configuration 类似,但它专门用于 Spring Boot 应用。这个注解和@ComponentScan注解结合到一起,达成了如下效果。(左边是xml文件配置形式,右边是SpringBoot注解的形式),这俩注解在一起,就是扫描主启动类所在包及其子包下的被@Controller、@Service、.....标注的类,将他们归到Spring容器里面去。

@EnableAutoConfiguration

@EnableAutoConfiguration注解: 这个注解不得了啊。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class) // 这里。
public @interface EnableAutoConfiguration {
	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
	Class<?>[] exclude() default {};
	String[] excludeName() default {};

}

发现@Import注解。这个注解的作用是干啥的?这里给出GPT的回答:@Import 是 Spring 框架提供的一个注解,用于将一个或多个配置类(@Configuration 类)组件类导入到当前的 Spring 应用上下文中。它可以用来显式地引入其他配置类或 Bean 定义,从而实现模块化配置和代码复用。

这里有一个非常重要的东西,那就是当前的Spring上下文,也就是主启动类所在包及其子包,@Import(AutoConfigurationImportSelector.class)这句话的意思是将SpringBoot官方写的AutoConfigurationImportSelector类导入到当前Spring上下文中,在当前Spring上下文注册这样一个bean。

AutoConfigurationImportSelector探究

public interface ImportSelector {
	String[] selectImports(AnnotationMetadata importingClassMetadata);
}
// 实现了DeferredImportSelector接口,DeferredImportSelector实现了ImportSelector
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, 
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
            @Override
            public String[] selectImports(AnnotationMetadata annotationMetadata) {
                ...........
                AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); // 1.进去
                return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
            }
            
            // 2.这个方法
            protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
                if (!isEnabled(annotationMetadata)) {
                    return EMPTY_ENTRY;
                }
                AnnotationAttributes attributes = getAttributes(annotationMetadata);
                // 3.进入这里
                List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
                ...........
            }
            
            // 4. 3调用的这个方法
            protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
                List<String> configurations = new ArrayList<>(
                       // 5. 继续往下
                        SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
                ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
                Assert.notEmpty(configurations,
                        "No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
                                + "are using a custom packaging, make sure that file is correct.");
                return configurations;
            }
            
            // 6. 5处调用的方法
            public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
                ...... // loadSpringFactories
                return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
            }
            
            // 7
            // 最终可以在loadSpringFactories方法里面看到这样一行代码
            //Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
            // public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
            // 结合官方给的解释The location to look for factories. Can be present in multiple JAR files. 寻找工厂的位置。可以存在于多个 JAR 文件中。
}

结合idea打断点调试:我们发现getCandidateConfigurations扫描到了引入的所有jar包的META-INF/spring.factories,共有156个xxxAutoConfigure类。但是这些都会用到吗,别忘了,SpringBoot有个非常强大的特点:那就是导入场景、对应的场景才会生效!!

继续往下走,请看下图:(发现只有50个了,例如上图里面的amqp,由于项目中没有用到,故在这里就没有了,经过了filter过滤)

以RabbitMq自动配置为例

@AutoConfiguration
@ConditionalOnClass({ RabbitTemplate.class, Channel.class }) // 这两个类存在时才生效,项目里面没导入相关jar包,故RabbitAutoConfiguration不会被导入Spring容器生效,下面就来说这个注解
@EnableConfigurationProperties(RabbitProperties.class)
@Import({ RabbitAnnotationDrivenConfiguration.class, RabbitStreamConfiguration.class })
public class RabbitAutoConfiguration {
    ................
}

@ConditionalOnClass

先看看其源码

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class) // 这里***
public @interface ConditionalOnClass {
	Class<?>[] value() default {};
	String[] name() default {};
}

@ConditionalOnClass注解的作用是当项目中存在某个类时才会使标有该注解的类或方法生效;

先看我们的主类【1. 此时,没有引入test.demo.test.list.Student所在的maven依赖】

/* 下面是这俩类
public class Cat {
    private Integer id;
    private String name;
}
public class Dog {
    private Integer id;
    private String name;
}*/

@SpringBootApplication
@MapperScan("com.feng.tackle.dao")
public class DateApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext context = SpringApplication.run(DateApplication.class, args);
        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        Iterator<String> iterator = beanFactory.getBeanNamesIterator();
        while (iterator.hasNext()) {
            String name = iterator.next();
            if ( name.equals("dog01") || name.equals("cat01") ) System.out.println(name); // 看看有没有这两个名字的bean
        }
    }
}
// 最后的输出结果
/*
cat01
*/

在配置类中,我们这样做

@Configuration
public class ConditionTestConfig {
    @ConditionalOnClass(name = "test.demo.test.list.Student") // 类路径有这个类,就往容器中放入该bean
    @Bean
    public Dog dog01() {
        return new Dog(2, "汪汪汪");
    }

    @ConditionalOnMissingClass("test.demo.test.list.Student") // 类路径没有这个类,就往容器中放入该bean
    @Bean
    public Cat cat01() {
        return new Cat(1, "喵喵喵");
    }
}

main最后的输出结果是cat01。"test.demo.test.list.Student"是另一个maven项目里面的类。

【2. 项目中引入Student所在的maven】

<dependency>
    <groupId>com.feng.test</groupId>
    <artifactId>test-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

再次启动main,输出结果dog01

通过这个例子,我们可以知道SpringBoot自动配置,导入具体starter,对应的场景才会生效。【由于没有导入MQ的starter,故此配置类不会生效】

总结

**自动配置: **

以spring-boot-starter-web为例子,引入该启动器,他的父级pom里面有spring-boot-starter,spring-boot-starter的父级pom有spring-boot-autoconfigure。

spring-boot-autoconfigure里面有Spring官方定义的所有场景。

  • @EnableAutoConfiguration 注解:这是自动配置的入口,通常由 @SpringBootApplication 注解组合引入。
  • spring.factories 文件:Spring Boot 通过 META-INF/spring.factories 文件加载自动配置类。
  • 条件化配置:通过 @Conditional 系列注解(如 @ConditionalOnClass@ConditionalOnMissingBean 等)实现按需加载配置。

自动配置的流程:

  1. 加载自动配置类
  • @SpringBootApplication 注解:该注解是一个组合注解,包含了 @EnableAutoConfiguration,用于启用自动配置。
  • @EnableAutoConfiguration 的作用:该注解会通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件中定义的自动配置类。
  1. 条件化配置

自动配置类通常使用 @Conditional 系列注解来控制是否生效

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class })
public class DataSourceAutoConfiguration {
    // 自动配置逻辑
}
//如果类路径中存在 DataSource 和 EmbeddedDatabaseType,且容器中没有 ConnectionFactory Bean,则自动配置 DataSource。
  1. 加载配置属性
  • @EnableConfigurationProperties:自动配置类通常会通过该注解加载配置属性。
  • application.propertiesapplication.yml:Spring Boot 会读取这些配置文件中的属性,并将其绑定到对应的配置类中。
@ConfigurationProperties(prefix = "spring.datasource")
public class DataSourceProperties {
    private String url;
    private String username;
    private String password;
    // getters and setters
}
  1. 注册bean
  • 自动配置类通过 @Bean 方法向容器中注册 Bean。
  • 这些 Bean 通常是框架的核心组件,如 DataSourceDispatcherServletSecurityFilterChain 等。

2. 启动过程

SpringApplication.run(ExampleApplication.class, args);这一行代码做了啥?这才是重点!

从run开始看,一层一层往下面点进去

// 1.
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) { // 会返回一个正在运行的Spring容器
    return run(new Class<?>[] { primarySource }, args); // 调用重载的run方法
}
// 2.
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args); // 先调用该构造方法,然后再调用run方法,将程序参数传进去
}
// 3.
public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources); //primarySource 是传入的主配置类(通常带有 @SpringBootApplication 注解的类),Spring Boot 会将其作为配置源。
}

// 4. [构造方法] 构造方法
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    ........ // 见下一节构造方法
}

先调用该构造方法,然后再调用run方法

①构造方法

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader; // null的
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));  // 一、设置主配置类
    this.webApplicationType = WebApplicationType.deduceFromClasspath();  // 二、推断应用类型
    this.bootstrapRegistryInitializers = new ArrayList<>( // 三、
        getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));// 四、
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));// 五、
    this.mainApplicationClass = deduceMainApplicationClass(); // 六、通过堆栈信息推断出主类(即 main 方法所在的类)。
}

1.推断应用类型

  • WebApplicationType.deduceFromClasspath() 方法会根据类路径中是否存在特定的类来推断应用类型。
  • 可能的类型包括:
    • WebApplicationType.SERVLET:基于 Servlet 的 Web 应用(如 Spring MVC)。
    • WebApplicationType.REACTIVE:基于 Reactive 的 Web 应用(如 Spring WebFlux)。
    • WebApplicationType.NONE:非 Web 应用。

2.加载并初始化 BootstrapRegistryInitializer 实例

分析一下核心方法getSpringFactoriesInstances,下面两个都会调用这个。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader)); // 下一步
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

// SpringFactoriesLoader.loadFactoryNames(type, classLoader) 到这儿来了
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

//loadSpringFactories(classLoaderToUse)
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap<>();
    // 从缓存map中拿到对应classLoader的map,如果该classLoader已经存在了,就不用走下面的了
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        /*
        public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
        结合官方给的解释The location to look for factories. Can be present in multiple JAR files. 寻找工厂的位置。可以存在于多个 JAR 文件中。
        */
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        ..............
    return result;
}

可以看到这里,在第一章《注解&自动配置》也出现了这个,如今在SpringApplication的构造方法,底层也调用到了这里。二者有什么区别或者联系呢?

SpringFactoriesLoader.loadFactoryNames是Spring Boot 提供的工具方法,用于从 META-INF/spring.factories 文件中加载指定类型的配置类。

两者都依赖于 META-INF/spring.factories 文件,该文件是 Spring Boot 自动配置和扩展机制的核心配置文件。两者都通过类路径扫描,加载所有 META-INF/spring.factories 文件,并解析出指定类型的配置类。

(1)调用位置

  • AutoConfigurationImportSelector
    • 位于 org.springframework.boot.autoconfigure 包中。
    • 是 Spring Boot 自动配置的核心组件之一,负责加载自动配置类。
    • 在 Spring 容器的配置类解析阶段被调用(具体是在 @EnableAutoConfiguration 注解的处理过程中)。
  • SpringApplication
    • 位于 org.springframework.boot 包中。
    • 是 Spring Boot 应用的启动类,负责初始化应用上下文、加载配置等。
    • 在应用启动时被调用。

(2)加载的配置类型

  • AutoConfigurationImportSelector
    • 主要加载 META-INF/spring.factories 文件中 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键下的自动配置类。
    • 这些配置类用于实现 Spring Boot 的自动配置功能(如 DataSourceAutoConfigurationWebMvcAutoConfiguration 等)。
  • SpringApplication
    • 加载多种类型的配置类,包括:
      • ApplicationContextInitializer
      • ApplicationListener
      • BootstrapRegistryInitializer
    • 这些配置类用于初始化应用上下文、监听应用事件等。

(3)调用时机

  • AutoConfigurationImportSelector
    • 在 Spring 容器解析配置类时调用(具体是在 ConfigurationClassPostProcessor 处理 @Configuration 类时)。
    • 属于 Spring 容器初始化的早期阶段。
  • SpringApplication
    • 在应用启动时调用(具体是在 SpringApplication 的构造方法或 run 方法中)。
    • 属于应用启动的早期阶段。

(4)返回值的使用

  • AutoConfigurationImportSelector
    • 返回的自动配置类会被 Spring 容器加载并处理,最终生成相应的 Bean 定义。
    • 这些配置类通常包含 @Configuration 注解和 @Conditional 注解,用于按需加载 Bean。
  • SpringApplication
    • 返回的配置类会被直接实例化并注册到应用中。
    • 例如,ApplicationContextInitializer 会被调用以初始化应用上下文,ApplicationListener 会被注册以监听应用事件。

3.加载并设置 ApplicationContextInitializer 实例

同2

4.加载并设置 ApplicationListener 实例

同2

总结

  • BootstrapRegistryInitializer:用于应用启动的最早阶段,初始化引导阶段的组件。
  • ApplicationContextInitializer:用于在 ApplicationContext 创建之后、刷新之前,对上下文进行自定义初始化。
  • ApplicationListener:用于监听应用生命周期中的事件,并在特定阶段执行逻辑。

这三者共同扩展了 Spring Boot 应用的启动过程,提供了灵活的扩展点,可以满足不同场景下的需求。

5.三者的实现

分别编写实现类

public class MyBootstrapInit implements BootstrapRegistryInitializer {
    @Override
    public void initialize(BootstrapRegistry registry) {
        System.out.println("【MyBootstrapInit】--------------方法执行了");
    }
}

public class MyApplicationContextInit implements ApplicationContextInitializer<ConfigurableApplicationContext> {
    @Override
    public void initialize(ConfigurableApplicationContext applicationContext) {
        System.out.println("【MyApplicationContextInit】------------" + applicationContext.getApplicationName());
    }
}

public class MyContextListener implements ApplicationListener<ApplicationStartedEvent> { // 该监听器对此事件感兴趣
    @Override
    public void onApplicationEvent(ApplicationStartedEvent event) {
        System.out.println("【MyContextListener】--监听器--" + event.getSource());
    }
}

然后在resource目录下面创建META-INF,在其中的spring.factories中

org.springframework.boot.BootstrapRegistryInitializer=com.feng.tackle.config.source.MyBootstrapInit
org.springframework.context.ApplicationContextInitializer=com.feng.tackle.config.source.MyApplicationContextInit
org.springframework.context.ApplicationListener=com.feng.tackle.config.source.MyContextListener

启动应用:

疑问? 我可以在这三者的实现类上面加@Configuration注解,不要spring.factories,可以实现类似效果吗?

答案是,只有ApplicationListener可以。为什么?请看下面的run方法里面

②run

public ConfigurableApplicationContext run(String... args) {
    // 记录应用启动的开始时间,用于后续计算启动耗时。
    long startTime = System.nanoTime(); 
    //创建一个 BootstrapContext,用于在应用启动的早期阶段提供一些基础设施支持(如环境配置、属性源等)================
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    ConfigurableApplicationContext context = null;
    // 设置系统属性 java.awt.headless,确保应用在没有图形界面环境(如服务器)中也能正常运行
    configureHeadlessProperty();
    /*
    获取所有 SpringApplicationRunListener 实例,用于监听应用启动的各个阶段。调用 listeners.starting(),通知监听器应用正在启动。
    */
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // 加载和配置应用的环境(Environment),包括配置文件(如 application.properties 或 application.yml)、命令行参数等。
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);
        // 打印 Spring Boot 的启动 Banner(默认或自定义)。
        Banner printedBanner = printBanner(environment);
        // 根据应用类型(Servlet、Reactive 等)创建相应的 ApplicationContext。
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        // 准备应用上下文
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        // 调用 AbstractApplicationContext.refresh() 方法,完成 Bean 的实例化、依赖注入和初始化。
        refreshContext(context);
        afterRefresh(context, applicationArguments);
        // 计算启动耗时并记录日志。
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
        }
        listeners.started(context, timeTakenToStartup);
        // 执行所有 ApplicationRunner 和 CommandLineRunner 的实现类,用于在应用启动后执行自定义逻辑。
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, listeners);
        throw new IllegalStateException(ex);
    }
    try {
        // 计算应用启动到就绪的总耗时。
        Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
        listeners.ready(context, timeTakenToReady);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

这个run这里分析得不够深入。受篇幅影响,在后续文章中再来分析。

end. 参考

  1. 尚硅谷雷丰阳老师的课
posted @ 2025-01-05 15:21  别来无恙✲  阅读(2)  评论(0编辑  收藏  举报