SpringBoot最大的特点是什么大家伙还记得吧?自动配置 + 约定大于配置原则。
在本文中,我自己手写的SpringBoot,我将其称为:ShenSpringBoot
本次手写模拟SpringBoot总共会分为上下两部分。这里是第一部分,在本阶段,需要完成的目标如下:
另外还有一点,关于【手写边界】需要跟大家确认。
5. 这边只是模拟SpringBoot手写一个Web工程,而并非是手写Spring这块
6. 因为是模拟Web请求,所以也会用到SpringMVC的内容,但是SpringMVC的东西显然也不在我们的模拟范围内
shenspringboot
:我的父工程shen-base-springboot
:实现手写SpringBoot的模块。注意这个包名,它不能跟模拟使用示例的报名一样shen-example
:示例模块,用来测试我手写的SpringBoot是否能够工作。注意这个包名,它不能跟手写模拟框架的SpringBoot一样因为是依赖Spring跟SpringMVC嘛,所以,我们肯定要在模拟SpringBoot的pom中加入前两者的依赖。另外我们也知道,SpringBoot它是内置容器的,比如Tomcat,所以,我们也需要在手写模块中添加这些依赖。如下:
注意:这里使用的Spring版本为:5.3.20
1)shen-base-springboot模块的pom.xml
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</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>
2)shen-example的pom.xml
<dependency>
<groupId>org.example</groupId>
<artifactId>shen-base-springboot</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
我们在shen-example
里面简单新增了一个controler
,用于接收一个web请求。
正常,会返回下面的结果给到客户端
显然,现在运行的化,不会,也不应该有任何效果。毕竟我们还没有完成启动类嘛
首先在模拟之前,我们来回忆下,你在项目中是如何启动一个SpringBoot的?应该是两步吧:
@SpringBootApplication
注解,然后在main
方法中使用SpringApplication.run
启动SpringBoot应用当然啦,这个启动配置类在两端都要写点代码,但主要还是在shen-base-springboot
这边。
首先新增一个@ShenSpringBootApplication
注解类
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface ShenSpringBootApplication {
}
注意,相比于原生的,我这边缺少了一个很重要的注解,
@EnableAutoConfiguration
,这个后面的【5.3】会提到
至于为什么注解内容是这样子的,当然是因为原来的@SpringBootApplication
就是这样子的呀
注意上图的一个注解:
@EnableAutoConfiguration
,后面【5.3】会提到
然后呢,还要新增一个单例,像这样的:
如下:
有了以上两者,我们就可以在客户端中使用了。
启动类如下:
@ShenSpringBootApplication
public class MyApplication {
public static void main(String[] args) {
ShenSpringApplication.run(MyApplication.class);
}
}
嘿,是不是有点感觉了。OK,理论上来说,客户端这边已经完成了所有了。按照我们正常使用SpringBoot,当我们点击运行之后,就可以开始我们的web请求了。显然,当我们点击这一步的时候,SpringBoot理应完成了以下两步,才能让整个程序正确的运作:(前面也讲过了)
这么一说,那 ShenSpringApplication.run(MyApplication.class);
应该做什么,大家有点思路了吧。好戏开场了,xdm
注意,该标题下代码都是写在
shen-base-springboot
模块下
注意,该标题下代码都是写在shen-base-springboot
模块下
注意,该标题下代码都是写在shen-base-springboot
模块下
OK,前面说过啊,这里要实现目的如下:
那行,我们就一步一步来呗
启动一个Spring容器,这个不在我们本次的研究范围内,怎么启动一个Spring容器大家直接抄就好了。代码如下:
public class ShenSpringApplication {
public static void run(Class clazz) {
// 步骤一:创建并启动一个Spring容器
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(clazz);
context.refresh();
}
}
为了美观,我通常喜欢把这种语义明确的代码写在一个独立的方法里面(偷偷告诉你们,这个是我看了Spring源码之后,学来的大神们的代码风格)。后面也基本会按照这种方式来写代码。美化后如下:
public class ShenSpringApplication {
public static void run(Class clazz) {
// 步骤一:创建并启动一个Spring容器
ApplicationContext context = startSpring(clazz);
}
/**
* 启动一个Spring容器
*
* @param clazz 配置类
* @return 一个创建完成的Spring ApplicationContext
*/
private static ApplicationContext startSpring(Class clazz) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(clazz);
context.refresh();
return context;
}
}
走到这里,大家多多少少知道,为什么run()
方法要传入一个Class
对象了吧,是的,充当配置类的。为了照顾Spring不太熟悉的朋友,简单说一下。
首先,我们传入的是客户端启动类MyApplication
嘛,然后,我们的MyApplication
不是被一个我们自定义的@ShenSpringBootApplication
修饰吗?然后这个注解呢,他又是存在@ComponentScan
注解的,所以自然,我们Spring容器想要知道怎么扫描,该扫描哪里,就需要程序员告知了。
所以,到此为止,之所以需要传入一个class
,是为了告知Spring容器需要扫描的包路径是什么。SpringBoot默认扫描的路径大家还知道吧?启动类所在的包以及子包嘛。
大家还知道SpringMVC是干什么的吗?O,我想处于SpringBoot时代的我们,对SpringMVC确实比较陌生一点。其实我自己也是,不过我呢,也只是很久没用过了,但是大致的内容跟原理还是懂的。这里给大家一篇文章,快速回忆学习下SpringMVC。
言归正传。SpringMVC是Spring框架中的一个模块,用于实现Java Web应用程序的Model-View-Controller(MVC)设计模式。它的主要作用是将请求和响应分开处理,使应用程序的逻辑更加清晰,提高代码的可维护性和可重用性。
它的运行原理如下:
DispatcherServlet
DispatcherServlet
将请求交给HandlerMapper
做解析,HandlerMapper
将要访问的Controller返回给DispatcherServlet
DispatcherServlet
将用户的请求发送给指定的Controller做业务处理,当业务处理完后,SpringMVC将需要传递的数据和跳转的视图名称封装为一个ModelAndView
对象,将ModelAndView
对象发送给DispatcherServlet
DispatcherServlet
从ModelAndView
对象里取出视图名称,交给视图解析器做解析,视图解析器中配置页面路径中的后缀和前缀,解析之后将要跳转的页面反馈给DispatcherServlet
DispatcherServlet
将数据发送给页面通过Response对象响应给用户SpringMVC,主要把上面出现的几个英文名词(核心组件)搞清楚大概也差不多了
OK,我为什么要贴一段简单的SpringMVC运行原理呢,哈,主要是告诉大家一点:SpringMVC运行之前,Tomcat容器应该先被启动,再然后才会去加载SpringMVC核心组件DispatcherServlet
。
所以,步骤二的代码其实跟Tomcat是融合在一起的,它们一起才是真正的步骤二。
我们这里使用的是内嵌的Tomcat版本,模拟SpringBoot嘛,而对于启动内嵌的Tomcat,这并不是我们本篇笔记在意的内容,所以直接网上抄一个就好了。
代码如下:
public class ShenSpringApplication {
public static void run(Class clazz) {
// 步骤一:创建并启动一个Spring容器
ApplicationContext context = startSpring(clazz);
// 步骤二:启动tomcat容器,并且初始化SpingMVC的核心组件
startTomcat(context);
}
/**
* 启动一个Spring容器
*
* @param clazz 配置类
* @return 一个创建完成的Spring ApplicationContext
*/
private static ApplicationContext startSpring(Class clazz) {
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(clazz);
context.refresh();
return context;
}
/**
* 启动tomcat容器,并且初始化SpingMVC的核心组件
*
* @param applicationContext Spring应用上下文
*/
private static void startTomcat(ApplicationContext 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);
// 初始化SpringMVC组件
tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet((AnnotationConfigWebApplicationContext) applicationContext));
context.addServletMappingDecoded("/*", "dispatcher");
try {
tomcat.start();
} catch (LifecycleException e) {
e.printStackTrace();
}
}
}
注意哦,我这边tomcat
绑定的接口改为了8081
,安全点嘛。至于其他代码为什么要这么写,啊,因为tomcat跟SpringMVC就是这么写的啊,我们也不关心啊。OK,到了这里,基本上一个简单的【模拟SpringBoot的Web工程】已经能提供简单的服务了。我们运行试试看效果。
点击运行后,首先出来的是tomcat的日志,没毛病。接着我们访问一下我们的接口看看,在浏览器输入http://localhost:8081/test试试看效果
输出结果如上,预期是:手写Springboot
。哈,乱码了。不过无所谓,反正手写模拟SpringBoot成功了。
到了这里,虽然基本的ShenSpringBoot跑起来了,但是,我们原生的SpringBoot的功能远不止如此。就比如,原生SpringBoot支持多种内置容器,有:Jetty、Undertown。然后默认是Tomcat这样子。在原有的SpringBoot中想要切换Jetty容器,只需要在pom.xml中的springboot-starter-web
依赖中排除tomcat
配置,然后添加jetty
的依赖就可以了。如下:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
<version>2.7.0version>
<exclusions>
<exclusion>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-tomcatartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-jettyartifactId>
<version>2.7.0version>
dependency>
那我现在我也想升级改造为支持自动切换的话,那也没啥问题,抄一下咯。整体来说思路是这样的:
如何判断是否有对应的依赖?告诉大家一个方法,也是SpringBoot自动配置的思路。那就是使用类加载器load一下一个jar包比较有代表性的class。比如:
classloader.loadClass("org.example.tomcat")
,通常能load成功即可,出现异常则肯定是不存在依赖的。
注意啊,从这里开始其实会有点绕。
因为,这个逻辑是SpringBoot自动帮我们实现的,对于程序员而言,只要在pom中添加相关的依赖就可以了,想用什么容器都是用户自己的选择。
另外,我想大家通过上面的代码也发现了,所谓容器,不管是tomcat还是jetty,都是Servlet容器而已,所以我们可以定义一个接口去约束、表示他们,多态嘛。在设计模式中也叫做【策略模式】。这里就暂时叫做WebServer
吧。
public interface WebServer {
/**
* 启动一个容器
*/
void start();
}
然后Tomcat实现:(注意,理论上这里需要实现Tomcat启动内容的,但是这里为了方便不写了,他也不是我们要关注的内容。我们随便看看效果)
public class TomcatWebServer implements WebServer{
@Override
public void start() {
// 这巴拉巴拉的应该是Tomcat容器启动代码,我们前面也演示过了,这里不重复了
System.out.println("启动一个Tomcat容器");
}
}
Jetty实现:(注意,理论上这里需要实现Jetty启动内容的,但是这里为了方便不写了,他也不是我们要关注的内容。我们随便看看效果)
public class JettyWebServer implements WebServer{
@Override
public void start() {
// 这巴拉巴拉的应该是Jetty容器启动代码
System.out.println("启动一个Jetty容器");
}
}
当然,既然我们引入了Jetty容器的实现,我们也要在手写模拟的shen-base-springboot
的pom
中引入Jetty
的依赖:(虽然我偷懒给了空实现,但是这一步是必要的)
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.46.v20220331</version>
</dependency>
最后,咱也别忘了修改ShenSpringApplication
的run
方法:
public static void run(Class clazz) {
// 步骤一:创建并启动一个Spring容器
ApplicationContext context = startSpring(clazz);
// // 步骤二:启动tomcat容器,并且初始化SpingMVC的核心组件
// startTomcat(context);
// 改进步骤二:获取一个容器,并启动,接着初始化SpringMVC的核心组件
final WebServer webServer = getWebServer();
webServer.start();
}
private static WebServer getWebServer() {
return null;
}
OK,这一步到这里就暂时收住了,剩下的代码就是考虑如何实现这个getWebServer
。
getWebServer
该如何实现呢?从实现思路来说,我觉得简单的可以分为2步:
其实这些都不难理解,大哥们,咱们都是在Spring容器中开发了,当然是在Spring拿啊。所以嘛,我们能在客户端中,通过Spring容器去获取我们的容器,那肯定是因为,在SpringBoot那边,向Spring容器注册了Bean啊。有点拗口,直接上代码大家就懂了:(代码在shen-base-springboot
)
@Configuration
public class WebServerAutoConfiguration{
@Bean
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}
是这样没错吧?哈哈,其实有一点点错误。为什么, 因为正常过来说,这两个容器不会同时存在的,他们的存在都是有条件的,咱前面不是说了吗,需要某个包存在嘛,所以很明显,这里的@Bean
还需要一个类似@ConditionalOnBean
的条件注解。是的,这也正是我们要自定义实现的一个条件注解。怎么实现?抄。
注意,不懂得如何实现一个自定义条件注解的朋友可以自行百度先
主要实现类有:@ShenConditionalOnClass
一个注解类,以及核心的条件实现类ShenOnClassConditional
。后者ShenOnClassConditional
才是真正做判断的地方。代码如下:(代码在shen-base-springboot
)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(ShenOnClassConditional.class)
public @interface ShenConditionalOnClass {
String value() default "";
}
ShenOnClassConditional
实现了Condition
接口的类,它才是真正得条件逻辑处理类哦。
public class ShenOnClassConditional implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
Map<String, Object> annotationAttributes = metadata.getAnnotationAttributes(ShenConditionalOnClass.class.getName());
String className = (String) annotationAttributes.get("value");
try {
context.getClassLoader().loadClass(className);
return true;
} catch (ClassNotFoundException e) {
// 找不到需要加载的类,会抛异常:ClassNotFoundException
// 既然找不到,所以返回false
return false;
}
}
}
具体逻辑为,拿到@ShenConditionalOnClass
中的value属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,类加载会抛出异常ClassNotFoundException
,则表示不符合条件。
那既然条件注解也实现了,接下来就是在WebServerConfig
使用他们就好了,修改后的代码如下:
@Configuration
public class WebServerAutoConfiguration {
/**
* Tomcat容器典型加载类
*/
private static final String TOMCAT_PATH = "org.apache.catalina.startup.Tomcat";
/**
* Jetty容器典型加载累
*/
private static final String JETTY_PATH = "org.eclipse.jetty.server.Server";
@Bean
@ShenConditionalOnClass(TOMCAT_PATH)
public TomcatWebServer tomcatWebServer() {
return new TomcatWebServer();
}
@Bean
@ShenConditionalOnClass(JETTY_PATH)
public JettyWebServer jettyWebServer() {
return new JettyWebServer();
}
}
OK,到这里一个简单的容器自动配置类就完成了。上面这段代码的最终实现语意如下:
org.apache.catalina.startup.Tomcat
类,那么我们手写模拟的ShenSpringBoot才会向Spring容器中注册TomcatWebServer这个Bean2.org.eclipse.jetty.server.Server
类,那么我们手写模拟的ShenSpringBoot才会向Spring容器中注册JettyWebServer这个Bean那最后,就是在getWebServer填上这样的逻辑咯
/**
* 从当前Spring容器中获取一个Servlet容器
*
* @param applicationContext Spring应用上下文
*/
private static WebServer getWebServer(ApplicationContext applicationContext) {
// key为beanName, value为Bean对象
Map<String, WebServer> webServers = applicationContext.getBeansOfType(WebServer.class);
if (webServers.isEmpty()) {
System.err.println("找不到可用的Web容器");
throw new NullPointerException();
}
if (webServers.size() > 1) {
System.err.println("找到了多个容器");
throw new IllegalStateException();
}
// 返回唯一的一个
return webServers.values().stream().findFirst().get();
}
这样整体SpringBoot启动逻辑就是这样的:
那朋友们,这样就OK了吗?测试一下呗:
诶?空指针?找不到可用的Web容器?点解啊?在shen-base-springboot
模块中不是依赖了Tomcat
跟Jetty
了吗?还做了自动配置。
对啊,你是在shen-base-springboot
做了自动配置,可童鞋们,它的目录是啥:shen.springframework
哦,而你现在的扫描路径是啥?com.shen.example
,都扫描不到了,给你注册个锤锤哦。
是的,这也是在SpringBoot中一样会遇到的问题,他们是怎么解决的呢?当然原理肯定是把这些包也加入到扫描包里面,而SpringBoot则是通过自定义的,一个叫做SPI(Service Provider Interface)
的服务发现
机制,来扫描注册一些基础的服务Bean。
说SPI可能大家比较陌生,其实就是我们比较熟知的
spring.factories
想要模拟的话,其实最好还是了解一下SPI
机制比较合适,但说实在,想要真的深入去了解一下的话,单单是这个内容都可以写一篇文章出来了。所以我比较建议,大家直接去看这篇文章《SpringBoot(二):springboot自动装配之SPI机制》。
不过对这个知识点不是很感兴趣的朋友,可以看看我这里的简单解释版本。
我们之所以聊到了SPI这个东西,是因为,我们在前面的推导中遇到了一个问题,那就是:
com.shen.example
,怎么去扫描位于手写模拟ShenSpringBoot
下shen.springframework
下的bean呢?为什么要重申这个现象,主要是提醒大家,需要SPI的场景,一个简单的现象是:它们通常都发生在两个独立的jar
包,或者说包路径上。它们彼此之间是第三方关系。理解这点非常非常重要!
那我再问大家一个问题?什么是API?它与SPI有什么区别?(对的,他们之间有联系的哦)
API即程序应用接口,它跟SPI一样,核心内容都包含接口。它和SPI最明显的区别是:API的接口与实现类存在于实现方;SPI是接口与实现类脱离,不在同一方。有点绕,由下图所示:
所以情况就是这么个情况,想要弄明白这个情况的话,还是要好好思考一下这个情况。哈哈哈
由于是别人实现的,所以,Java提供了一个机制去发现别人的实现,这就是SPI。
在Spring里面,也需要解决类似的问题:由于是别人声明的Bean,所以我需要一种机制去让它们的Bean也注册到我们的Spring容器中。
简单来说,它是这么实现的:
它们的实现,其实就是我在【2.1 启动配置类】少写的注解@EnableAutoConfiguration
里面!而 @EnableAutoConfiguration
集成了@Import
注解,这个注解对于springboot非常重要!
具体的实现我就不再继续讲了,确实知识点挺深的。感兴趣的同学自己去继续研究下吧。
感谢百度大佬【作者:一页一】的文章《SpringBoot(二):springboot自动装配之SPI机制》