SpringBoot 自动装配原理

文摘   2024-11-26 09:00   山东  

引言

本文篇幅稍长,请耐心读完,希望会对大家有所帮助。

首先来说说Spring 是什么,相信每位从事Java 开发的朋友都不陌生。Spring 是一款主流的Java EE 轻量级开源框架,以IOC(控制反转)和AOP(面向切面)为核心理念,极大地简化了Java 企业级应用的开发难度和开发周期。Spring 的官方介绍如下:

Spring makes programming Java quicker, easier, and safer for everybody. Spring's focus on speed, simplicity, and productivity has made it the world's most popular Java framework.

这句话的意思是:Spring 使每个人都能更快、更轻松、更安全地进行Java 编程。Spring 对速度、简单性和生产力的关注使其成为世界上最流行的Java 框架。那么问题来了,既然Spring 已经如此强大了,为什么还要推出Spring Boot 呢?

具体来说,使用Spring 进行开发时有两大痛点:

  • 繁冗的配置文件:传统的Spring 应用通常需要大量的XMLJava 配置来定义Bean 及其依赖关系。随着项目规模的扩大,这些配置文件会变得非常庞大且难以维护,增加了开发人员的学习曲线和工作负担。

  • 复杂的依赖版本控制:在项目中,不同模块可能依赖于不同版本的第三方库,这可能导致“依赖地狱”(即不同库之间的依赖版本冲突)。我们需要花费大量时间和精力来解决这些依赖问题才能确保所有组件能够兼容并协同工作。

为了解决这些问题,Spring Boot 应运而生。Spring Boot 是建立在Spring Framework 之上的一个子项目,它简化Spring 应用创建过程的手段之一就是自动装配Spring 的官方网站上对其进行了如下介绍:

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".We take an opinionated view of the Spring platform and third-party libraries so you can get started with minimum fuss. Most Spring Boot applications need minimal Spring configuration.

这段话的意思是:通过Spring Boot,就可以轻松创建基于Spring 的独立生产级应用程序,而且 "只需运行"即可。 我们对Spring 平台和第三方库持开放态度,因此您可以轻松上手。 大多数Spring Boot 应用程序只需要最少的Spring 配置。

综上所述,Spring Boot 的推出是为了进一步降低Spring 应用的开发门槛,提高开发效率,使开发者能够更加专注于业务逻辑的实现,而不是被繁琐的配置所困扰。

SPI 机制

SPI(Service Provider Interface) 的字面意思是:服务提供者的接口,它是一种服务发现机制。这里为什么要提到SPI 呢?因为Spring Boot 的自动装配是基于SPI 机制来实现的。SPI 其实是一种约定大于配置的思想,在我们使用Spring Boot 的自动装配时,需要遵守它的约定,在classpath 下的META-INF 文件夹下定义一个名为spring.factories 的文件,然后在它的配置文件中以key-value 的形式对我们需要自动装配的类型进行配置。配置示例如下:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.example.module1.Module1AutoConfiguration,\ com.example.module2.Module2AutoConfiguration

Spring Boot 启动时,它会扫描META-INF/spring.factories 文件并根据文件中配置的类型信息创建出Bean 对象,然后将这些对象注册到IOC 容器中进行管理。

通过SPI 机制,Spring Boot 提供了一种灵活的、可插拔的方式,使得第三方库或自定义模块能够无缝集成到Spring Boot 应用中。通过这种机制,Spring Boot 能够自动发现并加载所需的组件和服务,而无需显式配置。

自动装配原理解析

自动装配注解关系图

一切的起源

创建Spring Boot 应用时,启动类上都会加一个@SpringBootApplication 注解,用于标记这是一个Spring Boot 应用的入口点。从上图中可以看出,@SpringBootApplication 是一个复合注解,集成了多个核心注解的功能(@SpringBootConfiguration@ComponentScan@EnableAutoConfiguration)。官方网站上对其的介绍如下:

Many Spring Boot developers like their apps to use auto-configuration, component scan and be able to define extra configuration on their "application class". A single @SpringBootApplication annotation can be used to enable those three features

这段话的意思在说:使用@SpringBootApplication 注解即可以启用三个功能,即:"自动装配"、"组件扫描 "和 "启用额外的配置",这些功能的开启便是通过上述几个核心注解来实现的。

下面我们将对这几个核心注解逐一展开进行分析。

@SpringBootConfiguration 注解

@Configuration 注解允许在上下文中注册额外的Bean 或导入额外的配置类。 在被这个注解标注的配置类中,我们可以定义一个或多个用@Bean 注解标记的方法。SpringBoot 会将这些方法的返回值纳入到IOC 容器中进行统一管理,并且bean 的名称就是方法名。比如:

@Configuration
public class DemoConfig {
   @Bean
   public UserService userService() {
     return new UserServiceImpl();
   }
}

@SpringBootConfiguration 注解是在Spring Boot 中对Spring 标准@Configuration 注解的替代方案,这个注解本质上就是@Configuration 注解,从上述注解关系图中我们可以看出,@Configuration 其实又是@Component 注解,这意味着被这个注解标记的类其实就是一个Spring 配置类(configurationClass)。

@ComponentScan 注解解析

配置此注解可以执行组件扫描,如果useDefaultFilters 属性为true,那么他会将被@Component 注解标记以及以@Component 为元注解(@Component@Service@Repository 等)的注解标记类都扫描到IOC 容器中。

下面是@ComponentScan 注解源码中的部分注释内容:

/**
* <p>Either {@link #basePackageClasses} or {@link #basePackages} (or its alias
* {@link #value}) may be specified to define specific packages to scan. If specific
* packages are not defined, scanning will occur from the package of the
* class that declares this annotation.
*/

意思是:我们可以通过指定basePackageClassesbasePackages 或者其别名value 值来定义扫描特定的包。如果没有指定包,那么扫描将会从声明此注解的类开始。这就是为什么在Spring Boot 项目中,启动类通常要放在父目录的原因。

@EnableAutoConfiguration 注解解析

这个注解的作用是启用Spring Boot 的自动配置机制。在Spring 中,几乎所有以@Enable 开头的注解,都是通过@Import 注解将特定的类导入到容器中去的,@Import 注解可以导入的类型有 3 种:

  • 导入普通的类(普通的JavaBean),beanName 为类的全限定名

  • 导入ImportSelector 接口的实现

  • 导入ImportBeanDefinitionRegistrar 接口的实现

@EnableAutoConfiguration 注解导入的类是AutoConfigurationImportSelector,此类是ImportSelector 接口的实现,所以导入容器中的Bean 应该是这个接口方法的返回值——所有要放入容器的Bean 的全限定类名的字符串数组。

那么这个全限定类名字符串数组是从哪里来的呢?源码面前,了无秘密。

源码分析

AutoConfigurationImportSelector 类中对selectImports() 方法的实现主要关注getAutoConfigurationEntry() 方法,该方法的主要作用为加载自动配置的类。源码如下(源码出自Spring Boot 2.5.15):

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   // 首先判断自动装配是否开启
   if (!isEnabled(annotationMetadata)) {
       return EMPTY_ENTRY;
   }
   // 从 AnnotationMetadata 返回适当的 AnnotationAttributes,这里获取的是 EnableAutoConfiguration 注解中的 exclude 和 excludeName
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   // 通过 SpringFactoriesLoader 加载类路径中 META-INF/spring.factories 文件中需要自动装配的类
   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);
   // 发布自动装配导入事件,事件的监听器也配置在 spring.factories 文件中,通过 AutoConfigurationImportListener 指定
   fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

