likes
comments
collection
share

SpringBoot自动装配、启动流程及自定义starter

作者站长头像
站长
· 阅读数 20

作为一名Java程序员,Spring框架是我们平常工作中非常重要的一个框架。其为我们提供了IOC容器和AOP的功能,在很大程度上简化了我们项目中的开发,但随着我们使用Spring接入的组件的增加,相应的配置文件也越来越多,这些配置文件的维护也成为了一个比较繁琐的工作,这也是很多开发人员比较诟病的地方。

SpringBoot的出现便很好的解决了这个问题,在SpringBoot项目中我们不再关注那么多的配置文件,其提供的自动装配功能使我们能够更容易的用其来快速的搭建我们的应用。

在今天的文章中我们就介绍下SpringBoot的自动装配、应用的启动流程,并在最后介绍下如何自定义一个starter

1、示例代码

在介绍之前,我们先通过示例代码看看如何使用SpringBoot搭建一个web应用,示例代码使用的是SpringBoot的最新版本3.0.0,该版本依赖JDK17,如果本地使用的JDK8请使用低版本的SpringBoot或者升级JDK。

创建spring-boot-samplemaven项目并添加相关依赖,其pom文件如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.0</version>
    </parent>

    <modelVersion>4.0.0</modelVersion>

    <groupId>com.xh</groupId>
    <artifactId>spring-boot-sample</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>

</project>

在创建好的项目中编写一个启动类,其内容如下:

package com.xh.sample.spring.boot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @description: Create by IntelliJ IDEA
 * @author: xh
 * @createTime: 2022/12/3 18:31
 */
@RestController
@SpringBootApplication
public class SpringBootSampleApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringBootSampleApplication.class, args);
    }

    @RequestMapping(value = "/hello")
    public String hello() {
        return "Hello World!!!";
    }
}

至此我们的示例代码就编写完成了,我们直接执行启动类中的main方法,启动完成后可以访问http://localhost:8080/hello进行测试。

我们在pom中引入了spring-boot-starter-web的依赖,便集成好了SpringMVC的功能,是不是很简单方便呢,这便是自动装配的魔力。

启动类中只需要添加@SpringBootApplication注解并调用SpringApplication.run方法即可,在下面的内容中我们主要就是围绕着这两点来进行介绍。

2、自动装配

自动装配是SpringBoot的核心,是基于Spring的注解及SPI机制进行的实现。当SpringBoot启动时会加载各个依赖包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件(我们使用的是SpringBoot3.0.0版本,之前的版本中需要自动装配的类也是定义在spring.factories中的),并将该文件中配置的类加载到Spring容器中。

开启自动装配的逻辑关键在@SpringBootApplication这个注解上,这个注解的源码如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
    @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
  // 需要排除的类
  @AliasFor(annotation = EnableAutoConfiguration.class)
  Class<?>[] exclude() default {};

  @AliasFor(annotation = EnableAutoConfiguration.class)
  String[] excludeName() default {};
  // 扫描的包  默认是当前包路径
  @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
  String[] scanBasePackages() default {};

  @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
  Class<?>[] scanBasePackageClasses() default {};

  @AliasFor(annotation = ComponentScan.class, attribute = "nameGenerator")
  Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

  @AliasFor(annotation = Configuration.class)
  boolean proxyBeanMethods() default true;

}

从这段源码中我们可以看到@SpringBootApplication是一个复合注解;

  • @SpringBootConfiguration @Configuration的派生注解
  • @EnableAutoConfiguration 启用SpringBoot的自动装配
  • @ComponentScan Spring中的注意,用于扫描相应的包下被@Component及其派生注解修饰的类

因此,我们完全可以将这个注解替换为@Configuration@EnableAutoConfiguration@ComponentScan三个注解。

这里我们单独看下@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 {};

}

在这个注解中的@AutoConfigurationPackage注解的作用是将启动类所在包下的所有组件注册到Spring容器中,跟踪源码会发现其会调用到BeanDefinitionRegistry.register方法,这个是Spring中相关的类,我们这里就不展开了。

@Import注解中跟着的类,在Spring容器启动时会进行加载,这里面设置的是AutoConfigurationImportSelector,这个类的作用是加载自动装配类,这个类实现了DeferredimportSelector接口并实现了selectImports方法,这个方法的作用是收集所有需要加载到Spring容器中的类名。其逻辑如下:

