1. 程式人生 > >Spring boot ConditionalOnClass原理解析

Spring boot ConditionalOnClass原理解析

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

    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 {};

    }