Spring Boot - 基于 SPI 手撕一个 Spring Boot 框架出来

基本框架的说明和搭建

我们现在新建两个模块,一个是 zdemo-springboot,它是我们基于 spring、内嵌的 tomcat 以及 SPI 机制等技术模拟构建出来的 spring boot;另一个是 zdemo-app,它是我们新构建的 springboot 框架的使用者。我们现将这两个模块的基础架构搭建出来。


zdemo-springboot

依赖包:

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
35
36
37
38
39
40
41
42
43
44
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>6.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>6.2.17</version>
</dependency>
<dependency>
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.0</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>10.1.24</version>
</dependency>
<!--默认只引入tomcat容器,如果需要jetty,先排除tomcat,在显式引入jetty-->
<!--<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>12.0.33</version>
<optional>true</optional>
</dependency>-->
</dependencies>

MySpringBootApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mysb.autoconfigure;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@EnableWebMvc // 开启它,Spring 才会自动将 Jackson 装配进 DispatcherServlet
public @interface MySpringBootApplication {
}

MySpringApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mysb;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class MySpringApplication {
public static void run(Class<?> clazz, String[] args) {
// TODO
}
}


demo-app

依赖包:

1
2
3
4
5
6
7
8
9
10
11
<dependencies>
<dependency>
<groupId>com.owlias.janus</groupId>
<artifactId>zdemo-springboot</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>

手写一条基础的业务线:

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
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private Long userId;
private String username;
}

@Service
public class UserService {
public User getUserById(Long userId) {
return new User(userId, "USER_" + userId);
}
}

@RestController
@RequestMapping("/user")
public class UserController {

@Resource
private UserService userService;

@GetMapping("/{userId}")
public User getUserById(@PathVariable("userId") Long userId) {
return userService.getUserById(userId);
}
}

启动类:

1
2
3
4
5
6
7
8
9
10
11
package com.demo;

import com.mysb.MySpringApplication;
import com.mysb.autoconfigure.MySpringBootApplication;

@MySpringBootApplication
public class MyApp {
public static void main(String[] args) {
MySpringApplication.run(MyApp.class, args);
}
}

现在启动 MyApp,其实我们现在什么也干不了。现在我们就要实现这条基础业务线的正常运行。


加入 tomcat 和 springmvc

现在我们要让 UserController 这条业务线真正跑起来,需要加入 http 服务容器,为了方便集成,我们使用 springmvc 充当控制层。

第一步:需要完善 com.mysb.MySpringApplication 的 run 方法:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package com.mysb;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;

public class MySpringApplication {

public static void run(Class<?> clazz, String[] args) {
// 1. 创建一个基于注解的 spring web 容器(注意此时千万不要急着调用 refresh)
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);

// 2. 将容器丢给 Tomcat 拼装逻辑,由 Tomcat 初始化完 ServletContext 后再内部触发 refresh
startTomcat("localhost", 8080, applicationContext);
}

public static void startTomcat(String hostname, Integer port, AnnotationConfigWebApplicationContext applicationContext) {
Tomcat tomcat = new Tomcat();

Server server = tomcat.getServer();
Service service = server.findService("Tomcat");

StandardEngine engine = new StandardEngine();
engine.setDefaultHost(hostname);

StandardHost host = new StandardHost();
host.setName(hostname);
engine.addChild(host);

String contextPath = "";
StandardContext context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());

// 利用 Tomcat 的生命周期机制拦截点火时刻
// 当 Context 准备启动(CONFIGURE_START_EVENT)时,Tomcat 已经把它的 ServletContext 捏出来了
context.addLifecycleListener(event -> {
if ("configure_start".equals(event.getType())) {
// a. 趁热打铁,赶紧把 Tomcat 刚创建的 ServletContext 喂给 Spring 容器
applicationContext.setServletContext(context.getServletContext());
// b. 此时有了 ServletContext,再去安全地 refresh 容器,@EnableWebMvc 绝对不会报错了!
System.out.println("ServletContext is ready. Refreshing Spring Context...");
// c. 此时再刷新容器
applicationContext.refresh();
}
});
host.addChild(context);

