Springboot自動加載的原理

一.概括

我們用Springboot很方便的能將一個框架給搭建起來,是因爲它將以前我們需要手動配置的地方都利用自動配置來代替,利用約定大於配置的思想簡化了我們開發工作量。例如:在沒有springboot之前,我們要在工程裏面連接數據庫的時候,我們需要在applicationContext.xml文件裏面配置:

<bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> 
 
    <property name="driverClassName"  value="com.mysql.jdbc.Driver" />
 
     <property name="url" value="jdbc:mysql://localhost:3306/test" />
 
     <property name="username" value="root" />
 
     <property name="password" value="123456" />
 
    </bean>

但引入springboot後不需要聲明DriverManagerDataSource,只需要在applicationContext.properties裏面配置連接數據庫的所需要的URL,用戶名和密碼就可以了,然後在pom文件裏面引入相應的jar包就可以了,至於爲什麼可以這樣,下面會講解到

spring.datasource.druid.url=jdbc:mysql://127.0.01:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

所以自動加載對於springboot來說很重要。
springboot啓動類

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {

        SpringApplication.run(DemoApplication.class, args);
    }

}

@SpringBootApplication註解,@SpringBootApplication是一個複合型註解,裏面包含了三個很重要的註解:
@SpringBootConfiguration:說明DemoApplication這個類是一個ioc容器配置類,可以在DemoApplication類中聲明一些bean,然後通過@ComponentScan加載到ioc容器中,相當於我們配置文件裏面的

<beans>
    <bean id="**" class="******">
    </bean>
    
    .......
</beans>

@ComponentScan:掃描當前包下的所有類和當前包下面子包所包含的類,將帶有註解的類加載到ioc容器,@ComponentScan在springboot中的作用就是將代碼中帶有@Controller,@Service,@Repority等這些註解的類加載到ioc容器中。相當於我們在配置文件中寫的這樣一段代碼

<context:component-scan base-package="***.***.***"/>

@EnableAutoConfiguration:基於你配置的依賴項,也就是引入的jar包,掃描所有jar包下面的META-INF/spring.factories,spring.factories中都是這個jar的配置類,配置類裏面就有我們所需要的工具類。將所有複合自動配置條件的bean定義加載到ioc容器中,記住@EnableAutoConfiguration自動加載的是一些不需要我們自己去定義但是需要用到的“工具類”,例如上面提到的DriverManagerDataSource類。
@EnableAutoConfiguration能自動加載我們項目中所需要的“工具類”是用到了SpringFactoriesLoader,SpringFactoriesLoader能將指定的配置文件META-INF/spring.factories加載配置。

@Target({ElementType.TYPE})// 註解的適用範圍,其中TYPE用於描述類、接口(包括包註解類型)或enum聲明
@Retention(RetentionPolicy.RUNTIME)// 註解的生命週期,保留到class文件中
@Documented// 表明這個註解應該被javadoc記錄
@Inherited// 子類可以繼承該註解
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    Class<?>[] exclude() default {};

    @AliasFor(
        annotation = EnableAutoConfiguration.class
    )
    String[] excludeName() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackages"
    )
    String[] scanBasePackages() default {};

    @AliasFor(
        annotation = ComponentScan.class,
        attribute = "basePackageClasses"
    )
    Class<?>[] scanBasePackageClasses() default {};
}

二.@EnableAutoConfiguration的基本原理
來看看@EnableAutoConfiguration這個註解是如何實現

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

	String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	Class<?>[] exclude() default {};

	/**
	 * Exclude specific auto-configuration class names such that they will never be
	 * applied.
	 * @return the class names to exclude
	 * @since 1.3.0
	 */
	String[] excludeName() default {};

}

