• Spring boot 自动装配原理


    Spring boot 为了解决Bean的复杂配置,引入了自动装配机制;那么什么是自动装配,它的原理又是什么呢?我们先通过以下例子来了解以一下什么是自动装配。

    Spring boot 集成 redis

    • 引入依赖包

      		<dependency>
                  <groupId>org.springframework.boot</groupId>
                  <artifactId>spring-boot-starter-data-redis</artifactId>
                  <version>2.6.0</version>
              </dependency>
      
      • 1
      • 2
      • 3
      • 4
      • 5
    • 配置相关参数

      spring.redis.host=127.0.0.1
      spring.redis.port=6379
      spring.redis.password=
      spring.redis.database=0
      
      • 1
      • 2
      • 3
      • 4
    • controller

      @RestController
      @RequestMapping(value = "/redis")
      public class RedisController {
      
          @Autowired
          private RedisTemplate redisTemplate;
      
          @RequestMapping("/save")
          public String save(@RequestParam String key, @RequestParam String value) {
              redisTemplate.opsForValue().set(key, value);
              return "ok";
          }
      
          @RequestMapping("/get")
          public String get(@RequestParam String key) {
              return (String) redisTemplate.opsForValue().get(key);
          }
      
      }
      
      • 1
      • 2
      • 3
      • 4
      • 5
      • 6
      • 7
      • 8
      • 9
      • 10
      • 11
      • 12
      • 13
      • 14
      • 15
      • 16
      • 17
      • 18
      • 19

    这是一个很简单的集成 redis 的案例,通过这个案例我们可以看出:RedisTemplate 这个类的 bean 对象,我们并没有通过 xml 的形式或者 注解 的形式注入到 ioc 容器中,但是我们可以直接通过 Autowired 注解自动从容器里面拿到相应的 bean 对象,从而进行属性的注入。

    这就是Spring boot 中的自动装配,那这是怎么做到的呢?我们来分析一下它的原理,从而来理解RedisTemplate 是如何注入的。

    自动装配原理

    我们先来分析下上面案例的流程:

    首先我们在集成的时候只做了一件事:引入依赖包;然后启动项目后,对象就自动进入到 IOC 容器中了。

    那它是如何自动注入的呢?我们平常手动注入对象的时候,是通过 component 或者就是configuration中的bean注入。那它会不会也是呢?如果是这样,那按照理论来说应该也有一个componentscan来扫描对应路径,从而将对象注入;但是 spring boot 中可以引入各种各样的依赖,扫描的路径肯定也是千奇百怪的,那我们猜测应该是有一个固定的路径让其去扫描,从而能将依赖自动注入到容器中。

    这个逻辑是不是有点眼熟,这不就是SPI机制吗?接下来我们从源码中逐步看下是如何实现的。

    @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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    其中比较重要的是有以下三个注解:

    • @SpringBootConfiguration

      继承了 Configuration,表示当前是注解类

    • @EnableAutoConfiguration

      开启 spring boot 的注解功能,自动装配机制,借助 @import注解的功能。

    • @ComponentScan(excludeFilters = {})

      自动扫描并加载符合条件的组件(比如component,controller等),并将这些对象加载到 ioc 容器中。

      我们可以通过basepackages等属性来定制其自动扫描的范围,如果不指定,则默认会从声明。@ComponentScan所在类的 package 进行扫描,这就是为什么我们希望启动类放在根目录下的原因。

    @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 {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    其中最重要的两个注解如下:

    • @AutoConfigurationPackage
    • @Import({AutoConfigurationImportSelector.class})

    AutoConfigurationPackage

    我们先来看这个注解,看看这个注解做了那些事情:

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @Import({Registrar.class})
    public @interface AutoConfigurationPackage {
        String[] basePackages() default {};
    
        Class<?>[] basePackageClasses() default {};
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    首先通过 import注解导入了Registrar类:

    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }
    
        // 注册当前启动类的根 package
        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0]));
        }
    
        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata));
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们可以这样理解它的作用:将添加该注解的类所在的package 作为自动配置package进行管理。就是我们上文所说的当Spring boot应用启动时会将启动类所在的package作为自动配置的package。

    AutoConfigurationImportSelector

    接下来我们来看@Import({AutoConfigurationImportSelector.class}),借助AutoConfigurationImportSelector,EnableAutoConfiguration可以帮助spring boot 项目将所有符合条件(Spring.factories)的bean定义都加载到当前IOC容器中。

    该类最大的作用就是帮助我们找相关的配置类,而找配置类的过程就是我们上面所说的SPI机制。

    关于SPI机制可以看这篇文章:SPI 机制详解

    我们继续来看整体流程:首先看selectImports方法,该方法是找配置文件的入口。

    为什么是这个方法呢?因为这个类继承了DeferredImportSelector接口,接口又继承了ImportSelector接口,所以在spring的流程中会进行调用。

    public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    
        public AutoConfigurationImportSelector() {
        }
    
        // 找配置文件
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            if (!this.isEnabled(annotationMetadata)) {
                return NO_IMPORTS;
            } else {
                AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
                return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    然后进入getAutoConfigurationEntry方法:

    protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            // 获取配置信息
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.getConfigurationClassFilter().filter(configurations);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    继续往下,我们来看看它是如何获取配置信息的getCandidateConfigurations方法:

    该方法主要是去加载各个组件jar下的 spring.factories文件

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    看到SpringFactoriesLoader.loadFactoryNames是不是很眼熟?和我们SPI机制中的ServiceLoader.load是不是很类似?看来我们找对地方了,SpringFactoriesLoader的底层原理其实就是借鉴于JDK的SPI机制。

    我们知道SPI机制都是从classpath下的service目录查找对应的文件?那么SpringFactoriesLoader从哪里查找呢?我们进入该类查看:

    一进来我们就看到路径也被写死了,这样我们上面的问题就被解决了。

    public final class SpringFactoriesLoader {
        public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
        private static final Log logger = LogFactory.getLog(SpringFactoriesLoader.class);
        static final Map<ClassLoader, Map<String, List<String>>> cache = new ConcurrentReferenceHashMap();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    我们到spring boot 的自动装配jar中查看一下:

    整个流程是不是很符合SPI机制,这样redis对象自动装配是不是能稍微想通了?

    在这里插入图片描述

    既然找到了文件,接下来肯定就是读取配置了,我们打断点来看下效果:

    可以很清楚的看到spring.factories文件中的配置信息都读取到了。

    在这里插入图片描述

    我们来看下loadFactoryNames方法,它的入参为工厂类名称和对应的类加载器,方法会根据指定的类加载器,加载该类加载器搜索路径下的指定文件,即spring.factories文件。

    传入的工厂类为接口,而文件中对应的类则是接口的实现类,或最终作为实现类。

    配合@EnableAutoConfiguration使用的话,它更多是提供一种配置查找的功能支持,即根据@EnableAutoConfiguration的完整类名:org.springframework.boot.autoconfigure.EnableAutoConfiguration作为查找的KEY,获取对应的一组Configuration类。

    总结

    看过源码之后,我们对Spring boot 的装配原理进行一个简单的总结,方便我们记忆:

    • 首先 spring boot 启动

    • 扫描 @SpringBootApplication注解

    • 扫描@EnableAutoConfiguration注解

      这个注解里面带有一个@import注解,导入了AutoConfigurationImportSelector类,这个类实现了DeferredImportSelector接口,这个接口又实现了ImportSelector接口,在这个接口里面有一个叫做selectImports的方法,这个方法实现了配置类的寻找。

      整个寻找的过程就是Springboot 的SPI机制。

  • 相关阅读:
    毛玻璃员工卡片悬停效果
    【Vue.js 3.0源码】直击Vue核心的实现之组件更新完整的DOM diff流程(上)
    AOP是什么?如何使用AOP?
    Tesla_V100加速卡详细参数
    ISC技术分享:从RASP开启云上应用安全防护
    Pytorch深度强化学习1-2:详解K摇臂赌博机模型和ϵ-贪心算法
    Web权限&权限划分
    MySql数据是如何存储在磁盘上存储的?
    《Nature》论文插图的Matlab复刻第2期—单组多色横向柱状图(Part1-305)
    C++ if 语句
  • 原文地址:https://blog.csdn.net/qq_41432730/article/details/128166754