Spring boot如何自動加載
對於Springboot的ConditionalOnClass注解一直非常好奇,原因是我們的jar包里面可能沒有對應的class,而使用ConditionalOnClass標注的Configuration類又import了這個類,那么如果想加載Configuration類,就會報ClassNotFoundException,那么又如何取到這個類上的注解呢
SpringFactoriesLoader獲取"META-INF/spring.factories"路徑下的所有文件,解析出需要自動加載的類
判斷的邏輯為配置中的org.springframework.boot.autoconfigure.EnableAutoConfiguration=xxx
xxx即為我們配置的需要自動加載的@Configuration標注的類
解析出需要加載的所有Configuration類,無論該類是否被ConditionOnClass注解聲明,都使用OnClassCondition類進行match,判斷是否需要加載當前類
做這個解析有點耗時,spring boot將篩選出了所有Configuration類數目的一半,單獨放到另外一個線程中執行,這樣相當於並發兩個線程解析
可以參照OnClassCondition類的getOutcomes方法
具體執行解析操作的類為StandardOutcomesResolver,方法:resolveOutcomes()
以Gson自動配置類org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration 為例
package org.springframework.boot.autoconfigure.gson;
import com.google.gson.Gson;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Gson.
*
* @author David Liu
* @since 1.2.0
*/
@Configuration
@ConditionalOnClass(Gson.class)
public class GsonAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public Gson gson() {
return new Gson();
}
}
假如當前classpath下並沒有引入Gson類的jar包
private ConditionOutcome[] getOutcomes(final String[] autoConfigurationClasses,
int start, int end, AutoConfigurationMetadata autoConfigurationMetadata) {
ConditionOutcome[] outcomes = new ConditionOutcome[end - start];
for (int i = start; i < end; i++) {
String autoConfigurationClass = autoConfigurationClasses[i]; //GsonAutoConfiguration類的字符串
Set<String> candidates = autoConfigurationMetadata
.getSet(autoConfigurationClass, "ConditionalOnClass"); //獲取當前class上的ConditionOnClass注解配置
if (candidates != null) {
outcomes[i - start] = getOutcome(candidates);
}
}
return outcomes;
}
AutoConfigurationMetadataLoader類將加載META-INF/spring-autoconfigure-metadata.properties下所有的配置,如果你使用了ConditionalOnClass注解,需要寫到文件中,如
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration.ConditionalOnClass=io.searchbox.client.JestClient
這樣Set
下面的速度會明顯變慢,原因是將根據上述執行結果,找出對應的Class是否在classpath*下可以找到
private ConditionOutcome getOutcome(Set<String> candidates) {
try {
List<String> missing = getMatches(candidates, MatchType.MISSING,
this.beanClassLoader);
if (!missing.isEmpty()) {
return ConditionOutcome.noMatch(
ConditionMessage.forCondition(ConditionalOnClass.class)
.didNotFind("required class", "required classes")
.items(Style.QUOTE, missing));
}
}
catch (Exception ex) {
// We'll get another chance later
}
return null;
}
}
使用MatchType.MISSING來判斷,如果不為空,則說明缺少這個類了。
private enum MatchType {
PRESENT {
@Override
public boolean matches(String className, ClassLoader classLoader) {
return isPresent(className, classLoader);
}
},
MISSING {
@Override
public boolean matches(String className, ClassLoader classLoader) {
return !isPresent(className, classLoader);
}
};
private static boolean isPresent(String className, ClassLoader classLoader) {
if (classLoader == null) {
classLoader = ClassUtils.getDefaultClassLoader();
}
try {
forName(className, classLoader);
return true;
}
catch (Throwable ex) {
return false;
}
}
private static Class<?> forName(String className, ClassLoader classLoader)
throws ClassNotFoundException {
if (classLoader != null) {
return classLoader.loadClass(className);
}
return Class.forName(className);
}
public abstract boolean matches(String className, ClassLoader classLoader);
}
最終回到了原始的方法,調用classLoader.loadClass(className)來判斷類是否在classpath下,加載類相對於內存計算,比較耗時,這也是為什么需要再開一個線程和主線程一起工作的原因,使用Thread.join()來等待線程結束,並獲取最終結果
延伸
既然加載ConfigurationBean時,用ClassNotFound就能發現對應的類沒有在classpath下,又何必多此一舉,繞這么大個彎來發現沒有對應的class呢?
-
原因是ConditionalOnClass還支持輸入字符串類型的class name,在Configuration中可以面向接口編程的方式來生成bean
-
Spring boot還提供了類似ConditionalOnBean的注解,有可能一個class在classpath下,而不是spring里面的bean;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {/** * The classes that must be present. Since this annotation is parsed by loading class * bytecode, it is safe to specify classes here that may ultimately not be on the * classpath, only if this annotation is directly on the affected component and * <b>not</b> if this annotation is used as a composed, meta-annotation. In order to * use this annotation as a meta-annotation, only use the {@link #name} attribute. * @return the classes that must be present */ Class<?>[] value() default {}; /** * The classes names that must be present. * @return the class names that must be present. */ String[] name() default {};
}