• 【springboot】手写SpringBoot核心流程


    通过手写模拟实现一个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.springframeworkgroupId>
    	<artifactId>spring-webmvcartifactId>
    	<version>5.2.14.RELEASEversion>
    dependency>
    
    <dependency>
    	<groupId>javax.servletgroupId>
    	<artifactId>javax.servlet-apiartifactId>
    	<version>4.0.1version>
    dependency>
    
    <dependency>
    	<groupId>org.apache.tomcat.embedgroupId>
    	<artifactId>tomcat-embed-coreartifactId>
    	<version>9.0.60version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

    <dependency>
    	<groupId>com.morrisgroupId>
    	<artifactId>spring-bootartifactId>
    	<version>1.0-SNAPSHOTversion>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    业务代码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 {
    
        @Resource
        private UserService userService;
    
        @GetMapping("test")
        public String test() {
            return userService.test();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    UserService.java

    package com.morris.user.service;
    
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserService {
    
        public String test() {
            return "morris";
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    因为我们模拟实现的是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 "";
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

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

    public class SpringApplication {
    
        public static void run(Class clazz){
    		// TODO
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    注意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);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    现在用来是有模有样了,但中看不中用,所以我们要来好好实现以下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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    我们创建的是一个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) {
    	// 启动tomcat
    	System.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", new
    					  DispatcherServlet(applicationContext));
    
    	context.addServletMappingDecoded("/*", "dispatcher");
    
    	try {
    		tomcat.start();
    	} catch (LifecycleException e) {
    		e.printStackTrace();
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    代码虽然看上去比较多,但是逻辑并不复杂,比如配置了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);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    实际上代码写到这,一个极度精简版的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);
    }
    
    • 1
    • 2
    • 3

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

    public class TomcatWebServer implements WebServer{
    
        @Override
        public void start(WebApplicationContext applicationContext) {
            System.out.println("启动Tomcat");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    public class JettyWebServer implements WebServer{
    
        @Override
        public void start(WebApplicationContext applicationContext) {
           System.out.println("启动Jetty");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    而在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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

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

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

    模拟实现条件注解

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

    @Target({ ElementType.TYPE, ElementType.METHOD })
    @Retention(RetentionPolicy.RUNTIME)
    @Conditional(OnClassCondition.class)
    public @interface ConditionalOnClass {
        String value() default "";
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    public class OnClassCondition implements Condition {
    
        @Override
        public 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;
            }
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    具体逻辑为,拿到@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();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这个代码还是比较简单的,通过一个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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    这样整体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 {
    }
    
    • 1
    • 2

    WebServerAutoConfiguration需要实现AutoConfiguration接口。

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

    com.morris.springboot.WebServerAutoConfiguration
    
    • 1

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

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

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Configuration
    @ComponentScan
    @Import(AutoConfigurationSelector.class)
    public @interface MySpringBootApplication {
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    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 {
        @Override
        public 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]);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

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

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

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

    <dependency>
    	<groupId>com.morrisgroupId>
    	<artifactId>spring-bootartifactId>
    	<version>1.0-SNAPSHOTversion>
    dependency>
    
    <dependency>
    	<groupId>org.eclipse.jettygroupId>
    	<artifactId>jetty-serverartifactId>
    	<version>9.4.43.v20210629version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    那么启动MyApplication就会报错。

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

    <dependency>
    	<groupId>com.morrisgroupId>
    	<artifactId>spring-bootartifactId>
    	<version>1.0-SNAPSHOTversion>
    	<exclusions>
    		<exclusion>
    			<groupId>org.apache.tomcat.embedgroupId>
    			<artifactId>tomcat-embed-coreartifactId>
    		exclusion>
    	exclusions>
    dependency>
    
    <dependency>
    	<groupId>org.eclipse.jettygroupId>
    	<artifactId>jetty-serverartifactId>
    	<version>9.4.43.v20210629version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

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

  • 相关阅读:
    TikTok shop美国小店适合哪些人做?附常见运营问题解答
    docker 安装 onlyoffice
    python04- 函数、time
    刷题之完全二叉树的权值和小字辈及根据后序和中序遍历输出先序遍历
    Oracle-PDB资源限制
    关于原型链
    【预测模型-DELM分类】基于风驱动算法改进深度学习极限学习机实现数据分类附matlab代码
    在这个基础上再点缀一下让他焕然一新,该怎么点缀,比如让流星有闪烁的光芒
    9.1、面向对象编程
    pysimpleGui 使用之sg.SaveAs使用
  • 原文地址:https://blog.csdn.net/u022812849/article/details/126698122