关于java:SpringBoot-2x-系列自动装配

2次阅读

共计 11533 个字符,预计需要花费 29 分钟才能阅读完成。

概览

上一节介绍了 SpringBoot 中的配置体系,接下来将会介绍 SpringBoot 的中最根底、最外围的主动拆卸(AutoConfiguration)机制。正是有了主动拆卸机制,咱们能够很快的搭建一个 Web 我的项目,实现零配置,

看下 SpringBoot 是如何帮忙咱们实现主动拆卸的,咱们首先从 @SpringBootApplication 注解说起:

主动拆卸过程

@SpringBootApplication 启动类注解

每个 SpringBoot 我的项目都会有一个启动类,该类位于代码的根目录下,个别用 XXXApplication 命名,@SpringBootApplication 注解会增加到该类上。@SpringBootApplication 注解位于 spring-boot-autoconfigure 工程的 org.springframework.boot.autoconfigure 包中,定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
 @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {@AliasFor(annotation = EnableAutoConfiguration.class)
 Class<?>[] exclude() default {};
 
 @AliasFor(annotation = EnableAutoConfiguration.class)
 String[] excludeName() default {};
 
 @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
 String[] scanBasePackages() default {};
 
 @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
 Class<?>[] scanBasePackageClasses() default {};}

该注解相比拟显得有点简单,从定义上看,@SpringBootApplication 注解是一个组合注解,它是由三个注解组合而成,别离是:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan

咱们能够通过 exclude 和 excludeName 属性来配置不须要实现主动拆卸的类或类名,也能够通过 scanBasePackages 和 scanBasePackageClasses 属性来配置须要进行扫描的包门路和类门路。接下来咱们别离对这三个注解一一介绍:

@SpringBootConfiguration

@SpringBootConfiguration 注解比较简单,定义如下:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
 @AliasFor(annotation = Configuration.class)
 boolean proxyBeanMethods() default true;}

从定义来看,@SpringBootConfiguration 等同于 @Configuration 注解,@Configuration 比拟常见,用来标识该类是 JavaConfig 配置类

@ComponentScan

@ComponentScan 注解不是 Spring Boot 引入的新注解,而是属于 Spring 容器治理的内容。@ComponentScan 注解就是扫描基于 @Component 等注解所标注的类所在包下的所有须要注入的类,并把相干 Bean 定义批量加载到容器中。

@EnableAutoConfiguration 主动配置注解

@EnableAutoConfiguration 这个注解一看名字就很牛,它是 SpringBoot 实现主动拆卸的外围注解,先看定义:

@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(AutoConfigurationPackages.Registrar.class) 组合成,上面别离来介绍:

@AutoConfigurationPackage 注解

@AutoConfigurationPackage 注解定义如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {​}

该注解的作用是将增加该注解的类所在的包作为主动拆卸的包门路进行治理。在 @AutoConfigurationPackage 注解的定义中,咱们又发现了一个 @Import 注解,@Import 注解是由 Spring 提供的,作用是将某个类实例化并退出到 Spring IoC 容器中。所以咱们要想晓得 @Import(AutoConfigurationPackages.Registrar.class) 到底做了什么就须要理解 Registrar 这个类里蕴含了哪些办法。

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
​
 @Override
 public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {register(registry, new PackageImport(metadata).getPackageName());
 }
​
 @Override
 public Set<Object> determineImports(AnnotationMetadata metadata) {return Collections.singleton(new PackageImport(metadata));
 }
​
}

Registrar 类里一共有两个办法,别离是 determineImports 和 registerBeanDefinitions,

new PackageImport(metadata).getPackageName() 返回的就是 @SpringBootApplication 注解所在的类的包名

@Import(AutoConfigurationImportSelecor.class) 主动导入配置文件选择器

接下来咱们回到 @EnableAutoConfiguration 注解中的 @Import(AutoConfigurationImportSelecor.class) 局部来,

这个类下有个重要的办法 selectImports 办法,Spring 会把该办法返回的所有的类加载到 IOC 的容器中。所以这个类的次要作用是抉择 Spring 加载哪些类到 IOC 容器中。

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return NO_IMPORTS;}
 AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
 .loadMetadata(this.beanClassLoader);
 AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
 annotationMetadata);
 return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