在@EnableAutoConfiguration中有兩個比較重要的註解,一個是@AutoConfigurationPackage,另外一個是@Import(EnableAutoConfigurationImportSelector.class)
@AutoConfigurationPackage的作用就是將當前啓動類所在的包以bean的形式註冊到ioc容器中,至於在後面起什麼作用,我在網上查找了半天,也沒有一個說清楚的,有誰知道的,請在下面留言告訴我。
@Import(EnableAutoConfigurationImportSelector.class):藉助EnableAutoConfigurationImportSelector父類AutoConfigurationImportSelector的selectImports方法來讀取所有依賴的jar包下面META-INF/spring.factories文件,並且根據加載條件來加載項目所需要的類,這樣就完成了springboot的自動加載。
EnableAutoConfigurationImportSelector繼承了AutoConfigurationImportSelector,看看AutoConfigurationImportSelector的selectImports方法

       /**
        * 最主要的方法
        * annotationMetadata
        * [@org.springframework.boot.autoconfigure.SpringBootApplication
        * (scanBasePackageClasses=[], excludeName=[], exclude=[], scanBasePackages=[])]
        * @param annotationMetadata
        * @return
        */
       @Override
       public String[] selectImports(AnnotationMetadata annotationMetadata) {
              if (!isEnabled(annotationMetadata)) {
                     return NO_IMPORTS;
              }
              /**
               * 
               */
              AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                           .loadMetadata(this.beanClassLoader);
              /**
               * 得到註解中的所有屬性信息{excludeName=[], exclude=[]}
               */
              AnnotationAttributes attributes = getAttributes(annotationMetadata);
              /**
               *加載META-INF/spring-autoconfigure-metadata.properties,獲取所有支持自動配置的信息
               * 獲取所有支持EnableAutoConfiguration的組件信息,這部分信息配置在spring-boot-autoconfig包下的spring.factories下
               *
               *  使用了內部工具使用SpringFactoriesLoader,查找classpath上所有jar包中的
               *  META-INF\spring.factories,找出其中key爲
               *  org.springframework.boot.autoconfigure.EnableAutoConfiguration
               *  的屬性定義的工廠類名稱。
               */
              List<String> configurations = getCandidateConfigurations(annotationMetadata,
                           attributes);
              configurations = removeDuplicates(configurations);
              /**
               * 去除不需要的
               * @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, RedisAutoConfiguration.class,
                     DataSourceTransactionManagerAutoConfiguration.class, })
               */
              Set<String> exclusions = getExclusions(annotationMetadata, attributes);
              checkExcludedClasses(configurations, exclusions);
              configurations.removeAll(exclusions);
              /**
               * 然後使用AutoConfigurationImportFilter進行過濾,過濾的方式基本上是判斷現有系統是否引入了某個組件,(系統是否使用哪個組件是在pom定義的時候就確定了的)
               * ,如果有的話則進行相關配置。比如ServletWebServerFactoryAutoConfiguration
               * ,會在ServletRequest.class等條件存在的情況下進行配置,
               * 而EmbeddedTomcat會在Servlet.class, Tomcat.class存在的情況下創建TomcatServletWebServerFactory
               *
               * org.springframework.boot.autoconfigure.condition.OnClassCondition
               * 總而言之,此過濾器會檢查候選配置類的註解@ConditionalOnClass,如果要求的類在classpath 中不存在,則這個候選配置類會被排除掉
               */
              configurations = filter(configurations, autoConfigurationMetadata);
               /**
                * 現在已經找到所有需要被應用的候選配置類
                * 廣播事件AutoConfigurationImportEvent
                */
              fireAutoConfigurationImportEvents(configurations, exclusions);
              return StringUtils.toStringArray(configurations);
       }


private void fireAutoConfigurationImportEvents(List<String> configurations,
                     Set<String> exclusions) {
              List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
              if (!listeners.isEmpty()) {
                     AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
                                  configurations, exclusions);
                     for (AutoConfigurationImportListener listener : listeners) {
                           invokeAwareMethods(listener);
                           listener.onAutoConfigurationImportEvent(event);
                     }
              }
 }

getCandidateConfigurations方法會讀取到所有依賴jar包下面的META-INF/spring.factories,並將spring.factories中的配置類的全名稱獲取到。

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
    }

重點來了,springboot之所以能拿到spring.factories就是通過SpringFactoriesLoader來讀取的,SpringFactoriesLoader會將依賴包所有的spring.factories讀取出來,並用一個map來封裝讀取出來的vaule。SpringFactoriesLoader是spring提供的一種擴張方案,其主要功能就是從指定的配置文件META-INF/spring.factories加載配置。

public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
		String factoryClassName = factoryClass.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
	}
    /**
     *在springboot啓動的時候會將依賴包所有的spring.factories讀取出來,spring.factories
     *中不僅包括配置類,還有監聽器,初始化器等
     */
	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 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);
		}
	}

拿例如引入的mybatis舉例,引入baomidou的mybaitis包後,springboot啓動會去讀取這個jar包下面的配置文件

 <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusAutoConfiguration

我們再看看MybatisPlusAutoConfiguration這個配置類裏面的內容,可以看到MybatisPlusAutoConfiguration裏面產生的bean都是以前需要我們去xml文件裏面配置的類,當有了MybatisPlusAutoConfiguration後,我們不需要去xml文件利聲明這些bean,springboot在啓動的時候就將這些ban加載到了容器裏。

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisPlusAutoConfiguration {
    private static final Log logger = LogFactory.getLog(MybatisPlusAutoConfiguration.class);
    private final MybatisPlusProperties properties;
    private final Interceptor[] interceptors;
    private final ResourceLoader resourceLoader;
    private final DatabaseIdProvider databaseIdProvider;
    private final List<ConfigurationCustomizer> configurationCustomizers;

    public MybatisPlusAutoConfiguration(MybatisPlusProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
        this.properties = properties;
        this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
        this.resourceLoader = resourceLoader;
        this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
        this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
    }

    @PostConstruct
    public void checkConfigFileExists() {
        if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
            Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
            Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
        }

    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
        factory.setDataSource(dataSource);
        factory.setVfs(SpringBootVFS.class);
        if (StringUtils.hasText(this.properties.getConfigLocation())) {
            factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
        }

        MybatisConfiguration configuration = this.properties.getConfiguration();
        if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
            configuration = new MybatisConfiguration();
        }

        if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
            Iterator i$ = this.configurationCustomizers.iterator();

            while(i$.hasNext()) {
                ConfigurationCustomizer customizer = (ConfigurationCustomizer)i$.next();
                customizer.customize(configuration);
            }
        }

        configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
        factory.setConfiguration(configuration);
        if (this.properties.getConfigurationProperties() != null) {
            factory.setConfigurationProperties(this.properties.getConfigurationProperties());
        }

        if (!ObjectUtils.isEmpty(this.interceptors)) {
            factory.setPlugins(this.interceptors);
        }

        if (this.databaseIdProvider != null) {
            factory.setDatabaseIdProvider(this.databaseIdProvider);
        }

        if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
            factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
        }

        if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
            factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
        }

        if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
            factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
        }

        if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
            factory.setMapperLocations(this.properties.resolveMapperLocations());
        }

        if (!ObjectUtils.isEmpty(this.properties.getGlobalConfig())) {
            factory.setGlobalConfig(this.properties.getGlobalConfig().convertGlobalConfiguration());
        }

        return factory.getObject();
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        ExecutorType executorType = this.properties.getExecutorType();
        return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
    }

    @Configuration
    @Import({MybatisPlusAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
    @ConditionalOnMissingBean({MapperFactoryBean.class})
    public static class MapperScannerRegistrarNotFoundConfiguration {
        public MapperScannerRegistrarNotFoundConfiguration() {
        }

        @PostConstruct
        public void afterPropertiesSet() {
            MybatisPlusAutoConfiguration.logger.debug("No " + MapperFactoryBean.class.getName() + " found.");
        }
    }

    public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
        private BeanFactory beanFactory;
        private ResourceLoader resourceLoader;

        public AutoConfiguredMapperScannerRegistrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            MybatisPlusAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
            ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);

            try {
                if (this.resourceLoader != null) {
                    scanner.setResourceLoader(this.resourceLoader);
                }

                List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
                if (MybatisPlusAutoConfiguration.logger.isDebugEnabled()) {
                    Iterator i$ = packages.iterator();

                    while(i$.hasNext()) {
                        String pkg = (String)i$.next();
                        MybatisPlusAutoConfiguration.logger.debug("Using auto-configuration base package '" + pkg + "'");
                    }
                }

                scanner.setAnnotationClass(Mapper.class);
                scanner.registerFilters();
                scanner.doScan(StringUtils.toStringArray(packages));
            } catch (IllegalStateException var7) {
                MybatisPlusAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled." + var7);
            }

        }

        public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
            this.beanFactory = beanFactory;
        }

        public void setResourceLoader(ResourceLoader resourceLoader) {
            this.resourceLoader = resourceLoader;
        }
    }
}

