SpringBoot自动装配原理解析

什么是Spring Boot自动装配

Spring Boot自动装配是指在Spring Boot应用启动时,根据类路径下的jar包依赖、Bean定义、各种配置文件等信息,自动配置Spring应用上下文的Bean。

这种机制极大地简化了配置工作,使得开发者可以更加专注于业务逻辑的实现。

在深入自动装配原理前,我们先看下 SPI 机制

SPI 机制

SPI(Service Provider Interface)是一种动态替换服务提供者的机制。它允许一个服务接口有多个服务提供者,并且在程序运行时动态选择一个服务提供者。

SPI又分为 JDK SPISpring SPI

JDK SPI

在 Java 平台上,SPI 通常是通过 java.util.ServiceLoader 类实现的,这种机制在Java标准库中广泛应用,如JDBC驱动的管理。

SPI可以很灵活的让接口和实现分离,让服务提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展。

工作原理

Java SPI 的工作原理基于以下几个步骤:

  • 定义服务接口:首先定义一个服务接口(或抽象类),作为服务的规范。
  • 提供服务实现:编写接口的具体实现类。
  • 注册服务实现:在 META-INF/services 目录下创建一个以接口全限定名为名的文件,文件内容为接口实现类的全限定名。
  • 加载服务实现:使用 java.util.ServiceLoader 来加载并使用这些实现。

举例说明一下:

(1)创建一个 DemoDAO 的接口

java 复制代码
public interface DemoDao {}

(2)分别创建两个实现类

java 复制代码
public class MysqlDao implements DemoDao {}
cpp 复制代码
public class OracleDao implements DemoDao {}

(3)在resources下新建META-INF/services/目录,在该目录下新建接口全限定名的文件com.study.spring.z_spi.DemoDao,文件内容是上面那两个实现类。

properties 复制代码
com.study.spring.z_spi.MysqlDao
com.study.spring.z_spi.OracleDao

(4)使用 JDK 提供的ServiceLoader来测试下

java 复制代码
public class JdkSpiApplication {
    public static void main(String[] args) {
        ServiceLoader<DemoDao> demoDaos = ServiceLoader.load(DemoDao.class);
        demoDaos.iterator().forEachRemaining(t -> {
            System.out.println(t);
        });
    }
}

输出结果 :

com.study.spring.z_spi.MysqlDao@6e8cf4c6

com.study.spring.z_spi.OracleDao@12edcd21

JDBC DriverManager

Java SPI 机制在JDBC驱动管理中的应用主要体现在JDBC 4.0及以上版本的驱动自动发现和加载上。

在JDBC4.0之前,连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")先加载数据库相关的驱动,然后再进行获取连接等的操作。

而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,这种方式就是使用了Java的SPI扩展机制来实现。

定义服务接口

在JDBC中,服务接口是由Java平台定义的java.sql.Driver接口,所有的JDBC驱动都必须实现这个接口,以提供与数据库建立连接的能力。

提供服务实现

数据库厂商(如MySQL、Oracle等)为它们的数据库提供JDBC驱动程序,这些驱动程序实现了java.sql.Driver接口。

注册服务提供者

JDBC驱动的注册是通过在驱动程序的JAR包中的META-INF/services目录下创建一个名为java.sql.Driver的文件来完成的。这个文件包含了实现java.sql.Driver接口的驱动类的全限定名。当JVM启动时,它会查找这个文件,并加载其中指定的驱动类。

加载服务提供者

在JDBC 4.0及以上版本中,DriverManager类在初始化时会使用Java的SPI机制自动加载所有在META-INF/services/java.sql.Driver文件中指定的驱动类。

这是通过ServiceLoader类实现的,它会查找并加载所有可用的JDBC驱动实现。

连接管理

当应用程序尝试通过DriverManager.getConnection()方法连接数据库时,DriverManager会遍历所有已加载的驱动实例,尝试建立连接。一旦某个驱动成功建立连接,它就会返回这个连接,并且不会继续尝试其他的驱动实例。

