SpringBoot之自动装配

一、前言

Spring基于Spring4.0设计,不仅继承了Spring框架原有的优秀特性,而且还通过简化配置来进一步简化了Spring应用的整个搭建和开发过程。另外SpringBoot通过集成大量的框架使得依赖包的版本冲突,以及引用的不稳定性等问题得到了很好的解决。

例如没有SpringBoot的时候,我们写一个RestFul Web服务,还首先需要进行如下配置。

java 复制代码
@Configuration
public class RestConfiguration {

    @Bean
    public View jsonTemplate() {
        MappingJackson2JsonView view = new MappingJackson2JsonView();
        view.setPrettyPrint(true);
        return view;
    }

    @Bean
    public ViewResolver viewResolver() {
        return new BeanNameViewResolver();
    }
}

spring-servlet.xml

xml 复制代码
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xsi:schemaLocation="http://www.springframework.org/schema/beans 
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context/ 
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc/ 
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:component-scan base-package="com.java.demo" />
    <mvc:annotation-driven />

    <!-- 开启跳转视图解析器 -->
    <bean name="viewResolver"
          class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
    <!-- JSON Support -->
    <bean name="jsonTemplate"
          class="org.springframework.web.servlet.view.json.MappingJackson2JsonView"/>

</beans>

但是,SpringBoot项目,我们只需要添加相关依赖,无需配置,通过启动下面的main方法即可。

java 复制代码
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

并且,我们通过SpringBoot的全局配置文件application.propertiesapplication.yml即可对项目进行设置比如更换端口号,配置JPA属性等等。

为什么SpringBoot使用起来这么简便呢?这得益于其自动装配。自动装配可以说是SpringBoot的核心,那究竟什么是自动装配呢?

二、简介

现在提到自动装配的时候,一般会和SpringBoot联系在一起。但是,实际上Spring Framework早就实现了这个功能。SpringBoot只是在其基础上,通过SPI的方式,做了进一步优化。

SpringBoot定义了一套接口规范,这套规范规定:SpringBoot在启动时会扫描外部引用jar包中的META-INF/spring.factories文件,将文件中配置的类型信息加载到Spring容器(此处涉及到JVM类加载机制与Spring的容器知识),并执行类中定义的各种操作。对于外部jar来说,只需要按照SpringBoot定义的标准,就能将自己的功能装置进SpringBoot

没有SpringBoot的情况下,如果我们需要引入第三方依赖,需要手动配置,非常麻烦。但是,SpringBoot中,我们直接引入一个starter即可。

比如你想要在项目中使用redis的话,直接在项目中引入对应的starter即可。

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

引入starter之后,我们通过少量注解和一些简单的配置就能使用第三方组件提供的功能了。

在我看来,自动装配可以简单理解为:通过注解或者一些简单的配置就能在SpringBoot的帮助下实现某块功能。

三、自动装配原理

我们先看一下SpringBoot的核心注解类SpringBootApplication。这个注解是SpringBoot启动类上的一个注解,是一个组合注解,也就是由其他注解组合起来,它的主要作用就是标记说明这个类是SpringBoot的主配置类,SpringBoot应该运行这个类里面的main()方法来启动程序。

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 以上为元注解信息
@SpringBootConfiguration       // <1.>
@ComponentScan                 // <2.>
@EnableAutoConfiguration       // <3.>
public @interface SpringBootApplication {

    // ...

}

@SpringBootApplication主要由三个子注解组成:

  • @SpringBootConfiguration:允许在上下文中注册额外的bean或导入其他配置类。
  • @EnableAutoConfiguration:启用SpringBoot的自动配置机制(是实现自动装配的核心注解)。
  • @ComponentScan:扫描当前包及子包的注解。

3.1 SpringBootConfiguration注解

这个注解包含了@Configuration@Configuration里面又包含了一个@Component注解,也就是说,这个注解标注在哪个类上,就表示当前这个类是一个配置类,而配置类也是spring容器中的组件

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration // 实际上它也是一个配置类
public @interface SpringBootConfiguration {

    //...
}

3.2 EnableAutoConfiguration注解

EnableAutoConfiguration只是一个简单地注解,自动装配核心功能的实现实际是通过AutoConfigurationImportSelector类。@EnableAutoConfiguration是实现自动装配的重要注解。

java 复制代码
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage                      // 作用:将main包下的所欲组件注册到容器中
@Import({AutoConfigurationImportSelector.class})  // 加载自动装配类xxxAutoconfiguration
public @interface EnableAutoConfiguration {

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

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

    String[] excludeName() default {};
}

我们现在重点分析下AutoConfigurationImportSelector类到底做了什么?

3.2.1 AutoConfigurationImportSelector

AutoConfigurationImportSelector加载自动装配类,继承体系如下:

java 复制代码
public class AutoConfigurationImportSelector implements DeferredImportSelector,
        BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware,
        EnvironmentAware, Ordered {

}

