第十章 springboot源码系列-依赖管理和自动配置

目录

前言

一、依赖管理

1.1 spring-boot-starter-parent

1.2 spring-boot-starter-web

二、自动配置

2.1 引入@EnableAutoConfiguration 

2.2  准备了解@EnableAutoConfiguration

2.2.1 导入普通类

2.2.2 导入配置类 

2.2.3 导入ImportSelector 的实现类

2.2.4 导入ImportBeanDefinitionRegistrar

2.3 @EnableAutoConfiguration 

2.3.1 @AutoConfigurationPackage

2.3.2 @Import(AutoConfigurationImportSelector.class)


前言

   关于springboot,我们使用它的主要原因是配置极其简单,其中配置我们过去主要工作一是配置依赖,有时候甚至还要解决依赖冲突;二是需要进行大量的文件配置。springboot正是凭借着依赖管理和自动配置来实现这些功能,大大简化开发成本,当然springboot还有其他很多强大功能。今天我们就可以研究一下这两个功能是怎么实现的。

一、依赖管理

    在Spring Boot入门程序中,项目pom.xml文件有两个核心依赖,分别是spring-boot-starter-
parent和spring-boot-starter-web。你会发现有了这两个导入就可以跑起来一个web服务,依赖一些常用第三方框架在导入dependency时不需要指定版本。实际上spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,spring-boot-starter-web依赖启动器的主要作用是基于依赖传递打包了Web开发场景所需的底层所有依赖(springboot的引入第三方框架starter都是相同原理,这里以spring-boot-starter-web举例剖析)。

1.1 spring-boot-starter-parent

    项目中的pom.xml文件中找到spring-boot-starter-parent依赖,示例代码如下: 

<!-- Spring Boot父项目依赖管理 -->
<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>2.2.9.RELEASE</version>
  <relativePath/> <!-- lookup parent from repository -->
</parent>

    上述代码中,将spring-boot-starter-parent依赖作为Spring Boot项目的统一父项目依赖管理,使用“Ctrl+鼠标左键”进入并查看spring-boot-starter-parent底层源文件。 

首先看 spring-boot-starter-parent 的 properties 节点:

<properties>
   <main.basedir>${basedir}/../../..</main.basedir>
   <java.version>1.8</java.version>
   <resource.delimiter>@</resource.delimiter>
    <!-- delimiter that doesn'tclash with Spring ${} placeholders -->
   <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
   <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
   <maven.compiler.source>${java.version}</maven.compiler.source>
   <maven.compiler.target>${java.version}</maven.compiler.target>
</properties>

在这里 spring-boot-starter-parent 定义了:

  • 工程的Java版本为 1.8 
  • 工程代码的编译源文件编码格式为 UTF-8
  • 工程编译后的文件编码格式为 UTF-8
  • Maven打包编译的版本

 最后来看spring-boot-starter-parent的父依赖 spring-boot-dependencies,spring-boot dependencies的properties节点,这个才是SpringBoot项目的真正管理依赖的项目,里面定义了SpringBoot相关的版本。

 <properties>
    <activemq.version>5.15.13</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.81</appengine-sdk.version>
    <artemis.version>2.10.1</artemis.version>
    <aspectj.version>1.9.6</aspectj.version>
    <assertj.version>3.13.2</assertj.version>
    <atomikos.version>4.0.6</atomikos.version>
    <awaitility.version>4.0.3</awaitility.version>
    <bitronix.version>2.1.4</bitronix.version>
    <build-helper-maven-plugin.version>3.0.0</build-helper-maven-plugin.version>
    <byte-buddy.version>1.10.13</byte-buddy.version>
    <caffeine.version>2.8.5</caffeine.version>
    <cassandra-driver.version>3.7.2</cassandra-driver.version>
    <classmate.version>1.5.1</classmate.version>
    <commons-codec.version>1.13</commons-codec.version>
    <commons-dbcp2.version>2.7.0</commons-dbcp2.version>
    <commons-lang3.version>3.9</commons-lang3.version>
    <commons-pool.version>1.6</commons-pool.version>
   。。。。
</properties>

    spring-boot-starter-parent 通过继承 spring-boot-dependencies 从而实现了SpringBoot的版本依
赖管理,所以我们的SpringBoot工程继承spring-boot-starter-parent后已经具备版本锁定等配置了,这也就是在 Spring Boot 项目中部分依赖不需要写版本号的原因。

1.2 spring-boot-starter-web

    spring-boot-starter-parent父依赖启动器的主要作用是进行版本统一管理,那么项目运行依赖的JAR包是从何而来的?

查看spring-boot-starter-web依赖文件源码,核心代码具体如下:

<dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
      <version>2.2.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-json</artifactId>
      <version>2.2.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-tomcat</artifactId>
      <version>2.2.9.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-validation</artifactId>
      <version>2.2.9.RELEASE</version>
      <scope>compile</scope>
      <exclusions>
        <exclusion>
          <artifactId>tomcat-embed-el</artifactId>
          <groupId>org.apache.tomcat.embed</groupId>
        </exclusion>
      </exclusions>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>5.2.8.RELEASE</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>5.2.8.RELEASE</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>

    从上述代码可以发现,spring-boot-starter-web依赖启动器的主要作用是打包了Web开发场景所需的底层所有依赖(基于依赖传递,当前项目也存在对应的依赖jar包)正是如此,在pom.xml中引入spring-boot-starter-web依赖启动器时,就可以实现Web场景开发,而不需要额外导入Tomcat服务器以及其他Web依赖文件等。

Spring Boot除了提供有上述介绍的Web依赖启动器外,还提供了其他许多开发场景的相关依赖:

   Spring Boot官方并不是针对所有场景开发的技术框架都提供了场景启动器,各类第三方技术框架所在的开发团队主动与Spring Boot框架进行了整合,实现了各自的依赖启动器。在starter命名规范上一般可以区分,一般springboot整合的框架名称在后面,如spring-boot-starter-web,第三方命名自己框架名称在前(如druid-spring-boot-starter)。

二、自动配置

2.1 引入@EnableAutoConfiguration 

    大家都知道启动springboot,只需要在主启动类上标注 @SpringBootApplication 注解。下面我们看下他的源码。

@Target({ElementType.TYPE}) //注解的适用范围,Type表示注解可以描述在类、接口、注解或枚举中
@Retention(RetentionPolicy.RUNTIME) //表示注解的生命周期,Runtime运行时
@Documented //表示注解可以记录在javadoc中
@Inherited  //表示可以被子类继承该注解
@SpringBootConfiguration   // 标明该类为配置类
@EnableAutoConfiguration   // 启动自动配置功能
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes =
TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes =AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
。。。。
}

实际这个注解是一个组合注解,包括3个主要注解。标注它之后就会触发自动配置(@EnableAutoConfiguration)和组件扫描(@ComponentScan)。

   @ComponentScan 默认扫描当前配置类所在包及子包下的所有组件, exclude 属性会将主启动类、自动配置类屏蔽掉。

   @SpringBootConfiguration 并没有对@Configuration 其做实质性扩展,仅仅还是标注一个配置类,被spring当做配置文件使用。

实际做自动配置的是@EnableAutoConfiguration,下面我们重点说一下这个注解。

2.2  准备了解@EnableAutoConfiguration

    在了解 @EnableAutoConfiguration 之前,先了解 SpringFramework 的原生手动装配机制,这对后续阅读 @EnableAutoConfiguration 有很大帮助,因为它运用了这块的装配机制。

在原生的 SpringFramework 中,装配组件有三种方式:

  • 使用模式注解 @Component 等(Spring2.5+)
  • 使用配置类 @Configuration 与 @Bean (Spring3.0+)
  • 使用模块装配 @EnableXXX 与 @Import (Spring3.1+)

  @Component 及衍生注解很常见,咱开发中常用的套路,不再赘述,但模式注解只能在自己编写的代码中标注,无法控制是否装配jar包中的组件。为此可以使用 @Configuration 与 @Bean,手动装配组件,但这种方式不是自动的装配,一旦注册过多,会导致编码成本高,维护不灵活等问题。SpringFramework 提供了模块装配功能,通过给配置类标注 @EnableXXX 注解,再在注解上标注 @Import 注解,即可完成组件自动装配的效果,如果第三方框架使用的是这种方式加载一些类,需要时加上注解就可以,不需要不加注解,就不会装配进容器。

@Import 可以传入四种类型:

  • 普通类
  • 配置类
  • ImportSelector 的实现类
  • ImportBeanDefinitionRegistrar 的实现类

2.2.1 导入普通类

@Import({Red.class})
public @interface EnableColor {
    
}

@EnableColor
@Configuration
public class ColorConfiguration {
    
}

public class App {
    public static void main(String[] args) throws Exception {
        AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(ColorConfiguration.class);
        String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
        Stream.of(beanDefinitionNames).forEach(System.out::println);
    }
}

2.2.2 导入配置类 

@Configuration
public class ColorRegistrarConfiguration {
    
    @Bean
    public Yellow yellow() {
        return new Yellow();
    }  
}

@Import({Red.class,ColorRegistrarConfiguration.class})
public @interface EnableColor {
    
}


ColorConfiguration类和APP同2.2.1

 这样Red、ColorRegistrarConfiguration 和 Yellow 都已注册到IOC容器中。 

2.2.3 导入ImportSelector 的实现类

public class ColorImportSelector implements ImportSelector {
    
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[] {Blue.class.getName(), Green.class.getName()};
    }
}

@Import({Red.class, ColorRegistrarConfiguration.class, ColorImportSelector.class})
public @interface EnableColor {
    
}