SpringBoot SPI 机制

Spring Boot 对 SPI 机制进行了扩展,以支持其自动配置和模块化架构。

Spring Boot 利用 spring.factories (从 SpringBoot 2.7 起自动配置不推荐使用 /META-INF/spring.factories 文件,而是在/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)文件,这个文件列出了与自动配置相关的接口及其实现类,Spring Boot 启动时会加载这些配置。

spring.factories

这个文件里面使用键值对的格式列出了多种服务类型及其对应的实现类,常见的服务类型包括:

  • org.springframework.boot.autoconfigure.EnableAutoConfiguration:用于自动配置。

  • org.springframework.context.ApplicationListener:用于应用事件监听器。

  • org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider:用于模板引擎的可用性判断。

java 复制代码
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration

不使用SPI

现在我们先来看下,不使用 SPI 机制,怎么实现 bean 的配置。

新建一个项目,将新类MyAppDemo注入 IOC 容器。

在另一个项目 demo 中,引入 myApp 的依赖,并测试调用 test 方法。

java 复制代码
@SpringBootApplication
public class StarterDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}

@SpringBootTest
class StarterDemoApplicationTests {

	@Autowired
	private ApplicationContext applicationContext;

	@Test
	void contextLoads() {
		MyAppDemo demo = applicationContext.getBean(MyAppDemo.class);
		demo.test();
	}
}

测试发现找不到该 bean 对象

为什么引入的第三方依赖包中的 bean 没有生效呢?

  • 原因是因为,在类上添加@Component注解来声明bean对象时,还需要保证@Component注解能被Spring的组件扫描到。
  • SpringBoot项目中的@SpringBootApplication注解,具有包扫描的作用,但是它只会扫描启动类所在的当前包以及子包。
  • 当前包:com.starter.demo, 第三方依赖中提供的包:com.myapp.demo(扫描不到)

所以,有两种方案可以解决

  • @ComponentScan 组件扫描第三方依赖的包路径;
  • @Import 导入(使用@Import导入的类会被Spring加载到IOC容器中)。
@ComponentScan 组件扫描
java 复制代码
@SpringBootApplication
@ComponentScan(basePackages = {"com.starter","com.myapp"})
public class StarterDemoApplication {

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

缺点:当需要引入大量的第三方依赖,就需要在启动类上配置大量要扫描的包,这种方式会很繁琐。

Import 导入
java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration @Configuration}, {@link ImportSelector},
	 * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import.
	 */
	Class<?>[] value();
}

从源码可以看到,导入形式有以下几种

  • 普通类
  • 配置类
  • ImportSelector的实现类
  • ImportBeanDefinitionRegistrar的实现类
导入普通类
java 复制代码
@SpringBootApplication
@Import(MyAppDemo.class)
public class StarterDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}
导入配置类

去掉MyAppDemo类上的@Component注解,新建一个MyAppConfig配置类。

java 复制代码
@Configuration
public class MyAppConfig {
    @Bean
    public MyAppDemo myAppDemo() {
        return new MyAppDemo();
    }
}

在启动类上导入配置类

java 复制代码
@SpringBootApplication
@Import(MyAppConfig.class)
public class StarterDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}
导入ImportSelector的实现类

新建 ImportSelector的实现类

java 复制代码
public class MyAppImportSelector implements ImportSelector {

    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {"com.myapp.demo.MyAppDemo"};
    }
}

在启动类上导入MyAppImportSelector

java 复制代码
@SpringBootApplication
@Import(MyAppImportSelector.class)
public class StarterDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}
导入ImportBeanDefinitionRegistrar的实现类

新建 ImportBeanDefinitionRegistrar 的实现类

java 复制代码
public class MyAppImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry, BeanNameGenerator importBeanNameGenerator) {

        BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(MyAppDemo.class);