public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  // 是否开启了自动装配  判断的是spring.boot.enableautoconfiguration配置
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  // 获取注解的属性信息
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 获取所有需要自动装配的类
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 移除重复的类
  configurations = removeDuplicates(configurations);
  // 移除需要排除的类
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  configurations.removeAll(exclusions);
  configurations = getConfigurationClassFilter().filter(configurations);
  fireAutoConfigurationImportEvents(configurations, exclusions);
  return new AutoConfigurationEntry(configurations, exclusions);
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  // 获取META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的类
  List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader())
    .getCandidates();
  Assert.notEmpty(configurations,
                  "No auto configuration classes found 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;
}
// ImportCandidates类中的方法,LOCATION的值为:"META-INF/spring/%s.imports"
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
  Assert.notNull(annotation, "'annotation' must not be null");
  ClassLoader classLoaderToUse = decideClassloader(classLoader);
  String location = String.format(LOCATION, annotation.getName());
  Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
  List<String> importCandidates = new ArrayList<>();
  while (urls.hasMoreElements()) {
    URL url = urls.nextElement();
    importCandidates.addAll(readCandidateConfigurations(url));
  }
  return new ImportCandidates(importCandidates);
}

到这里关于SpringBoot自动装配相关的逻辑已经介绍完了,通过查看源码的方式,我们可以知道:

  • @SpringBootApplication 是一个复合注解,拥有@Configuration@EnableAutoConfiguration@ComponentScan的功能
  • @EnableAutoConfiguration 是开启自动装配的注解,在其中定义了@Import(AutoConfigurationImportSelector.class)
  • AutoConfigurationImportSelector 类会在容器启动时获取到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的类交给Spring进行初始化

3、启动过程

在上面的内容中我们介绍了SpringBoot的自动装配的原理,在接下来的内容中我们介绍下SpringBoot的启动过程。自动装配的逻辑是在@SpringBootApplication中,其启动过程我们应该跟踪SpringApplication.run方法。

跟进这个源码会调用到如下的方法中:

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
  // 创建SpringApplication对象并调用run方法
  return new SpringApplication(primarySources).run(args);
}

SpringApplication对象的创建逻辑如下:

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  // 这里传递的resourceLoader为null
  this.resourceLoader = resourceLoader;
  Assert.notNull(primarySources, "PrimarySources must not be null");
  // 这里的primarySources传递的是我们的启动类
  this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  // 获取应用类型 NONE SERVLET REACTIVE 我们的示例代码是一个web项目  这里的类型是SERVLET
  this.webApplicationType = WebApplicationType.deduceFromClasspath();
  // 获取META-INF/spring.factories配置的BootstrapRegistryInitializer实例
  this.bootstrapRegistryInitializers = new ArrayList<>(
    getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
  // 获取META-INF/spring.factories配置的BootstrapRegistryInitializer实例
  setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  // 获取META-INF/spring.factories配置的ApplicationListener实例
  setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  this.mainApplicationClass = deduceMainApplicationClass();
}

SpringApplication构造方法中设置应用类型并获取到一些在META-INF/spring.factories中配置的类实例。

应用类型有如下三种:

  • NONE
  • SERVLET
  • REACTIVE

getSpringFactoriesInstances方法最后会调用到SpringFactoriesLoader.load方法中,SpringFactoriesLoader类是Spring SPI的工具类,使用这个类可以获取到所有在META-INF/spring.factories中配置的信息并可按照某个类型获取实例列表。

通过这种方式获取对象的方法,都可以作为我们的扩展点,那么该如何使用呢?我们以BootstrapRegistryInitializer为例,我们在示例代码中定义一个SelfBootstrapRegistryInitializer类,创建META-INF/spring.factories文件并添加配置,代码如下:

public class SelfBootstrapRegistryInitializer implements BootstrapRegistryInitializer {
    @Override
    public void initialize(BootstrapRegistry registry) {
        System.out.println("=============== SelfBootstrapRegistryInitializer ==================");
    }
}
// spring.factories
org.springframework.boot.BootstrapRegistryInitializer=\
  com.xh.sample.spring.boot.SelfBootstrapRegistryInitializer

然后再重启项目,可以看到控制台会打印出我们输出的内容。

介绍完SpringApplication的构造方法后,我们看看这个对象中的run方法逻辑,这个方法中的逻辑就是启动SpringBoot的流程,其源码如下:

public ConfigurableApplicationContext run(String... args) {
  long startTime = System.nanoTime();
  //创建启动上下文对象  这里会调用在上面中获取到的BootstrapRegistryInitializer实例的initialize方法
  DefaultBootstrapContext bootstrapContext = createBootstrapContext();
  ConfigurableApplicationContext context = null;
  // 设置java.awt.headless属性
  configureHeadlessProperty();
  // 获取META-INF/spring.factories中配置的SpringApplicationRunListener实例
  // SpringApplicationRunListener里面提供了starting environmentPrepared contextPrepared 等方法
  // 使用这个扩展点可以完成对启动的各个阶段进行扩展
  SpringApplicationRunListeners listeners = getRunListeners(args);
  // 调用starting方法
  listeners.starting(bootstrapContext, this.mainApplicationClass);
  try {
    ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
    // 准备环境
    ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
    // 打印banner图  如果自定义banner图的话 只需在resources目录下创建banner.txt文件即可
    Banner printedBanner = printBanner(environment);
    // 创建上下文对象 创建的是AnnotationConfigServletWebServerApplicationContext
    context = createApplicationContext();
    context.setApplicationStartup(this.applicationStartup);
    prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
    // 启动容器的流程,这里会调用到AbstractApplicationContext.refresh方法中启动spring容器
    refreshContext(context);
    // 这里是个空方法,子类可以实现该方法做些扩展  模版方法
    afterRefresh(context, applicationArguments);
    Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
    if (this.logStartupInfo) {
      new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
    }
    // 容器启动成功  调用listeners的started方法
    listeners.started(context, timeTakenToStartup);
    // 从容器中获取所有的ApplicationRunner  CommandLineRunner对象  调用里面的run方法
    callRunners(context, applicationArguments);
  }
  catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
      throw ex;
    }
    handleRunFailure(context, ex, listeners);
    throw new IllegalStateException(ex);
  }
  try {
    if (context.isRunning()) {
      Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
      listeners.ready(context, timeTakenToReady);
    }
  }
  catch (Throwable ex) {
    if (ex instanceof AbandonedRunException) {
      throw ex;
    }
    handleRunFailure(context, ex, null);
    throw new IllegalStateException(ex);
  }
  return context;
}