// 创建一个网络连接器 Connector
Connector connector = new Connector();
connector.setPort(port);
service.setContainer(engine);
service.addConnector(connector);

// 使用 springmvc 的 DispatcherServlet 拦截和处理所有请求
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
context.addServletMappingDecoded("/*", "dispatcher");

try {
// 点火启动 tomcat!
System.out.println("Starting Embedded Tomcat on port " + port + "...");
tomcat.start();
tomcat.getServer().await();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}

第二步:因为我们的业务代码返回了一个对象 ,需要将对象转为 JSON,为例防止页面出现 “HTTP Status 406 – Not Acceptable” 的错误,我们需要开启 @EnableWebMvc 注解:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.demo;

import com.mysb.MySpringApplication;
import com.mysb.autoconfigure.MySpringBootApplication;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;

@MySpringBootApplication
@EnableWebMvc // 开启它,Spring 才会自动将 Jackson 装配进 DispatcherServlet
public class MyApp {
public static void main(String[] args) {
MySpringApplication.run(MyApp.class, args);
}
}

现在启动 MyApp,浏览器访问 http://localhost:8080/user/1 就可以得到正确的结果:

1
2
3
4
{
userId: 11,
username: "USER_11"
}


多容器支持的设计

上述代码我们只实现了 tomcat 容器,实际的 springmvc 除了 tomcat,还可以选择使用 jetty 等其他容器。这是怎么实现的呢?这就需要我们对 MySpringApplication 的 run 方法进行改造。在此之前,我们需要抽象出一个 WebServer 接口用于启动容器:

1
2
3
4
5
package com.mysb.web.server;

public interface WebServer {
void start();
}

WebServer 的 TomcatWebServer 实现:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package com.mysb.web.embedded.tomcat;

import com.mysb.web.server.WebServer;
import org.apache.catalina.Server;
import org.apache.catalina.Service;
import org.apache.catalina.connector.Connector;
import org.apache.catalina.core.StandardContext;
import org.apache.catalina.core.StandardEngine;
import org.apache.catalina.core.StandardHost;
import org.apache.catalina.startup.Tomcat;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import java.util.concurrent.CountDownLatch;

public class TomcatWebServer implements WebServer, ApplicationContextAware {

@Value("${server.host:localhost}")
private String hostname;
@Value("${server.port:8080}")
private Integer port;
private AnnotationConfigWebApplicationContext applicationContext;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = (AnnotationConfigWebApplicationContext) applicationContext;
}

@Override
public void start() {
Tomcat tomcat = new Tomcat();

Server server = tomcat.getServer();
Service service = server.findService("Tomcat");

StandardEngine engine = new StandardEngine();
engine.setDefaultHost(hostname);

StandardHost host = new StandardHost();
host.setName(hostname);
engine.addChild(host);

String contextPath = "";
StandardContext context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());

// --> 引入同步计数器,初始值为 1
CountDownLatch latch = new CountDownLatch(1);

// 极其重要:现在监听器只需要干一件事:把 Tomcat 的 ServletContext 喂给已经 refresh 了一半的容器
context.addLifecycleListener(event -> {
if ("configure_start".equals(event.getType())) {
applicationContext.setServletContext(context.getServletContext());
System.out.println("Tomcat ServletContext 绑定到 Spring 容器成功");

// --> 信号灯放行:通知主线程,ServletContext 已经稳稳妥妥地准备好了!
latch.countDown();
}
});
host.addChild(context);

// 创建一个网络连接器 Connector
Connector connector = new Connector();
connector.setPort(port);
service.setContainer(engine);
service.addConnector(connector);

// 实例化 DispatcherServlet,使用 springmvc 的 DispatcherServlet 拦截和处理所有请求
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext);
// 告诉 DispatcherServlet,这个容器是我亲手拉起并刷新的,
// 你进去之后老老实实用就行了,千万别在 init 时自作多情再去调用 refresh()!
dispatcherServlet.setPublishContext(false);
// 将配置好的 servlet 塞给 Tomcat
tomcat.addServlet(contextPath, "dispatcher", dispatcherServlet);
context.addServletMappingDecoded("/*", "dispatcher");

