前言
我置信,只有你用过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就能应用了,像这样:
@Autowiredprivate 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.Dogcn.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})@Configurationpublic 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})@Configurationpublic 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})@Configurationpublic class MainConfig {}
相似于第二种,当Spring扫描到该类时,将会调用registerBeanDefinitions
办法,在该办法中,咱们手动往Spring中注入了一个Baby的Bean,实践上能够通过这种形式不限量的注入任何的Bean
SpringBootApplication注解
咱们在应用SpringBoot我的项目时,用到的惟一的注解就是@SpringBootApplication
,所以咱们惟一能下手的也只有它了,关上它看看吧。
嘿!看看咱们发现了什么?EnableAutoConfiguration!妥妥的大线索呀
EnableAutoConfiguration实质上也是通过Import实现的,并且Import了一个Selector
让咱们瞧一瞧外面的代码逻辑吧~
selectImports
@Overridepublic 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...