Spring Boot自動裝配原理

Spring Boot相對於Spring的一大改變或者優勢來說就是“約定大於配置”的思想,不像Spring一樣所有的配置都需要我們自己去實現,Spring Boot集成了許多默認的配置。拿Spring MVC來舉例,原來Spring時代是通過寫兩個XML配置文件來實現的,一個web.xml,另一個applicationContext.xml。這些文件內容複雜,且大部分情況下不需要改變,在各個項目中的遷移也只是複製粘貼裏面的代碼而已,這無疑增加了使用成本。而在Spring Boot中,只需要引入相關spring-boot-starter-web依賴即可,其他的配置都不需要。即使有需要其他配置的地方,統一在application.properties配置文件中進行配置即可,該文件寫法是類似於json的鍵值對的格式,不像XML格式那樣的重量級。

那麼既然不需要相關配置,Spring Boot是如何實現自動裝配類的呢?如何在項目啓動的時候將需要加載的類都注入到Spring的IoC容器中?本文將探究這個問題。

通常在Spring Boot的啓動類上會加上@SpringBootApplication的註解,如下所示:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
其註解主要是由三個子註解構成的,分別是@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan:

@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 {
    //...
}
這樣我們可以直接使用@SpringBootApplication註解而不再需要使用以上三個註解來標識啓動類了。其中@EnableAutoConfiguration是開啓自動裝配的功能,該註解的代碼如下所示:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    //...
}
由上所示,@EnableAutoConfiguration註解需要引入AutoConfigurationImportSelector這個類或者其子類,如果沒有找到,則自動裝配失敗。

而在上述啓動時會調用SpringApplication的run方法。該方法會最終調用如下所示的重載run方法:

        /**
         * Run the Spring application, creating and refreshing a new
         * {@link ApplicationContext}.
         * @param args the application arguments (usually passed from a Java main method)
         * @return a running {@link ApplicationContext}
         */
        public ConfigurableApplicationContext run(String... args) {
                StopWatch stopWatch = new StopWatch();
                stopWatch.start();
                ConfigurableApplicationContext context = null;
                Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
                configureHeadlessProperty();
                SpringApplicationRunListeners listeners = getRunListeners(args);
                listeners.starting();
                try {
                        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                                        args);
                        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                                        applicationArguments);
                        configureIgnoreBeanInfo(environment);
                        Banner printedBanner = printBanner(environment);
                        context = createApplicationContext();
                        exceptionReporters = getSpringFactoriesInstances(
                                        SpringBootExceptionReporter.class,
                                        new Class[] { ConfigurableApplicationContext.class }, context);
                        prepareContext(context, environment, listeners, applicationArguments,
                                        printedBanner);
                        refreshContext(context);
                        afterRefresh(context, applicationArguments);
                        stopWatch.stop();
                        if (this.logStartupInfo) {
                                new StartupInfoLogger(this.mainApplicationClass)
                                                .logStarted(getApplicationLog(), stopWatch);
                        }
                        listeners.started(context);
                        callRunners(context, applicationArguments);
                }
                catch (Throwable ex) {
                        handleRunFailure(context, ex, exceptionReporters, listeners);
                        throw new IllegalStateException(ex);
                }

                try {
                        listeners.running(context);
                }
                catch (Throwable ex) {
                        handleRunFailure(context, ex, exceptionReporters, null);
                        throw new IllegalStateException(ex);
                }
                return context;
        }
以上方法會創建/刷新ApplicationContext、初始化Environment、listeners等一系列操作。在第23行代碼中,其會調用getSpringFactoriesInstances方法,同時會將SpringBootExceptionReporter.class這個參數傳入進去:

        private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
                        Class<?>[] parameterTypes, Object... args) {
                ClassLoader classLoader = getClassLoader();
                // Use names and ensure unique to protect against duplicates
                Set<String> names = new LinkedHashSet<>(
                                SpringFactoriesLoader.loadFactoryNames(type, classLoader));
                List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
                                classLoader, args, names);
                AnnotationAwareOrderComparator.sort(instances);
                return instances;
        }
在該方法中會調用SpringFactoriesLoader的loadFactoryNames方法。SpringFactoriesLoader類的作用是利用工廠的加載機制來讀取裝配資源的類,其部分源碼如下所示:

public final class SpringFactoriesLoader {

        /**
         * The location to look for factories.
         * <p>Can be present in multiple JAR files.
         */
        public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

        //...

        private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

        //...

        /**
         * Load the fully qualified class names of factory implementations of the
         * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
         * class loader.
         * @param factoryClass the interface or abstract class representing the factory
         * @param classLoader the ClassLoader to use for loading resources; can be
         * {@code null} to use the default
         * @throws IllegalArgumentException if an error occurs while loading factory names
         * @see #loadFactories
         */
        public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
                String factoryClassName = factoryClass.getName();
                return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
        }

        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 factoryClassName = ((String) entry.getKey()).trim();
                                        for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                                                result.add(factoryClassName, factoryName.trim());
                                        }
                                }
                        }
                        cache.put(classLoader, result);
                        return result;
                }
                catch (IOException ex) {
                        throw new IllegalArgumentException("Unable to load factories from location [" +
                                        FACTORIES_RESOURCE_LOCATION + "]", ex);
                }
        }

        //...

}
其中可以看到FACTORIES_RESOURCE_LOCATION這個常量,它就是讀取配置資源的文件地址,也就是說會從spring.factories文件中讀取所有需要注入的類出來,每一個jar包下基本上都會有一個spring.factories配置文件。而上面源碼中第25行的loadFactoryNames方法會調用第30行的loadSpringFactories方法,該方法的作用是讀取加載進來的所有jar包下的spring.factories配置文件中的內容,將其放到一個本地緩存cache中。緩存的意義在於第一次調用該方法時會讀取spring.factories文件並將讀取中的結果放到緩存中,之後再調用該方法時就不再讀取文件,而直接返回緩存中的內容就行了。

