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 的过程。