最後當springboot拿到這些配置文件裏面的全性定類名的時候,會通過反射(Java Refletion)實例化爲對應的標註了@Configuration的JavaConfig形式的IoC容器配置類,然後彙總爲一個並加載到IoC容器。
當收集到所有在spring.factories中指定的bean的類路徑,在processGroupImports方法中會以處理@Import註解一樣的邏輯將其導入進容器。
 

public void processGroupImports() {
    for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
        // getImports即上面得到的所有類路徑的封裝
        grouping.getImports().forEach(entry -> {
            ConfigurationClass configurationClass = this.configurationClasses.get(
                    entry.getMetadata());
            try {
                // 和處理@Import註解一樣
                processImports(configurationClass, asSourceClass(configurationClass),
                        asSourceClasses(entry.getImportClassName()), false);
            }
            catch (BeanDefinitionStoreException ex) {
                throw ex;
            }
            catch (Throwable ex) {
                throw new BeanDefinitionStoreException(
                        "Failed to process import candidates for configuration class [" +
                                configurationClass.getMetadata().getClassName() + "]", ex);
            }
        });
    }
}

private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
            Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
    ...
    // 遍歷收集到的類路徑
    for (SourceClass candidate : importCandidates) {
       ...
        //如果candidate是ImportSelector或ImportBeanDefinitionRegistrar類型其處理邏輯會不一樣,這裏不關注
        // Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
                        // process it as an @Configuration class
                        this.importStack.registerImport(
                                currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
        // 當作 @Configuration 處理         
        processConfigurationClass(candidate.asConfigClass(configClass));
   ...
}
            
    ...
}

springboot是如何讓applicationcon.properties裏面的配置生效的呢
以MybatisPlusAutoConfiguration舉例,在MybatisPlusAutoConfiguration類上面有一個EnableConfigurationProperties註解,這個註解的作用就是讓使用 @ConfigurationProperties 的類進行ioc注入,也就是將MybatisPlusProperties這個屬性類生效,

@Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisPlusAutoConfiguration {

我們再看看MybatisPlusProperties這類。在這裏看到了註解@ConfigurationProperties,這個註解的作用就是讀取配置文件裏面屬性,將讀到的值綁定到被註解的類中的屬性。

@ConfigurationProperties(
    prefix = "mybatis-plus"
)
public class MybatisPlusProperties {
    public static final String MYBATIS_PLUS_PREFIX = "mybatis-plus";
    private String configLocation;
    private String[] mapperLocations;
    private String typeAliasesPackage;
    private String typeEnumsPackage;
    private String typeHandlersPackage;
    private boolean checkConfigLocation = false;
    private ExecutorType executorType;
    private Properties configurationProperties;
    @NestedConfigurationProperty
    private GlobalConfig globalConfig;
    @NestedConfigurationProperty
    private MybatisConfiguration configuration;

再看看項目中的配置文件application.yml,發現對mybatis的配置中是能和MybatisPlusProperties這個類一一對於對應的。

mybatis-plus:
# xml文件路徑在package下面的 classpath:/com/yourpackage/*/mapper/*Mapper.xml
# xml文件路徑在resources下面 classpath:/mapper/*Mapper.xml
    mapper-locations: classpath:/mappers/*Mapper.xml
#掃描的pojo對象所在包
    type-aliases-package: com.example.demo.bean
    global-config:
        #\u9A7C\u5CF0\u4E0B\u5212\u7EBF\u8F6C\u6362
        db-column-underline: true
        #\u903B\u8F91\u5220\u9664\u914D\u7F6E\uFF08\u4E0B\u97623\u4E2A\u914D\u7F6E\uFF09
        logic-delete-value: 1
        logic-not-delete-value: 0
        sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector

通過@ConfigurationProperties註解將配置文件裏面的值讀到配置bean裏面,然後@EnableConfigurationProperties將註解bean注入到ioc容器中,這樣當項目需要配置信息的時候可以直接從容器中去取。

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