在spring-boot-autoconfigure的jar包下META-INF目錄中的spring.factories文件的部分內容如下:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

# Auto Configuration Import Listeners
org.springframework.boot.autoconfigure.AutoConfigurationImportListener=\
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfigurationImportListener

# Auto Configuration Import Filters
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

# 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,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.jest.JestAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.rest.RestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\

//...
由上可以看到,其文件格式由鍵值對組成,每一行末尾的反斜槓表示換行,其他jar包下spring.factories文件的內容也都會有所不同。

而在上面說過的SpringFactoriesLoader類的loadFactoryNames方法中會通過ClassLoader來讀取spring.factories文件中的內容。通過run方法中傳入的SpringBootExceptionReporter.class這個參數,找到文件中鍵是SpringBootExceptionReporter所對應的值的集合,通過反射機制來實例化相關的類再返回即可。如前面所說,這裏是第一次調用SpringFactoriesLoader類的工廠加載機制的地方,後續再調用的話會直接走緩存中的內容。

再回到最開始SpringApplication類中的run方法中的代碼中,之前說的都是基於其中的getSpringFactoriesInstances方法的延展,在該方法執行完畢後,在第28行會調用refreshContext方法,在其中會調用AutoConfigurationImportSelector類的getAutoConfigurationEntry方法,該方法就是自動裝配類的方法入口,而AutoConfigurationImportSelector類前面也說過,是@EnableAutoConfiguration這個註解必須要引入的類。在getAutoConfigurationEntry方法中會調用getCandidateConfigurations方法,這兩個方法的源碼如下:

        /**
         * Return the {@link AutoConfigurationEntry} based on the {@link AnnotationMetadata}
         * of the importing {@link Configuration @Configuration} class.
         * @param autoConfigurationMetadata the auto-configuration metadata
         * @param annotationMetadata the annotation metadata of the configuration class
         * @return the auto-configurations that should be imported
         */
        protected AutoConfigurationEntry getAutoConfigurationEntry(
                        AutoConfigurationMetadata autoConfigurationMetadata,
                        AnnotationMetadata annotationMetadata) {
                if (!isEnabled(annotationMetadata)) {
                        return EMPTY_ENTRY;
                }
                AnnotationAttributes attributes = getAttributes(annotationMetadata);
                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);
        }

        /**
         * Return the auto-configuration class names that should be considered. By default
         * this method will load candidates using {@link SpringFactoriesLoader} with
         * {@link #getSpringFactoriesLoaderFactoryClass()}.
         * @param metadata the source metadata
         * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
         * attributes}
         * @return a list of candidate configurations
         */
        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;
        }

        /**
         * Return the class used by {@link SpringFactoriesLoader} to load configuration
         * candidates.
         * @return the factory class
         */
        protected Class<?> getSpringFactoriesLoaderFactoryClass() {
                return EnableAutoConfiguration.class;
        }
getCandidateConfigurations方法中也會像上面說過的getSpringFactoriesInstances方法一樣,調用SpringFactoriesLoader的loadFactoryNames方法來獲取配置文件中的內容,只不過這次傳入的參數是getSpringFactoriesLoaderFactoryClass(),即EnableAutoConfiguration.class。也就是說會將需要自動裝配的類都拿到,通過EnableAutoConfiguration所對應的值的類名的集合。但這次並不會讀取spring.factories配置文件,而是會從緩存中讀取,因爲之前已經調用過loadSpringFactories方法了。讀取出的內容依然會使用反射來實例化,將實例化的結果返回回去並最終完成自動裝配的全過程。這些自動裝配的類會通過一些@ConditionalOnClass、@ConditionalOnMissingClass、@Import之類的註解來加載它們需要的資源。

以上就是Spring Boot完成自動裝配的大致核心流程,總結起來就是利用了SpringFactoriesLoader這個類來實現的工廠加載機制,讀取jar包下的META-INF目錄下的spring.factories配置文件中的內容,然後將需要的類名反射實例化即可。

需要自動裝配的類在以前的Spring時代都是需要我們自己寫的,但現在Spring Boot的自動裝配機制幫我們實現了,只需要引入相關的依賴即可。例如Redis的依賴就是spring-boot-starter-data-redis,引用它就可以了,同時在application.properties配置文件中做些簡單的配置即可,就可以直接用起來了,不再需要自己寫連接客戶端、連接池之類的代碼。在上面的spring.factories配置文件的示例中也出現了像RedisAutoConfiguration這樣的Redis的自動配置類。
更多免費技術資料可關注:annalin1203

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章