相应的逻辑在上面的备注中进行了介绍,通过跟踪源码,我们发现SpringBoot中也提供了很多扩展点,我们简单的梳理下如下:

  • 自定义BootstrapRegistryInitializer类 配置到spring.factories文件中
  • 自定义ApplicationContextInitializer类 配置到spring.factories文件中
  • 自定义ApplicationListener类 配置到spring.factories文件中
  • 自定义SpringApplicationRunListener类 配置到spring.factories文件中 用于扩展启动的各个阶段
  • 自定义banner图,在classpath下创建banner.txt文件
  • 自定义ApplicationRunner CommandLineRunner类,使用@Component交给spring容器管理,容器启动完成后调用其run方法

在我们之前的spring搭建的web项目,需要部署到tomcat或者其他的web服务容器中,但我们使用SpringBoot搭建的web应用只需执行一个SpringApplication.run方法便可对外提供web服务了,这是因为在SpringBoot中给我们内置了web容器,这块代码的逻辑在ServletWebServerApplicationContext.onRefresh方法中,看过spring容器启动源码的应该会发现,在容器启动完成后会调用这个方法。这里的代码就不进行粘贴了,在这里会创建WebServer对象,默认使用的是Tomcat,内容的容器有如下几种:

  • TomcatWebServer
  • JettyWebServer
  • NettyWebServer
  • UndertowWebServer
  • UndertowServletWebServer

3、自定义starter

在上面的内容中我们介绍了SpringBoot自动装配的逻辑,我们只需要按照他的规则便可以很方便的自定义一个starter。接下来的内容给出一个简单的示例。在示例代码中我们定义一个单机版redis的客户端redisson

创建一个redisson-spring-boot-starterMaven项目,并添加相关依赖如下:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot</artifactId>
  <version>3.0.0</version>
</dependency>

<dependency>
  <groupId>org.redisson</groupId>
  <artifactId>redisson</artifactId>
  <version>3.16.3</version>
</dependency>

创建RedissonProperties类,用于接收相关配置,内容如下:

@ConfigurationProperties(prefix = "redis")
public class RedissonProperties {
    private String host = "localhost";
    private Integer port = 6379;

    public String getHost() {
        return host;
    }

    public Integer getPort() {
        return port;
    }
}

创建RedissonAutoConfiguration类,内容如下:

@Configuration
@EnableConfigurationProperties(value = RedissonProperties.class)
public class RedissonAutoConfiguration {

    @Autowired
    private RedissonProperties redissonProperties;

    @Bean
    public RedissonClient redissonClient() {
        System.out.println("============= 初始化RedissonClient对象 ===============");
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + redissonProperties.getHost() + ":" + redissonProperties.getPort());

        return Redisson.create(config);
    }
}

META-INF/spring目录下创建org.springframework.boot.autoconfigure.AutoConfiguration.imports文件,并将RedissonAutoConfiguration类配置进去,内容如下:

com.xh.starter.redisson.RedissonAutoConfiguration

然后使用Maven install到本地仓库,在spring-boot-sample中引入其依赖,然后重启项目,可以在启动日志中看到我们打印的日志,代表我们自定义的starter可以正常使用了。

SpringBoot自动装配、启动流程及自定义starter

4、总结

今天的内容就这些了,在本文中介绍了SpringBoot的自动装配原理、启动过程及如何自定义starter。

本文中的示例代码使用的SpringBoot版本为3.0.0JDK版本为17

@SpringBootApplication是一个复合注解,是自动装配的关键,AutoConfigurationImportSelector会获取到项目中所有依赖包中META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件中配置的类交给spring容器。

SpringBoot启动过程中会启动spring容器和web服务容器,并在其启动过程中有一些扩展点。

欢迎大家关注我的公众号:【Bug搬运小能手】

转载自:https://juejin.cn/post/7174031833183551519
评论
请登录