SpringBoot自动装配原理详解

序言

Spring自动装配主要就是基于注解。我们只需要使用@SpringBootApplication这样一个注解就就能够完全启动一个项目了。它是由下面三个注解完成

SpringBootApplication 注解详情

  • @SpringBootConfiguration:我们点进去以后可以发现底层是 Configuration 注解,说白了就是支持 JavaConfig 的方式来进行配置 (使用 Configuration 配置类等同于 XML 文件)。
  • @EnableAutoConfiguration:开启自动配置功能,后面详细介绍,自动装配的核心
  • @ComponentScan:这个注解,学过 Spring 的同学应该对它不会陌生,就是扫描注解,默认是扫描当前类下的 package。将@Controller/@Service/@Component/@Repository等注解加载到 IOC 容器中。

我们先从一个异常开始入手

从一个异常谈起

刚接触springboot的时候踩过这样一个坑:在pom文件中不小心加了db相关的依赖,比如

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>

然后项目就启动不了

1574679886185.png

看报错中框出来的部分——数据源没有指定url。what?这个pom依赖只是我不小心加的啊,它怎么就自动给我配了一个数据源,还由于没配url这些数据源相关的配置导致报错了。

再回想起我们之前搭ssm框架的时候是如何配置datasource的呢?

  <!-- - - - -数据源配置 - - - - - -->
    <bean id="dataSource" class="com.xxxx.ComboPooledDataSource"
          destroy-method="close">
        <property name="driverClass" value="${driverClass}"/>
        <property name="jdbcUrl" value="${jdbcUrl}"/>
        <property name="user" value="${user}"/>
        <property name="password" value="${password}"/>
    </bean>

在spring配置xml中配置一个datasource的bean,注册进ioc容器。那么,我们是否可以猜想,springboot由于我们加了spring-boot-starter-jdbc依赖,所以它自动帮我们往ioc容器里注入了一个datasource的bean

这个报错可以在springboot启动类中排除datasource自动配置类来解决,原因后文会讲到。

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

springboot的自动配置,会自动将一些配置类的bean注册进ioc容器,我们可以需要的地方使用@autowired或者@resource等注解来使用它。

“自动”的表现形式就是我们只需要引我们想用功能的包,相关的配置我们完全不用管,springboot会自动注入这些配置bean,我们直接使用这些bean即可。

具体来讲就是在配置文件中,我们输入spring,会有很多配置提示,这些配置提示就是自动装配bean属性:

1574680258702.png

其实这就是根据下面自动装配类来得,它的路径——C:\Users\Administrator\.m2\repository\org\springframework\boot\spring-boot-autoconfigure\2.1.3.RELEASE\spring-boot-autoconfigure-2.1.3.RELEASE.jar!\org\springframework\boot\autoconfigure\data\redis\RedisProperties.class

public class RedisProperties {
    private int database = 0;
    private String url;
    private String host = "localhost";
    private String password;
    private int port = 6379;
    private boolean ssl;
    private Duration timeout;
    private RedisProperties.Sentinel sentinel;
    private RedisProperties.Cluster cluster;
    private final RedisProperties.Jedis jedis = new RedisProperties.Jedis();
    private final RedisProperties.Lettuce lettuce = new RedisProperties.Lettuce();
    ......
}

注意一下autoconfigure\data\redis

源码分析自动配置是如何实现

我们都知道springboot最核心的注解 @SpringBootApplication等于@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan等注解的作用,顾名思义,@EnableAutoConfiguration 显然就是springboot实现自动配置的核心所在。

EnableAutoConfiguration

简单来说,这个注解可以帮助我们自动载入应用程序所需要的所有默认配置

我们点进去看一下,发现有两个比较重要的注解

EnableAutoConfiguration 注解详情

  • @AutoConfigurationPackage:自动配置包
  • @Import:给 IOC 容器导入组件
@AutoConfigurationPackage//扫描
@Import({AutoConfigurationImportSelector.class})//装配
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

AutoConfigurationPackage

网上将这个@AutoConfigurationPackage注解解释成自动配置包,我们也看看@AutoConfigurationPackage里边有什么。也是用@import导入的

@Import({Registrar.class})
public @interface AutoConfigurationPackage {
}

继续看Register里面是什么

  static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
        }
    }

核心的就是registerBeanDefinitions

表示的含义就是在默认的情况下就是将:主配置类 (@SpringBootApplication) 的所在包及其子包里边的组件扫描到 Spring 容器中。

  • 看完这句话,会不会觉得,这不就是 ComponentScan 的功能吗?这俩不就重复了吗?

我开始也有这个疑问,直到看到文档的这句话:

it will be used when scanning for code @Entity classes.
It is generally recommended that you place EnableAutoConfiguration (if you’re
not using @SpringBootApplication) in a root package so that all sub-packages
and classes can be searched.

比如说,你用了 Spring Data JPA,可能会在实体类上写@Entity注解。这个@Entity注解由@AutoConfigurationPackage扫描并加载,而我们平时开发用的@Controller/@Service/@Component/@Repository这些注解是由ComponentScan来扫描并加载的。

简单理解:这二者扫描的对象是不一样

AutoConfigurationImportSelector

点进去看一下,便得到

public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
            return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
        }
    }


protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, 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);//移除exclude修饰的那些配置
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

看到方法名getCandidateConfigurations,获取候选配置,由此我们可以猜想这一步就是springboot获取所有用@Configuration注解修饰的配置类的名称,那么为什么叫做“候选”配置呢?往下看,根据方法名,我们就能知道方法做了什么,接下来就是从这里获取的候选配置的list里,剔除重复部分,再剔除一开始我们@SpringbootApplication 注解里exclude掉的配置,最终才得到配置类名集合。

