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

依赖包

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

springboot 模块,表示 springboot 源码实现;

user 模块,表示业务系统,使用 springboot 模块;

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

java 复制代码
<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 模块下引入依赖:

java 复制代码
<dependencies>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>springboot</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

定义对应的 controller 和 service:

java 复制代码
@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 注解:

java 复制代码
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
public @interface FireSpringBootApplication {
}

FireSpringApplication 启动类:

java 复制代码
public class FireSpringApplication {
    public static void run(Class clazz){
    }
}

在 MyApplication 中使用:

java 复制代码
@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 容器

java 复制代码
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,启动代码如下:

java 复制代码
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:

java 复制代码
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 源码中也叫这个)。

定义接口如下:

java 复制代码
public interface WebServer {
    
    public void start();
    
}

Tomcat 实现类:

java 复制代码
public class TomcatWebServer implements WebServer{

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

Jetty 实现类:

java 复制代码
public class JettyWebServer implements WebServer{

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

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

代码如下:

java 复制代码
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,对应代码如下:

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

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

java 复制代码
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 属性,然后用类加载器进行加载,如果加载到了所指定的这个类,那就表示符合条件,如果加载不到,则表示不符合条件。

模拟实现自动配置类

配置类代码如下:

java 复制代码
@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()方法实现:

java 复制代码
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

接口:

java 复制代码
public interface AutoConfiguration {
}

WebServiceAutoConfiguration 实现该接口:

java 复制代码
@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的定义上增加如下代码:

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

FireImportSelect:

java 复制代码
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 的,而且提供的功能也更加强大,后面会对这些功能进行更深入的剖析。

相关推荐
一只特立独行的猪6111 小时前
Java面试——集合篇
java·开发语言·面试
讓丄帝愛伱2 小时前
spring boot启动报错:so that it conforms to the canonical names requirements
java·spring boot·后端
weixin_586062022 小时前
Spring Boot 入门指南
java·spring boot·后端
雷袭月启2 小时前
SpringBoot实现OAuth客户端
spring boot·oauth客户端
Dola_Pan5 小时前
Linux文件IO(二)-文件操作使用详解
java·linux·服务器
wang_book5 小时前
Gitlab学习(007 gitlab项目操作)
java·运维·git·学习·spring·gitlab
蜗牛^^O^6 小时前
Docker和K8S
java·docker·kubernetes
从心归零6 小时前
sshj使用代理连接服务器
java·服务器·sshj
IT毕设梦工厂7 小时前
计算机毕业设计选题推荐-在线拍卖系统-Java/Python项目实战
java·spring boot·python·django·毕业设计·源码·课程设计
Ylucius8 小时前
动态语言? 静态语言? ------区别何在?java,js,c,c++,python分给是静态or动态语言?
java·c语言·javascript·c++·python·学习