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>
    
  • 配置相关参数

    spring.redis.host=127.0.0.1
    spring.redis.port=6379
    spring.redis.password=
    spring.redis.database=0
    
  • 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);
        }
    
    }
    

这是一个很简单的集成 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 {
}

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

  • @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 {};
}

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

  • @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 {};
}

首先通过 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));
    }
}

我们可以这样理解它的作用:将添加该注解的类所在的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());
        }
    }
}

然后进入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);
    }
}

继续往下,我们来看看它是如何获取配置信息的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;
}

看到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();
}

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

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

Spring boot 自动装配原理_第1张图片

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

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

Spring boot 自动装配原理_第2张图片

我们来看下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机制。

你可能感兴趣的:(#,Spring,spring,boot,java,redis)