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

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