​
protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
 AnnotationMetadata annotationMetadata) {if (!isEnabled(annotationMetadata)) {return EMPTY_ENTRY;}
 AnnotationAttributes attributes = getAttributes(annotationMetadata);
 
 // 获取 configurations 汇合
 List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
 configurations = removeDuplicates(configurations);
 Set<String> exclusions = getExclusions(annotationMetadata, attributes);
 checkExcludedClasses(configurations, exclusions);
 configurations.removeAll(exclusions);
 configurations = filter(configurations, autoConfigurationMetadata);
 fireAutoConfigurationImportEvents(configurations, exclusions);
 return new AutoConfigurationEntry(configurations, exclusions);
 }

这段代码的外围是通过 getCandidateConfigurations 办法获取 configurations 汇合并进行过滤。getCandidateConfigurations 办法如下所示:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {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;
}

这里咱们重点关注下 SpringFactoriesLoader.loadFactoryNames() 办法,该办法会从 META-INF/spring.factories 文件中去查找主动配置的类,在这里,不得不提到 JDK 的 SPI 机制,因为无论从 SpringFactoriesLoader 这个类的命名上,还是 META-INF/spring.factories 这个文件目录,两者之间都存在很大的相通性。

Java SPI 机制和 SpringFactoriesLoader

要想了解 SpringFactoriesLoader 类,咱们首先须要理解 JDK 中 SPI(Service Provider Interface,服务提供者接口)机制。简略的说 SPI 机制就是为接口寻找服务实现的机制,有点相似 Spring 的 IOC 的思维,不过将主动拆卸的控制权移到程序内部,能够防止程序中应用硬编码,在模块化设计中这种机制尤为重要。应用 Java SPI 机制须要以下几步:

  • 当服务提供者提供了接口的一种具体实现后,在 jar 包的 META-INF/services 目录下创立一个以“接口全路径名”为命名的文件,内容为实现类的全路径名
  • 当内部程序拆卸这个 jar 包时,就能通过该 jar 包 META-INF/services/ 目录中的配置文件找到具体的实现类名,并装载实例化,从而实现模块的注入

SpringFactoriesLoader 相似这种 SPI 机制,只不过以服务接口命名的文件是放在 META-INF/spring.factories 文件夹下,定义了一个 key 为 EnableAutoConfiguration,SpringFactoriesLoader 会查找所有 META-INF/spring.factories 文件夹中的配置文件,并把 Key 为 EnableAutoConfiguration 所对应的配置项通过反射实例化为配置类并加载到容器中。

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {MultiValueMap<String, String> result = cache.get(classLoader);
 if (result != null) {return result;}
​
 try {
 Enumeration<URL> urls = (classLoader != null ?
 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
 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);
 }
}

以上就是 SpringBoot 中基于 @SpringBootApplication 注解实现主动拆卸的根本过程和原理,不过 SpringBoot 默认状况下给咱们提供了 100 多个 AutoConfiguration 的类

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,
org.springframework.boot.autoconfigure.cloud.CloudServiceConnectorsAutoConfiguration,
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,

显然咱们不可能把所有的类全副引入,所以在主动拆卸的时候,须要会依据类门路去寻找是否有对应的配置类,如果有的话还须要依照条件进行判断,来决定是否要拆卸,这里我要引出 SpringBoot 中常常要用到的另外一批注解 @ConditionalOn 系列注解。

@ConditionalOn 系列条件注解

当咱们构建一个 Spring 利用的时候,有时咱们想在满足指定条件的时候才将某个 bean 加载到利用上下文中,在 Spring 4.0 时代,咱们能够通过 @Conditional 注解来实现这类操作,SpringBoot 在 @Conditional 注解的根底上进行了细化,定义了一系列的 @ConditionalOn 条件注解:

@ConditionalOnProperty:只有当所提供的属性属于 true 时才会实例化 Bean @ConditionalOnBean:只有在以后上下文中存在某个对象时才会实例化 Bean @ConditionalOnClass:只有当某个 Class 位于类门路上时才会实例化 Bean @ConditionalOnExpression:只有当表达式为 true 的时候才会实例化 Bean @ConditionalOnMissingBean:只有在以后上下文中不存在某个对象时才会实例化 Bean @ConditionalOnMissingClass:只有当某个 Class 在类门路上不存在的时候才会实例化 Bean @ConditionalOnNotWebApplication:只有当不是 Web 利用时才会实例化 Bean

因为 @ConditionalOn 系列条件注解十分多,咱们无心对所有这些组件进行开展。事实上这些注解的实现原理也大致相同,咱们只须要深刻理解其中一个就能做到举一反三。这里咱们筛选 @ConditionalOnClass 注解进行开展,该注解定义如下:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {Class<?>[] value() default {};
  String[] name() default {};}

能够看到,@ConditionalOnClass 注解自身带有两个属性,一个 Class 类型的 value,一个 String 类型的 name,所以咱们能够采纳这两种形式中的任意一种来应用该注解。同时 ConditionalOnClass 注解自身还带了一个 @Conditional(OnClassCondition.class) 注解。所以,ConditionalOnClass 注解的判断条件其实就蕴含在 OnClassCondition 这个类中。

OnClassCondition 是 SpringBootCondition 的子类,而 SpringBootCondition 又实现了 Condition 接口。Condition 接口只有一个 matches 办法,如下所示:

@Override
public final boolean matches(ConditionContext context,
            AnnotatedTypeMetadata metadata) {String classOrMethodName = getClassOrMethodName(metadata);
        try {ConditionOutcome outcome = getMatchOutcome(context, metadata);
            logOutcome(classOrMethodName, outcome);
            recordEvaluation(context, classOrMethodName, outcome);
            return outcome.isMatch();}
        // 省略其余办法
}

这里的 getClassOrMethodName 办法获取被增加了 @ConditionalOnClass 注解的类或者办法的名称,而 getMatchOutcome 办法用于获取匹配的输入。咱们看到 getMatchOutcome 办法实际上是一个形象办法,须要交由 SpringBootCondition 的各个子类实现实现,这里的子类就是 OnClassCondition 类。在了解 OnClassCondition 时,咱们须要明确在 Spring Boot 中,@ConditionalOnClass 或者 @ConditionalOnMissingClass 注解对应的条件类都是 OnClassCondition,所以在 OnClassCondition 的 getMatchOutcome 中会同时解决两种状况。这里咱们筛选解决 @ConditionalOnClass 注解的代码,外围逻辑如下所示:

ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata, ConditionalOnClass.class);
if (onClasses != null) {List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
            if (!missing.isEmpty()) {
                return ConditionOutcome
                        .noMatch(ConditionMessage.forCondition(ConditionalOnClass.class)
                                .didNotFind("required class", "required classes")
                                .items(Style.QUOTE, missing));
            }
            matchMessage = matchMessage.andCondition(ConditionalOnClass.class)
                    .found("required class", "required classes").items(Style.QUOTE, getMatches(onClasses, MatchType.PRESENT, classLoader));
}

这里有两个办法值得注意,一个是 getCandidates 办法,一个是 getMatches 办法。首先通过 getCandidates 办法获取了 ConditionalOnClass 的 name 属性和 value 属性。而后通过 getMatches 办法将这些属性值进行比对,失去这些属性所指定的但在类加载器中不存在的类。如果发现类加载器中应该存在但事实上又不存在的类,则返回一个匹配失败的 Condition;反之,如果类加载器中存在对应类的话,则把匹配信息进行记录并返回一个 ConditionOutcome。

小结

至此整个 SpringBoot 主动配置的全副过程和基本原理曾经讲完了,内容很多,整个流程总结一个图如下:
![上传中 …]()![ 上传失败,undefined]()

我的项目源码

github:https://github.com/dragon8844/springboot-learning/tree/main/java-spi

最初说一句

如果这篇文章对您有所帮忙,或者有所启发的话,帮忙关注一下,您的反对是我保持写作最大的能源,多谢反对。

此外,关注公众号:彩色的灯塔,专一 Java 后端技术分享,涵盖 Spring,Spring Boot,SpringCloud,Docker,Kubernetes 中间件等技术。

正文完
 0