SpringBoot自动装配原理

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: SpringBoot自动装配原理

一、引入

SpringBoot以其强大的自动装配功能简化了应用搭建的复杂性,让开发者能够更高效地构建应用。那么,SpringBoot是如何实现这种“智能配置”的呢?本文将深入探讨SpringBoot的自动装配原理,揭示其背后的“魔法”,帮助大家更好地理解和运用这一工具。

二、基本流程

自动配置流程图

SpringBoot的自动装配主要依赖于Spring框架的条件配置(Conditional Configuration)和Java的配置类(Java Config)功能。以下是自动装配的基本原理:

  1. 启动类注解:SpringBoot应用的启动类上通常会有一个@SpringBootApplication注解,这是一个复合注解,它包括了@EnableAutoConfiguration,正是这个注解开启了自动装配的功能。
  2. 自动配置类:在SpringBoot的jar包中,包含了许多以META-INF/spring.factories文件指定的自动配置类。这些类上通常会有@Configuration注解,表明它们是用来定义Bean的配置类。
  3. 条件化配置:这些自动配置类使用了诸如@ConditionalOnClass、@ConditionalOnBean、@ConditionalOnMissingBean等条件注解,来决定是否创建和配置某个Bean。这些条件注解根据类路径中是否存在某个类、是否已经定义了某个Bean等条件来决定配置是否生效。
  4. 依赖注入:当自动配置类中的条件满足时,SpringBoot会自动创建和配置相应的Bean,并通过依赖注入的方式将它们注入到其他需要它们的Bean中。

三、源码解读

为了深入理解自动装配的原理,我们可以查看SpringBoot的源码。以下是一个简化的源码解析过程:

3.1. 启动类

在构建SpringBoot项目的时候,启动类一般是这样的

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

3.2.@SpringBootApplication注解:

这个注解是一个复合注解,它包括了@EnableAutoConfiguration,而@EnableAutoConfiguration又导入了AutoConfigurationImportSelector类。

@Target(ElementType.TYPE)  
@Retention(RetentionPolicy.RUNTIME)  
@Documented  
@Inherited  
@SpringBootConfiguration  
@EnableAutoConfiguration  
@ComponentScan(excludeFilters = { ... })  
public @interface SpringBootApplication {  
    ...  
}

可以发现@SpringBootApplication注解其实是对@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三个注解的封装,也就是它们之间具有相互替代性。本文我们主要讲下自动配置,关注@EnableAutoConfiguration这个注解

@EnableAutoConfiguration主要是标记需要导入哪些配置,这个就是一个key(factoryType),之后依据key获取spring.facteries文件中的配置信息

3.3. @EnableAutoConfiguration

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 自动配置包
@AutoConfigurationPackage
// 导入自动配置的组件
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
  ...
}

有两个需要注意的地方,那就是@AutoConfigurationPackage、@Import(AutoConfigurationImportSelector.class),我们先来看下@AutoConfigurationPackage

3.3.1.@AutoConfigurationPackage

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
// 导入组件AutoConfigurationPackages.Registrar
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
  ...
}

3.3.2. @AutoConfigurationImportSelector类:

这个类是自动装配的关键,它实现了DeferredImportSelector接口,用于在配置类解析时动态地导入其他配置类。

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, EnvironmentAware {  
    ...  
    @Override  
    public String[] selectImports(AnnotationMetadata annotationMetadata) {  
        // 这里会加载META-INF/spring.factories中定义的自动配置类  
        ...  
    }  
    ...  
}

进入到组件AutoConfigurationPackages.Registrar

3.3.2.1. AutoConfigurationPackages.Registrar
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
      // 注册当前主程序的同级以及子集的包组件,其实就是注册了一个Bean的定义
      register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
    }
    @Override
    public Set<Object> determineImports(AnnotationMetadata metadata) {
      return Collections.singleton(new PackageImports(metadata));
    }
  }
  1. 第一步,我们需要获取被注解标记的类,即***Application。
  2. 在这个类所在的包及其子包中,我们将扫描所有的类文件。
  3. 当扫描到这些类时,我们将把它们导入到Spring容器中进行管理。这意味着它们可以被其他组件使用,并且可以被配置文件spring.factories引用。