try {
// 点火启动 tomcat!
System.out.println("Starting Embedded Tomcat on port " + port + "...");
tomcat.start();

// --> 主线程在此死死守住,直到后台的 configure_start 事件触发并完成绑定后才放行
latch.await();
System.out.println("ServletContext 校验通过,放行 Spring 容器继续初始化...");

// 为了防止阻塞导致整个 refresh 线程卡死,真正的 Spring Boot 此时会让 Tomcat 在后台异步存活
// 这里我们创建一个守护线程去 await,不卡死主 refresh 线程
Thread awaitThread = new Thread(() -> tomcat.getServer().await(), "tomcat-await");
awaitThread.setContextClassLoader(getClass().getClassLoader());
awaitThread.setDaemon(false);
awaitThread.start();
} catch (Exception e) {
e.printStackTrace();
}
}
}

WebServer 的 JettyWebServer 实现:

1
2
3
4
5
6
7
8
9
10
package com.mysb.web.embedded.jetty;

import com.mysb.web.server.WebServer;

public class JettyWebServer implements WebServer {
@Override
public void start() {
System.out.println("jetty web server start!");
}
}

MySpringApplication 的改造:

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
package com.mysb;

import com.mysb.web.context.support.MyAnnotationConfigWebApplicationContext;
import com.mysb.web.server.WebServer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import java.util.Map;

public class MySpringApplication {

public static void run(Class<?> clazz, String[] args) {
// 核心改动:不再使用 Spring 原生的上下文,使用我们自己重写的 Web 上下文
MyAnnotationConfigWebApplicationContext applicationContext = new MyAnnotationConfigWebApplicationContext();
applicationContext.register(clazz);

// 顺理成章地直接点火 refresh,在 onRefresh 的时候顺理成章地启动web容器(这也是 Spring Boot 的做法)
applicationContext.refresh();
}

private static WebServer getWebServer(AnnotationConfigWebApplicationContext applicationContext) {
Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);

if (webServers.isEmpty()) {
throw new NullPointerException();
}
if (webServers.size() > 1) {
throw new IllegalStateException();
}
return webServers.values().stream().findFirst().get();
}
}

MyAnnotationConfigWebApplicationContext

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
package com.mysb.web.context.support;

import com.mysb.web.server.WebServer;
import jakarta.servlet.ServletContext;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.web.context.ServletContextAware;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import java.util.Map;

public class MyAnnotationConfigWebApplicationContext extends AnnotationConfigWebApplicationContext {

@Override
protected void onRefresh() {
try {
// 1. 点火拉起 Tomcat,把 ServletContext 塞给当前 applicationContext
createWebServer();

// 2. 既然 Spring 的 AwareProcessor 挂不上,我们直接向底层 BeanFactory 注册两个致命依赖:
ServletContext servletContext = super.getServletContext();
if (servletContext != null) {
// a. 注册标准的 ServletContext 依赖值。这样任何组件通过 @Autowired 都能直接拿到它
this.getBeanFactory().registerResolvableDependency(ServletContext.class, servletContext);

// b. 关键核心:由于 WebMvcAutoConfiguration 继承自 WebMvcConfigurationSupport,
// 它依赖 BeanPostProcessor 来回调 setServletContext 方法。
// 既然官方的私有类不让我们 new,我们自己手写一个只有 3 行的后置处理器砸进去!
this.getBeanFactory().addBeanPostProcessor(new BeanPostProcessor() {
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) {
// 只要发现有任何 Bean 实现了 org.springframework.web.context.ServletContextAware
if (bean instanceof ServletContextAware awareBean) {
// 强行把我们刚拿到的、热乎的 ServletContext 喂给它!
awareBean.setServletContext(servletContext);
}
return bean;
}
});
System.out.println("强行挂载自定义 ServletContextAware 处理器成功!");
}
} catch (Exception e) {
throw new RuntimeException("自动启动内置 Web 服务器失败", e);
}

// 3. 此时所有 Bean 在实例化时,只要敢要 ServletContext,都必须经过我们上面的后置处理器
super.onRefresh();
}

