之前的文章介绍Raft算法,那么在学习nacos的过程,我们很容易和nacos接触的一个问题大概就是为啥我们引入nacos包后,加个注解就可以使用nacos.
相信很多人都可以脱口而出:自动装配。我们都知道在springboot中 约定大于配置 。当我们引入nacos的依赖后就和springBoot约定好了在环境中了nacos需要的相关配置。
那么这个自动装配又是怎么完成的呢?一起来回顾学习一下。
1.自动装配实战demo
只有卸下伪装,我们才能更好的观察到藏在背后的东西。
首先我们先来实现一个starter,这样我们才能更好的去理解这个自动装配装配了什么。
为了实现这个自动装配,我们首先需要来看看,约定的是什么规则才能直接自动装配。
规则如下:
- 在 resources/META-INF 目录下必须要有 spring.factories
- spring.factories 文件中必须要有 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置类
- 要保证响应的bean能被扫描到
话不多说,上代码
为了方便,starter里面引入的实体类,没有再分开一个包,直接在一个包里面。主要分了两个模块,一个是LearnStarter,一个是customer-api模块。
LearnStarter模块
CustomerConf代码
/**
* @Author kris
* @Date 2023/11/3 9:57
*/
@ConfigurationProperties(prefix = "com.nacos.learn")
public class CustomerConf {
private String version;
private String groupName;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return "CustomerConf{" +
"version='" + version + '\'' +
", groupName='" + groupName + '\'' +
'}';
}
}
/**
* @Author kris
* @Date 2023/11/3 10:43
*/
public class CustomerModule {
private String version;
private String groupName;
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public String getGroupName() {
return groupName;
}
public void setGroupName(String groupName) {
this.groupName = groupName;
}
@Override
public String toString() {
return "CustomerMudule{" +
"version='" + version + '\'' +
", groupName='" + groupName + '\'' +
'}';
}
}
/**
* @Author kris
* @Date 2023/11/2 17:43
*/
@Configuration
@EnableConfigurationProperties(CustomerConf.class)
@Slf4j
public class CustomerConfiguration {
@Bean
@ConditionalOnProperty(name = {"com.nacos.learn.version","com.nacos.learn.groupName"})
public CustomerModule matchCustomerConf(CustomerConf customerConf){
CustomerModule module = new CustomerModule();
module.setGroupName(customerConf.getGroupName());
module.setVersion(customerConf.getVersion());
log.info("init match conf:{}", module.toString());
return module;
}
}
spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.nacos.learn.CustomerConfiguration
customer-api
SpringBootApplication
@SpringBootApplication
public class CustomerApplication {
public static void main(String[] args) {
SpringApplication.run(CustomerApplication.class, args);
}
}
application.yml
server:
port: 9000
com:
nacos:
learn:
version: 1.0.0
groupName: studyNacos
pom文件添加
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.nacos.learn</groupId>
<artifactId>LearnStarter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
当customer-api引入learnStarter模块后,启动Application类
可以看到learnStarter模组已经自动装配并输出了日志。
至此,我们第一次完成了我们自定义的自动装配的项目。接下来,让我们一起断点看看springboot是如何自动装配的。
2.探索自动装配原理
经过上面的实践,我们只是引入了我们自定义的starter就完成了自动装配,而想让springboot扫描到类并加入到容器,很明显spring.factories文件的配置是关键,因为配置这个类,引入后被springboot扫描到
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.nacos.learn.CustomerConfiguration
接下来我们一起来探究一下springboot是如何完成的。
对于我们customer-api,很简单的一个服务,也就只有Application一个类,从结果上看,我们一起关注一下@SpringBootApplication这个注释,点进去。
@SpringBootApplication
重点的注解有两个,一个是@SpringBootConfiguration,一个是@EnableAutoConfiguration。
@SpringBootConfiguration注解
通过@SpringBootConfiguration标注为springboot的一个配置类
@EnableAutoConfiguration自动配置注解
点击注解进入源码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
// 用来覆盖配置开启 / 关闭自动配置的功能
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
// 根据类排除指定的自动配置
Class<?>[] exclude() default {};
// 根据类名排除指定的自动配置
String[] excludeName() default {};
}
里面重点的注解有两个,一个@AutoConfigurationPackage,一个是@Import。
@AutoConfigurationPackage 注解的功能就是将注解所在类的路径加入到packageNames集合里面去。至于加进去的原因应该是为了包扫描。
@Import注解这个注解我们在学习springboot的过程里面也经常看到,功能就是配置一个xxx配置类的功能,在明显的,在这个功能里面注入了一个AutoConfigurationImportSelector的配置类。接下来我们看看这个配置类做了什么呢?
AutoConfigurationImportSelector
先看看这个类的继承关系图
看到继承图,我们可以看到虽然实现的接口很多,但是都分为两个大类,一个Aware,一个是selector.
selector
先来看看DeferredImportSelector,提出一个小问题:为啥AutoConfigurationImportSelector实现的是DeferredImportSelector而不是ImportSelector呢?
假如我们问chatgpt的话,会得到这么一个回复
💡DeferredImportSelector 和 ImportSelector 是 Spring Boot 中用于动态导入配置类的接口。它们的区别在于 DeferredImportSelector 是 ImportSelector 的子接口,它可以在 ApplicationContext 加载完毕后再进行选择和加载导入的配置类,而 ImportSelector 则是在 ApplicationContext 加载前就进行选择和加载。AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口,这是因为 Spring Boot 自动配置的加载需要在应用上下文刷新之后才能进行。DeferredImportSelector 接口的实现类可以通过实现 getImportGroup() 方法来指定自己的配置类分组,以便在需要时进行选择和加载。这样可以确保自动配置类在应用上下文加载完成后再进行选择和加载,从而避免了可能出现的循环依赖和其他问题。
在DeferredImportSelector中,selectImports()方法返回一个DeferredImportSelector.Group对象,该对象包含一个或多个DeferredImportSelector.Entry对象。每个Entry对象都包含一个要导入的配置类全限定名,以及一个可选的条件(Predicate)。
Aware
第二类呢,Aware方式,我们可以看到实现了好多相关的接口。aware接口主要是为了在bean实例化的时候能拿到不同环境属性,因此在自动配置过程中,需要先在selectImport前拿到不同spring属性与资料,因此实现了多个aware方法。
列举一下Aware在其中的使用:
- EnvironmentAware:允许访问Spring应用程序上下文中的环境变量和属性。
- BeanFactoryAware:允许访问Spring应用程序上下文中的BeanFactory,可以用于获取其他bean。
- ResourceLoaderAware:允许访问Spring应用程序上下文中的资源加载器,可以用于加载资源文件。
- BeanClassLoaderAware:允许访问Spring应用程序上下文中的ClassLoader,可以用于加载类。
- BeanNameAware:允许访问Spring应用程序上下文中的bean的名称。
上面说了这么多,都是在说喂slectImportor做准备的,那么接下来,我们就一起来看看这个selectImprotor,到底是干什么的,又是什么时候执行。带着这个疑问,我们一起来看看源码。
public interface ImportSelector {
// 根据导入的Configuration类的注解元数据选择哪一个类需要被导入,返回的数组就是需要被导入的类名
String[] selectImports(AnnotationMetadata importingClassMetadata);
// 排除不需要导入的类
@Nullable
default Predicate<String> getExclusionFilter() {
return null;
}
}
这个是ImportSelector的基本实现,但是AutoConfigurationImportSelector又重新复写了selectImports方法
public String[] selectImports(AnnotationMetadata annotationMetadata) {
//是否开启自动配置功能
if (!this.isEnabled(annotationMetadata)) {
return NO_IMPORTS;
} else {
// 封装将被引入的自动配置信息
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
// 返回符合条件的配置类的全限定类名数组
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
}
看到这里,我们大致了解getAutoConfigurationEntry这个方法就是需要我们去了解的,通过这个接口拿到了全限定的类的路径。
似乎看到这里这里,我们觉得自己快看到曙光了,但是当我们去debug这个springboot的启动时,却发现了这个selectImports没有被执行,而是执行了AutoConfigurationImportSelector类中内部类AutoConfigurationGroup的process方法。为什么呢?让我们继续往下看。
先看看DeferredImportSelector的源码
public interface DeferredImportSelector extends ImportSelector {
@Nullable
default Class<? extends DeferredImportSelector.Group> getImportGroup() {
return null;
}
//新增一个接口Group,接口内
public interface Group {
void process(AnnotationMetadata var1, DeferredImportSelector var2);
Iterable<DeferredImportSelector.Group.Entry> selectImports();
public static class Entry {
private final AnnotationMetadata metadata;
private final String importClassName;
public Entry(AnnotationMetadata metadata, String importClassName) {
this.metadata = metadata;
this.importClassName = importClassName;
}
public AnnotationMetadata getMetadata() {
return this.metadata;
}
public String getImportClassName() {
return this.importClassName;
}
public boolean equals(@Nullable Object other) {
if (this == other) {
return true;
} else if (other != null && this.getClass() == other.getClass()) {
DeferredImportSelector.Group.Entry entry = (DeferredImportSelector.Group.Entry)other;
return this.metadata.equals(entry.metadata) && this.importClassName.equals(entry.importClassName);
} else {
return false;
}
}
public int hashCode() {
return this.metadata.hashCode() * 31 + this.importClassName.hashCode();
}
public String toString() {
return this.importClassName;
}
}
}
}
可以看到看到DeferredImportSelector新加了一个Group,而AutoConfigurationGroup则是AutoConfigurationImportSelector类中则实现了该Group接口的内部类。
private static class AutoConfigurationGroup implements Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {
private final Map<String, AnnotationMetadata> entries = new LinkedHashMap();
private final List<AutoConfigurationImportSelector.AutoConfigurationEntry> autoConfigurationEntries = new ArrayList();
private ClassLoader beanClassLoader;
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
private AutoConfigurationMetadata autoConfigurationMetadata;
private AutoConfigurationGroup() {
}
public void setBeanClassLoader(ClassLoader classLoader) {
this.beanClassLoader = classLoader;
}
public void setBeanFactory(BeanFactory beanFactory) {
this.beanFactory = beanFactory;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
//group新增方法
//process方法的作用是对所有的AutoConfiguration类进行排序和过滤,只保留符合条件的AutoConfiguration类。
//排序的原则是根据AutoConfiguration类上的@AutoConfigureOrder注解中的值进行排序,如果没有这个注解,则按照类名的字母顺序进行排序。
//过滤的原则是根据AutoConfiguration类上的@Conditional注解中的条件进行过滤,只保留符合条件的AutoConfiguration类。
@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
() -> String.format("Only %s implementations are supported, got %s",
AutoConfigurationImportSelector.class.getSimpleName(),
deferredImportSelector.getClass().getName()));
AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
.getAutoConfigurationEntry(annotationMetadata);
this.autoConfigurationEntries.add(autoConfigurationEntry);
for (String importClassName : autoConfigurationEntry.getConfigurations()) {
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}
//group新增方法selectImports方法,返回的对象与ImportSelector的有区别
//作用是返回所有符合条件的AutoConfiguration类的全限定名,这些全限定名会被添加到Spring容器中,从而实现自动配置的功能。
//在返回这些全限定名之前,还会对这些AutoConfiguration类进行去重操作,避免重复加载。
@Override
public Iterable<Entry> selectImports() {
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
}
Set<String> allExclusions = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
.map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
.collect(Collectors.toCollection(LinkedHashSet::new));
processedConfigurations.removeAll(allExclusions);
return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
.map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
.collect(Collectors.toList());
}
//省略部分代码
}
通过观察源码,在springboot启动时,进入refresh方法,springboot通过invokeBeanFactoryPostProcessors(beanFactory);方法确保ApplicationContext创建后所有的BeanFactoryPostProcessor都被调用。在这个过程中,在用ConfigurationClassParser解析配置类的过程中,使用其中默认创建的DeferredImportSelectorHandler对象的process方法,在process方法里新建了一个DeferredImportSelectorGroupingHandler对象并调用其processGroupImports方法,在这个方法中,调用了getImports方法,从而调用了AutoConfigurationGroup的process方法。以上逻辑非本地关注的重点,现在来看一下AutoConfigurationGroup中调用process里面调用的getAutoConfigurationEntry
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
StartupStep contextRefresh = this.applicationStartup.start("spring.context.refresh");
// Prepare this context for refreshing.
prepareRefresh();
// Tell the subclass to refresh the internal bean factory.
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();
// Prepare the bean factory for use in this context.
prepareBeanFactory(beanFactory);
try {
// Allows post-processing of the bean factory in context subclasses.
postProcessBeanFactory(beanFactory);
StartupStep beanPostProcess = this.applicationStartup.start("spring.context.beans.post-process");
// Invoke factory processors registered as beans in the context.
invokeBeanFactoryPostProcessors(beanFactory);
// Register bean processors that intercept bean creation.
registerBeanPostProcessors(beanFactory);
beanPostProcess.end();
// Initialize message source for this context.
initMessageSource();
// Initialize event multicaster for this context.
initApplicationEventMulticaster();
// Initialize other special beans in specific context subclasses.
onRefresh();
// Check for listener beans and register them.
registerListeners();
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);
// Last step: publish corresponding event.
finishRefresh();
}
catch (BeansException ex) {
if (logger.isWarnEnabled()) {
logger.warn("Exception encountered during context initialization - " +
"cancelling refresh attempt: " + ex);
}
// Destroy already created singletons to avoid dangling resources.
destroyBeans();
// Reset 'active' flag.
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}
finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
contextRefresh.end();
}
}
}
getAutoConfigurationEntry
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 是否开启自动装配
if (!isEnabled(annotationMetadata)) {
// 未开启,返回空
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 通过 SpringFactoriesLoader 类提供的方法加载类路径中 META-INF 目录下的 spring.factories 文件中针对 EnableAutoConfiguration 的注册配置类
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 队获得的注册配置类集合进行去重处理,防止多个项目引入同样的配置类
configurations = removeDuplicates(configurations);
// 获得注解中被 exclude 或 excludeName 所派出的类的集合
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查被排除类是否可实例化,是否被自动注册配置所使用,不符合条件则抛出异常
checkExcludedClasses(configurations, exclusions);
// 从自动配置类集合中取出被排除的类
configurations.removeAll(exclusions);
// 检查配置类的注解是否符合 spring.factories 文件中 AutoConfigurationImportFilter 指定的注解检查条件
configurations = getConfigurationClassFilter().filter(configurations);
// 将赛选完成的配置类和排查的配置类构建为事件类,并传入监听器。监听器的配置在于 spring.factories 文件中,通过 AutoConfigurationImportListener z
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
通过以上代码的注释,我们可以看到springboot先检查了一下spring.boot.enableautoconfiguration参数是true还是false,默认是true;
然后在getCandidateConfigurations方法里面加载了META-INF目录下的spring.factories针对 EnableAutoConfiguration 的注册配置类
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
//加载spring.factories下针对 EnableAutoConfiguration 的注册配置类
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,里面主要的执行逻辑在最后一行的loadSpringFactories,继续点进去看看
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories
从注释里面看到里面使用classLoader获取地址FACTORIES_RESOURCE_LOCATION的资源。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
//FACTORIES_RESOURCE_LOCATION写死且等于"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 = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
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;
}
因为SpringFactoriesLoader会读取所有类的路径,就会遍历所有依赖包下的META-INF/spring.factories资源。
removeDuplicates
代码非常简单,就是去重
getExclusions
里面调用了getExcludeAutoConfigurationsProperty方法,里面通过spring.autoconfigure.exclude = 全限定类名 的方式排除
protected Set<String> getExclusions(AnnotationMetadata metadata, AnnotationAttributes attributes) {
Set<String> excluded = new LinkedHashSet<>();
//排除注解配置的类
excluded.addAll(asList(attributes, "exclude"));
excluded.addAll(Arrays.asList(attributes.getStringArray("excludeName")));
//排除spring.autoconfigure.exclude配置的类
excluded.addAll(getExcludeAutoConfigurationsProperty());
return excluded;
}
checkExcludedClasses
该方法检查排除的类是否属于当前 ClassLoad 和不存在于需要自动装配类的集合中,如果不满足就报错了
removeAll
最后就是去除被排除的类了
至此,我们可以看到自动装配的基本流程到此结束。
初步总结
1.是否需要自动装配
2.通过加载 META-INF 目录下的 spring.factories下的资源获取全限定路径
3.获取需要排除的配置类
4.检查需要排除的类是否属于当前的classloader,和是否存在与需要自动装配的集合中
5.清除排除配置类