详细剖析SpringBoot自动装配的实现过程

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 详细剖析SpringBoot自动装配的实现过程

1、开场白

学springboot的同行们都知道,springboot最大的优点是:自动注册Bean以及初始化组件,简化我们的开发,实现真正意义上的自动装配,今天我们就来讲讲 Spring Boot 是如何深度整合 Spring 注解编程模型、@Enable 模块驱动及条件装配等 Spring 原生特性来实现自动装配的。

2、Spring Boot 自动装配实现

我们都知道 Spring Boot 的启动过程非常简单,只需要启动一个 main 方法,项目就可以运行,就算依赖了诸多外部模块如:MVC、Redis等,也不需要我们进行过多的配置,那它的底层原理是什么呢?接下来,我们就一起去看一看。

我们先来看一段 Spring Boot 的启动类代码:

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

我们需要关注的是 @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 {
  @AliasFor(annotation = EnableAutoConfiguration.class)
  Class<?>[] exclude() default {};
  @AliasFor(annotation = EnableAutoConfiguration.class)
  String[] excludeName() default {};
  @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
  String[] scanBasePackages() default {};
  @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
  Class<?>[] scanBasePackageClasses() default {};
}

我们来看一看它的组成部分:

  • @SpringBootConfiguration:它里面标注了 @Configuration 注解,上篇文章说过,表明这是个配置类,功能与 @Configuration 无异。
  • @EnableAutoConfiguration:这个就是实现自动装配的核心注解,是用来激活自动装配的,其中默认路径扫描以及组件装配、排除等都通过它来实现。
  • @ComponentScan:这是用来扫描被 @Component标注的类 ,只不过这里是用来过滤 Bean 的,指定哪些类不进行扫描,而且用的是自定义规则。
  • Class<?>[] exclude():根据class来排除,排除指定的类加入spring容器,传入的类型是class类型。且继承自 @EnableAutoConfiguration 中的属性。
  • String[] excludeName():根据class name来排除,排除特定的类加入spring容器,参数类型是class的全类名字符串数组。同样继承自 @EnableAutoConfiguration。
  • String[] scanBasePackages():可以指定多个包名进行扫描。继承自 @ComponentScan 。
  • Class<?>[] scanBasePackageClasses():可以指定多个类或接口的class,然后扫描 class 所在包下的所有组件。同样继承自 @ComponentScan 。
2.1、@EnableAutoConfiguration 实现

上面我们说到 @EnableAutoConfiguration 是实现自动装配的核心注解,是用来激活自动装配的,看注解前缀我们应该知道是上篇文章中所讲的 Spring @Enable 模块驱动的设计模式,所以它必然会有 @Import 导入的被 @Configuration 标注的类或实现 ImportSelector 或ImportBeanDefinitionRegistrar 接口的类。接着,我们来看看它的定义:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  ...
}

可以看到它由两部分组成:

  1. @AutoConfigurationPackage:这是用来将启动类所在包,以及下面所有子包里面的所有组件扫描到Spring容器中,这里的组件是指被 @Component或其派生注解标注的类。这也就是为什么不用标注@ComponentScan的原因。
  2. @Import(AutoConfigurationImportSelector.class):这里导入的是实现了 ImportSelector 接口的类,组件自动装配的逻辑均在重写的 selectImports 方法中实现。
    接下来我们就来看看这两者具体是怎么实现的。
2.1、 获取默认包扫描路径

我们先来看看 Spring Boot 是如何通过 @AutoConfigurationPackage 注解获取默认包扫描路径的,进入它的实现:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

可以看到它是通过 @Import 导入了 AutoConfigurationPackages.Registrar 类,该类实现了ImportBeanDefinitionRegistrar 接口,所以按照上篇文章所讲的,它是在重写的方法中直接注册相关组件。继续往下:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
      register(registry, new PackageImport(metadata).getPackageName());
    }
    ....
  }
private static final class PackageImport {
    private final String packageName;
    PackageImport(AnnotationMetadata metadata) {
      this.packageName = ClassUtils.getPackageName(metadata.getClassName());
    }
    ....
}