public interface DeferredImportSelector extends ImportSelector {

}

public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}
3.2.1.1 ImportSelector方法

可以看出,AutoConfigurationImportSelector类实现了ImportSelector接口,也就实现了这个接口中的selectImports方法,该方法主要用于获取所有符合条件的类的全限定类名,这些类需要被加载到IOC容器中

java 复制代码
private static final String[] NO_IMPORTS = new String[0];

public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // <1>. 判断自动装配开关是否打开
    if (!this.isEnabled(annotationMetadata)) {
        return NO_IMPORTS;
    } else {
        // <2>. 获取所有需要装配的bean
        AutoConfigurationMetadata autoConfigurationMetadata =
                    AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
        AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = 
                    this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
   }
}

这里我们需要重点关注一下getAutoConfigurationEntry()方法,这个方法主要负责加载自动配置类的。

该方法调用链如下:

3.2.1.2 getAutoConfigurationEntry方法

现在我们结合getAutoConfigurationEntry()的源码来详细分析一下:

java 复制代码
    private static final AutoConfigurationEntry EMPTY_ENTRY = new AutoConfigurationEntry();

    AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
             AnnotationMetadata annotationMetadata) {
        // <1>. 判断自动装配开关是否打开
        if (!this.isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        } else {
            // <2>. 获取EnableAutoConfiguration注解的exclude和excludeName属性值
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            // <3>. 获取spring.factories中EnableAutoConfiguration的配置值
            List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            // <4>. 去除重复项
            configurations = this.removeDuplicates(configurations);
            // 获取限制候选配置的所有排除项(找到不希望自动装配的配置类)
            Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
            // 对参数exclusions进行验证,exclusion必须为自动装配的类,否则抛出异常
            this.checkExcludedClasses(configurations, exclusions);
            // 移除exclusions
            configurations.removeAll(exclusions);
            // 根据maven导入的启动器过滤出需要导入的配置类
            configurations = this.filter(configurations, autoConfigurationMetadata);
            // 配置监听事件
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
        }
    }

第1步:

判断自动装配开关是否打开。默认spring.boot.enableautoconfiguration=true,可在application.propertiesapplication.yml中设置

第2步:

用于获取EnableAutoConfiguration注解中的excludeexcludeName

第3步:

获取需要自动装配的所有配置类,读取META-INF/spring.factories

java 复制代码
spring-boot/spring-boot-project/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories

从下图可以看到这个文件的配置内容都被我们读取到了。XXXAutoConfiguration的作用就是按需加载组件。

不光是这个依赖下的META-INF/spring.factories被读取到,所有SpringBoot Starter下的META-INF/spring.factories都会被读取到。

所以,你可以清楚滴看到,druid数据库连接池的SpringBoot Starter就创建了META-INF/spring.factories文件。

如果,我们自己要创建一个SpringBoot Starter,这一步是必不可少的。

第4步

spring.factories中这么多配置,每次启动都要全部加载么?

很明显,这是不现实的。我们debug到后面你会发现,configurations的值变小了。

因为,这一步有经历了一遍筛选,@ConditionalOnXXX中的所有条件都满足,该类才会生效。

java 复制代码
@Configuration
// 检查相关的类:RabbitTemplate和Channel是否存在
// 存在才会加载
@ConditionalOnClass({ RabbitTemplate.class, Channel.class })
@EnableConfigurationProperties(RabbitProperties.class)
@Import(RabbitAnnotationDrivenConfiguration.class)
public class RabbitAutoConfiguration {

    //...
}

以下是SpringBoot提供的条件注解

  • @ConditionalOnBean:当容器里有指定Bean的条件下
  • @ConditionalOnMissingBean:当容器里没有指定Bean的情况下
  • @ConditionalOnSingleCandidate:当指定Bean在容器中只有一个,或者虽然有多个但是指定首选Bean
  • @ConditionalOnClass:当类路径下有指定类的条件下
  • @ConditionalOnMissingClass:当类路径下没有指定类的条件下
  • @ConditionalOnProperty:指定的属性是否有指定的值
  • @ConditionalOnResource:类路径是否有指定的值
  • @ConditionalOnExpression:基于SpEL表达式作为判断条件
  • @ConditionalOnJava:基于Java版本作为判断条件
  • @ConditionalOnJndi:在JNDI存在的条件下差在指定的位置
  • @ConditionalOnNotWebApplication:当前项目不是Web项目的条件下
  • @ConditionalOnWebApplication:当前项目是Web项目的条件下
3.2.1.3 getCandidateConfigurations方法
java 复制代码
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;
}

通过loadFactoryNames方法,扫描classpath下的META-INF/spring.factories文件,里面是以key=value形式存储,读取其中key=EnableAutoConfigurationvalue就是需要装配的配置类,也就是getCandidateConfigurations返回的值。

3.3 ComponentScan注解