        registry.registerBeanDefinition("myAppDemoTest", builder.getBeanDefinition());
    }
}

在启动类上导入MyAppImportBeanDefinitionRegistrar

java 复制代码
@SpringBootApplication
@Import(MyAppImportBeanDefinitionRegistrar.class)
public class StarterDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}
模块装配

通过@Import 注解,我们可以导入第三方依赖包中的配置类。

但是基于上面的方式,我们在引入第三方依赖时,还要知道第三方依赖中有哪些配置类和哪些Bean对象?相当麻烦!

而第三方依赖自己最清楚自己有哪些配置类、有那些 Bean 对象,它提供一个注解,通过这个注解,外部系统可以引入自己所需要的 Bean 对象。

这个注解一般都以@EnableXxx开头,注解中封装的就是@Import注解,外部系统在使用时只需要加上@EnableXxxxx注解即可。

java 复制代码
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import(MyAppConfig.class)
public @interface EnableMyApp {}
java 复制代码
@SpringBootApplication
@EnableMyApp
public class StarterDemoApplication {
	public static void main(String[] args) {
		SpringApplication.run(StarterDemoApplication.class, args);
	}
}

spring 中的模块装配,是在3.1 之后引入了大量的@EnableXXX 注解,来快速整合激活相对应的模块。

如:

● EnableTransactionManagement :开启注解事务驱动

● EnableWebMvc :激活 SpringWebMvc

● EnableAspectJAutoProxy :开启注解 AOP 编程

● EnableScheduling :开启调度功能(定时任务)

模块装配的核心原则:自定义注解+@Import 导入组件

使用SPI

即使是采用@EnableXXX注解,还是觉得麻烦怎么办?

我想引入第三方依赖后,直接就去使用它,而不是再单独写一个什么什么注解。

下面我们来看看基于 Spring 的 SPI 机制怎么去实现。

在在resources目录下创建META-INF 目录,并新建 spring.factories文件,文件内容如下:

java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.myapp.demo.MyAppConfig
java 复制代码
@Configuration
public class MyAppConfig {

    @Bean
    public MyAppDemo myAppDemo() {
        return new MyAppDemo();
    }
}

我们只是引入了第三方依赖包,并没有手动配置,也没有写什么注解啊,就可以通过IOC容器或DI依赖拿到bean对象了,这就是SpringBoot自动配置的强大之处。

自动装配源码

从 SpringBoot 核心注解说起

java 复制代码
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}

重点看@EnableAutoConfiguration,核心中的核心,重点中的重点。

点进去 AutoConfigurationImportSelector

可以看到 AutoConfigurationImportSelector 实现了 DeferredImportSelector,而DeferredImportSelector 又继承了ImportSelector

AutoConfigurationImportSelector类中重写了ImportSelector接口的selectImports()方法:

再点进去

可以看到这个getCandidateConfigurations()方法,就是去获取META-INF/spring.factories文件中配置类的集合。

再接着点点点

相关推荐
豆角焖肉3 小时前
Maven进阶与搭建私服
java·maven
大不点wow3 小时前
Java序列化与反序列化:让对象走出JVM
java·开发语言·jvm
止语Lab3 小时前
好的 DX 不等于少写代码——三种语言的摩擦力设计课
后端
吃饱了得干活3 小时前
别再手动解析 LLM 输出了!LangChain 四种结构化输出方案对比
后端·python·langchain
噢,我明白了3 小时前
Java中日期和字符串的处理
java·开发语言·日期
程序员天天困3 小时前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端
Huiturn3 小时前
GPT 5.6 连续编码 10 小时,纯 Python 啃下 Word 二进制格式——doc2docx 实现拆解
后端
dkbnull4 小时前
Spring Boot请求处理组件对比详解
java·spring boot
jun_bai4 小时前
Orthanc服务器使用java上传dicom影像文件
java·运维·服务器
顺风尿一寸4 小时前
记一次 Spring AOP 与定时任务引发的死锁排查
java