前言
通过手写模拟一个简单Spring Boot框架,来熟悉Spring Boot大概是如何工作的。
分为以下4个方面
1、手写模拟SpringBoot启动过程
2、手写模拟SpringBoot条件注解功能
3、手写模拟SpringBoot自动配置功能
4、SpringBoot整合Tomcat底层源码分析
废话不多说,直接上代码
实战
依赖
建一个工程,两个Module:
- springboot模块,表示springboot框架的源码实现
- user包,表示用户业务系统,用来写业务代码来测试我们所模拟出来的SpringBoot
首先,SpringBoot是基于的Spring,所以要依赖Spring,然后我希望模拟出来的SpringBoot也支持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>
在User模块下我们进行正常的开发就行了,比如先添加SpringBoot依赖:
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
然后定义相关的Controller和Service:
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("test")
public String test(){
return userService.test();
}
}
因为模拟实现的是SpringBoot,而不是SpringMVC,所以直接在user包下定义了UserController和UserService,最终希望能运行MyApplication中的main方法,就直接启动了项目,并能在浏览器中正常的访问到UserController中的某个方法。
核心注解和核心类
在真正使用SpringBoot时,核心会用到SpringBoot一个类和注解:
- @SpringBootApplication,这个注解是加在应用启动类上的,也就是main方法所在的类
- SpringApplication,这个类中有个run()方法,用来启动SpringBoot应用的
所以我也来模拟实现他们。
一个@GuashuSpringBootApplication注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface GuashuSpringBootApplication {
}
一个用来实现启动逻辑的GuashuSpringApplication类。
public class GuashuSpringApplication {
public static void run(Class clazz){
}
}
注意run方法需要接收一个Class类型的参数,这个class是用来干嘛的,等会就知道了。
有了以上两者,我们就可以在MyApplication中来使用了,比如:
@GuashuSpringBootApplication
public class MyApplication {
public static void main(String[] args) {
GuashupringApplication.run(MyApplication.class);
}
}
现在用来是有模有样了,但中看不中用,所以我们要来好好实现以下run方法中的逻辑了。
run方法
首先,我希望run方法一旦执行完,就能在浏览器中访问到UserController,那势必在run方法中要启动Tomcat,通过Tomcat就能接收到请求了。
Spring MVC的底层原理有一个Servlet非常核心,那就是DispatcherServlet,这个DispatcherServlet需要绑定一个Spring容器,因为DispatcherServlet接收到请求后,就会从所绑定的Spring容器中找到所匹配的Controller,并执行所匹配的方法。
所以,在run方法中,我们要实现的逻辑如下:
- 创建一个Spring容器
- 创建Tomcat对象
- 生成DispatcherServlet对象,并且和前面创建出来的Spring容器进行绑定
- 将DispatcherServlet添加到Tomcat中
- 启动Tomcat
创建Spring容器
这个步骤比较简单,代码如下:
public class GuashuSpringApplication {
public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
}
}
我创建的是一个AnnotationConfigWebApplicationContext容器,并且把run方法传入进来的class作为容器的配置类,比如在MyApplication的run方法中,我就是把MyApplication.class传入到了run方法中,最终MyApplication就是所创建出来的Spring容器的配置类,并且由于MyApplication类上有@GuashuSpringBootApplication注解,而@GuashuSpringBootApplication注解的定义上又存在@ComponentScan注解,所以AnnotationConfigWebApplicationContext容器在执行refresh时,就会解析MyApplication这个配置类,从而发现定义了@ComponentScan注解,也就知道了要进行扫描,只不过扫描路径为空,而AnnotationConfigWebApplicationContext容器会处理这种情况,如果扫描路径会空,则会将MyApplication所在的包路径做为扫描路径,从而就会扫描到UserService和UserController。
所以Spring容器创建完之后,容器内部就拥有了UserService和UserController这两个Bean。
启动Tomcat
我们用的是Embed-Tomcat,也就是内嵌的Tomcat,真正的SpringBoot中也用的是内嵌的Tomcat,而对于启动内嵌的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);
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
代码虽然看上去比较多,但是逻辑并不复杂,比如配置了Tomcat绑定的端口为8081,后面向当前Tomcat中添加了DispatcherServlet,并设置了一个Mapping关系,最后启动,其他代码则不用太过关心。
而且在构造DispatcherServlet对象时,传入了一个ApplicationContext对象,也就是一个Spring容器,就是前文说的,DispatcherServlet对象和一个Spring容器进行绑定。
接下来,只需要在run方法中,调用startTomcat即可:
public static void run(Class clazz){
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
applicationContext.register(clazz);
applicationContext.refresh();
startTomcat(applicationContext);
}
实际上代码写到这,一个极度精简版的SpringBoot就写出来了,比如现在运行MyApplication,就能正常的启动项目,并能接收请求。
启动能看到Tomcat的启动日志:
然后在浏览器上访问:http://localhost:8081/test
也能正常的看到结果:
完成!!!