一、前言
springboot开发一个后端应用,我们只需引入依赖,添加简单的配置就能实现对其他框架的整合。他的奥妙就在于它各种各样的starter。
1.1 starter的作用
SpringBoot这些starter的作用就是根据我们配置,给我们初始化一些整合其他框架时需要初始化的一些bean,并加载到spring容器中。这样就减少了在SSM时代那些繁琐的是xml配置。
1.2 环境信息
springboot 2.4.10
二、原理
2.1 概述
那么springboot是如何实现通过,通过引入一些jar包的依赖就将这些jar包中定义的spring的bean加载到spring容器中的呢?答案就是spring的SPI机制。
2.2 什么是spi
SPI(Service Provider Interface)就是服务提供接口,它是实现服务解耦,插件自由插拔的一直机制。简单来说,就是通过一些类加载器,去加载classpath下指定目录文件,文件中定义的有需要加载的类的全权限定名,然后这些类会被识别并加载。平时开发中数据库连接驱动的加载和dubbo中都用到了SPI技术。正是使用了SPI技术,才实现了插件和服务的解耦和可插拔。
2.3 Spring的SPI
Spring的SPI使用由SpringFactoriesLoader.loadFactoryNames方法实现的,
它会加载所有依赖的jar包下classPath下META-INF/spring.factories文件,然后将解析properties文件,找到指定名称的配置后返回。
源码如下:
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
/**
* spring.factories文件的格式为:key=value1,value2,value3
* 从所有的jar包中找到META-INF/spring.factories文件
* 然后从文件中解析出key=factoryClass类名称的所有value值
*/
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
// 取得资源文件的URL
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
2.4 Springboot中对spring SPI的使用
springBoot对spring spi的使用具体体现在@EnableAutoConfiguration上。 springboot的启动类注解上有一个@EnableAutoConfiguration。
@EnableAutoConfiguration的作用是帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot,并创建对应配置类的Bean,并把该Bean实体交给IoC容器进行管理。
@EnableAutoConfiguration上面@Import注解指定了导入类的方法。具体是AutoConfigurationImportSelector的selectImports方法。
AutoConfigurationImportSelector源码
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
}
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 加载所有META-INF/spring.factories文件中定义的类名
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationEntry(configurations, exclusions);
}
// 加载
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;
}
在getCandidateConfigurations方法中我们就能看到上面讲的SpringFactoriesLoader.loadFactoryNames方法
分析springBoot starter的源码
3.1 引入依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.10</version>
</dependency>
META-INF/spring.factories指定了要加载的配置类的权限定名
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.alibaba.druid.spring.boot.autoconfigure.DruidDataSourceAutoConfigure
DruidDataSourceAutoConfigure里就是我们平时很熟悉的springboot添加配置类的方式了。DruidDataSourceAutoConfigure会给我们初始化一个DruidDataSourceWrapper 的spring bean。DruidDataSourceWrapper 又继承了DruidDataSource ,也就是初始化了一个DruidDataSource 的spring bean到spring容器中。
/**
* 配置类
*/
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({DruidSpringAopConfiguration.class,
DruidStatViewServletConfiguration.class,
DruidWebStatFilterConfiguration.class,
DruidFilterConfiguration.class})
public class DruidDataSourceAutoConfigure {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
/**
* 初始换了一个DruidDataSourceWrapper bean
*/
@Bean(initMethod = "init")
@ConditionalOnMissingBean
public DataSource dataSource() {
LOGGER.info("Init DruidDataSource");
return new DruidDataSourceWrapper();
}
}
DruidDataSourceWrapper 会读写springboot yml文件中spring.datasource.druid开头的配置,并把这值配置到初始化到DruidDataSource 的bean中。
@ConfigurationProperties("spring.datasource.druid")
class DruidDataSourceWrapper extends DruidDataSource implements InitializingBean {
@Autowired
private DataSourceProperties basicProperties;
@Override
public void afterPropertiesSet() throws Exception {
//if not found prefix 'spring.datasource.druid' jdbc properties ,'spring.datasource' prefix jdbc properties will be used.
if (super.getUsername() == null) {
super.setUsername(basicProperties.determineUsername());
}
if (super.getPassword() == null) {
super.setPassword(basicProperties.determinePassword());
}
if (super.getUrl() == null) {
super.setUrl(basicProperties.determineUrl());
}
if(super.getDriverClassName() == null){
super.setDriverClassName(basicProperties.getDriverClassName());
}
}
@Autowired(required = false)
public void addStatFilter(StatFilter statFilter) {
super.filters.add(statFilter);
}
@Autowired(required = false)
public void addConfigFilter(ConfigFilter configFilter) {
super.filters.add(configFilter);
}
@Autowired(required = false)
public void addEncodingConvertFilter(EncodingConvertFilter encodingConvertFilter) {
super.filters.add(encodingConvertFilter);
}
@Autowired(required = false)
public void addSlf4jLogFilter(Slf4jLogFilter slf4jLogFilter) {
super.filters.add(slf4jLogFilter);
}
@Autowired(required = false)
public void addLog4jFilter(Log4jFilter log4jFilter) {
super.filters.add(log4jFilter);
}
@Autowired(required = false)
public void addLog4j2Filter(Log4j2Filter log4j2Filter) {
super.filters.add(log4j2Filter);
}
@Autowired(required = false)
public void addCommonsLogFilter(CommonsLogFilter commonsLogFilter) {
super.filters.add(commonsLogFilter);
}
@Autowired(required = false)
public void addWallFilter(WallFilter wallFilter) {
super.filters.add(wallFilter);
}
}
在项目中的使用
在springboot的项目中我们需要用到Druid数据源的时候只需要引入druid-spring-boot-starter的依赖,然后在yml文件中添加spring.datasource.druid开头的相关配置即可