看完就會的SpringBoot自動裝配原理


前言

我相信,只要你用過Spring Boot,就會對這樣一個現象非常的好奇:

引入一個組件依賴,加個配置,這個組件就生效了。

舉個例子來說,比如我們常用的Redis, 在Spring Boot中的使用方式是這樣的:

1.引入依賴

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

2.編寫配置

spring:
  redis:
    database: 0
    timeout: 5000ms
    host: 127.0.0.1
    port: 6379
    password: 123456

好了,接下來只需要使用時注入RedisTemplate就能使用了,像這樣:

@Autowired
private RedisTemplate redisTemplate;

這期間,我們做了什么嘛?我們什么也沒有做,那么,這個RedisTemplate對象是怎么注入到Spring容器中的呢?

接下來,就讓我們帶着這樣的疑問逐步剖析其中的原理,這個原理就叫做自動裝配。

SPI

先不着急,在這之前,我們先來了解了解上古大法:SPI機制。

SPI ,全稱為 Service Provider Interface(服務提供者接口),是一種服務發現機制。它通過在classpath路徑下的META-INF/services文件夾查找文件,自動加載文件中所定義的類。

栗子

建一個工程,結構如下

provider 為服務提供方,可以理解為我們的框架

zoo 為使用方,因為我的服務提供接口叫Animal,所以所有實現都是動物~

pom.xml里面啥都沒有

1. 定義一個接口

在provider模塊中定義接口Animal

package cn.zijiancode.spi.provider;

/**
 * 服務提供者 動物
 */
public interface Animal {

    // 叫
    void call();
}

2. 使用該接口

在zoo模塊中引入provider

<dependency>
  <groupId>cn.zijiancode</groupId>
  <artifactId>provider</artifactId>
  <version>1.0.0</version>
</dependency>

寫一個小貓咪實現Animal接口

public class Cat implements Animal {
    @Override
    public void call() {
        System.out.println("喵喵喵~~");
    }
}

寫一個狗子也實現Animal接口

public class Dog implements Animal {

    @Override
    public void call() {
        System.out.println("汪汪汪!!!");
    }
}

3. 編寫配置文件

新建文件夾META-INF/services

在文件夾下新建文件cn.zijiancode.spi.provider.Animal

對,你沒看錯,接口的全限定類名就是文件名

編輯文件

cn.zijiancode.spi.zoo.Dog
cn.zijiancode.spi.zoo.Cat

里面放實現類的全限定類名

3. 測試

package cn.zijiancode.spi.zoo.test;

import cn.zijiancode.spi.provider.Animal;

import java.util.ServiceLoader;

public class SpiTest {

    public static void main(String[] args) {
        // 使用Java的ServiceLoader進行加載
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        load.forEach(Animal::call);
    }
}

測試結果:

汪汪汪!!!
喵喵喵~~

整個項目結構如下:

借助SPI理解自動裝配

回顧一下我們做了什么,我們在resources下創建了一個文件,里面放了些實現類,然后通過ServiceLoader這個類加載器就把它們加載出來了。

假設有人已經把編寫配置之類的前置步驟完成了,那么我們是不是只需要使用下面的這部分代碼,就能將Animal有關的所有實現類調度出來。

// 使用Java的ServiceLoader進行加載
ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
load.forEach(Animal::call);

再進一步講,如果再有人把上面這部分代碼也給寫了,然后把這些實現類全部注入到Spring容器里,那會發生什么?

哇塞,那我他喵的不是就能直接注入然后汪汪汪了嗎?!

相信到這里大家心里都已經有個譜了

找找Spring Boot中的配置文件

在SPI機制中,是通過在組件下放入一個配置文件完成的,那么Spring Boot是不是也這樣的呢?我們就來找一找吧。

打開redis的組件

咦,這里面卻並沒有看到有關自動裝配的文件,難道我們的猜想是錯的嘛?

別急,其實所有spring-boot-starter-x的組件配置都是放在spring-boot-autoconfigura的組件中的

這里有個spring.factories的文件,翻譯一下就是spring的工廠,咦,有點像了,打開看看

其他的我們先不用管,可以很明顯的看到最下面有個自動配置的注釋,key還是個EnableAutoConfiguration,開啟自動配置!噢噢噢噢噢!找到了找到了!

往下翻一下,看看有沒有Redis相關的。

再打開這個RedisAutoConfiguration類,看看里面是些什么代碼

OMG! 破案了破案了!

現在,配置文件我們也找到了:spring.factories,也實錘了就是通過這個配置文件進行的自動配置。

那么,我們來嘗試還原一下案情經過:通過某種方式讀取spring.factories文件,緊接着把里面所有的自動配置類加載到Spring容器中,然后就可以通過Spring的機制將配置類的@Bean注入到容器中了。

接下來,我們就來學習一下這個某種方式究竟是什么吧~

Spring中的一些注入方式

阿鑒先透露一下,這個某種方式,其實就是某一種注入方式,我們先來看看Spring中有哪些注入方式