protected boolean isEnabled(AnnotationMetadata metadata) {
   if (getClass() == AutoConfigurationImportSelector.class) {
       // 获取 spring.boot.enableautoconfiguration 属性配置,该属性可通过配置文件进行手动配置,默认值为 true
       return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
   }
   return true;
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
   // 加载 META-INF/spring.factories 文件中需要自动装配的类
   List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
           getBeanClassLoader());
   Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
           + "are using a custom packaging, make sure that file is correct.");
   return configurations;
}

到这里我们可以看出,获取需要自动装配的所有的配置类,是通过读取META-INF/spring.factories 文件来进行的,但是当Spring Boot 启动时,它不单单是加载当前应用的spring.factories 文件,而是加载classpath 下所有的(包括内置的和外部用户定义的)META-INF/spring.factories 文件,那么这么多这种文件都是每次启动就全部加载的吗?

按需加载

其实如果大家细心阅读了上述源码的话,心中可能已经有答案了。当执行到configurations = getConfigurationClassFilter().filter(configurations); 这行代码的时候,其实就是在按照自动配置类中的条件注解进行筛选过滤,将符合条件注解的类筛选出来。所谓按需加载,意思就是需要自动装配的就加载,不需要的不加载。比如下边这个示例:

@Bean
@ConditionalOnProperty(name = "esign.enabled", havingValue = "true")
public EsignConfig eSignConfig() {
   return new EsignConfig();
}

只有当满足esign.enabled = true 这个属性时,该Bean 才会被创建。

下边列举一些Spring Boot 中提供的条件注解及作用,感兴趣的朋友可以详细了解下:

  • @ConditionalOnClass‌:当类路径上存在指定的类时,创建Bean

  • @ConditionalOnMissingClass‌:当类路径上不存在指定的类时,创建Bean‌

  • @ConditionalOnResource‌:根据类路径中是否存在特定资源文件来决定是否创建一个Bean 或者配置一个类

  • @ConditionalOnProperty‌:根据配置文件中是否有指定属性配置来控制Bean 的创建 ‌

  • @ConditionalOnWebApplication‌:基于应用是否是一个Web 应用来控制配置或Bean 的创建 ‌

  • @ConditionalOnNotWebApplication‌:基于应用是否不是一个Web 应用来控制配置或Bean 的创建 ‌

  • @ConditionalOnExpression‌:根据指定SpEL 表达式的计算结果来判断条件是否匹配,控制Bean 的创建 ‌

  • @ConditionalOnBean:当容器里有指定Bean 的条件下

  • @ConditionalOnMissingBean:当容器里没有指定Bean 的条件下

通过这些注解,Spring Boot 可以根据应用的具体情况(如类路径中的依赖、配置文件中的属性等)来决定是否应用某个自动配置类。

场景分析(什么时候需要自定义 starter)

在企业级应用中,经常会有一些通用的功能模块,如安全认证、日志记录、缓存管理或者其它一些跟业务相关的模块等等。这些模块可以被多个项目复用,通过自定义Starter 可以方便地在不同项目中集成这些功能。

实际应用场景举例:比如公司中有线上的招投标业务以及物流运输业务,在这些业务中都需要进行电子合同的签署,这时候可能需要对接三方的合同服务,比如法大大、CFCA 等。因为在不同的业务中都要使用电子签章的功能,所以需要开发一个通用的调用签署功能的模块,这时候我们就可以将调用外部签署服务的功能封装成一个Starter 来简化代码的集成,提高开发效率。

结语

好啦!想必讲到这里,大家对Spring Boot 的自动装配机制已经有了一个全面而深入的理解。大家可以自己小试牛刀了哦~,赶快自定义Starter 实战起来吧!

感谢大家的阅读,如果有任何疑问或建议,欢迎在评论区留言交流。



【Java驿站】持续给大家更新


扫描下方二维码


关注【Java驿站】公众号


👇👇👇


Java驿站
这里是【Java驿站】,一个Java编程学习与交流平台。
 最新文章