【SpringBoot】手写模拟SpringBoot核心流程

依赖包

新建一个工程,包含两个 module:

springboot 模块,表示 springboot 源码实现;
user 模块,表示业务系统,使用 springboot 模块;

依赖包:Spring、SpringMVC、Tomcat 等,引入依赖如下:

<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 模块下引入依赖:

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

最终希望通过启动 MyApplication 的 main 方法,启动项目,能访问到 UserController。

核心注解和核心类

SpringBoot 的核心类和注解:

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

所以,自定义类和注解以实现上面的功能。
@FireSpringBootApplication 注解:

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

FireSpringApplication 启动类:

public class FireSpringApplication {
    public static void run(Class clazz){
    }
}

在 MyApplication 中使用:

@FireSpringBootApplication
public class MyApplication {

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

run 方法

需要在 run 方法中启动 tomcat,通过 tomcat 接收请求;
DispatchServlet 绑定 spring 容器,DispatchServlet 接收到请求后需要在 spring 容器中找到一个 controller 中对应的方法;

run 方法中需要实现的逻辑:

  1. 创建一个 Spring 容器
  2. 创建 Tomcat 对象
  3. 生成 DispatcherServlet 对象,并且和前面创建出来的 Spring 容器进行绑定
  4. 将 DispatcherServlet 添加到 Tomcat 中
  5. 启动 Tomcat

创建 Spring 容器

public class FireSpringApplication {

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

run 方法中传入的即使 MyApplication 类,被解析为 Spring 容器的配置类;
默认会将 MyApplication 所在的包作为扫描路径,从而扫描到 UserController 和 UserService,所以在 spring 容器启动后就会存在两个 bean 了;

启动 Tomcat

使用内嵌的 Tomact,即 Embed-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);
    // 添加DispatcherServlet,并且绑定一个Spring容器
    tomcat.addServlet(contextPath, "dispatcher", new DispatcherServlet(applicationContext));
	// 设置Mapping关系
    context.addServletMappingDecoded("/*", "dispatcher");
    
    try {
        tomcat.start();
    } catch (LifecycleException e) {
        e.printStackTrace();
    }
    
}

在 run 方法中调用 startTomcat 方法启动 tomcat:

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

到此,一个简单的 SpringBoot 就写出来了,运行 MyApplication 正常启动项目,通过浏览器就可以访问 UserController 了。

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

Tomcat 实现类:

public class TomcatWebServer implements WebServer{

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

Jetty 实现类:

public class JettyWebServer implements WebServer{

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

在 FireSpringApplication 中的 run 方法中,去获取对应的 WebServer,然后启动对应的 webServer。
代码如下:

public static void run(Class clazz){
    AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
    applicationContext.register(clazz);
    applicationContext.refresh();
    // 自动获取配置的Tomcat或者Jetty容器
    WebServer webServer = getWebServer(applicationContext);
    webServer.start();
    
}

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

模拟实现条件注解

首先实现一个条件注解@FireConditionalOnClass,对应代码如下:

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

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

public class FireOnClassCondition implements Condition {

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

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

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

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

模拟实现自动配置类

配置类代码如下:

@Configuration
public class WebServiceAutoConfiguration {

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

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

表示org.apache.catalina.startup.Tomcat存在,则有 tomcatWebServer 这个bean;
表示org.eclipse.jetty.server.Server存在,则有 jettyWebServer 这个bean;

FireSpringApplication#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 方法

发现自动配置类

WebServiceAutoConfiguration 需要被 SpringBoot 发现,可以通过 SPI 机制实现,比较 JDK 自带的 SPI 来实现。

在 springboot 项目中的 resources 目录下添加目录META-INF/services和文件 org.example.springboot.AutoConfiguration,文件内容为org.example.springboot.WebServiceAutoConfiguration

接口:

public interface AutoConfiguration {
}

WebServiceAutoConfiguration 实现该接口:

@Configuration
public class WebServiceAutoConfiguration implements AutoConfiguration {

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

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

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

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

FireImportSelect:

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

如此,Spring 容器可以装载 WebServiceAutoConfiguration 配置类了,对于 user 模块而言,不需要修改代码就可以自动识别 Tomcat 和 Jetty 了。

总结

到此,实现了一个简单版本的 SpringBoot,因为 SpringBoot 首先是基于 Spring 的,而且提供的功能也更加强大,后面会对这些功能进行更深入的剖析。

你可能感兴趣的:(#,SSM框架,spring,boot,java)