SpringBoot 自动装配原理
1. 什么是 SpringBoot 自动装配
SpringBoot 定义了一套接口规范,这套规范规定:SpringBoot 在启动时会扫描外部引用 jar 包中的 META-INF/spring.factories
文件,将文件中配置的类型信息加载到 Spring 容器并执行类中定义的各种操作。
对于外部 jar 包来说,只需要按照 SpringBoot 定义的标准,就能将自己的功能装置进 SpringBoot.
自动装配可以简单理解为:通过注解或者一些简单的配置就能在 SpringBoot 的帮助下实现某块功能。
2. SpringBoot 如何实现自动装配
自动装配避免了手写 xml 文件带来的繁琐可以轻松创建并管理 bean,简化了开发过程,它涉及以下几个关键步骤:
- 基于 Java 代码的 Bean 配置
- 自动配置条件依赖
- Bean 参数获取
- Bean 的发现
- Bean 的加载
2.1 自动配置
环境准备,需要引入 mybatis 的 maven 坐标:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
以 mybatis-spring-boot-starter
这个 jar 包为例,该 jar 包下面还包括其他关于 mybatis 的 jar 包,如下图所示:
其中,有一个与 mybatis 自动配置相关的称为 mybatis-spring-boot-autoconfigure
的子 jar 包 ,该 jar 包下找到 MybatisAutoConfiguration
自动配置类,如下图所示:
查看 MybatisAutoConfiguration
自动配置类的源码:
- 该类上面有
@Configuration
注解修饰,被该注解修饰的类可以看作是一个能够生产出让 Spring IOC 容器管理的 Bean 实例的工厂,也就是说MybatisAutoConfiguration
是一个生产 Bean 实例的工厂类 - 类中的
sqlSessionFactory
和sqlSessionTemplate
两个方法被@Bean
修饰,表示 bean 方法,这两个方法返回的对象可以注册到 Spring 容器中 @Configuration
和@Bean
这两个注解一起使用就可以创建一个基于 Java 代码的配置类,可以用来替代传统的 xml 配置文件MybatisAutoConfiguration
自动配置类帮助我们实现了以前在mybatis.xml
文件中繁琐的配置工作,包括日志、类型处理器、语言驱动、资源加载器以及个性化设置等等。
2.2 自动配置条件依赖
还是以 mybatis-spring-boot-starter
包下的 MybatisAutoConfiguration
自动配置类为例。
从该类上面的 @ConditionalOnClass
和 ConditionalOnSingleCandidate
可以发现,要完成 mybatis 的自动配置还需要依赖条件,那就是在类的路径中必须存在 SqlSessionFactory
和 SqlSessionFactoryBean
这两个类,以及存在 DataSource
类作为 bean.
下表是关于 springboot 提供的条件依赖的注解描述:
注解 | 描述 |
---|---|
@ConditionalOnBean | 仅在当前上下文中存在某个 bean 时,才会实例化这个 bean |
@ConditionalOnClass | 某个 class 位于类路径上,才会实例化这个 bean |
@ConditionalOnExpression | 当表达式为 true 的时候,才会实例化这个 bean |
@ConditionalOnMissingBean | 仅在当前上下文中不存在某个 bean 时,才会实例化这个 bean |
@ConditionalOnMissingClass | 某个 class 在类路径上不存在的时候,才会实例化这个 bean |
@ConditionalOnNotWebApplication | 不是 web 应用时才会实例化这个 bean |
@AutoConfigureAfter | 在某个 bean 完成自动配置后实例化这个 bean |
@AutoConfigureBefore | 在某个 bean 完成自动配置前实例化这个 bean |
@ConditionalOnBean | 仅在当前上下文中存在某个 bean 时,才会实例化这个 bean |
2.3 Bean 参数获取
要完成 mybatis 的自动配置,还需要我们在配置文件中提供数据源相关的配置参数。例如,数据库驱动、连接 url、数据库用户名、密码等。那么,springboot 就是通过读取 yml 或者 properites 配置文件的的参数来创建数据源对象的。
在 spring-boot-autoconfigure
子包下有一个自动配置类叫作 DataSourceAutoConfiguration
,该类实现了自动配置数据源相关的参数的功能。
可以看到在该类上面加了 @EnableConfigurationProperties
注解,这个注解的作用就是使 @ConfigurationProperties
生效。
继续查看 @EnableConfigurationProperties
注解括号里面类 DataSourceProperties
的源码,可以看到该类被 @ConfigurationProperties
修饰,这个注解的作用是把 yml 或者 properties 配置文件中的配置参数信息封装到 DataSourceProperties
类的相应属性上,源码截图如下所示:
2.4 Bean 的发现
对于启动类所在的包下的主类与子类的所有组件 springboot 默认是可以扫描的,但是不包括依赖包中的类,那么依赖包中的 bean 是如何被发现的?
首先看一下 SpringBoot 的核心注解 @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 {
}
@SpringBootApplication
其实可以看作是 @Configuration、@EnableAutoConfiguration、@ComponentScan
三个注解的集合。根据 SpringBoot 官网,这三个注解的作用分别是:
- EnableAutoConfiguration:启用 SpringBoot 的自动配置机制
- Configuration:允许在上下文中注册额外的 bean 或导入其他配置类
- ComponentScan: 扫描被
@Component (@Service,@Controller)
注解的 bean,注解默认会扫描启动类所在的包下所有的类 ,可以自定义不扫描某些 bean. 如下图所示,容器中将排除 TypeExcludeFilter 和 AutoConfigurationExcludeFilter
其中,@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 {};
}
从上面的源码中可以看到,只是一个接口,并没有实现什么功能,自动装配核心功能其实是通 AutoConfigurationImportSelector
类实现的(@Import
作用是导入需要自动配置的组件)。
查看 AutoConfigurationImportSelector
类的源码,部分源码截图如下所示:
从上图中可以看到,在该类的 getCandidateConfigurations
方法中调用了 SpringFactoriesLoader
类的 loadFactoryNames
方法,继续跟踪源码:
SpringFactoriesLoader
类的 loadFactoryNames
静态方法可以从所有的 jar 包中读取 META-INF/spring.factories
文件,而自动配置的类就在这个文件中进行配置,spring.factories
文件的内容如下所示:
那么,springboot 通过读取文件的内容,便可以发现 bean 了。
2.5 Bean 的加载
在发现依赖包中的 bean 之后,SpringBoot 便可以进行将这些 bean 加载到 Spring 容器进行管理了。
在 SpringBoot 应用中要让一个普通类交给 Spring 容器管理,通常有以下方法:
- 使用
@Configuration
与@Bean
注解 - 使用
@Controller
、@Service
、@Repository
、@Component
注解标注该类并且启用@ComponentScan
自动扫描 - 使用
@Import
注解
其中,SpringBoot 实现自动配置使用的是 @Import
注解这种方式。
AutoConfigurationImportSelector
类的 selectImports
方法返回一组从 META-INF/spring.factories
文件中读取的 bean
的全类名,这样 SprinBoot 就可以加载这些 bean 并完成实例的创建工作。
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//是否开启自动装配
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
//获取所有需要装配的bean
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
getAutoConfigurationEntry()
方法主要负责加载自动配置类,查看它的源码:
private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();
AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
//<1>.
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//<2>.
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//<3>.
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//<4>.
configurations = this.removeDuplicates(configurations);
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
this.checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = this.filter(configurations, autoConfigurationMetadata);
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}
代码解释:
- 判断自动装配开关是否打开。默认 spring.boot.enableautoconfiguration=true,可在 application.properties 或 application.yml 中设置
- 用于获取 EnableAutoConfiguration 注解中的 exclude 和 excludeName
- 获取需要自动装配的所有配置类,这不就是之前在 2.4 小节中提到的 bean 发现过程中提到的方法嘛!
- 加载
spring.factories
中的配置,但不是每次启动都会加载其中的所有配置,会有一个筛选的过程,剔除重复的
至此,以上就是 bean 的大致的加载过程。
3. 自定义 starter
那么在理解了 SpringBoot 的自动装配原理之后,可以遵循 SpringBoot 的接口规范自定义一个 starter 来加强对自动装配原理的印象。
自定义的 starter 工程的目录结构如下:
首先,创建一个 Project 命名为 hello-spring-boot-starter
,pom.xml 文件内容如下所示:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hzz</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--引入自动装配包 autoconfigure-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
</dependencies>
</project>
之后,创建属性类 HelloProperties.java
,该类在 com.hzz.config
包下。
package com.hzz.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 配置属性类,用于封装配置文件中配置的参数信息
*/
@ConfigurationProperties(prefix = "hello")
public class HelloProperties {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "HelloProperties{" +
"name='" + name + '\'' +
", address='" + address + '\'' +
'}';
}
}
创建服务类 HelloService.java
,该类在 com.hzz.service
包下。
package com.hzz.service;
public class HelloService {
private String name;
private String address;
public HelloService(String name, String address) {
this.name = name;
this.address = address;
}
public String sayHello() {
return "你好!我的名字叫"+name+",我来自"+address;
}
}
创建自动配置类 HelloServiceAutoConfiguration
,该类在 com.hzz.config
包下。
package com.hzz.config;
import com.hzz.service.HelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 自动配置类,用于自动配置HelloService对象
*/
@Configuration
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
private HelloProperties helloProperties;
//通过构造方法注入配置属性对象 HelloProperties,SpringBoot会帮我们自动注入,如果红线警告可以忽略
public HelloServiceAutoConfiguration(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
//实例化 HelloService 并载入 Spring IOC 容器
@ConditionalOnMissingBean
@Bean
public HelloService helloService() {
return new HelloService(helloProperties.getName(),helloProperties.getAddress());
}
}
然后,在 resources
目录下创建 META-INF/spring.factories
文件,文件内容如下
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hzz.config.HelloServiceAutoConfiguration
最后,使用 maven 工具将工程安装到本地的 maven 仓库中,供其他工程使用。
注意:为了避免 maven 仓库缓存的影响,避免其他工程发现不到自定义的 starter,建议将本地的 maven 仓库更新一下,如下图所示:
4. 使用自定义 starter
那么在自定义完成一个 starter 之后,就要去在另一个工程中去使用它了,还是新建一个新的工程。
工程的目录结构如下所示:
首选,工程命名为 myapp
,pom.xml 文件内容如下所示:
<?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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.hzz</groupId>
<artifactId>myapp</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.2.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!--引入自定义的 starter-->
<dependency>
<groupId>com.hzz</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
</project>
之后,创建 application.yml
文件
server:
port: 8080
hello: # HelloProperties 类头上配置的prefix
name: 华仔仔coding
address: 中国
然后,创建 HelloController.java,该文件在 com.hzz.controller
包下。
package com.hzz.controller;
import com.hzz.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/hello")
public class HelloController {
@Autowired
private HelloService helloService; //当前实例已由自定义的starter完成了创建
@GetMapping("/say")
public String sayHello() {
return helloService.sayHello();
}
}
创建主启动类 HelloApplication.java
,该文件在 com.hzz
包下。
package com.hzz;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HelloApplication {
public static void main(String[] args) {
SpringApplication.run(HelloApplication.class, args);
}
}
最后,运行主启动类,访问 /hello/say
接口,地址为 http://localhost:8080/hello/say
出现上图所示的运行效果表示成功访问!以上便是使用自定义 starter 的过程。