通过手写模拟实现一个Spring Boot,就能以非常简单的方式就能知道Spring Boot大概是如何工作的。

工程与依赖

建一个工程,两个Module:

  1. spring-boot模块:表示springboot框架的源码实现
  2. user模块:表示用户业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot

首先,SpringBoot是基于的Spring Framework,所以我们要依赖Spring Framework。由于这里模拟的是WEB功能,所以依赖只需要引入spring-webmvc即可,另外还需要servlet和tomcat等依赖,具体如下:

<dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.14.RELEASE</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>

在User模块下只需要添加我们自己的SpringBoot依赖:

<dependency><groupId>com.morris</groupId><artifactId>spring-boot</artifactId><version>1.0-SNAPSHOT</version>
</dependency>

业务代码user模块

UserController.java

package com.morris.user.controller;import com.morris.user.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.annotation.Resource;@RestController
public class UserController {@Resourceprivate UserService userService;@GetMapping("test")public String test() {return userService.test();}
}

UserService.java

package com.morris.user.service;import org.springframework.stereotype.Service;@Service
public class UserService {public String test() {return "morris";}
}

因为我们模拟实现的是SpringBoot,而不是SpringMVC,所以我直接在user模块下定义了UserController和UserService,最终我希望能运行UserApplication中的main方法,就直接启动了项目,并能在浏览器中正常的访问到UserController中的某个方法。

核心注解和核心类

我们在真正使用SpringBoot时,核心会用到SpringBoot一个类和注解:

  1. @SpringBootApplication,这个注解是加在应用启动类上的,也就是main方法所在的类
  2. SpringApplication,这个类中有个run()方法,用来启动SpringBoot应用的

所以我们也来模拟实现他们。

一个@MySpringBootApplication注解:

package com.morris.springboot;import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;import java.lang.annotation.*;@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ComponentScan
@Configuration
@Import(AutoConfigurationSelector.class)
public @interface MySpringBootApplication {String value() default "";}

一个用来实现启动逻辑的SpringApplication类。

public class SpringApplication {public static void run(Class clazz){// TODO}
}

注意run方法需要接收一个Class类型的参数,这个class是用来干嘛的,等会就知道了。

有了以上两者,我们就可以在UserApplication中来使用了,比如:

UserApplication.java

package com.morris.user;import com.morris.springboot.MySpringBootApplication;
import com.morris.springboot.SpringApplication;@MySpringBootApplication
public class UserApplication {public static void main(String[] args) {SpringApplication.run(UserApplication.class);}
}

现在用来是有模有样了,但中看不中用,所以我们要来好好实现以下run方法中的逻辑了。

SpringApplication.run()方法

run方法中需要实现什么具体的逻辑呢?

首先,我们希望run方法一旦执行完,我们就能在浏览器中访问到UserController,那势必在run方法中要启动Tomcat,通过Tomcat就能接收到请求了。

大家如果学过Spring MVC的底层原理就会知道,在SpringMVC中有一个Servlet非常核心,那就是DispatcherServlet,这个DispatcherServlet需要绑定一个Spring容器,因为DispatcherServlet接收到请求后,就会从所绑定的Spring容器中找到所匹配的Controller,并执行所匹配的方法。

所以,在run方法中,我们要实现的逻辑如下:

  1. 创建一个Spring容器
  2. 创建Tomcat对象
  3. 生成DispatcherServlet对象,并且和前面创建出来的Spring容器进行绑定
  4. 将DispatcherServlet添加到Tomcat中
  5. 启动Tomcat

创建Spring容器

这个步骤比较简单,代码如下:

public class SpringApplication {public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();}
}

我们创建的是一个AnnotationConfigWebApplicationContext容器,并且把run方法传入进来的class作为容器的配置类,比如在UserApplication的run方法中,我们就是把UserApplication.class传入到了run方法中,最终UserApplication就是所创建出来的Spring容器的配置类,并且由于UserApplication类上有@SpringBootApplication注解,而@SpringBootApplication注解的定义上又存在@ComponentScan注解,所以AnnotationConfigWebApplicationContext容器在执行refresh时,就会解析UserApplication这个配置类,从而发现定义了@ComponentScan注解,也就知道了要进行扫描,只不过扫描路径为空,而AnnotationConfigWebApplicationContext容器会处理这种情况,如果扫描路径会空,则会将UserApplication所在的包路径做为扫描路径,从而就会扫描到UserService和UserController。

所以Spring容器创建完之后,容器内部就拥有了UserService和UserController这两个Bean。

启动Tomcat

我们用的是Embed-Tomcat,也就是内嵌的Tomcat,真正的SpringBoot中也用的是内嵌的Tomcat,而对于启动内嵌的Tomcat,也并不麻烦,代码如下:

public static void start(WebApplicationContext applicationContext) {// 启动tomcatSystem.out.println("启动tomcat");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", newDispatcherServlet(applicationContext));context.addServletMappingDecoded("/*", "dispatcher");try {tomcat.start();} catch (LifecycleException e) {e.printStackTrace();}
}

代码虽然看上去比较多,但是逻辑并不复杂,比如配置了Tomcat绑定的端口为8080,后面向当前Tomcat中添加了DispatcherServlet,并设置了一个Mapping关系,最后启动,其他代码则不用太过关心。

而且在构造DispatcherServlet对象时,传入了一个ApplicationContext对象,也就是一个Spring容器,就是我们前文说的,DispatcherServlet对象和一个Spring容器进行绑定。

接下来,我们只需要在run方法中,调用start()即可:

public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();start(applicationContext);
}

实际上代码写到这,一个极度精简版的SpringBoot就写出来了,比如现在运行UserApplication,就能正常的启动项目,并能接收请求。

启动能看到Tomcat的启动日志,然后在浏览器上访问:http://localhost:8080/test,也能正常的看到结果。

此时,你可以继续去写其他的Controller和Service了,照样能正常访问到,而我们的业务代码中仍然只用到了SpringApplication类和@MySpringBootApplication注解。

实现Tomcat和Jetty的切换

虽然我们前面已经实现了一个比较简单的SpringBoot,不过我们可以继续来扩充它的功能,比如现在我有这么一个需求,这个需求就是我现在不想使用Tomcat了,而是想要用Jetty,那该怎么办?

我们前面代码中默认启动的是Tomcat,那我现在想改成这样子:

  1. 如果项目中有Tomcat的依赖,那就启动Tomcat
  2. 如果项目中有Jetty的依赖就启动Jetty
  3. 如果两者都没有则报错
  4. 如果两者都有也报错

这个逻辑希望SpringBoot自动帮我实现,对于程序员用户而言,只要在pom文件中添加相关依赖就可以了,想用Tomcat就加Tomcat依赖,想用Jetty就加Jetty依赖。

那SpringBoot该如何实现呢?

我们知道,不管是Tomcat还是Jetty,它们都是应用服务器,或者是Servlet容器,所以我们可以定义接口来表示它们,这个接口叫做WebServer(别问我为什么叫这个,因为真正的SpringBoot源码中也叫这个)。

并且在这个接口中定义一个start方法:

public interface WebServer {public void start(WebApplicationContext applicationContext);
}

有了WebServer接口之后,就针对Tomcat和Jetty提供两个实现类:

public class TomcatWebServer implements WebServer{@Overridepublic void start(WebApplicationContext applicationContext) {System.out.println("启动Tomcat");}
}
public class JettyWebServer implements WebServer{@Overridepublic void start(WebApplicationContext applicationContext) {System.out.println("启动Jetty");}
}

而在SpringApplication中的run方法中,我们就要去获取对应的WebServer,然后启动对应的webServer,代码为:

public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();WebServer webServer = getWebServer(applicationContext);webServer.start(applicationContext);
}public static WebServer getWebServer(WebApplicationContext applicationContext){return null;
}

这样,我们就只需要在getWebServer方法中去判断到底该返回TomcatWebServer还是JettyWebServer。

前面提到过,我们希望根据项目中的依赖情况,来决定到底用哪个WebServer,我就直接用SpringBoot中的源码实现方式来模拟了。

模拟实现条件注解

首先我们得实现一个条件注解@ConditionalOnClass,对应代码如下:

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {String value() default "";
}

注意核心为@Conditional(OnClassCondition.class)中的OnClassCondition,因为它才是真正的条件逻辑:

public class OnClassCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {Map<String, Object> annotationAttributes =
metadata.getAnnotationAttributes(ConditionalOnClass.class.getName());String className = (String) annotationAttributes.get("value");try {context.getClassLoader().loadClass(className);return true;} catch (ClassNotFoundException e) {return false;}}
}

具体逻辑为,拿到@ConditionalOnClass中的value属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,则表示不符合条件。

模拟实现自动配置类

有了条件注解,我们就可以来使用它了,那如何实现呢?

这里就要用到自动配置类的概念,我们先看代码:

@Configuration
public class WebServerAutoConfiguration {@Bean@ZhouyuConditionalOnClass("org.apache.catalina.startup.Tomcat")public TomcatWebServer tomcatWebServer(){return new TomcatWebServer();}@Bean@ZhouyuConditionalOnClass("org.eclipse.jetty.server.Server")public JettyWebServer jettyWebServer(){return new JettyWebServer();}
}

这个代码还是比较简单的,通过一个WebServiceAutoConfiguration的Spring配置类,在里面定义了两个Bean,一个TomcatWebServer,一个JettyWebServer,不过这两个要生效的前提是符合当前所指定的条件,比如:

  1. 只有存在"org.apache.catalina.startup.Tomcat"类,那么才有TomcatWebServer这个Bean
  2. 只有存在"org.eclipse.jetty.server.Server"类,那么才有TomcatWebServer这个Bean

并且我们只需要在SpringApplication中getWebServer方法,如此实现:

public static WebServer getWebServer(ApplicationContext applicationContext){// key为beanName, value为Bean对象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();
}

这样整体SpringBoot启动逻辑就是这样的:

  1. 创建一个AnnotationConfigWebApplicationContext容器
  2. 解析MyApplication类,然后进行扫描
  3. 通过getWebServer方法从Spring容器中获取WebServer类型的Bean
  4. 调用WebServer对象的start方法

有了以上步骤,我们还差了一个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个自动配置类,因为不管这个类里写了什么代码,Spring不去解析它,那都是没用的,此时我们需要SpringBoot在run方法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。

UserApplication是Spring的一个配置类,但是UserApplication是我们传递给SpringBoot,从而添加到Spring容器中去的,而WebServiceAutoConfiguration就需要SpringBoot去自动发现,而不需要程序员做任何配置才能把它添加到Spring容器中去,而且要注意的是,Spring容器扫描也是扫描不到WebServiceAutoConfiguration这个类的,因为我们的扫描路径是"com.morris.user",而WebServiceAutoConfiguration所在的包路径为"com.morris.springboot"。

那SpringBoot中是如何实现的呢?通过SPI,当然SpringBoot中自己实现了一套SPI机制,也就是我们熟知的spring.factories文件,那么我们模拟就不搞复杂了,就直接用JDK自带的SPI机制。

发现自动配置类

SPI接口:

public interface AutoConfiguration {
}

WebServerAutoConfiguration需要实现AutoConfiguration接口。

现在我们只需要在springboot项目中的resources/META-INF/services目录下添加com.morris.springboot.AutoConfiguration文件,内容为:

com.morris.springboot.WebServerAutoConfiguration

SPI的配置就完成了,相当于通过com.morris.springboot.AutoConfiguration文件配置了springboot中所提供的配置类。

然后我们再利用spring中的@Import技术来导入这些配置类,我们在@MySpringBootApplication的定义上增加如下代码:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(AutoConfigurationSelector.class)
public @interface MySpringBootApplication {
}

AutoConfigurationSelector类为:

package com.morris.springboot;import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;public class AutoConfigurationSelector implements DeferredImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata annotationMetadata) {ServiceLoader<AutoConfiguration> loader = ServiceLoader.load(AutoConfiguration.class);List<String> list = new ArrayList<>();for (AutoConfiguration configuration : loader) {list.add(configuration.getClass().getName());}return list.toArray(new String[0]);}
}

这就完成了从com.morris.springboot.AutoConfiguration文件中获取自动配置类的名字,并导入到Spring容器中,从而Spring容器就知道了这些配置类的存在,而对于user模块而言,是不需要修改代码的。

此时运行MyApplication,就能看到启动了Tomcat,因为SpringBoot默认在依赖中添加了Tomcat依赖。

而如果在User模块中再添加jetty的依赖:

<dependency><groupId>com.morris</groupId><artifactId>spring-boot</artifactId><version>1.0-SNAPSHOT</version>
</dependency><dependency><groupId>org.eclipse.jetty</groupId><artifactId>jetty-server</artifactId><version>9.4.43.v20210629</version>
</dependency>

那么启动MyApplication就会报错。

只有先排除到Tomcat的依赖,再添加Jetty的依赖才能启动Jetty:

<dependency><groupId>com.morris</groupId><artifactId>spring-boot</artifactId><version>1.0-SNAPSHOT</version><exclusions><exclusion><groupId>org.apache.tomcat.embed</groupId><artifactId>tomcat-embed-core</artifactId></exclusion></exclusions>
</dependency><dependency><groupId>org.eclipse.jetty</groupId><artifactId>jetty-server</artifactId><version>9.4.43.v20210629</version>
</dependency>

到此,我们实现了一个简单版本的SpringBoot。

【springboot】手写SpringBoot核心流程相关推荐

  1. 你还不会手写SpringBoot启动器吗

    Starter是什么 ? Spring Boot 对比 Spring MVC 最大的优点就是使用简单,约定大于配置.不用 Spring MVC 的时候,时不时被 xml 配置文件搞的晕头转向,冷不防还 ...

  2. babel原理_手写webpack核心原理,再也不怕面试官问我webpack原理

    手写webpack核心原理 一.核心打包原理 1.1 打包的主要流程如下 1.2 具体细节 二.基本准备工作 三.获取模块内容 四.分析模块 五.收集依赖 六.ES6转成ES5(AST) 七.递归获取 ...

  3. 05. 手写Spring核心框架

    目录 05 手写Spring核心框架 Pt1 手写IoC/DI Pt1.1 流程设计 Pt1.2 基础配置 application.properties pom.xml web.xml Pt1.3 注 ...

  4. 手写Vuex核心原理,再也不怕面试官问我Vuex原理

    手写Vuex核心原理 文章目录 手写Vuex核心原理 一.核心原理 二.基本准备工作 三.剖析Vuex本质 四.分析Vue.use 五.完善install方法 六.实现Vuex的state 七.实现g ...

  5. 手写Redux核心原理

    手写Redux核心原理 一.createStore核心逻辑实现 createStore的三个参数 reducer:根据actions类型对store中数据状态进行更改,返回一个新的状态 preload ...

  6. SpringBoot 手写过滤器amp;加载第三方过滤器

    版权声明:本文为博主原创文章,未经博主允许不得转载. https://blog.csdn.net/qq_36367789/article/details/81665638 如何手写一个过滤器呢.假设我 ...

  7. 一起手写Vue3核心模块源码,掌握阅读源码的正确方法

    最近和一个猎头聊天,说到现在前端供需脱节的境况.一方面用人方招不到想要的中高级前端,另一方面市场上有大量初级前端薪资要不上价. 特别是用 Vue 框架的,因为好上手,所以很多人将 Vue 作为入门框架 ...

  8. Pytorch实战从入门到精通第一部分——手写字符识别全流程

    下面是用MNIST手写字符数据从数据loader到全连接网络设计.模型训练.模型测试.模型存储的全过程完整代码,仔细品味可供学习使用. import torch import torch.nn as ...

  9. 源码分析 | 手写mybait-spring核心功能(干货好文一次学会工厂bean、类代理、bean注册的使用)

    小傅哥 | https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获.专注于原创专题案例编写,目前已完成的专题有:Netty4.x实战专题案例.用Java实现JVM.基于Ja ...

最新文章

  1. [经典排序算法][集锦]
  2. SAP/SD - 做SD你要知道的透明表
  3. String、StringBuuffer、StringBuilder三者的区别
  4. vue 设置全局变量、指定请求的 baseurl
  5. 数据库临时表空间设置
  6. linux-权限更改-符号更改法-rwx
  7. Nagios 监控平台快速安装
  8. According to a report from Bleeping Computer
  9. java ssm 分页_ssm实现分页查询的实例
  10. 活着是一种罪过,是上帝对你的另一种眷顾,叫做惩罚!活着痛苦!
  11. 全向轮移动平台参数校准
  12. markdown的学习
  13. js创建对象,用函数实现对象创建,并实现内函数共享
  14. AOJ-776 马的走法 动态规划
  15. Android相对布局简单案例(附完整源码)
  16. 电商商品爬虫,亚马逊amazon采集源码
  17. 移动端轮播图——网易云音乐手机端样式
  18. nuc10黑苹果无法wifi上网
  19. C#一个解决方案创建多个项目
  20. 群晖docker安装青龙面板自动狗东京豆领取

热门文章

  1. 《视觉SLAM十四讲》学习笔记-摄像机成像公式
  2. linux下mplayer卸载,Linux下播放器之Mplayer安装心得
  3. 个人简历”的Resume(java封装类)
  4. ISO/IEC 14496
  5. 怎样将cad布局导出来_cad如何复制布局(CAD怎么把一个布局移动到另一个布局)...
  6. JavaWeb——创建文件
  7. 【蓝桥软件学院】Android中五大Manager详解及使用技巧
  8. 图像分割性能评价指标
  9. Android获取拓展外置SD卡(可插拔)路径及读写外置SD卡的方法
  10. 巧用WinRAR压缩软件读取img文件【系统收藏】