在应用程序开发过程中,很难通过一个框架就实现所有的功能需求,也不存在满足各种应用场景的统一开发框架。尤其近几年来,随着微服务、云原生等概念的落地,诞生了一系列新型的开发技术和框架。Spring Boot作为Spring家族中最具代表性的开源框架,充分考虑到了与其他技术和框架的整合,逐步形成了一个生态圈。
Spring Boot通过Starter组件确保应用程序的扩展性和集成性。开发人员可以实现自定义的Spring Boot Starter来完成与Spring Boot框架的有效集成。通过这种方式,越来越多的应用程序内置了对Spring Boot开发模式的支持,使得Spring Boot的应用场景和范围得到了显著拓宽。这是SpringBoot构建生态圈的基础。
另外,Spring Boot也是Spring Cloud的基础。Spring Cloud是Spring家族中专门用来实现微服务架构的开发框架,而基于Spring Cloud开发的每个微服务本质上就是一个Spring Boot应用程序。
Spring Boot同样对云原生提供了开发框架方面的技术支持,专门实现了一个Spring Native框架。通过使用基于GraalVM的原生镜像,无论是应用程序的启动时间,还是运行时所占用的内存空间,都得到了显著的优化。
本章将对上述Spring Boot生态圈的相关内容详细讲解,并在最后给出测试Spring Boot应用程序的系统方法。
先来介绍Spring Boot生态中的第一部分内容——Spring Boot Starter组件,这是Spring Boot为开发人员提供扩展性的基础,也是各种外部系统与Spring Boot集成的基础。通过Spring Boot Starter组件,Spring Boot开发了各种各样的Starter,包括Spring官方提供的,第三方开源的,以及我们自己开发的。这些Starter构成了一个全面而强大的生态圈。在这个生态圈中,基本上普通应用程序开发所需要的开发组件都可以被找到。
在第1章中,我们已经明白Spring Boot与Spring MVC相比最大的优点就是简单,它采用的是约定大于配置的设计思想。而这种思想具体落地的形式就是Spring Boot Starter。Spring Boot Starter不但使用方式很简单,内部实现机制也并不复杂。
在本节中,我们将深入分析Spring Boot自动配置的实现原理,然后给出基于Spring Boot Starter组件集成Spring Boot的案例分析。
Spring Boot的配置体系强大而复杂,其中最基础、最核心的就是自动配置(Auto-Configuration)机制。本节我们就围绕这个话题展开详细讨论,看看Spring Boot如何实现自动配置。让我们先从@SpringBootApplication注解开始讲起。
1. @SpringBootApplication注解
@SpringBootApplication注解位于spring-boot-autoconfigure工程的
org.springframework.boot.autoconfigure包中,其定义如代码清单13-1所示。
代码清单13-1 @SpringBootApplication注解定义代码
@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 {
@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 {};
}
相较于一般的注解,@SpringBootApplication注解显得有点复杂。我们可以通过exclude和excludeName属性来配置不需要自动装配的类或类名,也可以通过scanBasePackages和scanBasePackageClasses属性来配置需要扫描的包路径和类路径。
注意,@SpringBootApplication注解实际上由三个注解组合而成,分别是@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan。
@ComponentScan注解不是被Spring Boot引入的新注解,而是属于Spring容器管理的内容。@ComponentScan注解就是扫描所有@Component等注解标注的包下的、需要注入的类,并把相关Bean定义批量加载到容器中。
显然,Spring Boot应用程序同样需要这个功能。而@SpringBootConfiguration注解比较简单,事实上它是一个空注解,只是使用了Spring中的@Configuration注解。@Configuration注解比较常见,提供了JAVAConfig配置类。
@EnableAutoConfiguration注解是需要重点剖析的对象,该注解的定义如代码清单13-2所示。
代码清单13-2 @EnableAutoConfiguration注解定义代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY =
"spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
这里我们关注两个新注解,@AutoConfigurationPackage和@Import(
AutoConfiguration-ImportSelector.class)。
(1)@AutoConfigurationPackage注解
从命名上讲,我们可以对@AutoConfigurationPackage注解所在包下的类进行自动配置,而在实现上用到了Spring中的@Import注解。
@AutoConfigurationPackage注解的定义如代码清单13-3所示。
代码清单13-3 @AutoConfigurationPackage注解定义代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}
在使用Spring Boot时,@Import也是一个非常常见的注解,可以动态创建Bean。为了便于理解后续内容,这里有必要对@Import注解的运行机制展开讲解,该注解的定义如代码清单13-4所示。
代码清单13-4 @Import注解定义代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {
Class<?>[] value();
}
在@Import注解的属性中可以设置需要引入的类名,例如@AutoConfigurationPackage注解上的@Import(
AutoConfigurationPackages.Registrar.class)就引入了AutoConfiguration-Packages包下的Registrar类。根据所引入类的不同类型,Spring容器对@Import注解有以下四种处理方式。
如果该类实现了ImportSelector接口,Spring容器就会实例化该类,并且调用其selectImports()方法完成类的导入。
如果该类实现了DeferredImportSelector接口,则Spring容器也会实例化该类并调用其selectImports()方法。DeferredImportSelector继承了ImportSelector,区别在于DeferredImportSelector实例的selectImports()方法的调用时机晚于ImportSelector实例,要等到@Configuration注解中相关的业务全部都处理完了才会调用。
如果该类实现了
ImportBeanDefinitionRegistrar接口,Spring容器就会实例化该类,并且调用其registerBeanDefinitions()方法。
如果该类没有实现上述三种接口中的任何一个,Spring容器就会直接实例化该类。
对@Import注解有了基本理解后,我们来看
AutoConfigurationPackages.Registrar类,该类定义如代码清单13-5所示。
代码清单13-5
AutoConfigurationPackages.Registrar类实现代码
static class Registrar implements ImportBeanDefinitionRegistrar,
DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
register(registry, new
PackageImport(metadata).getPackageName());
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImport(metadata));
}
}
可以看到Registrar类实现了前面第三种情况中提到的
ImportBeanDefinitionRegistrar接口并重写了registerBeanDefinitions()方法,该方法调用了AutoConfigurationPackages自身的register()方法,如代码清单13-6所示。
代码清单13-6 AutoConfigurationPackages中的register()方法代码
public static void register(BeanDefinitionRegistry registry, String...
packageNames) {
if (registry.contAInsBeanDefinition(BEAN)) {
BeanDefinition beanDefinition =
registry.getBeanDefinition(BEAN);
ConstructorArgumentValues constructorArguments =
beanDefinition.getConstructorArgumentValues();
constructorArguments.addIndexedArgumentValue(0,
addBasePackages(constructorArguments, packageNames));
}
else {
GenericBeanDefinition beanDefinition = new
GenericBeanDefinition();
beanDefinition.setBeanClass(BasePackages.class);
beanDefinition.getConstructorArgumentValues()
.addIndexedArgumentValue(0, packageNames);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
registry.registerBeanDefinition(BEAN, beanDefinition);
}
}
这个方法的逻辑是先判断这个Bean有没有被注册,如果已经被注册则获取Bean的定义,进而获取构造函数的参数并添加参数值;如果没有,则创建一个新的Bean定义,设置Bean的类型为AutoConfigurationPackages类型并进行Bean的注册。
(2)@Import(AutoConfigurationImportSelector)
然后我们来看@EnableAutoConfiguration注解中的@Import(
AutoConfigurationImportSel-ector.class)部分。首先我们明确AutoConfigurationImportSelector类实现了@Import注解第二种情况中的DeferredImportSelector接口,所以会执行如代码清单13-7所示的selectImports()方法。
代码清单13-7
AutoConfigurationImportSelector中的selectImports()方法代码
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata =
AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes =
getAttributes(annotationMetadata);
//获取configurations集合
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 StringUtils.toStringArray(configurations);
}
这段代码的核心是通过
getCandidateConfigurations()方法获取configurations集合并进行过滤。getCandidateConfigurations()方法如代码清单13-8所示。
代码清单13-8
AutoConfigurationImportSelector中的getCandidateConfigurations()方法代码
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;
}
在这段代码中,我们先关注这个Assert校验,该校验是一个非空校验,会提示“在META-INF/spring.factories中没有找到自动配置类”这个异常信息。讲到这里,不得不提到JDK中的SPI机制,因为SpringFactoriesLoader这个类的命名和META-INF/spring.factories这个文件目录,存在很大的相通性。关于JDK中的SPI机制,我们马上在后续介绍到。
从类名上看,
AutoConfigurationImportSelector类是一种选择器,负责从各种配置项中找到需要导入的具体配置类。该类的类层结构如图13-1所示。
图13-1 AutoConfigurationImportSelector类层结构图
显然,
AutoConfigurationImportSelector所依赖的最关键组件就是SpringFactoriesLoader,下面我们对其展开具体讨论。
2. SPI机制和SpringFactoriesLoader
要想理解SpringFactoriesLoader类,首先需要了解JDK中的SPI(Service Provider Interface,服务提供者接口)机制。
(1)JDK中的SPI机制JDK提供了一个工具类java.util.ServiceLoader来实现SPI机制,该类用于实现服务查找和加载。当服务提供者提供了服务接口的一种实现之后,我们可以在JAR包的META-INF/services/目录下创建一个以该服务接口命名的文件,并在这个文件中配置一组Key-Value,用于指定服务接口与其具体实现类的映射关系。当外部程序装配这个JAR包时,它就能通过该JAR包METAINF/services/目录中的配置文件找到具体的实现类名,并装载实例化,从而完成目标服务的注入。SPI提供了一种约定,基于该约定就能很有效地找到服务接口的实现类,而无须硬编码指定。JDK中SPI机制开发流程如图13-2所示。
图13-2 JDK中SPI机制开发流程图
(2)SpringFactoriesLoader
SpringFactoriesLoader与JDK中的SPI机制类似,只不过以服务接口命名的文件是放在META-INF/spring.factories文件夹下,对应的Key为EnableAutoConfiguration。Spring-FactoriesLoader会查找所有在METAINF/spring.factories文件夹中的配置文件,并把Key为EnableAutoConfiguration的对应配置项通过反射实例化为配置类,并加载到容器中。我们可以在如代码清单13-9所示的loadSpringFactories()方法中印证这一点。
代码清单13-9 SpringFactoriesLoader中loadSpringFactories()方法代码
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) {
...
}
}
代码清单13-10所示的就是spring-boot-autoconfigure工程中所使用的spring.factories配置文件片段,可以看到在
org.springframework.boot.autoconfigure.EnableAutoConfiguration配置项中有各式各样的配置项,这些配置项在Spring Boot启动过程中能够通过Spring-FactoriesLoader加载到运行时环境,从而实现自动化配置。
代码清单13-10 spring.factories配置文件片段
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmx
AutoConfiguration,
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,
org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,
org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfigur
ation,
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfigur
ation,
org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,
org.springframework.boot.autoconfigure.context.ConfigurationProperties
AutoConfiguration,
...
以上就是Spring Boot基于@SpringBootApplication注解实现自动配置的基本过程和原理。当然,@SpringBootApplication注解也可以通过外部配置文件加载配置信息。基于约定优于配置的思想,Spring Boot在加载外部配置文件的过程中大量使用了默认配置。
3. @ConditionalOn系列条件注解
Spring Boot默认提供了100多个AutoConfiguration类,显然我们不可能全部引入。所以在自动装配时,Spring Boot会去类路径下寻找是否有对应的配置类。如果有配置类,则按条件进行判断,决定是否需要装配。这就引出了在阅读Spring Boot源码时经常会碰到的另一批注解——@ConditionalOn系列条件注解。
(1)@ConditionalOn系列条件注解的示例
先通过一个简单的示例来了解@ConditionalOn系列条件注解的使用方式,代码清单13-11所示的
ConfigServicePropertySourceLocator类就是其中一种典型应用,该代码位于Spring Cloud Config的客户端代码工程springcloud-config-client中。
代码清单13-11
ConfigServicePropertySourceLocator类代码
@Bean
@ConditionalOnMissingBean(ConfigServicePropertySourceLocator.class)
@ConditionalOnProperty(value = "spring.cloud.config.enabled",
matchIfMissing = true)
public ConfigServicePropertySourceLocator
configServicePropertySource(ConfigClientProperties properties) {
ConfigServicePropertySourceLocator locator = new
ConfigServicePropertySourceLocator(properties);
return locator;
}
可以看到这里用到了两个@ConditionalOn注解,一个是@ConditionalOnMissingBean,另一个是@ConditionalOnProperty。再比如在Spring Cloud Config的服务器端代码工程
spring-cloud-config-server中,存在代码清单13-12所示的ConfigServerAutoConfiguration自动配置类。
代码清单13-12
ConfigServerAutoConfiguration自动配置类代码
@Configuration
@ConditionalOnBean(ConfigServerConfiguration.Marker.class)
@EnableConfigurationProperties(ConfigServerProperties.class)@Import({ EnvironmentRepositoryConfiguration.class,
CompositeConfiguration.class, ResourceRepositoryConfiguration.class,
ConfigServerEncryptionConfiguration.class,
ConfigServerMvcConfiguration.class })
public class ConfigServerAutoConfiguration {
}
这里用到了@ConditionalOnBean注解。实际上,Spring Boot提供了一系列条件注解,常见的如下。
@ConditionalOnProperty:只有当提供的属性属于true时才会实例化Bean。
@ConditionalOnBean:只有在当前上下文中存在某个对象时才会实例化Bean。
@ConditionalOnClass:只有当某个Class位于类路径上时才会实例化Bean。
@ConditionalOnExpression:只有当表达式为true时才会实例化Bean。
@ConditionalOnMissingBean:只有在当前上下文中不存在某个对象时才会实例化Bean。
@ConditionalOnMissingClass:只有当某个Class在类路径上不存在时才会实例化Bean。
@
ConditionalOnNotWebApplication:只有当不是Web应用时才会实例化Bean。
当然,Spring Boot还提供了一系列不大常用的@ConditionalOnxxx注解,这些注解都定义在
org.springframework.boot.autoconfigure.condition包中。
基于@ConditionalOn系列注解,我们明确了上述
ConfigServicePropertySourceLocator类只有在spring.cloud.config.enabled属性为true(通过matchIfMissing配置项则默认为true),以及类路径上不存在ConfigServicePropertySourceLocator时才会实例化。而ConfigServer-AutoConfiguration只有在类路径上存在ConfigServerConfiguration.Marker类时才会实例化。这是常用的自动配置控制技巧。
(2)@ConditionalOn系列条件注解的实现原理@ConditionalOn系列条件注解非常多,我们不对所有注解展开讲解。事实上这些注解的实现原理大致相同,我们只需要深入了解其中一个就能做到举一反三。这里我们挑选具有代表性的@ConditionalOnClass注解展开讲解,该注解的定义如代码清单13-13所示。
代码清单13-13 @ConditionalOnClass注解代码
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {
Class<?>[] value() default {};
String[] name() default {};
}
可以看到@ConditionalOnClass注解本身带有两个属性,一个是Class类型的value,另一个是String类型的name,我们可以采用这两个属性中的任意一个来使用该注解。同时,@ConditionalOnClass注解本身还带了一个@Conditional(OnClassCondition.class)注解。所以,@ConditionalOnClass注解的判断条件其实就包含在OnClassCondition这个类中。
OnClassCondition是SpringBootCondition的子类,而SpringBootCondition又实现了Condition接口。Condition接口只有一个matches()方法,如代码清单13-14所示。
代码清单13-14 Condition接口代码
public interface Condition {
boolean matches(ConditionContext context, AnnotatedTypeMetadata
metadata);
}
SpringBootCondition中的matches()方法如代码清单13-15所示。
代码清单13-15 SpringBootCondition的matches()方法代码
@Override
public final boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
String classOrMethodName = getClassOrMethodName(metadata);
try {
ConditionOutcome outcome = getMatchOutcome(context, metadata);
logOutcome(classOrMethodName, outcome);
recordEvaluation(context, classOrMethodName, outcome);
return outcome.isMatch();
}
//省略其他方法
}
这里的getClassOrMethodName()方法用于获取被添加了@ConditionalOnClass注解的类或者方法的名称,而getMatchOutcome()方法用于获取匹配的输出。我们可以看到getMatchOutcome()方法实际上是一个抽象方法,需要通过SpringBootCondition的各个子类完成实现,这里的子类就是OnClassCondition类。在理解OnClassCondition时,我们要明白在SpringBoot中,@ConditionalOnClass注解或者@ConditionalOnMissingClass注解对应的条件类都是OnClassCondition,所以OnClassCondition的getMatchOutcome()会同时处理两种情况。
这里我们挑选出处理@ConditionalOnClass注解的代码,其核心逻辑如代码清单13-16所示。
代码清单13-16 @ConditionalOnClass注解的核心逻辑代码
ClassLoader classLoader = context.getClassLoader();
ConditionMessage matchMessage = ConditionMessage.empty();
List<String> onClasses = getCandidates(metadata,
ConditionalOnClass.class);
if (onClasses != null) {
List<String> missing = getMatches(onClasses, MatchType.MISSING,
classLoader);
if (!missing.isEmpty()) {
return
ConditionOutcome.noMatch(ConditionMessage.forCondition(ConditionalOnCl
ass.class).didNotFind("required class", "required
classes").items(Style.QUOTE, missing));
}
matchMessage =
matchMessage.andCondition(ConditionalOnClass.class).found("required
class", "required classes").items(Style.QUOTE, getMatches(onClasses,
MatchType.PRESENT, classLoader));
}
这里有两个方法值得注意,一个是getCandidates()方法,另一个是getMatches()方法。首先,我们通过getCandidates()方法获取了ConditionalOnClass的name属性和value属性。然后通过getMatches()方法将这些属性值进行比对,得到这些属性所指定的、但在类加载器中还不存在的类。如果发现类加载器中应该存在但事实上又不存在的类,则返回匹配失败的Condition;反之,如果类加载器中存在对应类的话,则把匹配信息进行记录并返回ConditionOutcome。
在本节中,我们将给出在业务系统中基于Starter集成Spring Boot的案例分析。与前面所有介绍过的案例不同,本案例并不是从零开始实现一个Spring Boot Starter组件,而是参考业界主流的开源框架,通过源码解析来深入理解Spring Boot Starter的实现方式。
我们选择的开源框架是分布式数据库中间件ShardingSphere。作为Apache的顶级项目,ShardingSphere提供了一系列应对海量数据的方案,包括分库分表、读写分离、分布式事务、代理服务器等。
接下来,我们来看ShardingSphere实现一个自定义Spring Boot Starter的过程。在4.x版本中,ShardingSphere提供了sharding-jdbc-spring-bootstarter和
sharding-jdbc-orchestration-spring-boot-starter这两个Starter工程。篇幅关系,我们只关注sharding-jdbc-spring-boot-starter工程。
1. SpringBootConfiguration中的注解
接下来,我们就来看这个
sharding-jdbc-spring-boot-starter工程中的SpringBootConfiguration类,首先关注加在该类上的各种注解,如代码清单13-17所示。
代码清单13-17 SpringBootConfiguration注解定义代码
@Configuration
@ComponentScan("org.apache.shardingsphere.spring.boot.converter")
@EnableConfigurationProperties({
SpringBootShardingRuleConfigurationProperties.class,
SpringBootMasterSlaveRuleConfigurationProperties.class,
SpringBootEncryptRuleConfigurationProperties.class,
SpringBootPropertiesConfigurationProperties.class})
@ConditionalOnProperty(prefix = "spring.shardingsphere", name =
"enabled", havingValue = "true", matchIfMissing = true)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@RequiredArgsConstructor
public class SpringBootConfiguration implements EnvironmentAware首先,我们看到熟悉的@Configuration注解和@ComponentScan注解。注意,这里通过@ComponentScan注解扫描的包路径位于另一个代码工程sharding-spring-boot-util的
org.apache.shardingsphere.spring.boot.converter包中。
然后,我们看到@
EnableConfigurationProperties注解,该注解的作用就是使添加了@ConfigurationProperties注解的类生效。在Spring Boot中,如果一个类只使用了@ConfigurationProperties注解,并且没有在扫描路径下或者没有使用@Component等注解,就无法被扫描为一个有效的Bean。
这时候就必须在配置类上使用@
EnableConfiguration-Properties注解指定这个类,才能使@ConfigurationProperties注解生效,并作为一个Bean被添加进Spring容器中。这里的@EnableConfigurationProperties注解包含了四个具体的ConfigurationProperties。以SpringBootShardingRuleConfigurationProperties为例,该类的定义如代码清单13-18所示。可以看到,这里直接继承了sharding-core-common代码工程中用于设置分片规则的YamlShardingRuleConfiguration配置类。
代码清单13-18
SpringBootShardingRuleConfigurationProperties类代码
@ConfigurationProperties(prefix = "spring.shardingsphere.sharding")
public class SpringBootShardingRuleConfigurationProperties extends
YamlShardingRuleConfiguration {
}
添加到SpringBootConfiguration的下一个注解是@ConditionalOnProperty,该注解只有当所提供的属性属于true时才会实例化Bean。
最后一个与自动加载相关的注解是@AutoConfigureBefore。如果该注解被添加在类名上,其作用是标识在加载当前类之前需要加载注解中所设置的配置类。基于这一点,我们明确在加载SpringBootConfiguration类之前,Spring Boot会先加载
DataSource-AutoConfiguration。这一步的作用与我们后面要看到的各种DataSource创建过程相关。
2. SpringBootConfiguration的功能
介绍完这些注解之后,我们来看一下SpringBootConfiguration类提供的功能。
我们知道对于ShardingSphere而言,其对外的入口实际上就是各种DataSource。因此,SpringBootConfiguration提供了一批创建不同DataSource的入口方法,例如代码清单13-19所示的shardingDataSource()方法。
代码清单13-19 shardingDataSource()方法代码
@Bean
@Conditional(ShardingRuleCondition.class)
public DataSource shardingDataSource() throws SQLException {
return ShardingDataSourceFactory.createDataSource(dataSourceMap,
new ShardingRuleConfigurationYamlSwapper().swap(shardingRule),
props.getProps());
}
该方法添加了两个注解,一个是常见的@Bean,另一个则是@Conditional注解,该注解的作用是只有满足指定条件才能加载这个Bean。我们看到在@Conditional注解中设置了一个ShardingRuleCondition,该类如代码清单13-20所示。
代码清单13-20 ShardingRuleCondition类代码
public final class ShardingRuleCondition extends SpringBootCondition { @Override
public ConditionOutcome getMatchOutcome(final ConditionContext
conditionContext, final AnnotatedTypeMetadata annotatedTypeMetadata) {
boolean isMasterSlaveRule = new
MasterSlaveRuleCondition().getMatchOutcome(conditionContext,
annotatedTypeMetadata).isMatch();
boolean isEncryptRule = new
EncryptRuleCondition().getMatchOutcome(conditionContext,
annotatedTypeMetadata).isMatch();
return isMasterSlaveRule || isEncryptRule ?
ConditionOutcome.noMatch("Have found master-slave or encrypt rule in
environment") : ConditionOutcome. match();
}
}
可以看到ShardingRuleCondition是一个标准的SpringBootCondition,实现了13.1.1节介绍的getMatchOutcome()抽象方法。我们知道SpringBootCondition代表一种用于注册类或加载Bean的条件,而ShardingRuleCondition类在实现上分别调用了MasterSlaveRuleCondition和EncryptRuleCondition来判断是否满足这两个SpringBootCondition。显然,对于ShardingRule-Condition而言,只有在两个条件都不满足的情况下才应该被加载。对于masterSlaveData-Source()和encryptDataSource()这两个方法而言,处理逻辑也类似,不做赘述。
最后,我们注意到SpringBootConfiguration还实现了Spring的EnvironmentAware接口。在Spring中,当一个类实现了EnvironmentAware接口并重写了其中的setEnvironment()方法之后,在代码工程启动时就可以获得application.properties配置文件中各个配置项的属性值。
SpringBootConfiguration中所重写的setEnvironment()方法如代码清单13-21所示。
代码清单13-21 SpringBootConfiguration的setEnvironment()方法代码
@Override
public final void setEnvironment(final Environment environment) {
String prefix = "spring.shardingsphere.datasource.";
for (String each : getDataSourceNames(environment, prefix)) {
try {
dataSourceMap.put(each, getDataSource(environment, prefix,
each));
} catch (...) {
...
}
}
}
这里的代码逻辑是获取
spring.shardingsphere.datasource.name或spring.shardingsphere.datasource.names配置项,然后根据该配置项中所指定的DataSource信息构建新的Data-Source并加载到dataSourceMap这个LinkedHashMap中。这点我们可以结合Sharding-Sphere的常用配置项来加深理解,如代码清单13-22所示。可以看到,这里我们定义了两个DataSource。
代码清单13-22 ShardingSphere常用配置项
spring.shardingsphere.datasource.names=ds0,ds1
spring.shardingsphere.datasource.ds0.type=com.alibaba.druid.pool.Druid
DataSource
spring.shardingsphere.datasource.ds0.driver-class
name=com.MySQL.jdbc.Driver
spring.shardingsphere.datasource.ds0.url=jdbc:mysql://localhost/ds0
spring.shardingsphere.datasource.ds0.username=root
spring.shardingsphere.datasource.ds0.password=root
spring.shardingsphere.datasource.ds1.type=com.alibaba.druid.pool.Druid
DataSource
spring.shardingsphere.datasource.ds1.driver-class
name=com.mysql.jdbc.Driverspring.shardingsphere.datasource.ds1.url=jdbc:mysql://localhost/ds1
spring.shardingsphere.datasource.ds1.username=root
spring.shardingsphere.datasource.ds1.password=root
至此,对整个SpringBootConfiguration的实现过程介绍完毕,ShardingSphere基于Starter机制完成了与Spring Boot的集成。