本文通过手写模拟实现一个简易版的Spring Boot 程序,让大家能以非常简单的方式知道Spring Boot大概的工作流程。
工程依赖
创建maven工程,并创建两个module
springboot模块:手写模拟springboot框架的源码实现
test模块:业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot
springboot模块依赖:SpringBoot基于的Spring,依赖Spring同上也支持Spring MVC,所以也要依赖Spring MVC,包括Tomcat等,在SpringBoot模块中要添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>5.3.18</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>9.0.60</version>
</dependency>
</dependencies>
test模块依赖:依赖springboot模块依赖即可:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
然后再test模块中创建相关的controller和service测试类:
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:05
*/
@RestController
public class TestController {
@Autowired
private TestService testService;
@GetMapping("/test")
public String test(@RequestParam("name") String name){
return testService.sayHello(name);
}
}
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:06
*/
@Component
public class TestService {
public String sayHello(String name) {
return "Hello " + name + "!";
}
}
创建核心注解和核心执行类
在springboot中,最重要的是一个注解和一个启动类上的:
SpringApplication:该类有个run方法,主启动main中进行调用
@SpringBootApplication:该注解是添加在主启动类上的
因此我们可以在springboot模块中模拟实现以上核心注解和核心类。
@LPSpringBootApplication 注解:
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:10
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface LPSpringBootApplication {
}
启动类:
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:11
*/
public class LPSpringApplication {
public static void run(Class clazz) {
}
}
然后在test模块中LPApplication类的进行使用
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:12
*/
@LPSpringBootApplication
public class LPApplication {
public static void main(String[] args) {
LPSpringApplication.run(LPApplication.class);
}
}
填充完善run方法
首先,我们希望run方法执行完后,能在浏览器中访问到UserController,那么run方法
中需要启动Tomcat,通过Tomcat接收到http请求。
在SpringMVC中有一个Servlet非常核心,那就是DispatcherServlet,这个DispatcherServlet需要绑定一个Spring容器,因为DispatcherServlet接收到请求后,就会从所绑定的Spring容器中找到所匹配的Controller,并执行所匹配的方法。
因此,在run方法中,我们需要实现如下步骤:
创建Spring容器
创建Tomcat对象
生成DispatcherServlet对象,并且创建出来的Spring容器进行绑定
将DispatcherServlet添加到Tomcat中
启动Tomcat
创建Spring容器
/**
* @author liupeng
* @version 1.0
* @description: TODO
* @date 2024/5/7 17:11
*/
public class LPSpringApplication {
public static void run(Class clazz) {
System.out.println("Hello World!");
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
webApplicationContext.register(clazz);
webApplicationContext.refresh();
}
我们创建AnnotationConfigWebApplicationContext 容器,并且将run方法传入的Class作为容器的配置类,比如上面的LPApplication.Class传入到了run方法中,于是LPApplication就变成容器的配置类,由于LPApplication 类上添加了@LPSpringBootApplication注解,同时该注解中存在@ComponentScan注解,于是该配置类会去扫描LPApplication所在的包路径,从而会将TestController 和TestService 类进行扫描并添加在容器内部,并在容器内部存在了这两个bean对象。
Tomcat启动
使用Embed-Tomcat,模拟真正的springboot使用的内嵌Tomcat
public static void startTomcat(WebApplicationContext applicationContext){
Tomcat tomcat = new Tomcat();
Server server = tomcat.getServer();
Service service = server.findService("Tomcat");
Connector connector = new Connector();
connector.setPort(8080);
Engine engine = new StandardEngine();
engine.setDefaultHost("localhost");
Host host = new StandardHost();
host.setName("localhost");
String contextPath = "";
Context context = new StandardContext();
context.setPath(contextPath);
context.addLifecycleListener(new Tomcat.FixContextListener());
host.addChild(context);
engine.addChild(host);
service.setContainer(engine);
service.addConnector(connector);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
然后在run方法中执行startTomcat方法便可启动Tomcat容器
效果如下
然后访问controller中的测试方法
我们再test的业务模块中只需要用到@LPSpringBootApplication注解和LPSpringApplication类即可。
实现多http(servlet)服务器的切换
上面我们模拟实现了简单的sprinboot,但在真正的springboot中是可以进行使用多种http(servlet)服务器的比如Tomcat、undertow、jetty等。
那么我们就需要进行动态切换
如果项目中有Tomcat的依赖,那就启动Tomcat
如果项目中有Jetty的依赖就启动Jetty
如果两者都没有或者都没有则进行报错
我们可以定义一个webserver接口,在接口进行定义抽象的start方法,不同的服务器进行各种的自定义实现
public interface WebServer {
public void start();
}
public class TomcatWebServer implements WebServer{
@Override
public void start() {
System.out.println("TomcatServer is starting...");
}
public class JettyWebServer implements WebServer {
@Override
public void start() {
System.out.println("JettyWebServer is starting...");
}
}
然后再run方法中我们需要进行获取对应的webserver实现,然后进行启动start方法
public class LPSpringApplication {
public static void run(Class clazz) {
System.out.println("Hello World!");
AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
webApplicationContext.register(clazz);
webApplicationContext.refresh();
// TomcatWebServer.startTomcat(webApplicationContext);
getWebServer(webApplicationContext).start();
}
public static WebServer getWebServer(WebApplicationContext applicationContext){
Map<String, WebServer> beansOfType = applicationContext.getBeansOfType(WebServer.class);
if (beansOfType.isEmpty()) {
throw new NullPointerException();
}
if (beansOfType.size() > 1) {
throw new IllegalStateException();
}
return beansOfType.values().stream().findFirst().get();
}
}
模拟使用条件注解
首先我们需要实现Contion接口和一个自定义的条件注解@LPConditionalOnClass
public class LPCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(LPConditionalOnClass.class.getName());
String className = (String) annotationAttributes.get("value");
try {
context.getClassLoader().loadClass(className);
return true;
} catch (ClassNotFoundException e) {
return false;
}
}
}
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(LPCondition.class)
public @interface LPConditionalOnClass {
String value();
}
LPCondition 的逻辑是拿到LPConditionalOnClass 注解的value值然后进行类加载器加载,加载成功则符合条件,反之亦然。
模拟实现自动配置类
创建了条件注解我们应该咋样使用呢?
我们需要使用自动配置的概念,我们只需要将满足条件的类进行注入到spring容器中即可
public interface AutoConfiguration {
}
@Configuration
public class WebServerAutoConfiguration implements AutoConfiguration {
@Bean
@LPConditionalOnClass("org.apache.catalina.startup.Tomcat")
public TomcatWebServer tomcatWebServer(){
return new TomcatWebServer();
}
@Bean
@LPConditionalOnClass("org.eclipse.jetty.server.Server")
public JettyWebServer jettyWebServer(){
return new JettyWebServer();
}
}
然后再容器中进行根据WebServer类型进行加载便可获取到对应的bean对象:
public static WebServer getWebServer(WebApplicationContext applicationContext){
Map<String, WebServer> beansOfType = applicationContext.getBeansOfType(WebServer.class);
if (beansOfType.isEmpty()) {
throw new NullPointerException();
}
if (beansOfType.size() > 1) {
throw new IllegalStateException();
}
return beansOfType.values().stream().findFirst().get();
}
整体SpringBoot大概启动逻辑:
创建一个AnnotationConfigWebApplicationContext容器
解析LPApplication类,然后进行扫描
通过getWebServer方法从Spring容器中获取WebServer类型的Bean
调用WebServer对象的start方法
有了以上步骤,但是还差了一个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个自动配置类,此时我们需要SpringBoot在run方法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。
为了让Spring解析到WebServiceAutoConfiguration
这个自动配置类并将其添加到Spring容器中,Spring Boot在run
方法中需要找到这个自动配置类。虽然LPApplication
是Spring的配置类,并且我们可以将其传递给Spring Boot以添加到Spring容器中,但WebServiceAutoConfiguration
的自动发现需要借助Spring Boot的机制,而不是通过手动配置。
由于LPApplication
是我们显式提供给Spring Boot的配置类,Spring Boot会将其添加到Spring容器中。然而,WebServiceAutoConfiguration
需要Spring Boot自动发现,并且不能依赖常规的组件扫描,因为它的包路径不同于应用程序的扫描路径。LPApplication
所在的扫描路径是"com.lp.test"
,而WebServiceAutoConfiguration
在"com.lp.springboot"
中。
Spring Boot是如何实现自动发现并将自动配置类添加到Spring容器中的呢?关键在于Spring Boot的SPI机制。Spring Boot实现了自己的Service Provider Interface (SPI)机制,通过spring.factories
文件来自动配置应用程序所需的类。通过这个机制,Spring Boot能够自动发现WebServiceAutoConfiguration
这样的自动配置类,而无需显式配置。
因此,如果我们希望Spring Boot自动发现自定义的自动配置类,可以通过在spring.factories
文件中列出相应的配置类来实现。该文件通常位于META-INF
目录中,Spring Boot在启动时会读取它,并将指定的自动配置类添加到Spring容器中。
这个过程使得Spring Boot能够灵活地扩展和自动配置,而无需开发人员显式指定所有配置类。这种自动化机制大大简化了配置过程,并提供了高度可扩展性。
那么我们模拟就可以直接用JDK自带的SPI机制。
解析自动配置类
我们只需要在springboot模块的resource文件下创建如下文件
也就是上面的AutoConfiguration接口,然后通过SPI机制和Import机制将WebServerAutoConfiguration配置类型进行导入到spring容器中
public class LPDeferredImportSelector implements DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
ServiceLoader<AutoConfiguration> load = ServiceLoader.load(AutoConfiguration.class);
List<String> list = new ArrayList<>();
for (AutoConfiguration autoConfiguration : load) {
list.add(autoConfiguration.getClass().getName());
}
return list.toArray(new String[0]);
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(LPDeferredImportSelector.class)
public @interface LPSpringBootApplication {
}
这样就完成了在从com.lp.springboot.AutoConfiguration文件中获取自动配置类的名称并且进行导入到spring容器中,然后spring容器就可以获取到对应的配置类信息。
然后在test模块中添加jetty的依赖同时保留Tomcat依赖
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.43.v20210629</version>
</dependency>
然后启动test的启动类
便会进行异常处理。
这样我们就可以在test业务工程进行自定义使用http(servlet)服务器的切换。
结语
我们通过模拟了springboot的主要启动流程:核心注解、核心启动类、创建spring容器、对内嵌Tomcat的启动和注入spring的DispatcherServlet容器、使用多servlet服务器的切换、条件注解、自动配置类的解析等关键功能节点完成了一个简单版本的Springboot,使用这样的方式让我们对Springboot项目能有个更加深刻的理解。