接下来看看getCandidateConfigurations 里面的内容

getCandidateConfigurations

内容

 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 来加载

这里包装了一层,我们看到的只是通过 SpringFactoriesLoader 来加载,还没看到关键信息,继续进去

loadFactoryNames

 public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }

最终来到loadSpringFactoriespublic static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

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 {
                // 类加载器对象存在则用这个加载器获取上面说的常量路径里的资源,不存在则用系统类加载器去获取
                //当前classloader是appclassloader,getResources能获取所有依赖jar里面的META-INF/spring.factories的完整路径
            ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();

                // 遍历上述返回的url集合
                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                     // URL类可以获取来自流,web,甚至jar包里面的资源
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        // 解析spring.factories
                        String factoryClassName = ((String)entry.getKey()).trim();
                        String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;
                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            // spring.facories中配置的不仅仅有自动配置相关的内容,还有其他比如                                 // ApplicationContextInitializer等等各种springboot启动的时候,初始化spring环                     // 境需要的配置,自动配置只是其中一项。这个cache也是在springboot启动阶段就赋值的
                            result.add(factoryClassName, factoryName.trim());
                        }
                    }
                }

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

简单梳理:

  • FACTORIES_RESOURCE_LOCATION的值是META-INF/spring.factories
  • Spring 启动的时候会扫描所有 jar 路径下的META-INF/spring.factories,将其文件包装成 Properties 对象
  • 从 Properties 对象获取到 key 值为EnableAutoConfiguration的数据,然后添加到容器里边。

C:\Users\Administrator\.m2\repository\org\springframework\spring-beans\5.1.5.RELEASE\spring-beans-5.1.5.RELEASE\META-INF\spring.factories

5611237-72a9e12a2d4c9df3.png

小结

springboot自身的autoconfigure包里有大量的java配置类,我们也可以在自己的工程中写这些配置类,这些配置类需要在相应的META-INF/spring.facotries文件中配置好,如下

5611237-72a9e12a2d4c9df3.png

这样就会因为在@EnableAutoConfiguration注解的存在,这些配置类里面的bean被注册进ioc容器,不过也是有条件的,条件注解ConditionOnxxx。下面列一些常用的Condition注解:

@ConditionalOnBean(仅仅在当前上下文中存在某个对象时,才会实例化一个Bean)
@ConditionalOnExpression(当表达式为true的时候,才会实例化一个Bean)
@ConditionalOnMissingBean(仅仅在当前上下文中不存在某个对象时,才会实例化一个Bean)
@ConditionalOnMissingClass(某个class类路径上不存在的时候,才会实例化一个Bean)
@ConditionalOnNotWebApplication(不是web应用)

@ConditionalOnClass(当注解在方法上,某个class位于类路径上,才会实例化一个Bean)
@ConditionalOnClass (当注解于类上, 某个class位于类路径上,否则不解析该注解修饰的配置类)

附录

配置bean以及注册bean的几种方式

复习一下springboot的配置类和把配置类注册进ioc容器的几种方式。springboot的优点之一就是近乎零配置,我们可以抛弃繁杂的xml,使用代码来配置bean。

定义一个bean

public class Person {
    public void sayHi() {
        System.out.println("Hi");
    }
}

@Service或者@Component等注解

在上面的配置类上加@Service或者@Component等注解,springboot会扫描启动类所在的包下面所有带有这些注解的类,实例化bean加到ioc容器。

//@Service
@Component
public class Person {
    public void sayHi() {
        System.out.println("Hi");
    }
}

@Configuration@Bean

使用@Configuration@Bean注解来配置bean到ioc容器,这个类也需要在springboot启动类所在的包或者子包下面,否则无法扫到。

定义一个BeanConfig,并用@Bean配置容器

@Configuration
public class BeanConfig {
    @Bean
    public Person person() {
        return new Person();
    }
}

使用@Import注解

使用@Import注解:相信很多人对@EnableScheduling@EnableCaching等@Enablexxxx系列的注解都不陌生,它们就是使用的是@Import注解来实现开启xx功能的。比如说我们熟悉的@EnableScheduling 注解。这些注解是在AutoConfigurationPackage扫描到的。

在springboot启动类上加一行代码@Import(Person.class)

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@Import(Person.class)
public class AutoConfDemoApplication {

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

这样,我们依然可以把我们自定义的ClockService实例化一个bean加到ioc容器

注意 :1.2点其实是可以给bean设置名称的,比如说@Bean(name=“xx”),这样在一个类型多个实例bean的时候可以按名称注入(@Autowire是默认按类型注入,但可以用Qualifier注解来指定bean的名称; @Resource是按名称注入),第3点只适用于一个类只有一个实例bean的情况下。

ps:@Import注解其实并不是只能导入@Configuration修饰的配置类,还有很多更强大的功能

安装上面分析的,使用自动装配

后面再写…..

扩展阅读

Spring Import 三种用法与源码解读

springboot自动配置以及原理分析


 上一篇
Spring IOC详解 Spring IOC详解
IOC简介IoC(Inversion of Control) 控制反转。取这个名字看起来真的有些高大上,其实背后的道理很简单。——利用反射技术,通过配置文件的方式生成对象,而不是通过New。 Spring 的 IoC 设计支持以下功能:
下一篇 
SpringBoot零配置原理详解 SpringBoot零配置原理详解
序言在正式开始讲解原理之前,着重说明一下如何去学习一门技术。 学习一门新技术,最好的方式就是阅读官网文档、跟着官方文档例子一步一步走。当然如果是为了先感受一下技术的效果,建议先找个例子跑起来,感受感受。 Spring官网 Spring、
  目录