spice and wolfspice and wolf Be the One you wanna Be

Spring自动装配详解

定义

Spring boot自动装配指的是通过约定优于配置的思想,自动将第三方库或常用组件的bean加载到spring容器中,从而大幅减少配置工作的机制,是spring boot的核心特性之一。

自动装配的优点

  • 简化配置。传统的手动装配方式有xml和注解方式,需要手动创建xml文件或者java类来完成bean的相关配置,但是spring boot遵从约定优于配置的思想,将大量的bean配置和注入的相关逻辑集成到starter启动类中,减少了冗余配置工作。
  • 开箱即用。通过极少的配置,就能让应用快速部署并投入使用。
  • 实现了版本控制和依赖管理。自动装配特性将版本兼容代码和依赖控制的代码逻辑交由启动类实现,简化了开发人员的相关工作。

实现原理

当spring启动类运行时会执行run方法,run方法会加载spring容器,容器会根据主配置类的注解执行不同的操作:

@SpringBootApplication

主要应用在主启动类上,表示需要通过这个类的main方法启动应用程序,而@SpringBootApplication是个复合注解,包含了另外三个注解:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

先说说spring如何解析复合注解。spring解析复合注解的核心机制:

  • java反射。对注解处理的解析java的反射机制是必须的,反射主要用于获取类、属性和方法上的注解信息,典型的获取方法包括Class.getAnnotations()、Method.getDeclaredAnnotations()等方法。
  • AnnotationUtils工具类。该工具类封装了查找、合并和处理注解的相关方法,尤其在处理继承或组合注解时发挥关键作用。
  • 递归解析。当遇到一个注解时,Spring会检查它是否是复合注解,如果是,则递归深入其元注解,直到提取出所有基础注解。
  • 合成注解。Spring能将复合注解中的多个子注解合成一个逻辑整体,视为直接应用于目标元素,从而自动应用其对应的行为。

@ComponentScan

让spring容器扫描当前包及子包下的类,并将把带有@Component、@Service、@Controller等注解的类注册为bean

@SpringBootConfiguration

单纯让Spring容器把主启动类当作一个配置类处理

@EnableAutoConfiguration

开启自动装配的关键注解,该注解也是一个复合注解,包含了另外两个注解:

  • @AutoConfigurationPackage
  • @Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage。会让spring容器扫描主配置类所在包和子包,将所有组件扫描并加载到spring的容器中,这就是为什么在spring中编写的业务代码的包需要放在主配置类统计目录下。

@Import(AutoConfigurationImportSelector.class)

使用@Import注解导入的类会被Spring加载到IOC容器中,@import导入bean容器的方式有4中:

  • 导入bean(@Import(User.class))
  • 导入配置类(例如有用@Configuration修饰的配置类MyConfig,并且类中有带有@Bean注解方法,创建对象存到IOC容器,bean名称为:默认方法名称)
  • 导入ImportSelector的实现类。一般用于加载配置文件中的类。当在Spring配置类中使用@Import注解并指定实现了ImportSelector接口的类时,Spring容器加载时会调用该类的selectImports()方法,根据方法返回的配置类的全限定类名来加载相应的配置类。
  • 导入ImportBeanDefinitionRegister的实现类

这里使用的是第三种方式,所以我们可以根据selectImports()方法的实现来看自动装配的具体逻辑:

首先我们要知道,这里返回的是需要自动装配的类的全限定名的字符串列表(看下图autoConfigurationEntry.getConfigurations())

再来我们可以浅看下spring在ConfigurationClassParser中对Import注解的处理流程。首先,如果是ImportSelector的实现类时,会将被import修饰的类的元注解作为参数传入selectImports方法中,所以上面AnnotationMetadata传参是非空的,然后我们再看下isEnabled方法,这里先判断了是否是子类,如果是子类就返回true,如果不是子类就判断环境变量是否禁用了自动装配,如果禁用了自动装配,则返回false,所以isEnabled一般是返回的true

进入getAutoConfigurationEntry函数中,会先作isEnabled判断,如果为false则返回新的AutoConfigurationEntry对象,列表是空的,所以最后是返回空数组。如果不为空,则先获取元注解的属性,执行getCandidateConfigurations方法获取配置信息