扫描被@Component(@Service@Controller@Repostory@RestController)注解的bean,注解默认会扫描启动类所在的包下所有的类,可以自定义不扫描某些bean。如下所示,容器中将排除TypeExcludeFilterAutoConfigurationExcludeFilter

java 复制代码
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class}),
        @Filter(type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class})
})
public @interface SpringBootApplication {

    //...
}

3.4 小结

SpringBoot自动装配的流程

  1. SpringBoot启动的时候会创建一个SpringApplication对象,在对象的构造方法里面会进行一些参数的初始化工作,最主要的是判断当前应用程序的类型以及设置初始化器以及监听器,并在这个过程中会加载整个应用程序的spring.factories文件,将文件中的内容放到缓存当中,方便后续获取;
  2. SpringApplication对象创建完成之后会执行run()方法来完成整个应用程序的启动,启动的过程中有两个最主要的方法prepareContext()refreshContext(),在这两个方法中完成了自动装配的核心功能,在run()方法里还执行了一些包括上下文对象的创建,打印banner图,异常报告期的准备等各个准备工作,方便后续进行调用;
  3. prepareContext()中主要完成的是对上下文对象的初始化操作,包括属性的设置,比如设置环境变量。在整个过程中有一个load()方法,它主要是完成一件事,那就是将启动类作为一个beanDefinition注册到registry,方便后续在进行BeanFactoryPostProcessor调用执行的时候,可以找到对应执行的主类,来完成对@SpringBootApplication@EnableAutoConfiguration等注解的解析工作;
  4. refreshContext()方法中会进行整个容器的刷新过程,会调用spring中的refresh()方法,refresh()方法中有13个非常关键的方法,来完成整个应用程序的启动。而在自动装配过程中,会调用的关键的一个方法就是invokeBeanFactoryPostProcessors()方法,在这个方法中主要是对ConfigurationClassPostProcessor类的处理,这个类是BFPP(BeanFactoryPostProcessor)的子类,因为实现了BDRPP(BeanDefinitionRegistryPostProcessor)接口,在调用的时候会先调用BDRPP中的postProcessBeanDefinitionRegistry()方法,然后再调用BFPP中的postProcessBeanFactory()方法,在执行postProcessBeanDefinitionRegistry()方法的时候会解析处理各种的注解,包含@PropertySource@ComponentScan@Bean@Import等注解,最主要的是对@Import注解的解析;
  5. 在解析@Import注解的时候,会有一个getImport()方法,从主类开始递归解析注解,把所有包含@Import的注解都解析到,然后在processImport()方法中对import的类进行分类,例如AutoConfigurationImportSelect归属于ImportSelect的子类,在后续的过程中会调用DeferredImportSelectorHandler类里面的process方法,来完成整个EnableAutoConfiguration的加载。

四、自定义实现Starter

如何实现自定义线程池starter,分为以下几步:

  1. 创建threadpool-spring-boot-starter工程
  2. pom.xml引入SpringBoot相关依赖
xml 复制代码
<properties>
    <spring-boot.version>2.1.6.RELEASE</spring-boot.version>
    <gson.version>2.6</gson.version>
</properties>

<!-- spring-boot所有项目依赖包 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-dependencies</artifactId>
            <version>${spring-boot.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
  1. 创建ThreadPoolAutoConfiguration
java 复制代码
@Configuration
public class ThreadPoolAutoConfiguration {

    @bean
    @ConditionalOnClass(ThreadPoolExecutor.class)
    public ThreadPoolExecutor MyThreadPool() {
        return new ThreadPoolExecutor(10, 10, 10, TimeUnit.SECONDS, new ArrayBlockingQueue(10));
    }
}
  1. threadpool-spring-boot-starter工程的resources包下创建META-INF/spring.factories文件
java 复制代码
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.example.ThreadPoolAutoConfiguration
  1. 在工程pom.xml中引入threadpool-spring-boot-starter
xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>threadpool-spring-boot-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

测试类

java 复制代码
@SpringBootTest
@RunWith(SpringRunner.class)
public class Test {
    @Resource
    private ThreadPoolExecutor threadPool;

    @Test
    public void test() {
        System.out.println("corePoolSize = " + threadPool.getCorePoolSize());
    }
}

测试通过!

五、总结

SpringBoot通过@EnableAutoConfiguration开启自动装配,通过SpringFactoriesLoader最终加载META-INF/spring.factories中的自动配置类实现自动装配,自动配置类其实就是通过@Conditional按需加载的配置类,想要其生效必须引入spring-boot-starter-xxx包实现起步依赖。

参考文章

相关推荐
爱勇宝3 小时前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
IT_陈寒8 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码9 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo9 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志10 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..10 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects10 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰10 小时前
一些基础和问题解决
后端
陈随易10 小时前
前端项目部署只要30秒
前端·后端·程序员
YIAN10 小时前
从零手写文件读取 MCP 服务:一文吃透 Model Context Protocol 全链路通信原理
前端·后端·mcp