学习SpringBoot源码之手写一个简易版SpringBoot

很多人和我一样可能都想知道我们开发用的SpringB底层原理是什么,怎么运行的,但奈何自己能力不足读不懂源码,下面我们通过手写一个SpringBoot来了解一下SpringBoot大概原理是什么,怎么运行的。(此文是学习图灵周瑜老师手写SpringBoot的学习笔记,老师笔记原文链接:https://www.yuque.com/renyong-jmovm/pi8gi5/aaz9l4)。

PS: 下文中Xjx为本人名字缩写,读者可以自行更改为自己的名字缩写,这样整个写完还是挺有成就感的,建议改为自己名字缩写哦

首先我们创建出一个项目,然后再这个项目下面创建两个模块:

image-20221206092748852

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

首先,SpringBoot是基于的Spring,所以我们手写SpringBoot的这个项目要依赖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..1</version>
	</dependency>
	<dependency>
		<groupId>org.apache.tomcat.embed</groupId>
		<artifactId>tomcat-embed-core</artifactId>
		<version>9..60</version>
	</dependency>
</dependencies>

然后我们在User模块下就可以进行正常的测试我们手写的SpringBoot了,但是需要先添加我们手写的SpringBoot的依赖:

<dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>springboot</artifactId>
            <version>1.0-SNAPSHOT</version>  
        </dependency>
</dependencies>

然后我们在User模块下定义相关的Controller和Service,它们用来对我们写的SpringBoot进行功能测试。

image-20221206095403669

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

@RestController
public class UserController {

    @Autowired
    private UserService userService;

    @GetMapping("test")
    public String test(){
        return userService.test();
    }
}

这里准备工作和测试类我们就完成了,下面开始我们SpringBoot的手写工作

核心注解和核心类

还记得我们是怎么使用SpringBoot的吗?

  1. 创建启动类,写出main方法
  2. 在启动类的上面加上@SpringBootApplication这个注解
  3. 在main方法中调用SpringApplic类的run方法,把启动类传进去,然后就可以执行启动类来启动SpringBoot了。

这里我们需要注意@SpringBootApplication这个注解并不是一定要加在启动类上的,这个注解只是表明这个类需要在SpringBoot启动时需要被加载。

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

  1. @SpringBootApplication,这个注解是加在应用启动类上的,也就是main方法所在的类

  2. SpringApplication,这个类中有个run()方法,用来启动SpringBoot应用的

所以我们需要首先来手写代码实现它们。

一个@XjxSpringBootApplication注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface XjxSpringBootApplication {
}

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

public class ZhouyuSpringApplication {
    public static void run(Class clazz){

    }
}

注意这里run方法需要接收一个Class类型的参数(和我们以前使用SpringBoot一样,通常是传入启动类)

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

@XjxSpringBootApplication
public class MyApplication {
    public static void main(String[] args) {
        XjxSpringApplication.run(MyApplication.class);
    }
}

然后我们就可以运行这个启动类了,虽然可以运行成功,但是只是一个空壳子,因为run方法中并没有执行任何代码,所以我们要来好好实现一下run方法中的逻辑了。

run方法

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

首先,我们希望run方法一旦执行完,我们就能在浏览器中访问到UserController,那势必在run方法中要启动Tomcat,通过Tomcat就能接收到请求了。这里我们模拟的就是SpringBoot启动时默认会启动一个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 XjxSpringApplication {
    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类上有@XjxSpringBootApplication注解,而@XjxSpringBootApplication注解的定义上又存在@ComponentScan注解,所以AnnotationConfigWebApplicationContext容器在执行refresh时,就会解析MyApplication这个配置类,从而进一步发现定义了@ComponentScan注解,也就知道了要进行扫描,只不过扫描路径为空,而AnnotationConfigWebApplicationContext容器会处理这种情况,如果扫描路径会空,则会将MyApplication所在的包路径作为扫描路径(扫描类所在包及其子包),从而就会扫描到UserService和UserController,然后就会把这两个类视为bean加载进Spring容器。

所以run方法执行时就会创建Spring容器,创建完成之后容器内部就拥有了UserService和UserController这两个Bean。

启动Tomcat

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

private 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容器进行绑定。这样DispatcherServlet对象接收到请求后,就会从所绑定的Spring容器中找到请求路径所匹配的Controller,并执行所匹配的方法。

接下来,我们只需要在run方法中,调用startTomcat即可(注意这里要把spring容器传进去):

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

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

启动能看到Tomcat的启动日志:

image-20221206110717877

然后在浏览器上访问:http://localhost:8081/test

也能正常的看到结果:

image-20221206110755712

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

实现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();
}

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

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

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

public static WebServer getWebServer(ApplicationContext applicationContext){
	return null;
}

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

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

模拟实现条件注解

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

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

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

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

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

即只有我们导入这个类所在的依赖之后,我们才可以加载出这个类,否则是加载不出来这个类的,那么也就不会得到这个类的对象。
自动配置的原理就是Spring在解析某个自动配置类时,会先检查该自动配置类上是否有条件注解,如果有,则进一步判断该条件注解所指定的条件当前能不能满足,如果满足了则继续解析该配置类,如果不满足则不进行解析了,也就是配置类所定义的Bean都得不到解析,也就是相当于没有这些Bean了。

模拟实现自动配置类

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

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

@Configuration
public class WebServiceAutoConfiguration {

    @Bean
    @XjxConditionalOnClass("org.apache.catalina.startup.Tomcat")
    public TomcatWebServer tomcatWebServer(){
        return new TomcatWebServer();
    }

    @Bean
    @XjxConditionalOnClass("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

并且我们只需要在XjxSpringApplication中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容器中。

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

PS:这里我写的时候包名都是com.xjx,发现直接就可以运行了。原因可能是:

springboot默认扫描启动类同级包和同级的子包内容。这里包括不同模块,只要启动类所在的模块引入了其他所有模块的依赖。那么在编译成jar时,同包会合并,所以这两个模块的的包会合并扫描,因为都是com.xjx名字的包,那么就可以扫描到了。

这里还可能有一些同学和我有同样的疑问,我们不是在XjxSpringBootApplication注解上也加上了@ComponentScan注解吗?

那么扫描这个我们自己写的SpringBoot注解不是应该扫描这个包下的注解吗,其实是不会扫描的。因为我们并没有在这个注解中指定包扫描路径,而SpringBoot源码中虽然也没有指定路径但是在初始化时如果为空,源码中会有相关代码默认扫描启动类所在包的。

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

这里视频中老师是讲的@import导入我们所需要引入的类。但是这样并不好,因为如果我们有多个自动配置类的话,我们不是要引入多次吗?所以下面我们学习SpringBoot中源码的做法,将所有自动配置类写入一个文件中,然后扫描这个文件。

发现自动配置类

为了实现这个功能,以及为了最后的效果演示,我们需要把springboot源码和业务代码源码拆分两个maven模块,也就相当于两个项目,这里我们User模块要依赖我们自己手写的springboot模块。

然后我们只需要在springboot项目中的resources目录下添加如下目录(META-INF/services)和文件:

image-20221206160430607

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

并且提供一个接口:

public interface AutoConfiguration {
}

并且WebServiceAutoConfiguration实现该接口:

@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {

    @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();
    }
}

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

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

XjxImportSelect类为:

public class XjxImportSelect implements DeferredImportSelector {
    @Override
    public 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]);
    }
}

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