再往下看getCandidateConfigurations的方法逻辑,发现该方法的两个形参实际没有任何卵用,单纯返回了SpringFactoriesLoader.loadFactoryNames方法的返回结果

this.getSpringFacoriesLoaderFactoryClass直接就返回了EnableAutoConfiguration.class,这是一个注解类,我们后面会知道这里为什么传这个参数

loadFactoryNames方法返回的是loadSpringFactories方法返回集中以EnableAutoConfiguration.class的全限定名为键的列表

最后来看看loadSpringFactories方法返回的map内容是啥

    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                MultiValueMap<String, String> result = new LinkedMultiValueMap();

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Map.Entry<?, ?> entry = (Map.Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;

                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryImplementationName = var9[var11];
                            result.add(factoryTypeName, factoryImplementationName.trim());
                        }
                    }
                }

                cache.put(classLoader, result);
                return result;
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
            }
        }
    }

首先判断缓存中对应类加载器的内容是否为空,不为空返回,为空则进行资源加载,因为类加载器不为空,且是AppClassLoader,所以会加载classpath下的所有符合路径META-INF/spring.factories的文件,然后遍历每个文件资源中的键值对,因为值可能是逗号分隔的字符串,所以需要进行字符串处理加载成String-List<String>并存到result中,最后返回结果数据

这里以一个spring.factories的文件为例解析一下文件格式(如下),可以看出来书写格式是以键值对的方式

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaClientConfigServerAutoConfiguration,\
org.springframework.cloud.netflix.eureka.config.DiscoveryClientOptionalArgsConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaClientAutoConfiguration,\
org.springframework.cloud.netflix.ribbon.eureka.RibbonEurekaAutoConfiguration,\
org.springframework.cloud.netflix.eureka.EurekaDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.reactive.EurekaReactiveDiscoveryClientConfiguration,\
org.springframework.cloud.netflix.eureka.loadbalancer.LoadBalancerEurekaAutoConfiguration

org.springframework.cloud.bootstrap.BootstrapConfiguration=\
org.springframework.cloud.netflix.eureka.config.EurekaConfigServerBootstrapConfiguration

spring.factories中不同的键表示不同的含义,比如org.springframework.context.ApplicationListener表示spring上下文的事件监听接口,值表示的是实现这个借口的事件监听器;org.springframework.cloud.bootstrap.BootstrapConfiguration则是个注解类,对应的值表示需要在自动装配之前加载的配置类

自动装配需要获取的是自动装配所需的类,不难发现在loadFactoryNames中取的是以factoryTypeName这个形参为key的相应值,我们向上看看此形参的实际传值是什么呢?发现恰好就是EnableAutoConfiguration注解类本身。这里就很好的体现了spring boot的约定优于配置的思想,spring其实是基于Spring.factories的配置规则和相应的类上的自动配置的注解来给出的一套默认的约定规则,避免了大部分情况下的冗余配置。

自动装配的精髓

可以指定配置的加载条件和加载顺序,例如:

如果不使用自动装配

@Configuration
public class StudentConfig {
    @Bean
    public Student student() {
        // TODO
        return new Student();
    }
}
@Configuration
public class SchoolConfig {
    @ConditionalOnBean(Student.class)
    @Bean
    public School school() {
        // TODO
        return new School();
    }
}

则上面两个配置类是按字母顺序加载的,会先加载SchoolConfig类,而SchoolConfig类的加载条件是StudentConfig类必须存在,这时因为beanDefinitionMap中并没有加载StudentConfig,所以StudentConfig配置类并不会生效。

如果使用自动装配(spring.factories中的org.springframework.boot.autoconfigure.EnableAutoConfiguration中包含以下两个类的全限定名)

public class StudentConfig {
    @Bean
    public Student student() {
        // TODO
        return new Student();
    }
}
@AutoConfigureAfter(StudentConfig.class)
public class SchoolConfig {
    @ConditionalOnBean(Student.class)
    @Bean
    public School school() {
        // TODO
        return new School();
    }
}

因为加入了@AutoConfigureAfter注解,SchoolConfig会在StudentConfig类之后加载,这样就能成功加载到spring容器中

