springboot mybatis starter 自动配置的原理

用过springboot的同学都是知道,springboot配置简单,更提供了大量的starter方便集成各个组件。每个组件基本只要依赖了jar包,基本不需要或者需要少量的配置信息就可以直接使用了,那么在这简单的starter背后的原理是什么呢?

原理

统一的命名规范 XXX-spring-boot-starter (非必须)

你如果去看每个starter的的名字,比如mybatis的starter名字如下:

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>

你会发现每个starter的artifactId都是以spring-boot-starter结尾,这个是普遍认同的一个规范,不是必须按照这个名字来,也可以自定义名字,原因下面介绍。

统一的配置文件 spring.factories

必须在classpath下的META-INF文件夹下创建一个spring.factories,本质是一个Properties,所以得按照key-value的形式进行配置。自动配置的key是org.springframework.boot.autoconfigure.EnableAutoConfiguration。我们看下mybatis的配置:

1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration

当然你也可以使用其他的一些key,可以参考:spring-boot-autoconfigure 的spring.factories文件内容

这里解释一下为什么jar包的artifactId名字不是必须按照规范来,让我们看下源码的实现:

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

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/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 = 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);
}
}

源码中使用了ClassLoader的getResources方法,该方法传入一个文件相对路径也就是META-INF/spring.factories,会返回代码中所有找到的路径,这个跟jar包的名字没有任何关系。

注解 Configuration

Configuration 是springboot提供的,表示这是一个配置类,其实它的作用跟@Bean是一样的,都是让spring实例化该类。

同时springboot还额外提供了一下注解用来实现自动配置。

@ConditionalOnClass

表示存在某些类的时候,才进行初始化。

@AutoConfigureBefore

表示必须先初始化某个类

@AutoConfigureAfter

表示初始化完成之后加载某个类

@ConditionalOnMissingBean

可以跟@Bean配合使用,避免多次初始化,来看一个具体的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public A getA() {
return new A();
}

@Bean
@ConditionalOnMissingBean
public A getA2() {
return new A();
}

static class A {
public A() {
System.out.println("=====init A");
}
}

输出如下,只会打印一次

1
=====init A

现在我们调整上面的代码继续看输出的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
@ConditionalOnMissingBean
public A getA() {
return new A();
}

@Bean
public A getA2() {
return new A();
}

static class A {
public A() {
System.out.println("=====init A");
}
}

输出如下,打印两次

1
2
=====init A
=====init A

打印两次的原因是我们在getA2方法上没有添加ConditionalOnMissingBean注解,所以不会判断是否已经存在A的实例