基本框架的说明和搭建
我们现在新建两个模块,一个是 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>
</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 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) { } }
|
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) { AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext(); applicationContext.register(clazz);
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());
context.addLifecycleListener(event -> { if ("configure_start".equals(event.getType())) { applicationContext.setServletContext(context.getServletContext()); System.out.println("ServletContext is ready. Refreshing Spring Context..."); applicationContext.refresh(); } }); host.addChild(context);
Connector connector = new Connector(); connector.setPort(port); service.setContainer(engine); service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext)); context.addServletMappingDecoded("/*", "dispatcher");
try { 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 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());
CountDownLatch latch = new CountDownLatch(1);
context.addLifecycleListener(event -> { if ("configure_start".equals(event.getType())) { applicationContext.setServletContext(context.getServletContext()); System.out.println("Tomcat ServletContext 绑定到 Spring 容器成功");
latch.countDown(); } }); host.addChild(context);
Connector connector = new Connector(); connector.setPort(port); service.setContainer(engine); service.addConnector(connector);
DispatcherServlet dispatcherServlet = new DispatcherServlet(applicationContext); dispatcherServlet.setPublishContext(false); tomcat.addServlet(contextPath, "dispatcher", dispatcherServlet); context.addServletMappingDecoded("/*", "dispatcher");
try { System.out.println("Starting Embedded Tomcat on port " + port + "..."); tomcat.start();
latch.await(); System.out.println("ServletContext 校验通过,放行 Spring 容器继续初始化...");
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) { MyAnnotationConfigWebApplicationContext applicationContext = new MyAnnotationConfigWebApplicationContext(); applicationContext.register(clazz);
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 { createWebServer();
ServletContext servletContext = super.getServletContext(); if (servletContext != null) { this.getBeanFactory().registerResolvableDependency(ServletContext.class, servletContext);
this.getBeanFactory().addBeanPostProcessor(new BeanPostProcessor() { @Override public Object postProcessBeforeInitialization(Object bean, String beanName) { if (bean instanceof ServletContextAware awareBean) { awareBean.setServletContext(servletContext); } return bean; } }); System.out.println("强行挂载自定义 ServletContextAware 处理器成功!"); } } catch (Exception e) { throw new RuntimeException("自动启动内置 Web 服务器失败", e); }
super.onRefresh(); }
private void createWebServer() { Map<String, WebServer> webServers = getBeansOfType(WebServer.class);
if (webServers.isEmpty()) { throw new IllegalStateException("未找到任何 WebServer 组件,请检查配置!"); } if (webServers.size() > 1) { throw new IllegalStateException("发现了多个 WebServer 组件 (" + webServers.keySet() + "),无法确定启动哪一个!"); }
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() { PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer(); configurer.setIgnoreUnresolvablePlaceholders(true); return configurer; }
@Configuration @MyConditionalOnClass("org.springframework.web.servlet.DispatcherServlet") public static class WebMvcAutoConfiguration extends DelegatingWebMvcConfiguration { }
@Bean @MyConditionalOnClass("org.apache.catalina.startup.Tomcat") public TomcatWebServer tomcatWebServer() { return new TomcatWebServer(); }
@Bean @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 {
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) { 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) 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 { 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
| org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
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 框架实现 “开箱即用” 的绝对核心枢纽。
标题:
Spring Boot - 基于 SPI 手撕一个 Spring Boot 框架出来