乐趣区

关于springboot:看完就会的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/categor…

对于注入形式,置信小伙伴必定也是:就这?

相似于 @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/archive…

退出移动版