ColorImportSelector 没有注册到IOC容器中,两个新的颜色类被注册。

2.2.4 导入ImportBeanDefinitionRegistrar

public class ColorImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        registry.registerBeanDefinition("black", new RootBeanDefinition(Black.class));
    } 
}

@Import({Red.class, ColorRegistrarConfiguration.class, ColorImportSelector.class, ColorImportBeanDefinitionRegistrar.class})
public @interface EnableColor {
    
}

ColorImportBeanDefinitionRegistrar 没有注册到IOC容器中,black类被注册。

以上就是 SpringFramework 的手动装配方法。那 SpringBoot 又是如何做自动装配的呢?

2.3 @EnableAutoConfiguration 

   我们先来看下@EnableAutoConfiguration的源码:


// 自动配置包
@AutoConfigurationPackage
// Spring的底层注解@Import,给容器中导入一个组件;
// 导入的组件是AutoConfigurationPackages.Registrar.class
@Import(AutoConfigurationImportSelector.class)
// 告诉SpringBoot开启自动配置功能,这样自动配置才能生效。
public @interface EnableAutoConfiguration {
      。。。。
}

 这里我们看到@EnableAutoConfiguration也是一个组合注解。

2.3.1 @AutoConfigurationPackage

@Import(AutoConfigurationPackages.Registrar.class) // 导入Registrar中注册的组件
public @interface AutoConfigurationPackage {
}

    咱从一开始学 SpringBoot 就知道一件事:主启动类必须放在所有自定义组件的包的最外层,以保证Spring能扫描到它们。由此可知是它起的作用。它的实现原理是在注解上标注了 @Import,导入了一个 AutoConfigurationPackages.Registrar 。

/**
 * {@link ImportBeanDefinitionRegistrar} to store the base package from the importing
 * configuration.用于保存导入的配置类所在的根包。
 */
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

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

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

}

很明显,它就是实现把主配置所在根包保存起来以便后期扫描用的。分析源码:

 Registrar 实现了 ImportBeanDefinitionRegistrar 接口,它向IOC容器中要手动注册组件。

在重写的 registerBeanDefinitions 方法中,它要调用外部类 AutoConfigurationPackages 的register方法,这里传入的参数就是主启动类的所在包。再看register方法:

private static final String BEAN = AutoConfigurationPackages.class.getName();

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
    // 判断 BeanFactory 中是否包含 AutoConfigurationPackages
    if (registry.containsBeanDefinition(BEAN)) {
        BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
        ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
        // addBasePackages:添加根包扫描包
        constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
    }
    else {
        GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
        beanDefinition.setBeanClass(BasePackages.class);
        beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
        beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
        registry.registerBeanDefinition(BEAN, beanDefinition);
    }
}

AutoConfigurationPackages.Registrar这个类就干一个事,注册一个 Bean ,这个 Bean 就是org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages ,它有一个参数,这个参数是使用了 @AutoConfigurationPackage 这个注解的类所在的包路径,保存自动配置
类以供之后的使用。

2.3.2 @Import(AutoConfigurationImportSelector.class)

 这里@Import导入的就是一个ImportSelector类,我们来看这个AutoConfigurationImportSelector。

它的核心部分,就是实现的 ImportSelector 的 selectImport 方法,然后里面重要的方法getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata) 。

/**
 * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
 * of the importing {@link Configuration @Configuration} class.
 * 
 * 根据导入的@Configuration类的AnnotationMetadata返回AutoConfigurationImportSelector.AutoConfigurationEntry。
 */
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
         AnnotationMetadata annotationMetadata) {
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 【核心】加载候选的自动配置类
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    configurations = removeDuplicates(configurations);
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    checkExcludedClasses(configurations, exclusions);
    configurations.removeAll(exclusions);
    configurations = filter(configurations, autoConfigurationMetadata);
    fireAutoConfigurationImportEvents(configurations, exclusions);
    return new AutoConfigurationEntry(configurations, exclusions);
}

这个方法里有一个非常关键的集合:configurations,这个 集合的数据,都是通过 getCandidateConfigurations 方法来获取:

protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
}

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // SPI机制加载自动配置类
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
             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方法:

源码中使用 classLoader 去加载了指定常量路径下的资源: FACTORIES_RESOURCE_LOCATION ,而这个常量指定的路径实际是:META-INF/spring.factories ,这个文件内容:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
......

之后拿到这个资源文件,以 Properties 的形式加载,并取出 org.springframework.boot.autoconfigure.EnableAutoConfiguration 指定的所有自动配置类(是一个很大的字符串,里面都是自动配置类的全限定类名),装配到IOC容器中,之后自动配置类就会通过 ImportSelector 和 @Import 的机制被创建出来,之后就生效了。

这也就解释了为什么 即便没有任何配置文件,SpringBoot的Web应用都能正常运行,实际就是采取了SPI的机制

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值