3.3.2.3. AutoConfigurationImportSelector类
看下@Import(AutoConfigurationImportSelector.class),通过Spring底层注解@Import,给容器导入一个组件,这个组件就是AutoConfigurationImportSelector.class,它实现了DeferredImportSelector,关键方法是selectImports()
/**
   * 获取需要导入的全限定类名数组
   *
   * @param annotationMetadata
   * @return
   */
  @Override
  public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 配置参数 spring.boot.enableautoconfiguration 是否打开,默认开启
    if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
    }
    // 获取自动配置对象,对象包含需要配置的全限定类名列表和需要排除的列表
    AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
  }
  /**
   * spring.boot.enableautoconfiguration 是否打开,默认处于打开状态
   *
   * @param metadata
   * @return
   */
  protected boolean isEnabled(AnnotationMetadata metadata) {
    if (getClass() == AutoConfigurationImportSelector.class) {
      return getEnvironment().getProperty(EnableAutoConfiguration.ENABLED_OVERRIDE_PROPERTY, Boolean.class, true);
    }
    return true;
  }

通过getAutoConfigurationEntry方法来获取的配置

/**
   * 基于annotationMetadata发现标有{@link Configuration @Configuration}的配置类并返回{@link AutoConfigurationEntry}
   *
   * @param annotationMetadata 配置类的注解元数据
   * @return 应该被导入的自动配置
   */
  protected AutoConfigurationEntry getAutoConfigurationEntry(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 = getConfigurationClassFilter().filter(configurations);
    // 触发导入自动配置事件
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 返回自动配置对象
    return new AutoConfigurationEntry(configurations, exclusions);
  }

获取原始的配置集合方法是getCandidateConfigurations

/**
   * 返回需要自动配置的类名列表
   *
   * @param metadata   the source metadata
   * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
   *                   attributes}
   * @return a list of candidate configurations
   */
  protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 获取需要被加载的FactoryClass,也就是key
    Class<?> clazz = getSpringFactoriesLoaderFactoryClass();
    // 获取需要配置的全限定类名集合
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(clazz, 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;
  }
  
  /**
   * 获取需要加载的工厂类
   * 也就是key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的配置都需要被加载到IoC
   *
   * @return the factory class
   */
  protected Class<?> getSpringFactoriesLoaderFactoryClass() {
    return EnableAutoConfiguration.class;
  }

SpringFactoriesLoader类属于Spring框架私有的一种扩展方案,其主要功能就是从指定的配置文件

META-INF/spring.factories加载配置,来加载到需要自动配置的类的全限定名列表,接下来我们到Spring框架中去看下SpringFactoriesLoader.loadFactoryNames()静态方法.

/**
   * 使用给定的类加载器,从META-INF/spring.factories中加载给定的工厂类型实现
   * 在spring5.3中,如果给定的工厂类型下的实现类名发现不止一次,会进行去重处理
   *
   * @param factoryType factoryType,eg:org.springframework.beans.BeanInfoFactory=xxx
   * @param classLoader 用于加载资源的类加载器
   *                    {@code null} to use the default
   * @throws IllegalArgumentException if an error occurs while loading factory names
   * @see #loadFactories
   */
  public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
      classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    // 加载Factories文件
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
  }

关键方法在loadSpringFactories

