上一节介绍了SpringBoot中的配置体系,接下来将会介绍SpringBoot的中最基础、最核心的自动装配(AutoConfiguration)机制。正是有了自动装配机制,咱们能够很快的搭建一个Web项目,实现零配置,java
看下SpringBoot是如何帮忙咱们实现自动装配的,咱们首先从@SpringBootApplication注解提及:git
每一个SpringBoot项目都会有一个启动类,该类位于代码的根目录下,通常用XXXApplication命名,@SpringBootApplication注解会添加到该类上。@SpringBootApplication 注解位于 spring-boot-autoconfigure 工程的 org.springframework.boot.autoconfigure 包中,定义以下:github
@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、@ComponentScanspring
咱们能够经过exclude 和 excludeName 属性来配置不须要实现自动装配的类或类名,也能够经过 scanBasePackages 和 scanBasePackageClasses 属性来配置须要进行扫描的包路径和类路径。接下来咱们分别对这三个注解一一介绍:后端
@SpringBootConfiguration注解比较简单,定义以下:springboot
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Configuration public @interface SpringBootConfiguration { @AliasFor( annotation = Configuration.class ) boolean proxyBeanMethods() default true; }
从定义来看,@SpringBootConfiguration等同于@Configuration注解,@Configuration比较常见,用来标识该类是JavaConfig配置类ide
@ComponentScan 注解不是 Spring Boot 引入的新注解,而是属于 Spring 容器管理的内容。@ComponentScan 注解就是扫描基于 @Component 等注解所标注的类所在包下的全部须要注入的类,并把相关 Bean 定义批量加载到容器中。模块化
@EnableAutoConfiguration 这个注解一看名字就很牛,它是SpringBoot实现自动装配的核心注解,先看定义:spring-boot
@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)组合成,下面分别来介绍:ui
@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注解所在的类的包名
接下来咱们回到@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 这个文件目录,二者之间都存在很大的相通性。
要想理解 SpringFactoriesLoader 类,咱们首先须要了解 JDK 中 SPI(Service Provider Interface,服务提供者接口)机制。简单的说SPI机制就是为接口寻找服务实现的机制,有点相似Spring的IOC的思想,不过将自动装配的控制权移到程序外部,能够避免程序中使用硬编码,在模块化设计中这种机制尤其重要。使用Java SPI机制须要如下几步:
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系列注解。
当咱们构建一个 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自动配置的所有过程和基本原理已经讲完了,内容不少,整个流程总结一个图以下:
github:https://github.com/dragon8844/springboot-learning/tree/main/java-spi
若是这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下,您的支持是我坚持写做最大的动力,多谢支持。
此外,关注公众号:黑色的灯塔,专一Java后端技术分享,涵盖Spring,Spring Boot,SpringCloud,Docker,Kubernetes中间件等技术。