作者
樊远航
推荐理由
spring boot核心能力之自动装配的原理分析,讲解了自己实现自动装配的两种方式,并提供流程图让读者更好理解,排版规整。
前言
SpringBoot作为敏捷开发的常用封装框架,对于企业开发速度的提升、配置的简化、业务的专注三个方面进行了相对于SpringMVC等架构的升级。
那么,作为使用过SpringMVC的开发同学来说,初次使用SpringBoot时会感觉原本繁杂的xml配置工作一下子没有了。项目创建完成后,基本可以直接开始进行业务代码的编写(引入依赖除外)。那么,原本繁杂的配置工作是怎么消失掉的呢?这就不得不说下SpringBoot的核心特性之一的自动装配。
约定大于配置
SpringBoot能够顺利支持自动装配的原因,主要就是所有基于SpringBoot的开发者都遵循“约定大于配置”这一规则。
在SpringBoot中约定大于配置的具体体现在:
1、SpringBoot项目的目录结构下的resource目录统一用于存放配置文件
2、默认打包方式为jar
3、spring-boot-stater-web中包含springMVC相关的依赖以及内置的Tomcat容器,使得构建一个web应用更加方便
4、默认提供application.properties/yml文件用于项目配置
5、通过key(spring.profiles.active)属性来决定运行环境的分配和选择
6、EnableAutoConfiguration默认对于依赖的stater进行加载
自动装配
定义
什么是自动装配?回想我们使用SpringMVC时,我们需要在xml文件中写入很多以标签包装的信息,将这些Bean交予Spring 的IOC容器进行管理。而自动装配顾名思义,由框架本身帮我们自动去生成这些Bean并交予IOC容器管理,省去我们的配置工作。
前置准备
那么,SpringBoot中是如何做到自动装配,其原理是什么?下面,我将分享我的理解和心得。
IDEA中SpringBoot的创建
区别于传统通过maven创建项目的方式,我们按下面的流程进行SpringBoot的项目创建:
设置我们的项目坐标:
这里,我们可以选择一些配置:
之后一路next,我们就可以创建一个SpringBoot的项目了。创建好的项目结构如下:
java目录:我们的代码存放位置
resource目录:我们项目的配置文件的存放位置
test目录:测试代码的存放位置
target目录:项目的class文件的存放位置
看到这里,我们就能明白了为什么SpringBoot的核心是“约定大于配置”。只有统一了项目的结构规范,那么才能实现后续自动配置的相关操作(扫描注定结构路劲下的指定文件)。而传统的MVC项目中,我们可以随意的建立项目的目录结构,这就导致项目结构无法统一。所以老话说的好:”无规矩,不成方圆“。
SpringBootApplication注解
项目创建好后,会帮我们创建一个启动类:
/**
* @author fanyuanhang
*/
@SpringBootApplication
public class SpringBootFirstApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootFirstApplication.class, args);
}
}
此时,我们不需要任何配置,就可以直接启动我们的SpringBoot项目。那么, 相比较于传统MVC项目的繁琐配置,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 {
其中,核心注解为以下三个:
1、Configuration(SpringBootConfiguration)
2、EnableAutoConfiguration
3、ComponentScan
下面,我们由简到难来分别说明下这三个注解的作用是什么。
Configuration注解
这个注解熟悉JavaConfig的同学应该不陌生,其本质就是Spring容器IOC的一种基于Bean的配置注解(相当于MVC中的beans.xml)。通过这个注解,我们可以将被标记的类视为IOC容器的配置类,类中的每一个通过@Bean注解声明的对象全部交予IOC容器进行管理。
举例:
/**
* 配置类
* @author fanyuanhang
* */
@Configuration
public class SpringConfig {
// Bean名称默认以方法名,可以通过name属性定义
@Bean
// @Scope IOC容器管理Bean,默认单例,可通过@Scop注解修改。
public DefaultBean defaultBean(){
return new DefaultBean();
}
}
通过上面的方式,我们将传统基于xml的配置方式替换成为了JavaConfig形式的配置类来向IOC容器中添加Bean对象。Bean的名字默认以方法名为主,同样可以在@Bean注解中通过name属性来声明当前Bean对象的名称。
所以,不难理解,在Spring项目启动时,通过Configuration注解将配置类中的Bean初始化创建好后统一交予IOC容器进行管理。
ComponentScan注解
这个注解大家应该不陌生,主要是进行包扫描的注解。将当前声明路径下的包中所有符合加载条件的Bean或者组件注册进IOC容器中。
在SpringBoot中主要是如下注解:
@Component、 @Repository、@Service、@Controller
有以上注解声明的类或者对象,都会被交予IOC进行统一管理。
举例:
创建一个controller类,放到java根目录下:
@RestController
public class HelloController {
@GetMapping("/hello")
public String sayHello(){
return "Hello Mic";
}
}
修改启动类:
public class TestMain {
public static void main(String[] args) {
AnnotationConfigApplicationContext applicationContext = new
AnnotationConfigApplicationContext(SpringConfig.class);
String[] defNames = applicationContext.getBeanDefinitionNames();
for (int i = 0; i < defNames.length; i++) {
System.out.println(defNames[i]);
}
}
}
执行后,我们可以看到此时IOC容器中已经有了helloController这个Bean对象:
Import注解
在Spring中,从3.1版本开始提供了一系列Enable开头的注解:@EnableWebMVC、@EnableScheduling…
我们进入这些以Enable开头的注解中,都可以看到另外一个注解的存在:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc
从上面的源码中,我们可以看到:每一个Enable开头的注解内部都有@Import注解的存在。
对比xml式的配置方式,我们可以知道这个注解的作用:容器的合并。
举例:
/**
* 另一Bean
*
* @author fanyuanhang
*/
public class OtherBean {
}
@Configuration
public class OtherConfig {
@Bean
public OtherBean otherBean() {
return new OtherBean();
}
}
/**
* 配置类
*
* @author fanyuanhang
*/
@Import(OtherConfig.class)
@Configuration
public class SpringConfig {
@Bean
@Scope
public DefaultBean defaultBean() {
return new DefaultBean();
}
}
我们在开始的SpringConfig配置类中通过@Import注解导入OtherConfig配置类,此时我们可以看到,IOC容器中同时存在了这两个Bean对象:
defaultBean
otherBean
动态加载Bean
不知道大家有没有思考这么一个问题:我上面的代码示例中都是在配置类中声明了一个Bean对象之后,再进行的其他操作。
可以,像Spring中不可能每一个Bean都在IOC容器中进行加载。比如,数据库驱动的Bean。数据库有很多种,我们不知道到底使用的是哪一个数据库。那么,此时将所有的数据库驱动都加载到IOC容器中无疑是一种浪费系统资源的操作。那么,Spring中是如何进行Bean对象的动态加载的呢?
其实,Spring给我们提供了关于Bean的三种配置方式:
1、通过@Configuration注解标识的配置类
2、ImportSelector接口的实现
3、AutoConfigurationPackages.Registrar接口的实现
后面,我会逐一说明用法以及在自动装配中的应用。
SPI机制
我先提出一个问题大家可以思考下。当我们有很多很多的Bean对象需要被放入IOC容器中管理,并且这些Bean对象存在替换策略(比如数据库驱动)。那么,我们除了在xml或者JavaConfig中进行配置外没有别的方法可以进行快速、可扩展、可替换的方式了吗?答案是肯定的。
SPI全称(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。(图来自网上)
Java SPI实际上是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
SpringFactoriesLoader工具类
那么,我们在了解了关于动态加载的实现方式以及理论基础(SPI)。下面我来介绍下在SpringBoot的自动装配中是如何做到Bean的动态加载的。
这里,我先说明下SpringFactoriesLoader这个工具类。它是SPI机制中为我们从配置类中转换出我们需要的类的一个工具类。这里,我简单说明下它的作用和原理。
SpringFactoriesLoader的主要作用就是依据SPI规则从META-INF/spring.factories中的文件中加载对应的Bean实例类。其中,在spring.factories中主要分为以下的key:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
...
# Application Listeners
org.springframework.context.ApplicationListener=\
...
# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
...
# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
...
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
...
# Failure analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
...
# Template availability providers
org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider=\
通过key进行分类,value为对应配置的需要初始化的Bean的实例的全路径。
而SpringFactoriesLoader为我们提供了loadFactories、loadFactoryNames三个静态方式来从spring.factories中通过反射机制获取相对应的Bean实例。
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
举例:
我通过EnableAutoConfiguration来获取自动装配时需要的Bean
public static void main(String[] args) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,
new ClassLoader() {
});
configurations.forEach(configuration -> {
System.out.println(configuration);
});
}
结果:
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration
...
Process finished with exit code 0
总结:
其实,这就是一种工厂+策略模式的结合体现。我们通过传入对应的策略参数后,由工厂类统一返回我们需要的数据。所以是SPI思想的最佳实践。
原理探究
至此,在说明自动装配原理的前置知识已经补充差不多了,我们下面正式开始探究SpringBoot自动装配原理的路程。
AutoConfigurationImportSelector类
进入EnableAutoConfiguration注解中,我们会发现这里的@Import注解不走寻常路:
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
这里导入了AutoConfigurationImportSelector这个类。结合上面说的,Spring为我们提供的三种Bean的配置方式中其中之一就有实现ImportSelector接口。那么,我们一起研究下这个类。
这个类实现自ImportSelector,主要方法就是selectImports方法。
public interface ImportSelector {
/**
* Select and return the names of which class(es) should be imported based on
* the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
*/
String[] selectImports(AnnotationMetadata importingClassMetadata);
}
这个方法通过@Import注解触发,然后我们可以在里面做一些逻辑处理,去动态生成我们需要的Bean对象。而AutoConfigurationImportSelector就是一个最佳的例子。
AutoConfigurationImportSelector中,提供的selectImports方法主要为我们返回IOC管理的自动装配时需要初始化Bean的名称。
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
其中,getAutoConfigurationEntry方法主要是获取的方法:
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 1、读取spring.factory文件加载Bean
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 2、去重
configurations = removeDuplicates(configurations);
// 3、获取过滤的Bean -- 不加载的Bean
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 4、再次从配置文件中过滤不需要的Bean
checkExcludedClasses(configurations, exclusions);
// 5、移除不加载的Bean
configurations.removeAll(exclusions);
configurations = filter(configurations, autoConfigurationMetadata);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
如上面源码中的我的注释,这里主要分为四大步骤:
1、getCandidateConfigurations方法中通过SpringFactoriesLoader从spring.factories中加载自动装配需要的Bean。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
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;
}
当然,这里可以仿照加载我们自己希望的Bean。只要传入你自己的策略类就可以改变加载的key。(参考我上面的实例代码)
2、对于加载出的Bean名称进行去重,主要就是转换set操作。
protected final <T> List<T> removeDuplicates(List<T> list) {
return new ArrayList<>(new LinkedHashSet<>(list));
}
3、获取启动时不希望加载的Bean。(条件加载)
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
4、移除过滤的Bean后,Spring就可以对于这些Bean进行IOC容器的注入了。最后在SpringBoot项目启动时,通过EnableAutoConfiguration注解将所有的Bean合并。
AutoConfigurationPackages.Registrar
同样的,AutoConfigurationPackages.Registrar方法也可以帮助我们实现动态的Bean管理。同样在EnableAutoConfiguration注解中,还有一个注解@AutoConfigurationPackage:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
这个注解内部调用了Registrar这个方法:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
这里提供了Bean的注册和销毁的两个方法。实现自ImportBeanDefinitionRegistrar这个接口:
public interface ImportBeanDefinitionRegistrar {
void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry);
}
接口主要为我们提供了registerBeanDefinitions方法,这个方法用于Bean实例向IOC容器中的注册。所以,我们实现这个接口后,也可以实现自定义的Bean的动态管理。
举例:
/**
* 实现Bean的动态管理
*
* @author fanyuanhang
*/
public class LoggerDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry beanDefinitionRegistry) {
Class beanClass = LoggerService.class;
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass);
// 首字母小写转化
String beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
beanDefinitionRegistry.registerBeanDefinition(beanName, beanDefinition);
}
}
上面是我写的一个日志的Bean管理方法。我们可以在这里进行自己的逻辑判断和处理。
同理,这里操作的话,也需要通过EnableAutoConfiguration注解将Bean合并起来,所以在EnableAutoConfiguration注解内部增加了AutoConfigurationPackage注解。
条件过滤
AutoConfigurationMetadataLoader类
在AutoConfigurationImportSelector#selectImports方法中一开始就调用了这个类中的loadMetadata方法用来过滤一些根据其他Bean或者配置的条件来判断是否创建的Bean。
在分析 AutoConfigurationImportSelector 的源码时,会 先扫描 spring-autoconfiguration-metadata.properties 文件,最后在扫描 spring.factories 对应的类时,会结合 前面的元数据进行过滤,为什么要过滤呢? 原因是很多 的@Configuration 其实是依托于其他的框架来加载的, 如果当前的 classpath 环境下没有相关联的依赖,则意味 着这些类没必要进行加载,所以,通过这种条件过滤可以 有效的减少@configuration 类的数量从而降低 SpringBoot 的启动时间。
这些Bean主要维护在:
protected static final String PATH = “META-INF/” + “spring-autoconfigure-metadata.properties”;
这里主要分为如下的几种情况:(对照注解形式)
@ConditionalOnBean 在存在某个 bean 的时候
@ConditionalOnMissingBean 不存在某个 bean 的时候
@ConditionalOnClass 当前 classpath 可以找到某个类型的类时
@ConditionalOnMissingClass 当前 classpath 不可以找到某个类型的类 时
@ConditionalOnResource 当前 classpath 是否存在某个资源文件
@ConditionalOnProperty 当前 jvm 是否包含某个系统属性为某个值
@ConditionalOnWebApplication 当前 spring context 是否是 web 应用程序
举例:
@SpringBootApplication
public class SpringBootFirstApplication {
public static void main(String[] args) {
ConfigurableApplicationContext ca =
SpringApplication.run(SpringBootFirstApplication.class, args);
Object condition = ca.getBean("TestClass");
System.out.println(condition);
}
}
@Configuration
public class TestClassConfig {
@Bean(name = "TestClass")
@ConditionalOnBean(name = "condition")
public TestClass getTestClass() {
return new TestClass();
}
}
@Configuration
public class ConditionConfig {
@Bean("condition")
public Condition getCondition() {
return new Condition();
}
}
此时,若没有condition这个Bean,TestClass这个Bean就会获取失败;有的话就会成功。
流程图
总结
SpringBoot提供的自动装配功能,简单来说就是可以通过SPI思想来动态的选择加载项目启动时需要的Bean,从而达到不许用开发者去配置Bean,对于常用的外部依赖,添加相关的配置类,并提供多种配置策略来创建Bean。