这里主要是通过 metadata 元数据信息构造 PackageImport 类。先获取启动类的类名,再通过ClassUtils.getPackageName 获取启动类所在的包名。我们接着往下看:

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    if (registry.containsBeanDefinition(BEAN)) {
      BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
      ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
      constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
    }
    else {
      GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
      beanDefinition.setBeanClass(BasePackages.class);
      beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
      beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
      registry.registerBeanDefinition(BEAN, beanDefinition);
    }
  }

最后就是将这个包名保存至 BasePackages 类中,然后通过 BeanDefinitionRegistry 将其注册,进行后续处理,至此该流程结束。

2.1、获取自动装配的组件

该部分就是实现自动装配的入口,从上面得知这里也是通过 @Import 来实现的,来看看导入的类:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
    ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    ....
  @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());
  }
  ....
}

主要关注重写的 selectImports 方法,其中

AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); 是加载自动装配的元信息。而AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata)该方法返回的就是自动装配的组件,我们进去看看:

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  // 获取 @EnableAutoConfigoration 标注类的元信息,也就是获取该注解 exclude 和 excludeName 属性值
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 该方法就是获取自动装配的类名集合
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 去除重复的自动装配组件,就是将List转为Set进行去重
  configurations = removeDuplicates(configurations);
  // 这部分就是根据上面获取的 exclude 及 excludeName 属性值,排除指定的类
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  configurations.removeAll(exclusions);
  // 这里是过滤那些依赖不满足的自动装配 Class
  configurations = filter(configurations, autoConfigurationMetadata);
  fireAutoConfigurationImportEvents(configurations, exclusions);
  // 返回的就是经过一系列去重、排除、过滤等操作后的自动装配组件
  return new AutoConfigurationEntry(configurations, exclusions);
}

该方法中就是先获取待自动装配组件的类名集合,然后通过一些列的去重、排除、过滤,最终返回自动装配的类名集合。主要关注

getCandidateConfigurations(annotationMetadata, attributes) 这个方法,里面是如何获取自动装配的类名集合:

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;
}

其中

getSpringFactoriesLoaderFactoryClass()返回的是EnableAutoConfiguration.class。继续往下,执行的是 SpringFactoriesLoader#loadFactoryNames 方法:

public final class SpringFactoriesLoader {
    ...
    public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        // 前面可以看到,这里的 factoryClass 是 EnableAutoConfiguration.class
    String factoryClassName = factoryClass.getName();
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
  }
  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);
    }
  }
  ...
}

最终的实现逻辑都在这里,主要过程如下:

  1. 搜索classpath路径下以及所有外部jar包下的META-INF文件夹中的spring.factories文件。主要是spring-boot-autoconfigure包下的
**Initializers**
org.springframework.context.ApplicationContextInitializer=\\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
**Application Listeners**
org.springframework.context.ApplicationListener=\\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
**Auto Configuration Import Listeners**
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener
**Auto Configuration Import Filters**
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition
**Auto Configure**
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
...

可以看到其中内容,存储的是key-value格式的数据,且key是一个类的全路径名称,value是多个类的全路径名称,且以逗号分割。

  1. 将所有的spring.factories文件转成Properties格式,将里面key-value格式的数据转成Map,该Map的value是一个List,之后将相同Key的value合并到List中,将该Map作为方法返回值返回。
  2. 返回到 loadFactoryNames 方法,通过上面得知factoryClassName的值为EnableAutoConfiguration,所以通过 getOrDefault(factoryClassName, Collections.emptyList())方法,获取 key 为EnableAutoConfiguration的类名集合。

ps:getOrDefault第一个入参是key的name,如果key不存在,则直接返回第二个参数值

至此,流程结束,最后返回的就是自动装配的组件,其中有我们比较熟悉的Redis、JDBC、SpringMVC等,可以看到一个特点,这些自动装配的组件都是以 AutoConfiguration 结尾。但该组件列表只是候选组件,因为后面还有去重、排除、过滤等一系列操作,这里就不再详细述说。下面我们来看看自动装配的组件内部是怎么样的。

2.2、自动装配的组件内部实现

就拿比较熟悉的 Web MVC 来看,看看是如何实现 Web MVC 自动装配的。先来代码组成部分:

@Configuration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
        ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
    ...
    @Configuration
    @Import(EnableWebMvcConfiguration.class)
    @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class})
    @Order(0)
    public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ResourceLoaderAware {
        ...
        @Bean
        @ConditionalOnBean(View.class)
        @ConditionalOnMissingBean
        public BeanNameViewResolver beanNameViewResolver() {
            ...
        }
        ...
    }
    @Configuration
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
        @Bean
        @Override
        public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
            ...
        }
        @Bean
        @Primary
        @Override
        public RequestMappingHandlerMapping requestMappingHandlerMapping() {
            ...
        }
    }
    ...
}

注解部分:

@Configuration:这个大家都比较熟悉,标识该类是一个配置类

@ConditionalXXX:这是 Spring 条件装配,只不过经由 Spring Boot 扩展形成了自己的条件化自动装配,且都是 @Conditional 的派生注解。

@ConditionalOnWebApplication:参数值是 Type 类型的枚举,当前项目类型是任意、Web、Reactive其中之一则实例化该 Bean。这里指定如果为 Web 项目才满足条件。

@ConditionalOnClass:参数是 Class 数组,当给定的类名在类路径上存在,则实例化当前Bean。这里当Servlet.class、 DispatcherServlet.class、 WebMvcConfigurer.class存在才满足条件。

@ConditionalOnMissingBean:参数是也是 Class 数组,当给定的类没有实例化时,则实例化当前Bean。这里指定当 WebMvcConfigurationSupport 该类没有实例化时,才满足条件。 装配顺序 @AutoConfigureOrder:参数是int类型的数值,数越小越先初始化。

@AutoConfigureAfter:参数是 Class 数组,在指定的配置类初始化后再加载。

@AutoConfigureBefore:参数同样是 Class 数组,在指定的配置类初始化前加载。

代码部分: 这部分就比较直接了,实例化了和 Web MVC 相关的Bean,如 HandlerAdapter、HandlerMapping、ViewResolver等。其中,出现了 DelegatingWebMvcConfiguration 类,这是@EnableWebMvc 所 @Import 导入的配置类。

可以看到,在Spring Boot 自动装配的类中,经过了一系列的 @Conditional 条件判断,然后实例化某个模块需要的Bean,且无需我们配置任何东西,当然,这都是默认实现,当这些不满足我们的要求时,我们还得手动操作。

3、总结

关于Spring boot自动装配的内容就告一段落,不难看出Spring Boot自动装配所依赖的注解驱动、@Enable模块驱动、条件装配等特性均来自 Spring Framework。而自动装配的配置类均来源于spring.factories文件中。核心则是基于“约定大于配置”理念,通俗的说,就是Spring boot为我们提供了一套默认的配置,只有当默认的配置不满足我们的需求时,我们再去修改默认配置。当然它也存在缺点就是组件的高度集成,使用的时候很难知道底层实现,加深了理解难度。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
WXM
|
2月前
|
存储 缓存 Java
|
4月前
|
缓存 Java 开发者
SpringBoot自动装配原理
SpringBoot自动装配原理
52 1
|
XML 安全 Java
《SpringBoot系列九》:SpringBoot自动装配机制原理
《SpringBoot系列九》:SpringBoot自动装配机制原理
219 0
《SpringBoot系列九》:SpringBoot自动装配机制原理
|
XML 存储 Java
SpringBoot bean自动装配原理,这一篇就够了! 1
SpringBoot bean自动装配原理,这一篇就够了!
|
Java 开发者 Spring
springboot自动装配原理
springboot自动装配原理
92 0
|
Java 开发者 Spring
SpringBoot自动装配的原理
Spring Boot 的自动装配(Auto-Configuration)是其核心特性之一,它通过约定优于配置的原则,为开发者简化了项目的配置和部署过程。
89 0
|
XML SQL Java
SpringBoot bean自动装配原理,这一篇就够了! 2
SpringBoot bean自动装配原理,这一篇就够了!
|
消息中间件 缓存 Java
SpringBoot bean自动装配原理,这一篇就 够了! 3
SpringBoot bean自动装配原理,这一篇就够了!
|
Java 容器 Spring
Springboot源码:自动装配流程解析
Springboot源码:自动装配流程解析
67 0
|
Java Spring 容器
SpringBoot - 自动装配 源码解析
SpringBoot - 自动装配 源码解析
96 0