注:本系列源碼分析基於springboot 2.2.2.RELEASE,對應的spring版本爲5.2.2.RELEASE,源碼的gitee倉庫倉庫地址:funcy/spring-boot.
本文是springboot條件註解分析的第二篇,上文我們總結了springboot的幾個條件總結:
註解類型 | 註解類型 | 條件判斷類 |
---|---|---|
class 條件註解 | @ConditionalOnClass /@ConditionalOnMissingClass |
OnClassCondition |
bean 條件註解 | @ConditionalOnBean /@ConditionalOnMissingBean |
OnBeanCondition |
屬性條件註解 | @ConditionalOnProperty |
OnPropertyCondition |
Resource 條件註解 | @ConditionalOnResource |
OnResourceCondition |
Web 應用條件註解 | @ConditionalOnWebApplication / @ConditionalOnNotWebApplication |
OnWebApplicationCondition |
spring表達式條件註解 | @ConditionalOnExpression |
OnExpressionCondition |
本文繼續分析條件判斷。
5. @ConditionalOnProperty
:OnPropertyCondition#getMatchOutcome
我們再來看看@ConditionalOnProperty
的處理,進入OnPropertyCondition#getMatchOutcome
方法:
class OnPropertyCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 獲取 @ConditionalOnProperty 的屬性值
List<AnnotationAttributes> allAnnotationAttributes = annotationAttributesFromMultiValueMap(
metadata.getAllAnnotationAttributes(ConditionalOnProperty.class.getName()));
List<ConditionMessage> noMatch = new ArrayList<>();
List<ConditionMessage> match = new ArrayList<>();
for (AnnotationAttributes annotationAttributes : allAnnotationAttributes) {
// 在 determineOutcome(...) 方法中進行判斷,注意參數:context.getEnvironment()
ConditionOutcome outcome = determineOutcome(annotationAttributes,
context.getEnvironment());
(outcome.isMatch() ? match : noMatch).add(outcome.getConditionMessage());
}
if (!noMatch.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.of(noMatch));
}
return ConditionOutcome.match(ConditionMessage.of(match));
}
...
}
這個方法還是比較簡單的,先是獲取 @ConditionalOnProperty
的屬性值,再調用determineOutcome(...)
方法進行處理,讓我們再進行OnPropertyCondition#determineOutcome
方法:
/**
* 處理結果
* 注意:resolver 傳入的的是 Environment,這就是 applicationContext 中的 Environment
*/
private ConditionOutcome determineOutcome(AnnotationAttributes annotationAttributes,
PropertyResolver resolver) {
Spec spec = new Spec(annotationAttributes);
List<String> missingProperties = new ArrayList<>();
List<String> nonMatchingProperties = new ArrayList<>();
// 處理操作
spec.collectProperties(resolver, missingProperties, nonMatchingProperties);
// 判斷結果
if (!missingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec)
.didNotFind("property", "properties").items(Style.QUOTE, missingProperties));
}
// 判斷結果
if (!nonMatchingProperties.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec)
.found("different value in property", "different value in properties")
.items(Style.QUOTE, nonMatchingProperties));
}
// 判斷結果
return ConditionOutcome.match(ConditionMessage
.forCondition(ConditionalOnProperty.class, spec).because("matched"));
}
/**
* 處理屬性
*/
private void collectProperties(PropertyResolver resolver, List<String> missing,
List<String> nonMatching) {
for (String name : this.names) {
String key = this.prefix + name;
// resolver 傳入的 environment
// properties 條件判斷就是判斷 environment 裏有沒有相應屬性
if (resolver.containsProperty(key)) {
if (!isMatch(resolver.getProperty(key), this.havingValue)) {
nonMatching.add(name);
}
}
else {
if (!this.matchIfMissing) {
missing.add(name);
}
}
}
}
可以看到,@ConditionalOnProperty
最終是通過判斷environment
中是否有該屬性來處理條件判斷的。
6. @ConditionalOnResource
:OnResourceCondition#getMatchOutcome
我們再來看看@ConditionalOnResource
的處理,一般我們這樣使用:
@Bean
@ConditionalOnResource(resources = "classpath:config.properties")
public Config config() {
return config;
}
表示當classpath
中存在config.properties
時,config
纔會被初始化springbean。
再進入OnResourceCondition#getOutcomes
方法:
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
MultiValueMap<String, Object> attributes = metadata
.getAllAnnotationAttributes(ConditionalOnResource.class.getName(), true);
// 獲取 ResourceLoader
ResourceLoader loader = context.getResourceLoader();
List<String> locations = new ArrayList<>();
collectValues(locations, attributes.get("resources"));
Assert.isTrue(!locations.isEmpty(),
"@ConditionalOnResource annotations must specify at least one resource location");
List<String> missing = new ArrayList<>();
// 遍歷判斷資源是否存在
for (String location : locations) {
// location 中可能有佔位符,在這裏處理
String resource = context.getEnvironment().resolvePlaceholders(location);
// 判斷 resource 是否存在
if (!loader.getResource(resource).exists()) {
missing.add(location);
}
}
// 處理結果
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnResource.class)
.didNotFind("resource", "resources").items(Style.QUOTE, missing));
}
return ConditionOutcome.match(ConditionMessage.forCondition(ConditionalOnResource.class)
.found("location", "locations").items(locations));
}
先是通過OnResourceCondition#getOutcomes
方法來獲取ResourceLoader
,通過調試方式發現當前的ResourceLoader
爲AnnotationConfigServletWebServerApplicationContext
:
獲取到ResourceLoader
後,調用ResourceLoader#getResource(String)
來獲取資源,然後調用Resource#exists
來判斷資源是否存在,最後處理匹配結果。
整個流程的關鍵是在ResourceLoader#getResource(String)
,我們來看看該方法的處理,進入到GenericApplicationContext#getResource
方法:
@Override
public Resource getResource(String location) {
if (this.resourceLoader != null) {
return this.resourceLoader.getResource(location);
}
return super.getResource(location);
}
這裏的this.resourceLoader
爲null
,進入父類的方法DefaultResourceLoader#getResource
:
public Resource getResource(String location) {
Assert.notNull(location, "Location must not be null");
for (ProtocolResolver protocolResolver : getProtocolResolvers()) {
Resource resource = protocolResolver.resolve(location, this);
if (resource != null) {
return resource;
}
}
// 處理/開頭的資源
if (location.startsWith("/")) {
return getResourceByPath(location);
}
else if (location.startsWith(CLASSPATH_URL_PREFIX)) {
// 處理classpath開頭的資源
return new ClassPathResource(
location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
}
else {
try {
// 以上都不滿足,使用 url 來解析
URL url = new URL(location);
return (ResourceUtils.isFileURL(url)
? new FileUrlResource(url) : new UrlResource(url));
}
catch (MalformedURLException ex) {
// url解析出了問題,最終還是用 getResourceByPath(...) 來解析
return getResourceByPath(location);
}
}
}
/**
* 通過路徑得到 Resource
*/
protected Resource getResourceByPath(String path) {
return new ClassPathContextResource(path, getClassLoader());
}
可以看到,DefaultResourceLoader#getResource
通過判斷location
的前綴,得到了4種Resource
:
ClassPathContextResource
FileUrlResource
UrlResource
得到Resource
後,接着就是判斷該Resource
是否存在了,我們先來看看ClassPathContextResource#exist
方法,該方法在ClassPathResource#exists
:
/**
* 判斷 Resource 是否存在
*/
@Override
public boolean exists() {
return (resolveURL() != null);
}
/**
* 資源能獲取到,則返回資源對應的url,否則返回null
*/
@Nullable
protected URL resolveURL() {
if (this.clazz != null) {
// 使用當前的 class 對應的 classLoader 來獲取
return this.clazz.getResource(this.path);
}
else if (this.classLoader != null) {
// 使用指定的 classLoader 來獲取
return this.classLoader.getResource(this.path);
}
else {
// 獲取系統類加載器獲取
return ClassLoader.getSystemResource(this.path);
}
}
從代碼可以看到,最終是通過classLoader
獲取文件的url
,通過判斷文件url
是否爲null
來判斷resource
是否存在。
再來看看 FileUrlResource
的判斷,實際上 FileUrlResource
與 UrlResource
的exist()
方法都是AbstractFileResolvingResource#exists
,這裏統一分析就可以了,該方法內容如下:
public boolean exists() {
try {
URL url = getURL();
if (ResourceUtils.isFileURL(url)) {
// 如果是文件,直接判斷文件是否存在
return getFile().exists();
}
else {
// 否則使用網絡文件來處理
URLConnection con = url.openConnection();
customizeConnection(con);
HttpURLConnection httpCon =
(con instanceof HttpURLConnection ? (HttpURLConnection) con : null);
// 如果是http,則判斷看看鏈接返回的狀態碼
if (httpCon != null) {
int code = httpCon.getResponseCode();
if (code == HttpURLConnection.HTTP_OK) {
return true;
}
else if (code == HttpURLConnection.HTTP_NOT_FOUND) {
return false;
}
}
// 連接 contentLengthLong 大於0,也當成是true
if (con.getContentLengthLong() > 0) {
return true;
}
if (httpCon != null) {
httpCon.disconnect();
return false;
}
else {
getInputStream().close();
return true;
}
}
}
catch (IOException ex) {
return false;
}
}
如果是本地文件,直接使用File#exists()
方法判斷文件是否存在,否則就判斷網絡文件是否存在,判斷方式這裏就不細說了。
總的來說,springboot 對@ConditionalOnResource
的判斷還是有些複雜的,這裏總結如下:
- 如果是
classpath
文件,通過classloader
獲取文件對應的url
是否爲null
來判斷文件是否存在; - 如果是普通文件,則直接
File#exists()
方法判斷文件是否存在; - 如果是網絡文件,先打開一個網絡連接,判斷文件是否存在。
7. @ConditionalOnWebApplication
:OnWebApplicationCondition#getMatchOutcome
我們再來看看@ConditionalOnWebApplication
的處理,進入OnWebApplicationCondition#getOutcomes
方法:
@Override
protected ConditionOutcome[] getOutcomes(String[] autoConfigurationClasses,
AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[autoConfigurationClasses.length];
for (int i = 0; i < outcomes.length; i++) {
String autoConfigurationClass = autoConfigurationClasses[i];
if (autoConfigurationClass != null) {
// 處理結果
outcomes[i] = getOutcome(autoConfigurationMetadata.get(autoConfigurationClass,
"ConditionalOnWebApplication"));
}
}
return outcomes;
}
/**
* 處理結果
* springboot支持的web類型有兩種:SERVLET,REACTIVE
*/
private ConditionOutcome getOutcome(String type) {
if (type == null) {
return null;
}
ConditionMessage.Builder message = ConditionMessage
.forCondition(ConditionalOnWebApplication.class);
// 如果指定的類型是 SERVLET
if (ConditionalOnWebApplication.Type.SERVLET.name().equals(type)) {
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("servlet web application classes").atAll());
}
}
// 如果指定的類型是 REACTIVE
if (ConditionalOnWebApplication.Type.REACTIVE.name().equals(type)) {
if (!ClassNameFilter.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("reactive web application classes").atAll());
}
}
// 如果沒有指定web類型
if (!ClassNameFilter.isPresent(SERVLET_WEB_APPLICATION_CLASS, getBeanClassLoader())
&& !ClassUtils.isPresent(REACTIVE_WEB_APPLICATION_CLASS, getBeanClassLoader())) {
return ConditionOutcome.noMatch(
message.didNotFind("reactive or servlet web application classes").atAll());
}
return null;
}
這個方法很簡單,處理邏輯爲:根據@ConditionalOnWebApplication
中指定的類型,判斷對應的類是否存在,判斷方式與@ConditionalOnClass
判斷類是否存在一致,而兩種類型對應的類如下:
- Servlet:
org.springframework.web.context.support.GenericWebApplicationContext
- Reactive:
org.springframework.web.reactive.HandlerResult
8. @ConditionalOnExpression
:OnExpressionCondition#getMatchOutcome
我們再來看看@ConditionalOnExpression
的處理,進入OnExpressionCondition#getOutcomes
方法:
/**
* 處理匹配結果
*/
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
// 獲取表達式
String expression = (String) metadata.getAnnotationAttributes(
ConditionalOnExpression.class.getName()).get("value");
expression = wrapIfNecessary(expression);
ConditionMessage.Builder messageBuilder = ConditionMessage
.forCondition(ConditionalOnExpression.class, "(" + expression + ")");
// 處理佔位符
expression = context.getEnvironment().resolvePlaceholders(expression);
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
if (beanFactory != null) {
// 計算表達式的值
boolean result = evaluateExpression(beanFactory, expression);
return new ConditionOutcome(result, messageBuilder.resultedIn(result));
}
return ConditionOutcome.noMatch(messageBuilder.because("no BeanFactory available."));
}
/**
* 計算表達式的值
*/
private Boolean evaluateExpression(ConfigurableListableBeanFactory beanFactory,
String expression) {
BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
if (resolver == null) {
resolver = new StandardBeanExpressionResolver();
}
// 在這裏解析表達式的值
BeanExpressionContext expressionContext = new BeanExpressionContext(beanFactory, null);
Object result = resolver.evaluate(expression, expressionContext);
return (result != null && (boolean) result);
}
可以看到,springboot最終是通過 BeanExpressionResolver#evaluate
方法來計算表達式結果,關於spring表達式,本文就不展開分析了。
好了,spring條件註解的分析就到這裏了,需要說明的是,springboot 還 有其他條件註解:
這些註解的判斷方式與本文的方式相類似,就不一一進行分析了。
本文原文鏈接:https://my.oschina.net/funcy/blog/4921590 ,限於作者個人水平,文中難免有錯誤之處,歡迎指正!原創不易,商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。