此时运行MyApplication,就能看到启动了Tomcat:

image-20221206160458212

因为SpringBoot默认在依赖中添加了Tomcat依赖,而如果在User模块中再添加jetty的依赖:

<dependencies>
	<dependency>
		<groupId>org.example</groupId>
		<artifactId>springboot</artifactId>
		<version>1.0-SNAPSHOT</version>
	</dependency>

	<dependency>
		<groupId>org.eclipse.jetty</groupId>
		<artifactId>jetty-server</artifactId>
		<version>9.4.43.v20210629</version>
	</dependency>
</dependencies>

那么启动MyApplication就会报错:

image-20221206160859203

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

image-20221206161100788

注意:由于没有了Tomcat的依赖,记得把最开始写的startTomcat方法给注释掉,并删除掉相关依赖。

总结

到此,我们实现了一个简单版本的SpringBoot,这里我们简单回顾一下整个手写SpringBoot的流程:

  1. 首先我们创建了一个XjxSpringApplication,这个类的作用是启动SpringBoot,其中有一个run方法,这个方法需要将我们项目的启动类传进去
  2. 创建了一个@XjxSpringBootApplication注解,这个注解是用来加在启动类上面的,即main方法所在的那个类,这个main方法就是用来执行XjxSpringApplication的run方法的,即整个项目的启动。
  3. 我们实现XjxSpringApplication的run方法,这个方法中需要实现的是SpringBoot在启动时需要做的工作,比如绑定Spring容器(创建一个AnnotationConfigWebApplicationContext容器解析启动类),创建出内嵌Tomcat,这都是在run方法中做的。到这里如果我们只需要我们手写的SpringBoot具有web功能的话,那么这里的简易SpringBoot就已经完成了。
  4. 我们想要实现根据依赖自动切换相应的组件该怎么办呢?这时我们使用的就是根据类来完成的(依赖存在则其中的类必然存在)。我们创建一个@XjxConditionOnClass注解,这个注解有一个value属性,这个属性就是用来保存我们要判断依赖中的类是否存在用的,它保存的就是那个类的名字。这个注解上面还有一个@Conditional注解,这个注解中传入的就是真正的条件逻辑(传入一个类)
  5. 下面我们创建XjxOnClassCondition类,这个类实现了Condition接口,然后重写matches方法,然后就可以获取@XjxConditionalOnClass中的value属性(即类名),然后使用类加载器进行加载,能加载就true不能就false。
  6. 这里我们就完成条件注解的编写,下面就可以使用了,把XjxConditionOnClass注解放在我们需要动态切换组件的方法上面,value值就是那个组件对应的依赖中的一个类的名字。
  7. 到这里就基本完成了,最后我们只需要让WebServiceAutoConfiguration这个自动配置类被Spring解析即可,我们可以使用@import导入,当然最好还是使用自动配置文件AutoConfiguration(文件中提供的就是需要Spring加载的自动配置类),然后我们创建一个接口AutoConfiguration让自动配置类WebServiceAutoConfiguration实现这个接口。在@XjxSpringBootApplication启动注解上导入一个类XjxImportSelect。
  8. 最后创建这个类XjxImportSelect这个类需要实现DeferredImportSelector接口,然后实现这个类就可以完成从AutoConfiguration文件中获取自动配置类的名字,并导入到Spring容器中。至此,我们就完成了一个可以启动的并且根据依赖动态切换组件的简易版SpringBoot了。

感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值