所以自动装配的精髓是可以指定配置的加载条件和加载顺序。典型的注解有

  • ConditionalOnProperty
  • ConditionalOnMissingBean
  • ConditionalOnBean
  • ConditionalOnMissingClass
  • AutoConfigureAfter
  • AutoConfigureBefore
  • AutoConfigureOrder

面向面试总结

讲讲Spring boot的自动装配?
  1. 先讲自动装配的定义。Spring boot自动装配指的是基于约定优于配置的思想,自动将第三方库或常用组件的bean加载到spring容器中,从而大幅减少配置工作的机制,是spring boot的核心特性之一
  2. 再说自动装配的好处
    • 简化配置。传统的手动装配方式有xml和注解方式,需要手动创建xml文件或者java类来完成bean的相关配置,但是自动装配将大量的bean配置和注入的相关逻辑集成到启动类中,只需通过maven引入相关的starter包就能实现自动配置,减少了冗余配置工作。
    • 开箱即用。只用极少的配置,就能让应用快速部署并投入使用。
    • 实现了版本控制和依赖管理。自动装配特性将版本兼容代码和依赖控制的代码逻辑交由启动类实现,简化了开发人员的相关工作。
  3. 最后说自动装配的原理。当spring启动类运行时会执行run方法,run方法会加载spring容器,容器会根据主配置类的注解执行不同的操作,修饰主启动类的注解为@SpringBootAppliction,它是一个复合注解,包含了@SpringBootConfiguration、@ComponentScan和@EnableAutoConfiguration这三个注解组成,@SpringBootConfiguration注解基本等同于@Configuration注解,表示主启动类是一个配置类,spring容器会将其注册为一个bean;而被@ComponentScan修饰的类,spring容器会扫描当前包和子包下的所有类,将被@Service、@Component、@Controller等注解修饰的类注册为bean;而@EnableAutoConfiguration包含两个注解@AutoConfigurationPackage和@Import(AutoConfigurationImportSelector.class),其中@AutoConfigurationPackage用于扫描当前包和子包下的所有类,将所有组件扫描并加载到spring的容器中,与ComponentScan不同的是其扫描的是第三方包注解的bean;@Import(AutoConfigurationImportSelector.class),@import注解一般用于将类作为bean导入spring容器中,方式有四种,分别是导入bean、导入配置类、导入ImportSelector的实现类、导入ImportBeanDefinitionRegister的实现类,这里用的是第三种方式,会将实现类中的selectImports方法的返回值列表作为全限定类名来加载相应的配置类,这里实现的逻辑流程是:
    1. 先判断是否启用了自动装配。判断当前类是否是AutoConfigurationImportSelector.class,如果不是,说明是这个类的子类,则直接true;如果是,再返回当前环境变量中的spring.boot.enableautoconfiguration的值,如果是false,则不进行自动装配,返回空列表
    2. 再通过loadSpringFactories(@Nullable ClassLoader classLoader)方法获取spring.factories的配置信息。
      1. 先从以classloader-MultiValueMap<String, String>作为键值对的map中获取传入的类加载器对应的配置信息,如果存在,则返回,如果不存在则
      2. 判断传入的类加载器是否为空,如果不为空,则获取classLoader.getResources(“META-INF/spring.factories”)作为结果数据,如果为空则获取ClassLoader.getSystemResources(“META-INF/spring.factories”)作为结果数据
      3. 处理结果数据。上一部拿到的仅仅是原数据,因为spring.factories存储的键和值都是字符串数据,涉及到复数个值会以逗号分隔,所以这里会将值的数据处理成list并和key封装Map<String, List<String>>结构存储在数据结构中(MultiValueMap本身就继承自Map<K, List<V>>)
      4. 取EnableAutoConfiguration.class的全限定名为key的值,即所有的自动装配配置类的全限定名的list返回
    3. 最后基于返回的全限定名加载配置类
  4. 自动装配原理总结:spring boot其实是通过spring.factories文件的配置规约和spring对文件中自动配置类的加载逻辑实现了自动装配,充分体现了约定大于配置的设计思想
  5. 自动装配的精髓。可以指定配置的加载条件和加载顺序,例如…
spring boot自动装配的原理是什么(同上3)?

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

Press ESC to close