AutoConfiguration事件注册
在完成了以上步骤的过滤、筛选之后,我们最终获得了要进行自动配置的类的集合,在将该集合返回之前,在 AutoConfigurationlmportSelector 类中完 成的最后一步操作就是相关事件的封装和广播,相关代码如下。
代码语言:javascript复制private void fireAutoConfigurationImportEvents(List<String> configurations ,
Set<String> exclusions) {
List<AutoConfigurationImportListener> listeners = getAutoConfigurat ionImp
ortL isteners();
if (!listeners. isEmpty()) {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(t
his,
onfigurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods (listener);
listener . onAutoConf igurationImportEvent(event);
}
}
protected List<AutoConfigurationImportListener> getAutoConfigurat ionImportL
is-
teners() {
return SpringFactoriesLoader . loadFactories (AutoConf igurationImportL istene
r.class ,
this . beanClassLoader);
}
以上代码首先通过 SpringFactoriesLoader 类提供的 loadFactories 方法将 spring.factories中配置的接口 AutoConfigurationlmportListener 的实现类加载出来。然后,将筛选出的自动配置类集合和被排除的自动配置类集合封装成 AutoConfigurationImportEvent 事件对象,并传入该事件对象通过监听器提供的 onAutoConfigurationlmportEvent 方法,最后进行事件广播。关于事件及事件监听相关的内容不在此过多展开。
spring.factories 中自动配置监听器相关配置代码如下。
代码语言:javascript复制org. springframework . boot . autoconfigure . AutoConfigurat ionImportL istener=org .
springframework . boot . autoconfigure . condition . ConditionEvaluat ionReportAuto
ConfigurationImportListener
@Conditional 条件注解
前 面 我 们 完 成 了 自 动 配 置 类 的 读 取 和 筛 选 , 在 这 个 过 程 中 已经涉及了像@Conditional-OnClass 这 样 的 条 件 注 解 。打 开 每 一 个 自 动 配 置 类 , 我 们 都 会 看 到@Conditional 或其衍生的条件注解。下面就先认识一 下 @Conditional 注解。
认识条件注解
@Conditional 注解是由 Spring 4.0 版本弓|入的新特性,可根据是否满足指定的条件来决定是否进行 Bean 的实例化及装配,比如,设定当类路径下包含某个 jar 包的时候才会对注解的类进行实例化操作。总之,就是根据一-些特定条件来控制 Bean 实例化的行为,@Conditional 注解代码如下。
代码语言:javascript复制@Target({ElementType. TYPE, ElementType .METHOD})
@Retent ion(RetentionPolicy . RUNTIME)
@Documented
public @interface Conditional {
Class<? extends Condition>[ ] value();
}
@Conditional 注解唯一的元素 属性是接口 Condition 的数组,只有在数组中指定的所有Condition 的 matches 方法都返回 true 的情况下,被注解的类才会被加载。我们前面讲到的OnClassCondition 类就是 Condition 的子类之一,相关代码如下。
代码语言:javascript复制@FunctionalInterface
public interface Condition {
//决定条件是否匹配
boolean matches (ConditionContext context, AnnotatedTypeMetadata metadata);}
matches 方法的第一个参数为 ConditionContext, 可通过该接口提供的方法来获得 Spring应用的上下文信息,ConditionContext 接口定义如下。
代码语言:javascript复制public interface ConditionContext {
//返 BeanDefinitionRegistry 注册表,可以检 查 Bean 的定义
BeanDefinitionRegistry getRegistry();
//返回 ConfigurableL is tableBeanFactory, 可以检查 Bean 是否已经存在, 进-步检查
Bean
属性
@Nullable
ConfigurableL istableBeanFactory getBeanFactory();
//返回 Environment,可以获得 当前应用环境变量,检测当前环境变量是否存在
Environment getEnvironment();
//返 ResourceLoader,用于读取或检查所加载的资源
ResourceLoader getResourceLoader();
//返回 ClassLoader,用于检查类是否存在
@Nullable
ClassLoader getClassLoader();
}
matches 方法的第二个 参数为 AnnotatedTypeMetadata,该接口提供了访问特定类或方法的注解功能,并且不需要加载类,可以用来检查带有@Bean 注解的方法上是否还有其他注解, AnnotatedTypeMetadata 接口定 义如下。
代码语言:javascript复制public interface AnnotatedTypeMetadata {
boolean isAnnotated(String annotationName);
@Nullable
Map<String, Object> getAnnotat ionAttributes (String annotationName) ;
@Nullable
Map<String, object> getAnnotationAttributes (String annotat ionName, boolean
classValuesAsString);
@Nullable
MultiValueMap<String, object> getAllAnnotat ionAttributes (String annotationName);
@Nullable
MultiValueMap<String, Object> getAllAnnotationAttributes (String annotationName,
boolean classValuesAsString);
}
该接口的 isAnnotated 方法能够提供判断带有@Bean 注解的方法上是否还有其他注解的功能。其他方法提供不同形式的获取@Bean 注解的方法上其他注解的属性信息。
条件注解的衍生注解
在 Spring Boot 的 autoconfigure 项目中提供了各类基于@Conditional 注解的衍生注解,它们适用不同的场景并提供了不同的功能。以下相关注解均位于 spring-boot-autoconfigure项目的 org.springframework.boot.autoconfigure.condition 包下。
.@ConditionalOnBean: 在容器中有指定 Bean 的条件下。
.@ConditionalOnClass: 在 classpath 类路径下有指定类的条件下。
.@ ConditionalOnCloudPlatform: 当指定的云平台处于 active 状态时。
.@ConditionalOnExpression: 基于 SpEL 表达式的条件判断。
.@ConditionalOnJawa:基于 JVM 版本作为判断条件。
. @ConditionalOnJndi: 在 JNDI 存在的条件下查找指定的位置。
.@ConditionalOnMissingBean:当容器里没有指定 Bean 的条件时。
.@ ConditionalOnMissingClass: 当类路径下没有指定类的条件时。
.@ ConditionalOnNotWebApplication: 在项目不是一个 Web 项目的条件下。
. @ConditionalOnProperty: 在指定的属性有指定值的条件下。
.@ConditionalOnResource: 类路径是否有指定的值。
.@ ConditionalOnSingleCandidate: 当指定的 Bean 在容器中只有一个或者有多个但是指定了首选的 Bean 时。
.@ ConditionalOnWebApplication:在项目是一个 Web 项目的条件下。
如果仔细观察这些注解的源码,你会发现它们其实都组合了@Conditional 注解,不同之处是它们在注解中指定的条件( Condition)不同。下面我们以@ ConditionalOnWebApplication为例来对衍生 条件注解进行一个简单的分析。
代码语言:javascript复制@Target({ ElementType . TYPE,ElementType.METHOD })
@Retent ion(RetentionPolicy . RUNTIME)
@Documented
@Condit ional (OnWebApplicat ionCondition. class)
public @interface Condit iona lOnWebApplication {
//所需的 web 应用类型
Type type() default Type . ANY;
//可选应用类型枚举
enum Type {
//任何类型
ANY,
//基于 servlet 的 web 应用
SERVLET,
//基 Freactive 的 web 应用
REACTIVE
}
}
@ ConditionalOnWebApplication 注解的源代码中组合了@Conditional 注解,并且指定了对应的 Condition 为 OnWebApplicationCondition 。
OnWebApplicationCondition 类的结构与前面讲到的 OnClassCondition-样,都继承自SpringBootCondition 并 实 现 AutoConfigurationlmportFilter 接 口 。关 于 实 现AutoConfigurationImportFilter 接口的 match 方法在前面已经讲解过,这里重点讲解关于继承 SpringBootCondition 和实现 Condition 接口的功能。
图 2-6 展示了以 OnWebApplicationCondition 为例的衍生注解的关系结构,其中省略了之前章节讲过的 Filter 相关内容,重点描述了 Condition 的功能和方法。
上一节我们已经学习了 Condition 接口的源码,那么抽象类 SpringBootCondition 是如何实现该方法的呢?相关源代码如下。
代码语言:javascript复制public abstract class SpringBootCondition implements Condition {
@Override
public final boolean matches (ConditionContext context ,
AnnotatedTypeMetadata metadata) {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
return outcome . isMatch();
}
public abstract ConditionOutcome getMatchOutcome ( ConditionContext context,
AnnotatedTypeMetadata metadata) ;
}
在抽象类 SpringBootCondition 中实现了 matches 方法,而该方法中最核心的部分是通过调用新定义的抽象方法 getMatchOutcome 并交由子类来实现,在 matches 方法中根据子类返回的结果判断是否匹配。下面我们来看 OnWebApplicationCondition 的源代码是如何实现相关功能的。
代码语言:javascript复制@Order(Ordered . HIGHEST_ PRECEDENCE 20)
class OnWebApplicationCondition extends FilteringSpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context ,
AnnotatedTypeMetadata metadata) {
boolean required = metadata . isAnnotated (Condit ionalOnWebApplication.cla
55.
getName());
ConditionOutcome outcome = isWebApplication(context, metadata, require
d);
if (required && !outcome. isMatch()) {
return ConditionOutcome . noMatch(outcome. getConditionMessage());
if (!required && outcome . isMatch()) {
return ConditionOutcome . noMatch(outcome . getConditionMessage());
return ConditionOutcome . match(outcome . getConditionMessage());
}
可 以 看 出 , 是 否 匹 配 是 由 两 个 条 件 决 定 的 : 被 注 解 的 类 或 方 法 是 否 包 含 ConditionalOn-WebApplication 注解,是否为 Web 应用。
.如 果包含 ConditionalOn WebApplication 注解,并且不是 Web 应用,那么返回不匹配。
.如果不包含 ConditionalOnWebApplication 注解,并且是 Web 应用,那么返回不匹配。
.其他情况,返回匹配。
下面我们以 SERVLET Web 应用为例,看相关源代码是如何判断是否为 Web 应用的。REACTIVE Web 应用和其他类型的 Web 应用可参照学习。
代码语言:javascript复制@Order (Ordered .HIGHEST PRECEDENCE 20)
class OnWebApplicationCondition extends FilteringSpringBootConditionprivate static final String SERVLET_ WEB_ APPLICATION_ CLASS = "org. springfr
amework .
web. . context . support . Gener icWebApplicationContext";
//推断 web 应用是否匹配
private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadat
a, boolean required) {
switch (deduceType(metadata)) {
case SERVLET:
//是否为 SERVLET
return isServletWebApplication(context);
case REACTIVE:
'是否为 REACTIVE
return isReactiveWebApplication(context);
default:
//其他
return isAnyWebApplication(context, required);
}
private ConditionOutcome isServletWebApplication(ConditionContext contex
t) {
ConditionMessage . Builder message = ConditionMessage . forCondition("");
//判断常量定义类是否存在
if (!ClassNameFilter . isPresent(SERVLET_ WEB_ APPLICATION_ CLASS,
context . getClassLoader())) {
return ConditionOutcome . noMatch(
message. didNotFind("servlet web application classes").atAll());
}
//判断 BeanFactory 是否存在
if (context . getBeanFactory() != null) {
String[] scopes = context. getBeanFactory() . getRegisteredScopeNames();
if (ObjectUtils . containsElement(scopes, "session")) {
return ConditionOutcome . match(message . foundExactly("'session' scop
e"));
}
//判断 Environment 的类型是否为 Configurabl evebEnvironment 类型
if (context . getEnvironment() instanceof ConfigurableWebEnvironment) {return ConditionOutcome
.match(message . foundExactly("ConfigurableWebEnvironment"));
//判断 ResourceLoader 的类型是否为 webAppl icat ionContext 类型
if (context . getResourceLoader() instanceof WebApplicat ionContext) {
return ConditionOutcome . match(message . foundExactly( "WebApplicationCon
return ConditionOut come . noMatch(message . because("not a servlet web-
application"));
// MAnnotatedTypeMetad
ata 中获取 type 值
private Type deduceType
(AnnotatedTypeMetadata metadata) {
Map<String, 0bject> a
ttributes = metadata
. getAnnotat ionAttri
butes(Condit iona lOnWebApplicat ion. class . getName());
if (attributes != nul
1) {
return (Type) attri
butes.get("type");
return Type . ANY;
}
}
首 先 在 isWebApplication 方 法 中 讲 行 Web 应用类型的推断 。这 里 使 用AnnotatedTypeMetadata 的 getAnnotationAttributes 方 法 获 取 所 有 关 于@ ConditionalOnWebApplication 的注解属性。返回值为 null 说明未配置任何属性,默认为Type.ANY,如果配置属性,会获得 type 属性对应的值。
如果返回值为 Type.SERVLET,调用 isServletWebApplication 方 法来进行判断。该方法的判断有以下条件。
:GenericWebApplicationContext 类是否在类路径下。
.容器内是否存在注册名称为 session 的 scope。
.容器的 Environment 是否为 ConfigurableWebEnvironment。
.容器的 ResourceLoader 是否为 WebApplicationContext.
在完成了以上判断之后,得出的最终结果封装为 ConditionOutcome 对象返回,并在抽象类SpringBootCondition 的 matches 方法中完成判断, 返回最终结果。
实例解析
在了解整个 Spring Boot 的运作原理之后,我们以 Spring Boot 内置的 http 编码功能为例,分析一下整个自动配置的过程。
在常规的 Web 项目中该配置位于 web .xml,通过<filter>来进行配置。
在常规的 Web 项目中该配置位于 web.xml,通过<filter>来进行配置。
代码语言:javascript复制<filter>
<filter-name>encodingFilter</ filter-name>
<filter-class>org . springframework. web. filter . CharacterEncodingFilter
</filter-class>
<init - param>
<param- name> encoding</ param- name>
<param-value>UTF - 8</ param-value>
</init-param>
<init-param>
<param-name> forceEncoding< / param- name>
<param-value>true</param-value>
</init- param>
</filter>
而在 Spring Boot 中通过内置的 HttpEncodingAutoConfiguration 来完成这一功能。下面我们具体分析一下该功能都涉及哪些配置和实现。
根据前面讲的操作流程,我们先来看一下 META-INF/spring.factories 中对该自动配置的注册。
代码语言:javascript复制# Auto Configure
org. springframework. boot . autoconfigure . EnableAutoConfiguration=
org. springframework . boot . autoconfigure .web . servlet . HttpEncodingAutoConfigur
ation,
当完成注册之后,在加载的过程中会使用元数据的配置进行过滤,对应的配置内容在 META-INF/spring-autoconfigure-metadata.properties 文件中。
org. springframework. boot . autoconfigure . web. servlet .HttpEncodingAutoConfigur
ation. Conditional0nClass=org . springfr amework . web. filter. CharacterEncodingFilter
在 过 滤 的 过 程 中 要 判 断 自 动 配 置 类 HttpEncodingAutoConfiguration 是 否 被@ConditionalOnClass 注解,源代码如下。
代码语言:javascript复制@Configuration
@EnableConfigurat ionProperties (HttpProperties.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication. Type . SERVLE@Conditional0nClass (CharacterEncodingFilter . class)
@ConditionalOnProperty(prefix = "spring. http. encoding", value = "enabled" ,
matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
private final HttpProperties . Encoding properties ;
public HttpEncodingAutoConf igurat ion(HttpProperties properties) {
this. properties = properties. getEncoding();
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter. setEncoding(this . properties . getCharset() .name());
filter. setForceRequestEncoding( this . properties . shouldForce(Type . REQUES
T));
filter . setForceResponseEncoding( this . properties . shouldForce(Type. RESPON
SE));
return filter;
}
}
很明显,它被@ConditionalOnClass 注解, 并且指定实例化的条件为类路径下必须有CharacterEncodingFilter 存在。再看一下该类的其他注解。
.@Configuration:指定该类作为配置项来进行实例化操作。
.@ EnableConfigurationProperties:参数为 HttpProperties.class,开启属性注入,会将参数中的 HttpProperties 注入该类。
. @ ConditionalOnWebApplication: 参数为 Type .SERVLET, 说明该类只有在基于 servlet的 Web 应用中才会被实例化。
.@ConditionalOnClass:参数为 CharacterEncodingFilter.class,只有该参数存在,才会被实例化。
@ConditionalOnProperty:指定配置文件内 spring .ttp.encoding 对应的值,如果为 enabled才会进行实例化,没有配置则默认为 true。
.@ConditionalOnMissingBean: 注释于方法上,与@Bean 配合,当容器中没有该 Bean 的实例化对象时才会进行实例化。
其中 HttpProperties 类的属性值对应着 application.yml 或 application.properties的配通过注解@ConfigurationProperties(prefix="sprig.http")实现的属性注入。关于属性注入,后面章节会详细讲解,这里我们先看一下源代码和对应的配置文件参数。@ConfigurationProperties(prefix = "spring.http" )
代码语言:javascript复制public class HttpProperties {
...
public static class Encoding {
public static final Charset DEFAULT_ CHARSET = StandardCharsets.UTF_ 8;
private Charset charset = DEFAULT CHARSET;
private Boolean force;
private Boolean forceRequest;
private Boolean forceResponse;
private Map<Locale, Charset> mapping;
...
}
}
而在 application.properties 中,我们会进行如下对应配置:
代码语言:javascript复制spring . http. encoding . force=true
spring . http. encoding. charset=UTF-8
spring . http. encoding . force-request=true
...
小结
本章围绕SpringBoot的核心功能展开,带大家从总体上了解 Spring Boot 自动配置的原理以及自动配置核心组件的运作过程。只有掌握了这些基础的组建内容及其功能,我们在后续集成其他三方类库的自动配置时,才能够更加清晰地了解它们都运用了自动配置的哪些功能。本章需重点学习自动配置原理、@EnableAutoConfiguration、@Import、ImportSelector、@Conditional 以及示例解析部分的内容。
本文给大家讲解的内容是AutoConfiguration事件注册和@Conditional 条件注解、实例解析;
- 下篇文章给大家讲解的是SpringBoot构造流程源码分析;
- 觉得文章不错的朋友可以转发此文关注小编;
- 感谢大家的支持!
本文就是愿天堂没有BUG给大家分享的内容,大家有收获的话可以分享下,想学习更多的话可以到微信公众号里找我,我等你哦。