private void createWebServer() {
// 从当前容器中寻找 WebServer 类型的 Bean
Map<String, WebServer> webServers = getBeansOfType(WebServer.class);

if (webServers.isEmpty()) {
throw new IllegalStateException("未找到任何 WebServer 组件,请检查配置!");
}
if (webServers.size() > 1) {
// 这也就是为什么你在装配类里同时塞了 Tomcat 和 Jetty 会报错的原因,后续我们会用条件注解解决
throw new IllegalStateException("发现了多个 WebServer 组件 (" + webServers.keySet() + "),无法确定启动哪一个!");
}

// 捞出唯一的 WebServer,点火启动!
WebServer webServer = webServers.values().stream().findFirst().get();
webServer.start();
}
}

现在我们的 demo-springboot 框架模块中有两个 WebServer,一个是 TomcatWebServer,另一个是 JettyWebServer,我们需要把这两个 Bean 一起注入到容器中。在 demo-springboot 中增加配置类 WebServerAutoConfiguration:

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
35
36
37
38
39
40
41
42
43
package com.mysb.autoconfigure.web;

import com.mysb.autoconfigure.condition.MyConditionalOnClass;
import com.mysb.web.embedded.jetty.JettyWebServer;
import com.mysb.web.embedded.tomcat.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.web.servlet.config.annotation.DelegatingWebMvcConfiguration;

@Configuration
public class WebServerAutoConfiguration {

@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() { // 必须加 static!确保它在所有普通 Bean(如 TomcatWebServer)实例化之前生效
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
configurer.setIgnoreUnresolvablePlaceholders(true); // 允许找不到配置项时不报错,直接用冒号后面的默认值
return configurer;
}

// 替代 @EnableWebMvc 的正统底层实现
// 继承或直接注册 DelegatingWebMvcConfiguration,等同于在框架内部悄悄开启了 @EnableWebMvc
@Configuration
@MyConditionalOnClass("org.springframework.web.servlet.DispatcherServlet")
public static class WebMvcAutoConfiguration extends DelegatingWebMvcConfiguration {
// 这里继承了 WebMvcConfigurationSupport,它会默默帮我们注册起所有 MVC 核心组件
// 并且会自动检测类路径下的 Jackson 依赖,将其转化为 JSON 转换器!
}

@Bean
// 只有当能找到 Tomcat 核心类时,才装配 Tomcat 服务器
@MyConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}

@Bean
// 只有当能找到 Jetty 核心类时,才装配 Jetty 服务器
@MyConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}

因为 WebServerAutoConfiguration 中我们已经使用 WebMvcAutoConfiguration 代替 @EnableWebMvc,所以 demo-app 模块中,MyApp 上的 @EnableWebMvc 可以直接去掉,看着更加清爽。

1
2
3
4
5
6
@MySpringBootApplication
public class MyApp {
public static void main(String[] args) {
MySpringApplication.run(MyApp.class, args);
}
}

高级的条件注解是springboot实现的,这里 WebServerAutoConfiguration 中的条件注解的我们自己的实现:

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
35
36
37
38
39
40
41
42
43
44
package com.mysb.autoconfigure.condition;
import org.springframework.context.annotation.Conditional;
import java.lang.annotation.*;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(MyOnClassCondition.class) // 核心:绑定我们刚才手写的判定逻辑
public @interface MyConditionalOnClass {

/**
* 要检查的全限定类名(例如 "org.apache.catalina.startup.Tomcat")
*/
String value();
}


package com.mysb.autoconfigure.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import java.util.Map;

