Spring Boot Starter简化开发原理

580 阅读7分钟

Spring Boot Starter简化开发原理

在我刚学spring的时候,让我印象深刻的是各种xml的配置,而且很多配置是这个项目配一遍,开新项目时又把旧项目的配置复制一遍,而且比较容易出错,调试一下又要费不少的时间。Starter简化了这些繁琐的配置,遵循Spring Boot“约定大约配置”的理念,在我们不主动进行配置时给予默认的属性,而且很多情况下这些属性都不需要更改。这是怎么实现的呢? 在SpringApplication这个类,也就是启动类上有一个@SpringApplication的注解,该注解是由@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 {
    //不重要的内容忽略
}

点开@EnableAutoConfiguration,可以看到该注解导入了一个AutoConfigurationImportSelector的类

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.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 {};

}

接下来我们重点看这个类:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
      
        // 忽略不重要的内容  
         
        @Override
	    public String[] selectImports(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return NO_IMPORTS;
		}
		AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
				.loadMetadata(this.beanClassLoader);
		AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
				annotationMetadata);
		return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
	}

该类继承了DeferredImportSelector,而DeferredImportSelector继承了ImportSelector。这里有一个小知识: 在使用@Import注解来注册bean的时候,Import注解的值可以是ImportSelector的实现类,spring容器会实例化这个实现类,并执行其selectImports方法 因此,spring会调用该类的selectImports方法,而大家注意,这个类继承的是DeferredImportSelector,因此,这个类会在其他@Configuration加载后才会进行加载。接下来我们重点看selectImports方法,该方法调用了getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata),进入这个方法:

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

其中,这个方法调用了getCandidateConfigurations(annotationMetadata, attributes);

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

继续进入SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader());

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}

这个方法又调用了loadSpringFactories(classLoader),重点来了:

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 factoryTypeName = ((String) entry.getKey()).trim();
					for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
						result.add(factoryTypeName, factoryImplementationName.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中获取需要自动配置的类,而FACTORIES_RESOURCE_LOCATION的具体内容如下:

	/**
	 * 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";

因此,我们可以得出结论spring会从包的类"META-INF/spring.factories"中读取需要自动配置的类,从而实现springboot的自动配置。下面我们用一个常用的starter,验证一下我们得出的结论是否正确

mybatis-spring-boot-starter 原理探索

打开mybatis-spring-boot-starter这个jar,可以看到只有这几个文件 mybatis-spring-boot-starter

点开pom文件:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <parent>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot</artifactId>
    <version>2.1.0</version>
  </parent>
  <artifactId>mybatis-spring-boot-starter</artifactId>
  <name>mybatis-spring-boot-starter</name>
  <properties>
    <module.name>org.mybatis.spring.boot.starter</module.name>
  </properties>
  <dependencies>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
    </dependency>
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
    </dependency>
  </dependencies>
</project>

这里有一个点,就是starter的作用有两个,一个是依赖管理,另外一个是启动某些功能,这个pom文件体现的就是依赖管理的作用,将该功能所需要的依赖自动引入。而下面这个依赖则是mybatis自动配置的关键

<dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-autoconfigure</artifactId>
    </dependency>

mybatis-spring-boot-autoconfigure

我们主要看两个类,一个是MybatisProperties,一个是MybatisAutoConfiguration,这两个类的命名是有规范的,以AutoConfiguration结尾的,是一个javaConfig形式的配置类,对标spring中以xml形式配置的类,而以Properties结尾的类,其作是从配置文件中读取属性。而AutoConfiguration类会从对应的Properties中获取属性进行配置。打开MybatisProperties类:

/**
 * Configuration properties for MyBatis.
 *
 * @author Eddú Meléndez
 * @author Kazuki Shimizu
 */
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {

  public static final String MYBATIS_PREFIX = "mybatis";

  private static final ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();

  /**
   * Location of MyBatis xml config file.
   */
  private String configLocation;

  /**
   * Locations of MyBatis mapper files.
   */
  private String[] mapperLocations;
  //    忽略下面的代码
}

我们可以看到,这个类使用了@ConfigurationProperties注解,这个注解的作用是从配置文件中读取前缀为mybatis的属性,注入到匹配的属性上,如key为mybatis.configLocation的值,就会被注入到该类configLocation的属性上面。接下来我们看一下MyBatisAutoConfiguration的代码

@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {

  private static final Logger logger = LoggerFactory.getLogger(MybatisAutoConfiguration.class);

  private final MybatisProperties properties;

  private final Interceptor[] interceptors;

  private final TypeHandler[] typeHandlers;

  private final LanguageDriver[] languageDrivers;

  private final ResourceLoader resourceLoader;

  private final DatabaseIdProvider databaseIdProvider;

  private final List<ConfigurationCustomizer> configurationCustomizers;

  public MybatisAutoConfiguration(MybatisProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider,
      ObjectProvider<TypeHandler[]> typeHandlersProvider, ObjectProvider<LanguageDriver[]> languageDriversProvider,
      ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider,
      ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
    this.properties = properties;
    this.interceptors = interceptorsProvider.getIfAvailable();
    this.typeHandlers = typeHandlersProvider.getIfAvailable();
    this.languageDrivers = languageDriversProvider.getIfAvailable();
    this.resourceLoader = resourceLoader;
    this.databaseIdProvider = databaseIdProvider.getIfAvailable();
    this.configurationCustomizers = configurationCustomizersProvider.getIfAvailable();
  }
    //省略部分代码

  @Bean
  @ConditionalOnMissingBean
  public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
    SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
    factory.setDataSource(dataSource);
    factory.setVfs(SpringBootVFS.class);
    if (StringUtils.hasText(this.properties.getConfigLocation())) {
      factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
    }
    applyConfiguration(factory);
    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 (this.properties.getTypeAliasesSuperType() != null) {
      factory.setTypeAliasesSuperType(this.properties.getTypeAliasesSuperType());
    }
    if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
      factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
    }
    if (!ObjectUtils.isEmpty(this.typeHandlers)) {
      factory.setTypeHandlers(this.typeHandlers);
    }
    if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
      factory.setMapperLocations(this.properties.resolveMapperLocations());
    }
    Set<String> factoryPropertyNames = Stream
        .of(new BeanWrapperImpl(SqlSessionFactoryBean.class).getPropertyDescriptors()).map(PropertyDescriptor::getName)
        .collect(Collectors.toSet());
    Class<? extends LanguageDriver> defaultLanguageDriver = this.properties.getDefaultScriptingLanguageDriver();
    if (factoryPropertyNames.contains("scriptingLanguageDrivers") && !ObjectUtils.isEmpty(this.languageDrivers)) {
      // Need to mybatis-spring 2.0.2+
      factory.setScriptingLanguageDrivers(this.languageDrivers);
      if (defaultLanguageDriver == null && this.languageDrivers.length == 1) {
        defaultLanguageDriver = this.languageDrivers[0].getClass();
      }
    }
    if (factoryPropertyNames.contains("defaultScriptingLanguageDriver")) {
      // Need to mybatis-spring 2.0.2+
      factory.setDefaultScriptingLanguageDriver(defaultLanguageDriver);
    }

    return factory.getObject();
  }

     //省略部分代码
  @Bean
  @ConditionalOnMissingBean
  public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
    ExecutorType executorType = this.properties.getExecutorType();
    if (executorType != null) {
      return new SqlSessionTemplate(sqlSessionFactory, executorType);
    } else {
      return new SqlSessionTemplate(sqlSessionFactory);
    }
  }

}

我们先从注解看起: @ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class }) 这个注解表示只有存在sqlSessionFactory和SqlSessionFactoryBean这两个类时,才会初始化该类; @ConditionalOnSingleCandidate(DataSource.class) 这个注解表示,只有在仅有一个DataSource.class的或者有多个DataSource,但是指定了首选dataSource时,才会初始化本类(这就是在配置多数据源时要指定首选数据源的原因) @EnableConfigurationProperties(MybatisProperties.class) 这个注解的作用是使MybatisProperties上的@ConfigurationProperties生效 @AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class }) 这个注解的作用是指定当前类加载的顺序 接下来是sqlSessionFactory方法和sqlSessionTemplate方法,这两个方法都有@Bean和@ConditionalOnMissingBean注解, 这表明spring会在用户没有自定义SqlSessionFactory或SqlSessionTemplate的时候自动配置这两个bean。 最后我们打开META-INF文件夹,这里我们看到了一个熟悉的文件:spring.factories,打开文件可以看到:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration,\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

根据我们上一部分所学的知识,spring会自动的去配置这两个类,至此,我们可以稍微理一下整个自动加载的流程是怎样的:

1.SpringBootApplication启动类在运行时扫描到@SpringBootApplication注解,该注解是由多个注解组合而成,其中一个注解是@Import(AutoConfigurationImportSelector.class),该类实现ImportSelector接口,当@Import所指定的类是一个ImportSelector的实现类时,springbootApplication会运行其selectImports方法,selectImport方法会扫描所有jar包META-INF文件夹下面的spring.factories文件,然后会以org.springframework.boot.autoconfigure.EnableAutoConfiguration为key,获取所有value,组成一个List,接着会扫描这些List中的类

2.由于mybatis-spring-boot-starter中的spring.factories,因此spring会扫描org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration 这个类,而由于该类上的注解,因此该类只会在存在 1.sqlSessionFactory和SqlSessionFactoryBean这两个类 2.只有在仅有一个DataSource.class的或者有多个DataSource,但是指定了首选dataSource 这两个条件都符合的情况下初始化,且由于@AutoConfigureAfter注解,该类的初始化顺序是在 DataSourceAutoConfiguration和MybatisLanguageDriverAutoConfiguration后面 同时,该类上的@EnableConfigurationProperties注解会使MybatisProperties这个类上的@ConfigurationProperties生效,因此会读取配置文件上所有mybatis为前缀的属性,并匹配到该类的属性上。

3.在初始化MybatisAutoConfiguration时,由于sqlSessionFactory(DataSource dataSource)和sqlSessionTemplate(SqlSessionFactory sqlSessionFactory)这两个方法上有@Bean和 @ConditionalOnMissingBean 这两个注解,因此spring会在用户没有自定义sqlSessionFactory或sqlSessionTemplate的情况下自动使用MybatisProperties这个类从配置文件读取的值来进行初始化,若没有在配置文件读取到对应到属性,则会使用默认值

4.最后,我们感受到的效果是,只要引入mybatis-spring-boot-starter这个依赖,就可以使用mybatis对数据进行访问,而dataSource,sqlSessionFactory这些东西有时候我们自己配置也可以,不配置也可以的原因。