/**
   * 加载Factories文件
   *
   * @param classLoader factories文件解析之后的map
   * @return
   */
  private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    // 现在缓存中查找,classLoader为key
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
      return result;
    }
    result = new HashMap<>();
    try {
      // 获取资源,这里固定目录为:META-INF/spring.factories
      Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
      while (urls.hasMoreElements()) {
        URL url = urls.nextElement();
        UrlResource resource = new UrlResource(url);
        // 将资源加载为Properties
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        // 遍历配置
        for (Map.Entry<?, ?> entry : properties.entrySet()) {
          String factoryTypeName = ((String) entry.getKey()).trim();
          // 获取value的数组,一般为类的全限定名
          // 比如 org.springframework.beans.BeanInfoFactory=org.springframework.beans.ExtendedBeanInfoFactory
          String[] factoryImplementationNames =
              StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
          // 添加到result
          for (String factoryImplementationName : factoryImplementationNames) {
            result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                .add(factoryImplementationName.trim());
          }
        }
      }
      // 对value进行去重
      result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
          .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
      // 添加到缓存中
      cache.put(classLoader, result);
    } catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
          FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
    return result;
  }

获取配置集合,主要流程如下:

  1. 检查缓存:首先,程序会检查缓存(例如一个内存中的数据结构,如ConcurrentHashMap)中是否已经有需要的配置集合。如果有,则直接返回,避免重复加载和解析配置文件。
  2. 加载资源:如果缓存中没有找到配置,程序会利用ClassLoader的getResources方法从类路径(classpath)中加载所有匹配的资源文件。对于Spring
    Boot来说,这通常是查找所有META-INF/spring.factories文件。
  3. 解析文件:加载到资源文件后,程序会解析这些文件的内容。这通常涉及读取文件的每一行,并将每一行解析成key=value的格式,然后保存到集合中。
  4. 缓存结果:解析完所有文件后,程序会将结果保存到缓存中,以便下次可以直接从缓存中获取,无需再次加载和解析文件。
  5. 返回结果:最后,程序返回解析后的配置集合,通常是一个Map<String,
    List>类型的数据结构,其中key是工厂类型(如org.springframework.boot.autoconfigure.EnableAutoConfiguration),value是与该类型关联的配置值列表。

3.4. META-INF/spring.factories文件:

这个文件位于SpringBoot的jar包中,它定义了要加载的自动配置类。

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\  
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\  
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\  
...

4、使用注意事项

虽然SpringBoot的自动装配功能强大且方便,但在使用时还是需要注意以下几点:

避免自动配置冲突:有时候,自动配置可能会与你的手动配置冲突。例如,如果你在配置文件中定义了一个DataSource Bean,而SpringBoot又尝试自动配置一个DataSource Bean,这可能会导致冲突。此时,你可以使用@Primary注解来指定优先使用的Bean,或者使用@EnableAutoConfiguration(exclude={...}来排除某些自动配置类。

注意依赖版本:确保你使用的SpringBoot版本与其他依赖的版本兼容。不兼容的版本可能导致自动装配失败或出现异常行为。

仔细阅读文档:SpringBoot的官方文档非常详尽,包含了大量关于自动装配的信息和示例。在遇到问题时,首先应该查阅官方文档。

调试和日志:如果遇到自动装配相关的问题,可以开启Spring的调试日志(通过设置logging.level.org.springframework=DEBUG),这将帮助你更好地理解自动装配过程中的细节。

5、小结

配合@EnableAutoConfiguration注解使用时,它主要扮演的是配置查找器的角色。这个注解利用其自身的完整类名org.springframework.boot.autoconfigure.EnableAutoConfiguration作为搜索的关键词(Key),来定位并加载一组相关的@Configuration类。

因此,@EnableAutoConfiguration的大致自动配置流程可以描述为以下几个步骤:

  1. 首先,它会在classpath中扫描所有的META-INF/spring.factories配置文件。
  2. 接着,它会从这些配置文件中提取所有以org.springframework.boot.autoconfigure.EnableAutoConfiguration为key的配置项(注意,这里的key应严格匹配,包括大小写,且不应有笔误,如EnableautoConfiguration应为EnableAutoConfiguration)。
  3. 最后,通过Java反射机制,它会实例化那些标注有@Configuration注解的Java配置类,这些类采用JavaConfig的方式定义了IoC容器的配置。完成实例化后,这些配置会被统一注册到Spring的IoC容器中,从而完成了自动配置的过程。
相关文章
|
8天前
|
XML Java 开发者
Spring Boot开箱即用可插拔实现过程演练与原理剖析
【11月更文挑战第20天】Spring Boot是一个基于Spring框架的项目,其设计目的是简化Spring应用的初始搭建以及开发过程。Spring Boot通过提供约定优于配置的理念,减少了大量的XML配置和手动设置,使得开发者能够更专注于业务逻辑的实现。本文将深入探讨Spring Boot的背景历史、业务场景、功能点以及底层原理,并通过Java代码手写模拟Spring Boot的启动过程,为开发者提供一个全面的理解。
19 0
|
12天前
|
Java Spring
SpringBoot自动装配的原理
在Spring Boot项目中,启动引导类通常使用`@SpringBootApplication`注解。该注解集成了`@SpringBootConfiguration`、`@ComponentScan`和`@EnableAutoConfiguration`三个注解,分别用于标记配置类、开启组件扫描和启用自动配置。
49 17
|
1月前
|
Java Spring 容器
springboot @RequiredArgsConstructor @Lazy解决循环依赖的原理
【10月更文挑战第15天】在Spring Boot应用中,循环依赖是一个常见问题,当两个或多个Bean相互依赖时,会导致Spring容器陷入死循环。本文通过比较@RequiredArgsConstructor和@Lazy注解,探讨它们解决循环依赖的原理和优缺点。@RequiredArgsConstructor通过构造函数注入依赖,使代码更简洁;@Lazy则通过延迟Bean的初始化,打破创建顺序依赖。两者各有优势,需根据具体场景选择合适的方法。
57 4
|
4月前
|
Java 应用服务中间件 开发者
Java面试题:解释Spring Boot的优势及其自动配置原理
Java面试题:解释Spring Boot的优势及其自动配置原理
119 0
|
2月前
|
Java 应用服务中间件 API
Vertx高并发理论原理以及对比SpringBoot
Vertx 是一个基于 Netty 的响应式工具包,不同于传统框架如 Spring,它的侵入性较小,甚至可在 Spring Boot 中使用。响应式编程(Reactive Programming)基于事件模式,通过事件流触发任务执行,其核心在于事件流 Stream。相比多线程异步,响应式编程能以更少线程完成更多任务,减少内存消耗与上下文切换开销,提高 CPU 利用率。Vertx 适用于高并发系统,如 IM 系统、高性能中间件及需要较少服务器支持大规模 WEB 应用的场景。随着 JDK 21 引入协程,未来 Tomcat 也将优化支持更高并发,降低响应式框架的必要性。
Vertx高并发理论原理以及对比SpringBoot
|
2月前
|
Java 开发者 数据格式
【Java笔记+踩坑】SpringBoot基础4——原理篇
bean的8种加载方式,自动配置原理、自定义starter开发、SpringBoot程序启动流程解析
【Java笔记+踩坑】SpringBoot基础4——原理篇
|
4月前
|
SQL Java 数据库连接
springboot~mybatis-pagehelper原理与使用
【7月更文挑战第15天】MyBatis-PageHelper是用于MyBatis的分页插件,基于MyBatis的拦截器机制实现。它通过在SQL执行前动态修改SQL语句添加LIMIT子句以支持分页。使用时需在`pom.xml`添加依赖并配置方言等参数。示例代码: PageHelper.startPage(2, 10); List&lt;User&gt; users = userMapper.getAllUsers(); PageInfo&lt;User&gt; pageInfo = new PageInfo&lt;&gt;(users); 这使得分页查询变得简单且能获取总记录数等信息。
108 2
|
4月前
|
Java 开发者 Spring
深入理解Spring Boot中的自动配置原理
深入理解Spring Boot中的自动配置原理
|
4月前
|
开发框架 Java 开发者
Spring Boot中的自动装配原理
Spring Boot中的自动装配原理
|
5月前
|
消息中间件 Java Maven
深入理解Spring Boot Starter:概念、特点、场景、原理及自定义starter
深入理解Spring Boot Starter:概念、特点、场景、原理及自定义starter

热门文章

最新文章