聊起Spring,我可是老手了,有興趣的小伙伴可以看看我的Spring源碼分析系列:https://zijiancode.cn/categories/source-framework

關於注入方式,相信小伙伴肯定也是:就這?

類似於@Component,@Bean這些,阿鑒就不說了,大家肯定見過一種這樣的注解:EnableXxxxx

比如:EnableAsync開啟異步,EnableTransactionManagement開啟事務

大家好不好奇這樣的注解是怎么生效的?

點開看看唄

嘿,其實里面是個Import注解

Import注解的3種使用方式

我知道,肯定有小伙伴懂得Import注解如何使用,但是為了照顧不懂的小伙伴,阿鑒還是要講一講,懂的小伙伴就當復習啦

1.普通的組件

public class Man {

    public Man(){
        System.out.println("Man was init!");
    }
}
@Import({Man.class})
@Configuration
public class MainConfig {
}

在配置類上使用@Import注解,值放入需要注入的Bean就可以啦

2.實現ImportSelector接口

public class Child {

    public Child(){
        System.out.println("Child was init!");
    }
}
public class MyImport implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.my.source.spring.start.forimport.Child"};
    }
}
@Import({MyImport.class})
@Configuration
public class MainConfig {
}

這種方式往Spring中注入的是一個ImportSelector,當Spring掃描到MyImport,將會調用selectImports方法,將selectImports中返回的String數組中的類注入到容器中。

3.實現ImportBeanDefinitionRegistrar接口

public class Baby {

    public Baby(){
        System.out.println("Baby was init!");
    }
}
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        BeanDefinition beanDefinition = new RootBeanDefinition(Baby.class);
        registry.registerBeanDefinition("my-baby",beanDefinition);
    }
}
@Import({MyImportBeanDefinitionRegistrar.class})
@Configuration
public class MainConfig {
}

類似於第二種,當Spring掃描到該類時,將會調用registerBeanDefinitions方法,在該方法中,我們手動往Spring中注入了一個Baby的Bean,理論上可以通過這種方式不限量的注入任何的Bean

SpringBootApplication注解

我們在使用SpringBoot項目時,用到的唯一的注解就是@SpringBootApplication,所以我們唯一能下手的也只有它了,打開它看看吧。

嘿!看看我們發現了什么?EnableAutoConfiguration!妥妥的大線索呀

EnableAutoConfiguration本質上也是通過Import完成的,並且Import了一個Selector

讓我們瞧一瞧里面的代碼邏輯吧~

selectImports

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry

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);
  // 過濾掉不具備注入條件的配置類,通過Conditional注解
  configurations = getConfigurationClassFilter().filter(configurations);
  // 通知自動配置相關的監聽器
  fireAutoConfigurationImportEvents(configurations, exclusions);
  // 返回所有自動配置類
  return new AutoConfigurationEntry(configurations, exclusions);
}

我們主要看看是如何從配置文件讀取的

getCandidateConfigurations

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  // 這里就是關鍵,使用SpringFactoriesLoader加載所有配置類,是不是像我們SPI的ServicesLoader
  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;
}

getSpringFactoriesLoaderFactoryClass

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

結合上一步,就是加載配置文件,並且讀取key為EnableAutoConfiguration的配置

loadFactoryNames

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

  try {
    // FACTORIES_RESOURCE_LOCATION的值為:META-INF/spring.factories
    // 這步就是意味中讀取classpath下的META-INF/spring.factories文件
    Enumeration<URL> urls = (classLoader != null ?
                             classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                             ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
    // 接下來就是讀取出文件內容,封裝成map的操作了
    result = new LinkedMultiValueMap<>();
    while (urls.hasMoreElements()) {
      URL url = urls.nextElement();
      UrlResource resource = new UrlResource(url);
      Properties properties = PropertiesLoaderUtils.loadProperties(resource);
      for (Map.Entry<?, ?> entry : properties.entrySet()) {
        String factoryTypeName = ((String) entry.getKey()).trim();
        for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
          result.add(factoryTypeName, factoryImplementationName.trim());
        }
      }
    }
    cache.put(classLoader, result);
    return result;
  }
  catch (IOException ex) {
    throw new IllegalArgumentException("Unable to load factories from location [" +
                                       FACTORIES_RESOURCE_LOCATION + "]", ex);
  }
}

over, 后面的過濾邏輯阿鑒就不在這里說了,畢竟本節的重點是自動裝配機制,小伙伴明白了原理就ok啦

ps: 因為后面的邏輯其實挺復雜的,展開了說就太多啦

小結

本篇介紹了關於SpringBoot的自動裝配原理,我們先通過SPI機制進行了小小的熱身,然后再根據SPI的機制進行推導Spring的自動裝配原理,中間還帶大家回顧了一下@Import注解的使用,最后成功破案~

下節預告:實現自定義starter

看完之后想必有所收獲吧~ 想要了解更多精彩內容,歡迎關注公眾號:程序員阿鑒,阿鑒在公眾號歡迎你的到來~

個人博客空間:https://zijiancode.cn/archives/springbootautoconfig


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM