【SpringBoot】手写模拟SpringBoot核心流程
依赖包
新建一个工程,包含两个 module:
springboot 模块,表示 springboot 源码实现;
user 模块,表示业务系统,使用 springboot 模块;
依赖包:Spring、SpringMVC、Tomcat 等,引入依赖如下:
<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>
在 user 模块下引入依赖:
<dependencies><dependency><groupId>org.example</groupId><artifactId>springboot</artifactId><version>1.0-SNAPSHOT</version></dependency>
</dependencies>
定义对应的 controller 和 service:
@RestController
public class UserController {@Autowiredprivate UserService userService;@GetMapping("test")public String test(){return userService.test();}
}
最终希望通过启动 MyApplication 的 main 方法,启动项目,能访问到 UserController。
核心注解和核心类
SpringBoot 的核心类和注解:
@SpringBootApplication,这个注解是加在应用启动类上的,也就是 main 方法所在的类;
SpringApplication,这个类中有个 run() 方法,用来启动 SpringBoot 应用的;
所以,自定义类和注解以实现上面的功能。
@FireSpringBootApplication 注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface FireSpringBootApplication {
}
FireSpringApplication 启动类:
public class FireSpringApplication {public static void run(Class clazz){}
}
在 MyApplication 中使用:
@FireSpringBootApplication
public class MyApplication {public static void main(String[] args) {FireSpringApplication.run(MyApplication.class);}
}
run 方法
需要在 run 方法中启动 tomcat,通过 tomcat 接收请求;
DispatchServlet 绑定 spring 容器,DispatchServlet 接收到请求后需要在 spring 容器中找到一个 controller 中对应的方法;
run 方法中需要实现的逻辑:
- 创建一个 Spring 容器
- 创建 Tomcat 对象
- 生成 DispatcherServlet 对象,并且和前面创建出来的 Spring 容器进行绑定
- 将 DispatcherServlet 添加到 Tomcat 中
- 启动 Tomcat
创建 Spring 容器
public class FireSpringApplication {public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();}
}
run 方法中传入的即使 MyApplication 类,被解析为 Spring 容器的配置类;
默认会将 MyApplication 所在的包作为扫描路径,从而扫描到 UserController 和 UserService,所以在 spring 容器启动后就会存在两个 bean 了;
启动 Tomcat
使用内嵌的 Tomact,即 Embed-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(8081);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);// 添加DispatcherServlet,并且绑定一个Spring容器tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));// 设置Mapping关系context.addServletMappingDecoded("/*", "dispatcher");try {tomcat.start();} catch (LifecycleException e) {e.printStackTrace();}}
在 run 方法中调用 startTomcat 方法启动 tomcat:
public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();// 启动tomcatstartTomcat(applicationContext);}
到此,一个简单的 SpringBoot 就写出来了,运行 MyApplication 正常启动项目,通过浏览器就可以访问 UserController 了。
实现 Tomcat 和 Jetty 的切换
前面代码中默认启动的是 Tomcat,现在想改成这样子:
- 如果项目中有 Tomcat 的依赖,那就启动 Tomcat
- 如果项目中有 Jetty的依赖就启动 Jetty
- 如果两者都没有则报错
- 如果两者都有也报错
这个逻辑希望 SpringBoot 自动实现,对于程序员用户而言,只要在 Pom 文件中添加相关依赖就可以了,想用 Tomcat 就加 Tomcat 依赖,想用 Jetty 就加 Jetty 依赖。
Tomcat 和 Jetty 都是应用服务器,或者是 Servlet 容器,可以定义接口来表示它们,这个接口交 WebServer(SpringBoot 源码中也叫这个)。
定义接口如下:
public interface WebServer {public void start();}
Tomcat 实现类:
public class TomcatWebServer implements WebServer{@Overridepublic void start() {System.out.println("启动Tomcat");}
}
Jetty 实现类:
public class JettyWebServer implements WebServer{@Overridepublic void start() {System.out.println("启动Jetty");}
}
在 FireSpringApplication 中的 run 方法中,去获取对应的 WebServer,然后启动对应的 webServer。
代码如下:
public static void run(Class clazz){AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();applicationContext.register(clazz);applicationContext.refresh();// 自动获取配置的Tomcat或者Jetty容器WebServer webServer = getWebServer(applicationContext);webServer.start();}public static WebServer getWebServer(ApplicationContext applicationContext){return null;
}
模拟实现条件注解
首先实现一个条件注解@FireConditionalOnClass,对应代码如下:
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Conditional(FireOnClassCondition.class)
public @interface FireConditionalOnClass {String value() default "";
}
注意核心为@Conditional(FireOnClassCondition.class)中的 FireOnClassCondition,因为它才是真正得条件逻辑:
public class FireOnClassCondition implements Condition {@Overridepublic boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(FireConditionalOnClass.class.getName());String className = (String) annotationAttributes.get("value");try {context.getClassLoader().loadClass(className);return true;} catch (ClassNotFoundException e) {return false;}}
}
具体逻辑为,拿到@FireConditionalOnClass中的 value 属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,则表示不符合条件。
模拟实现自动配置类
配置类代码如下:
@Configuration
public class WebServiceAutoConfiguration {@Bean@FireConditionalOnClass("org.apache.catalina.startup.Tomcat")public TomcatWebServer tomcatWebServer(){return new TomcatWebServer();}@Bean@FireConditionalOnClass("org.eclipse.jetty.server.Server")public JettyWebServer jettyWebServer(){return new JettyWebServer();}
}
表示org.apache.catalina.startup.Tomcat存在,则有 tomcatWebServer 这个bean;
表示org.eclipse.jetty.server.Server存在,则有 jettyWebServer 这个bean;
FireSpringApplication#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 启动逻辑就是这样的:
- 创建一个 AnnotationConfigWebApplicationContext 容器
- 解析 MyApplication 类,然后进行扫描
- 通过 getWebServer 方法从 Spring 容器中获取 WebServer 类型的 Bean
- 调用 WebServer 对象的 start 方法
发现自动配置类
WebServiceAutoConfiguration 需要被 SpringBoot 发现,可以通过 SPI 机制实现,比较 JDK 自带的 SPI 来实现。
在 springboot 项目中的 resources 目录下添加目录META-INF/services和文件 org.example.springboot.AutoConfiguration,文件内容为org.example.springboot.WebServiceAutoConfiguration。
接口:
public interface AutoConfiguration {
}
WebServiceAutoConfiguration 实现该接口:
@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {@Bean@FireConditionalOnClass("org.apache.catalina.startup.Tomcat")public TomcatWebServer tomcatWebServer(){return new TomcatWebServer();}@Bean@FireConditionalOnClass("org.eclipse.jetty.server.Server")public JettyWebServer jettyWebServer(){return new JettyWebServer();}
}
再利用 spring 中的@Import技术来导入这些配置类,我们在@FireSpringBootApplication的定义上增加如下代码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(FireImportSelect.class)
public @interface FireSpringBootApplication {
}
FireImportSelect:
public class FireImportSelect implements DeferredImportSelector {@Overridepublic String[] selectImports(AnnotationMetadata importingClassMetadata) {ServiceLoader<AutoConfiguration> serviceLoader = ServiceLoader.load(AutoConfiguration.class);List<String> list = new ArrayList<>();for (AutoConfiguration autoConfiguration : serviceLoader) {list.add(autoConfiguration.getClass().getName());}return list.toArray(new String[0]);}
}
如此,Spring 容器可以装载 WebServiceAutoConfiguration 配置类了,对于 user 模块而言,不需要修改代码就可以自动识别 Tomcat 和 Jetty 了。
总结
到此,实现了一个简单版本的 SpringBoot,因为 SpringBoot 首先是基于 Spring 的,而且提供的功能也更加强大,后面会对这些功能进行更深入的剖析。
相关文章:
【SpringBoot】手写模拟SpringBoot核心流程
依赖包 新建一个工程,包含两个 module: springboot 模块,表示 springboot 源码实现;user 模块,表示业务系统,使用 springboot 模块; 依赖包:Spring、SpringMVC、Tomcat 等ÿ…...
应对.locked勒索病毒:恢复、预防全方位攻略
导言: .locked勒索病毒并非简单的数字威胁,它是一场对个人和企业数字资产的精密审判。这种病毒通过各种方式感染系统,从而以瞬间之间将用户的关键文件变成数字拼图,无情地要求赎金以换取解锁的密钥。如果您正在经历勒索病毒数据恢…...
基于DS1302时钟液晶12864显示2路闹钟仿真及源程序
一、系统方案 1、本设计采用51单片机作为主控器。 2、DS1302采集年月日时分秒送到液晶12864显示。 3、按键年月日时分秒,两路闹钟。 二、硬件设计 原理图如下: 三、单片机软件设计 1、首先是系统初始化 uchar clock_time[6] {0X00,0X59,0X23,0X09,0X…...
AGC034E Complete Compress
AGC034E Complete Compress 洛谷[AGC034E] Complete Compress 题目大意 给你一棵有 n n n个节点的树,并用 01 01 01串告诉你哪些节点上有棋子(恰好一棵)。 你可以进行若干次操作,每次操作可以将两颗距离至少为 2 2 2的棋子向彼…...
python设计模式12:状态模式
什么是状态机? 关键属性: 状态和转换 状态: 系统当前状态 转换:一种状态到另外一种状态的变化。 转换由触发事件或是条件启动。 状态机-状态图 状态机使用场景: 自动售货机 电梯 交通灯 组合锁 停车计时…...
JS对图片尺寸和DPI进行编辑修改(1寸照修改为2寸照)
各种报名都对照片有大小限制,鉴于这种情况,网上搜了后拼凑出了如下代码,用于解决1寸照片修改为2寸照片,同时将DPI修改为300,当然也可以根据自己的情况修改代码: HTML <input type"file" id&…...
EDA实验----四选一多路选择器设计(QuartusII)
目录 一.实验目的 二.实验仪器设备 三.实验原理: 四.实验要求 五.实验内容及步骤 1.实验内容 2.实验步骤 六.实验报告 七.实验过程 1.创建Verilog文件,写代码 2.波形仿真 …...
从windows iso文件中提取install.wim
1、首先从微软官方下载需要的windows镜像 https://www.microsoft.com/zh-cn/software-download/windows10/ 2、在下载的iso文件右键,打开压缩包,在sources文件夹下,应该就可以看到install.wim了。但似乎在最新的win10版本,微软采…...
Python的flask网页编程的GET和POST方法的区别
关于flask网页编程的GET及POST方法之间存在哪些区别问题,我们主要从以下六个关键点予以详细阐述: 首先需要明确的是,GET与POST两种不同类型的HTTP方法所采用的请求模式有所差别。其中,GET方法采用的是基于URL请求的机制ÿ…...
15 # 手写 throttle 节流方法
什么是节流 节流是限制事件触发的频率,当持续触发事件时,在一定时间内只执行一次事件,这个效果跟英雄联盟里的闪现技能释放差不多。 函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次…...
puzzle(1612)拼单词、wordlegame
目录 拼单词 wordlegame 拼单词 在线play 找出尽可能多的单词。 如果相邻的话(在任何方向上),你可以拖拽鼠标从一个字母(方格)到另一个字母(方格)。在一个单词中,你不能多次使用…...
【解决方案】pytion 运行时提示 import psutil ModuleNotFoundError: No module named ‘psutil‘
报错原因分析 import psutil ModuleNotFoundError: No module named psutil报错原因分析 当前环境pytion中缺少了psutil包,使用pip命令进行安装 解决方案 pip install psutil...
CSS3 过度效果、动画、多列
一、CSS3过度: CSS3过渡是元素从一种样式逐渐改变为另一种的效果。要实现这一点,必须规定两相内容:指定要添加效果的CSS属性;指定效果的持续时间。如果为指定持续时间,transition将没有任何效果。 <style> div…...
java使用geotools解析矢量数据kml、geojson、shp文件
geotools解析kml、geojson geotools环境准备公共获取属性方法解析kml解析geojson解析shp geotools环境准备 这里使用的是maven引用geotools包,引用geotools包需要添加maven仓库,pom.xml文件如下: <properties><!-- geotools版本 -…...
原生 JS DOM 常用操作大全
DOM DOM文档对象模型 又称为DOM树 DOM树 由文档、元素、节点 组成文档:一个页面就是一个文档,元素:文档中的所有标签都称为元素。DOM中使用Element表示节点:文档中的所有内容,在文档中都是节点(标签、属性…...
昇腾CANN 7.0 黑科技:DVPP硬件加速训练数据预处理,友好解决Host CPU预处理瓶颈
在NPU/GPU上进行模型训练计算,为了充分使用计算资源,一般采用批量数据处理方式,因此一般情况下为提升整体吞吐率,batch值会设置的比较大,常见的batch数为256/512,这样一来,对数据预处理处理速度…...
Aria2 任意文件写入漏洞复现
漏洞描述 Aria2 是一款轻量级、多协议、多源下载工具(支持 HTTP/HTTPS、FTP、BitTorrent、Metalink),内置 XML-RPC 和 JSON-RPC 接口。 我们可以使用 RPC 接口来操作 aria2 并将文件下载到任意目录,从而造成任意文件写入漏洞。 …...
思维模型 多看效应
本系列文章 主要是 分享 思维模型,涉及各个领域,重在提升认知。越熟悉,越喜欢。 1 多看效应的应用 1.1 多看效应在广告和营销领域的应用 1 可口可乐之歌 可口可乐公司在 20 世纪 60 年代推出了“可口可乐之歌”广告,这个广告通…...
持续集成交付CICD:Jenkins Pipeline与远程构建触发器
目录 一、实验 1.Jenkins Pipeline本地构建触发器 2.Jenkins Pipeline与远程构建触发器(第一种方式) 3.Jenkins Pipeline与远程构建触发器(第二种方式) 4.Jenkins Pipeline与远程构建触发器(第三种方式࿰…...
【无标题(PC+WAP)花卉租赁盆栽绿植类pbootcms站模板
(PCWAP)花卉租赁盆栽绿植类pbootcms网站模板 PbootCMS内核开发的网站模板,该模板适用于盆栽绿植网站等企业,当然其他行业也可以做,只需要把文字图片换成其他行业的即可; PCWAP,同一个后台,数据即时同步&…...
在软件开发中正确使用MySQL日期时间类型的深度解析
在日常软件开发场景中,时间信息的存储是底层且核心的需求。从金融交易的精确记账时间、用户操作的行为日志,到供应链系统的物流节点时间戳,时间数据的准确性直接决定业务逻辑的可靠性。MySQL作为主流关系型数据库,其日期时间类型的…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
蓝桥杯 2024 15届国赛 A组 儿童节快乐
P10576 [蓝桥杯 2024 国 A] 儿童节快乐 题目描述 五彩斑斓的气球在蓝天下悠然飘荡,轻快的音乐在耳边持续回荡,小朋友们手牵着手一同畅快欢笑。在这样一片安乐祥和的氛围下,六一来了。 今天是六一儿童节,小蓝老师为了让大家在节…...
Qt Http Server模块功能及架构
Qt Http Server 是 Qt 6.0 中引入的一个新模块,它提供了一个轻量级的 HTTP 服务器实现,主要用于构建基于 HTTP 的应用程序和服务。 功能介绍: 主要功能 HTTP服务器功能: 支持 HTTP/1.1 协议 简单的请求/响应处理模型 支持 GET…...
Module Federation 和 Native Federation 的比较
前言 Module Federation 是 Webpack 5 引入的微前端架构方案,允许不同独立构建的应用在运行时动态共享模块。 Native Federation 是 Angular 官方基于 Module Federation 理念实现的专为 Angular 优化的微前端方案。 概念解析 Module Federation (模块联邦) Modul…...
微服务商城-商品微服务
数据表 CREATE TABLE product (id bigint(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 商品id,cateid smallint(6) UNSIGNED NOT NULL DEFAULT 0 COMMENT 类别Id,name varchar(100) NOT NULL DEFAULT COMMENT 商品名称,subtitle varchar(200) NOT NULL DEFAULT COMMENT 商…...
12.找到字符串中所有字母异位词
🧠 题目解析 题目描述: 给定两个字符串 s 和 p,找出 s 中所有 p 的字母异位词的起始索引。 返回的答案以数组形式表示。 字母异位词定义: 若两个字符串包含的字符种类和出现次数完全相同,顺序无所谓,则互为…...
零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...
安卓基础(aar)
重新设置java21的环境,临时设置 $env:JAVA_HOME "D:\Android Studio\jbr" 查看当前环境变量 JAVA_HOME 的值 echo $env:JAVA_HOME 构建ARR文件 ./gradlew :private-lib:assembleRelease 目录是这样的: MyApp/ ├── app/ …...
【Go语言基础【12】】指针:声明、取地址、解引用
文章目录 零、概述:指针 vs. 引用(类比其他语言)一、指针基础概念二、指针声明与初始化三、指针操作符1. &:取地址(拿到内存地址)2. *:解引用(拿到值) 四、空指针&am…...
