java spi第二章:如何定义自己的starter

starter背景


在第一章中,已经简单的介绍了java spi以及spi的用法以及优缺点。那spring中是否有用到spi机制呢?我们如何自定义一个starter呢?

我们先简单的定义一个springboot版 starter
在springboot starter官方命名推荐中,有两种形式

官方命名

spring-boot-starter-模块名

eg:spring-boot-starter-web、spring-boot-starter-jdbc、spring-boot-starter-thymeleaf

自定义命名

模块名xxx-spring-boot-starter

eg:mybatis-spring-boot-start

在定义starter的时候,也有相应的规则

启动器模块是一个空 JAR 文件,仅提供辅助性依赖管理,这些依赖可能用于自动 装配或者其他类库

  • 启动器只用来做依赖导入

  • 专门来写一个自动配置模块;

  • 启动器依赖自动配置模块,项目中引入相应的starter就会引入启动器的所有传递依赖

starter示例


我们按照这个规范定义一个demo

1.先定义一个starter项目

1
demo-spring-boot-starter

再在starter下定义一个autoconfigure模块

1
hello-starter-autoconfigure

starter pom文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-spring-boot-starter</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>hello-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>


</project>

hello-starter-autoconfigure pom文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>hello-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>hello-starter-autoconfigure</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>


</project>

只需要引入最基础的spring-boot-starter即可,注意需要在pom文件中去掉

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

否则没有main方法主类无法打包

  1. 在hello-starter-autoconfigure模块下建立所需要的文件,在这里最简单化

包含HelloService和实现类,HelloAutoConfiguration配置类,具体如下

1
2
3
public interface HelloService {
String sayHello();
}
1
2
3
4
5
6
7
public class HelloServiceImpl implements HelloService {

@Override
public String sayHello() {
return "shaizx";
}
}
1
2
3
4
5
6
7
8
@Configuration
@ConditionalOnWebApplication
public class HelloAutoConfiguration {
@Bean
public HelloService helloService(){
return new HelloServiceImpl();
}
}

最重要的一个步骤,在resources下建立META-INF文件夹,然后建立spring.factories文件,在spring.factories文件中对需要扫描的配置类进行配置,在这里是

##Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.example.hellostarterautoconfigure.autoconfigure.HelloAutoConfiguration

然后对hello-starter-autoconfigure打包,再对demo-spring-boot-starter进行打包,因为demo-spring-boot-starter包含hello-starter-autoconfigure,所以顺序不能反。

这样一个可用的简单版的starter就搭建好了,我们在其他项目中导入测试一下

我们导入starter的坐标

1
2
3
4
5
<dependency>
<groupId>com.example</groupId>
<artifactId>demo-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

建立一个HelloController测试类

1
2
3
4
5
6
7
8
9
@RestController
public class HelloController {
@Autowired
private HelloService helloService;
@RequestMapping("/sayHello")
public String sayHello(){
return helloService.sayHello();
}
}

开启测试

返回shuaizx,starter测试完成

starter原理


想要知道starter是怎么运行的,首先要知道springboot是怎么运行的,starter中的配置加载只是springboot加载中的一个过程。

我们直接找到main方法的@SpringBootApplication注解,在观察@EnableAutoConfiguration注解,这个注解的意思就是启动自动配置的能力意思,我们刚才在starter spring.factories文件也配置了这个注解的key,value形式,那自动配置又是怎么做到的呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@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 {};

}

我们看到在EnableAutoConfiguration上有
@Import(AutoConfigurationImportSelector.class)
的自动引入字样

@Import 可以配置三种不同的class

  • 普通bean或者有@Configuration 的bean
  • 实现ImportSelector 接口进行动态注入
  • 实现ImportBeanDefinitionRegistrar 接口进行动态注入
    AutoConfigurationImportSelector

通过名字可以猜到它是基于第二种情况实现bean 的加载功能

关键就在这里

AutoConfigurationImportSelector这个类实现了DeferredImportSelector接口,DeferredImportSelector接口继承了ImportSelector接口

1
public interface DeferredImportSelector extends ImportSelector

ImportSelector接口是Spring导入外部配置的核心接口,在SpringBoot的自动化配置和@EnableXXX(功能性注解)中起到了决定性的作用。当在@Configuration标注的Class上使用@Import引入了一个 ImportSelector实现类后,会把实现类中返回的Class名称都定义为bean。

DeferredImportSelector接口继承自ImportSelector,它和ImportSelector的区别在于装载bean的时机上,DeferredImportSelector需要等所有的@Configuration都执行完毕后才会进行装载。

AutoConfigurationImportSelector类中的selectImports方法实现如下

1
2
3
4
5
6
7
8
9
10
11
@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());
}

String[]返回的字符串会全部被注入到bean容器中,我们看getAutoConfigurationEntry这里面的实现。在这个实现中对获取到了的类进行了去重加过滤等,最后过滤出来的configurations截图如下,有我们在前面设置的com.example.hellostarterautoconfigure.autoconfigure.HelloAutoConfiguration,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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);
}

configurations类路径字符串又是从getCandidateConfigurations方法中获取的,再继续进入getCandidateConfigurations方法查看里面的关键实现

1
2
3
4
5
6
7
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()方法返回的是EnableAutoConfiguration.class,,待会会通过这个类对获取到的map进行过滤

1
2
3
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class,;
}
1
2
3
4
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}

loadFactoryNames方法调用了loadSpringFactories方法,然后获取到的map在进行过滤,通过刚才传入的EnableAutoConfiguration.class,没有则返回空数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);
}
}

SpringFactoriesLoader这个类中的属性FACTORIES_RESOURCE_LOCATION 就是我们熟悉的META-INF/spring.factories路径

1
2
3
4
5
/**
* 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";

loadSpringFactories方法在springboot初始化中就调用过,在初始化时会对这个地方会进行缓存,后续调用这个方法直接通过cache获取到map,然后过滤。

1
2
3
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));

classLoader.getResources(FACTORIES_RESOURCE_LOCATION) 就是对META-INF/spring.factories下的资源进行获取。后面处理的时候会返回类的全路径然后过滤。最终返回我们设置的starter配置的类路径。

总结一下就是

  1. AutoConfigurationImportSelector实现了ImportSelector接口,实现selectImports方法,返回的字符串会定义为bean
  2. getCandidateConfigurations方法调用了SpringFactoriesLoader.loadFactoryNames方法,传入了EnableAutoConfiguration.class进行过滤,返回所有被EnableAutoConfiguration修饰的类路径
  3. 对getCandidateConfigurations返回的路径再次过滤,得到我们想要的类路径,进行后续操作

这就是自动装配的原理,也是我们定义一个starter必备的过程。

参考文章:
https://www.lagou.com/lgeduarticle/82172.html