public class MyOnClassCondition implements Condition {

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
// 获取注解 @MyConditionalOnClass 上的属性值
Map<String, Object> attributes = metadata.getAnnotationAttributes(MyConditionalOnClass.class.getName());
if (attributes != null) {
String className = (String) attributes.get("value");
try {
// 尝试用类加载器去加载这个类
Class.forName(className, false, context.getClassLoader());
// 加载成功,说明类路径下有这个组件,条件成立!
return true;
} catch (ClassNotFoundException e) {
// 加载失败,说明类路径下没有这个组件,条件不成立
return false;
}
}
return false;
}
}

可是我们的 WebServerAutoConfiguration 是写在 demo-springboot 框架模块中的,而 @MySpringBootApplication 是注解在 demo-app 的 MyApp 上的,目前肯定是扫描不到我们的 WebServerAutoConfiguration。那么怎么才能将 WebServerAutoConfiguration 注入容器中呢?答案是使用 @Import。改造 MySpringBootApplication 注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.mysb.autoconfigure;

import com.mysb.autoconfigure.web.WebServerAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan // 这个注解在哪里使用,就扫描哪个包路径下面的所有组件
@Import(WebServerAutoConfiguration.class) // 核心:通过 @Import 加载 WebServerAutoConfiguration 配置类
public @interface MySpringBootApplication {
}

好,通过以上设计,现在我们自己写的 springboot 就支持了多容器的选择。想要使用 tomcat 容器就导入 tomcat 的 jar 包,想要使用 jetty 容器,就导入 jetty 的 jar 包。


可扩展的自动配置类

具体的实现

上述 @Import(WebServerAutoConfiguration.class) 只是注入了一个 WebServerAutoConfiguration.class 配置类,如果我们需要导入很多自动配置类,岂不是要写很多@Import?(事实上一个注解类也只能被一个 @Import 修饰)。答案是当然不需要 ,我们可以对其继续优化。继续改造 MySpringBootApplication 注解。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.mysb.autoconfigure;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(MyAutoConfigurationImportSelector.class) // 核心:通过这个选择器动态去加载其他模块的配置类
public @interface MySpringBootApplication {
}

MyAutoConfigurationImportSelector:

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
35
36
37
38
39
40
package com.mysb.autoconfigure;

import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;

public class MyAutoConfigurationImportSelector implements DeferredImportSelector {

@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
List<String> configurations = new ArrayList<>();
try {
// 模拟 Spring Boot 扫描类路径下特定的 imports 文件
Enumeration<URL> urls = MyAutoConfigurationImportSelector.class.getClassLoader()
.getResources("META-INF/spring/com.mysb.autoconfigure.imports");

while (urls.hasMoreElements()) {
URL url = urls.nextElement();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream()))) {
String line;
while ((line = reader.readLine()) != null) {
line = line.trim();
// 过滤掉注释和空行
if (!line.isEmpty() && !line.startsWith("#")) {
configurations.add(line);
}
}
}
}
} catch (Exception e) {
throw new IllegalStateException("读取自动配置失败", e);
}
return configurations.toArray(new String[0]);
}
}

在 demo-springboot 框架模块的 resources 目录下增加 META-INF/spring/com.mysb.autoconfigure.imports 文件,文件的内容(一行一个,这个文件中可以写很多 spring boot 自动注入的组件配置类):

1
com.mysb.autoconfigure.web.WebServerAutoConfiguration

