1、手写模拟SpringBoot启动过程

创建工程,两个Module

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

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>

User模块定义controller和servic;

@RestController
public class UserController {

    @Autowired
    private UserService userService;

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

    public String test(){
        return "heiyu";
    }
}

模拟springboot的启动流程,希望能运行MyApplication中的main方法,就直接启动了项目,并能在浏览器中正常的访问到UserController中的某个方法。

核心注解

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

  1. @SpringBootApplication,这个注解是加在应用启动类上的,也就是main方法所在的类
  2. SpringApplication,这个类中有个run()方法,用来启动SpringBoot应用的

模拟实现:

/**
 * @Description 模拟springboot启动类的注解
 * @Author hei.yu
 * @Date 2022-11-02 17:49
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface HeiyuSpringBootApplication {
}

一个用来实现启动逻辑的HeiyuSpringApplication类:

public class HeiyuSpringApplication {

    public static void run(Class clazz){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(clazz);
        context.refresh();

    }
}

MyApplication的代码

@HeiyuSpringBootApplication
public class MyApplication {

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

实现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

public class HeiyuSpringApplication {

    public static void run(Class clazz){
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
        context.register(clazz);
        context.refresh();

    }
}

项目运行,我们创建的是一个AnnotationConfigWebApplicationContext容器,并且把run方法传入进来的class作为容器的配置类,比如在MyApplication的run方法中,我们就是把MyApplication.class传入到了run方法中,最终MyApplication就是所创建出来的Spring容器的配置类,并且由于MyApplication类上有@HeiyuSpringBootApplication注解,而@HeiyuSpringBootApplication注解的定义上又存在@ComponentScan注解,所以AnnotationConfigWebApplicationContext容器在执行refresh时,就会解析MyApplication这个配置类,从而发现定义了@ComponentScan注解,也就知道了要进行扫描,只不过扫描路径为空,而AnnotationConfigWebApplicationContext容器会处理这种情况,如果扫描路径会空,则会将MyApplication所在的包路径做为扫描路径,从而就会扫描到UserService和UserController。

所以Spring容器创建完之后,容器内部就拥有了UserService和UserController这两个Bean。

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

}

在run方法中运行startTomcat():

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

startTomcat(applicationContext);

}

运行结果如图:

spring mock模拟server_spring mock模拟server

2、手写模拟SpringBoot条件注解功能

实现Tomcat和Jetty的切换

我们前面代码中默认启动的是Tomcat,那我现在想改成这样子:

1、如果项目中有Tomcat的依赖,那就启动Tomcat
2、如果项目中有Jetty的依赖就启动Jetty
3、如果两者都没有则报错
4、如果两者都有也报错

这个逻辑希望SpringBoot自动帮我实现,对于程序员用户而言,只要在Pom文件中添加相关依赖就可以了,想用Tomcat就加Tomcat依赖,想用Jetty就加Jetty依赖。

我们知道,不管是Tomcat还是Jetty,它们都是应用服务器,或者是Servlet容器,所以我们可以定义接口来表示它们,这个接口叫做WebServer(别问我为什么叫这个,因为真正的SpringBoot源码中也叫这个)。

定义接口:

public interface WebServer {
    
    public void start();

}

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

public class JettyWebServer implements WebServer{

    @Override
    public void start() {
       System.out.println("启动Tomcat");
    }
}
public class TomcatWebServer implements WebServer{

    @Override
    public void start() {
        System.out.println("启动Jetty");
    }
}

而在HeiyuSpringApplication中的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;
}

模拟实现条件注解

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

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

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

public class HeiyuOnClassCondition implements Condition {

    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        Map<String, Object> annotationAttributes = 
metadata.getAnnotationAttributes(ZhouyuConditionalOnClass.class.getName());

        String className = (String) annotationAttributes.get("value");

        try {
            context.getClassLoader().loadClass(className);
            return true;
        } catch (ClassNotFoundException e) {
            return false;
        }
    }
}

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

模拟实现自动配置类

@Configuration
public class WebServiceAutoConfiguration {

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

    @Bean
    @HeiyuConditionalOnClass("org.eclipse.jetty.server.Server")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }
}

在HeiyuSpringApplication中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方法

有了以上步骤,我们还差了一个关键步骤,就是Spring要能解析到WebServiceAutoConfiguration这个自动配置类,因为不管这个类里写了什么代码,Spring不去解析它,那都是没用的,此时我们需要SpringBoot在run方法中,能找到WebServiceAutoConfiguration这个配置类并添加到Spring容器中。

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

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

3、手写模拟SpringBoot自动配置功能

发现自动配置类

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

spring mock模拟server_tomcat_02


定义一个接口

public interface AutoConfiguration {
}

并且WebServiceAutoConfiguration实现该接口:

@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {

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

    @Bean
    @HeiyuConditionalOnClass("org.eclipse.jetty.server.jetty")
    public JettyWebServer jettyWebServer(){
        return new JettyWebServer();
    }
}

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

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

HeiyuImportSelect类为:

public class HeiyuImportSelect 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]);
    }
}

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

spring mock模拟server_spring_03