这样,如果新的自动配置类需要加入,只需要将全限类名写在 META-INF/spring/com.mysb.autoconfigure.imports 即可。实际的 springboot 框架也是这么做的,它自己的框架相关的自动配置类都放在了 spring-boot-autoconfigure 包下:

  • META-INF/spring.factories:从 Spring Boot 1.0 开始(最古老的核心机制),内容是标准的 properties 键值对配置方式,一个文件里塞满了各种扩展点的配置(比如自动配置、环境后置处理器、监听器等),为了配置几十个类,必须使用 \ 进行极其臃肿的换行连接:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    # 必须指定一个超长的 Key 
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
    org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration

    # 另一个扩展点的 Key 也混在这里
    org.springframework.context.ApplicationListener=\
    org.springframework.boot.ClearCachesApplicationListener
  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:从 Spring Boot 2.7 引入过渡,Spring Boot 3.x 全面彻底取代前者。它是纯文本格式,文件内部没有任何特殊符号,一行一个类名,解析效率也得到极大提升。

    1
    2
    3
    org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
    org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
    org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
  • 在老版本中,spring.factories 扮演的是一个 SPI(服务发现)的总入口。无论是自动配置类(AutoConfiguration)、应用监听器(ApplicationListener)、还是初始化器(ApplicationContextInitializer),全都得往这个文件里塞。而在新版本中,Spring Boot 进行了文件层面的解耦。不同的功能,拥有自己独立名字的 .imports 文件

    • 自动配置类:认准 org.springframework.boot.autoconfigure.AutoConfiguration.imports。
    • 如果有其他扩展点,就会有对应的 Xxxx.imports。每个文件只负责一类事情,职责非常单一和内聚。
  • 第三方依赖,比如 dubbo-spring-boot-autoconfigure 也可以引入自己的自动配置,在其 META-INFO/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 目录下。


为什么不用原生的 Java SPI

无论是 Java 原生的 java.util.ServiceLoader,还是 Dubbo 的 Extension 机制,亦或是 Spring Boot 的 xxx.imports(以及旧版的 spring.factories),它们在核心设计哲学上都是一模一样的:面向接口编程 + 策略模式 + 配置文件动态加载。它们的本质就是标准且纯粹的 SPI(Service Provider Interface,服务提供者接口)机制。一个标准的 SPI 机制必然包含四个核心要素,我们拿 Java 原生 SPI和Spring Boot 自动装配对比:

既然都是 SPI,Spring Boot 为什么要自己造轮子?这是因为原生的 Java SPI 存在几个对于框架来说致命的痛点,Spring Boot 对其进行了针对性的改良:

  • 痛点一:原生 SPI 会一次性实例化所有类(性能浪费)
    • Java 原生 SPI:一旦你调用 ServiceLoader.load(Driver.class),它会把该接口下在所有 jar 包里配置的实现类全部实例化(New 出来)一遍。如果你有 100 个驱动,哪怕你只用一个,另外 99 个也会被强行创建,极大地浪费了内存和启动时间。
    • Spring Boot 的改良:.imports 文件被加载时,Spring Boot 仅仅是把这些类名读取为字符串。随后它会利用强大的条件注解(如 @ConditionalOnClass、@ConditionalOnMissingBean)进行地毯式过滤。只有条件完全满足的配置类,才会被真正变成 Bean 放入容器。
  • 痛点二:原生 SPI 无法控制加载顺序和优先级
    • Java 原生 SPI:类的加载顺序完全取决于 Classloader 扫描 jar 包的物理顺序,开发者很难精准控制谁先实例化、谁后实例化。
    • Spring Boot 的改良:通过 .imports 引入类后,Spring 支持在配置类上使用 @Order、@AutoConfigureOrder、@AutoConfigureBefore、@AutoConfigureAfter 等注解。这让自动配置类之间有了严密的拓扑排序,确保像内置 WebServer 这种基础设施一定在最前面启动,而业务组件在后面跟进。
  • 痛点三:更符合 Spring 容器的生态(IoC/DI)
    • Java 原生 SPI:通过 ServiceLoader 创建出来的对象,是一个孤立的、游离在 Spring 容器之外的普通 Java 对象,它无法直接享受 Spring 的依赖注入(@Autowired)、切面编程(AOP)等高级功能。
    • Spring Boot 的改良:.imports 形式读取到类名后,是将其作为 BeanDefinition 注册到 Spring 的 BeanFactory 中。这样,这些自动配置类就完美融入了 Spring 的整套生态生命周期。

所以,当我们看到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 时,完全可以把它等价为:这是 Spring Boot 框架专属的、升级改良版的 SPI 声明文件。它通过极简的一行一个类名的纯文本形式,既保留了 SPI 解耦核心框架与第三方组件的灵魂,又通过条件注解和延迟加载,克服了传统 SPI 的性能弊端,成为了现代 Java 框架实现 “开箱即用” 的绝对核心枢纽。