手写模拟SpringBoot组件核心原理

前言

        想必写Java的都知道spring这个框架,使用其实也是很方便了,但是仍然需要大量的配置,为了进一步提升开发的效率,业内大佬就开发出了一个springboot的框架,只要进行一些简单的配置,就可以进入快速的开发,那么它的底层是怎么做到这个呢,其实仍然是基于spring进行开发的,只是自动帮我们配置了很多类,我们就不需要再自己进行配置了,这篇文章就是个人对于这个机制的一些简单理解

项目模块

 模块介绍

其中zxcBoot是用来模拟springBoot启动原理的,而user则是我们平时的使用模块,主要涉及的不多,因为这是简单的模拟

auto: 自动导入配置类

condition: 条件判断bean是否要生成

config: 自动配置类

core: 核心的主键和启动类

server: 服务器启动相关

下面就一个个来分析这些包是干啥的

注:这篇文章只是在探究springboot的原理,所以对于spring的知识并不会深入讲解,也就是说如果不懂spring的机制有些地方可能会看不懂

springBoot核心机制

        我们自己如果要实现一个springBoot,关注的问题主要有以下几个

1. 既然boot是基于spring的,那么就需要创建spring容器

2. 为了实现可以在浏览器进行访问,那么就必须启动Tomcat之类的容器

3. 由于要实现Tomcat,Jetty等容器的自动切换需要依赖于spring的条件注解

4. 配置类要统一生效需要依赖spring的DeferredImportSelector接口

5. 为了自动加载这些配置类利用了SPI机制

6. 最终为了让用户更容易使用,提供了最底层的注解

以下基于这些问题来进行说明

核心注解

        这里先放着用户需要使用的一个注解,也是最核心的,如下,然后再展开一点点讲

package com.zxc.boot.core;

import com.zxc.boot.auto.ZxcImportAutoSelector;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Configuration标明为配置类
 * ComponentScan扫描包路径,默认是当前包及子包
 * @Import(ZxcImportAutoSelector.class) 导入了自动配置类所在的路径
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(ZxcImportAutoSelector.class)
public @interface ZxcSpringBoot {
}

spring容器创建和tomcat启动

package com.zxc.boot.core;

import com.zxc.boot.server.WebServer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

public class ZxcApplication {

    public static void run(Class clazz) {
        //创建spring容器
        AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
        //注册clazz
        webApplicationContext.register(clazz);
        //刷新容器
        webApplicationContext.refresh();
        //从容器获取一个WebServer并启动,这是核心的扩展点位置..
        //从容器获取,也是实现自动化切换实现的关键所在,,,
        WebServer webServer = getWebServer(webApplicationContext);
        webServer.start();
    }

    public static WebServer getWebServer(AnnotationConfigWebApplicationContext webApplicationContext) {
        //这个方法不会初始化数据,有一定提升性能帮助
        String[] beanNamesForType = webApplicationContext.getBeanNamesForType(WebServer.class);
        if(beanNamesForType.length <= 0) {
           throw new RuntimeException("没有WebServer实现类,请检查");
        }
        if(beanNamesForType.length > 1) {
            throw new RuntimeException("存在多个WebServer实现类,请检查");
        }
        return webApplicationContext.getBean(WebServer.class);
    }
}

这部分没什么好说的,就是创建一个容器,然后把clazz放到容器中,通常这个clazz都是spring的一个配置类,然后刷新容器,此时spring容器会创建所有单例并且非懒加载的bean,接着是从容器中获取一个WebServer接口(TomcatServer实现的接口),然后启动

        这里的精华在于是从Spring容器中获取对象的,为后面的自动切换会用户的替换提供了基础,还有个细节是在不需要获取到bean实施的时候可以调用getBeanNamesForType方法,该方法只是获取bean定义,并不会生成bean,这也算是一个优化了,有了这些基础的东西,就可以慢慢的实现springboot自动装配原理了

WebServer自动装配类

 

package com.zxc.boot.config;

import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebServerAutoConfig implements ZxcAutoConfig {

    @Bean
    public TomcatWebServer tomcatWebServer() {
        return new TomcatWebServer();
    }

    @Bean
    public JettyServer jettyServer() {
        return new JettyServer();
    }
}

看起来很简单,只是往spring容器放置了一些bean,两个都是WebServer接口的实现类,熟练spring的人一看就能发现其实这是有问题的,因为上面从容器中只是获取一个WebServer的实现类,而现在容器中有两个,肯定是会抛出异常的,所以这样肯定是不行的,为了解决这个问题,就需要利用spring提供的条件注解了

条件注解

package com.zxc.boot.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

import java.util.Map;

public class MyConditionOnClassImpl implements Condition {

    @Override
    public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {

        Map<String, Object> attributes = annotatedTypeMetadata.getAnnotationAttributes(MyConditionOnClass.class.getName());
        Object className = attributes.get("className");

        try {
            conditionContext.getClassLoader().loadClass(className.toString());
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}
package com.zxc.boot.condition;

import org.springframework.context.annotation.Conditional;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Configuration标明为配置类
 * ComponentScan扫描包路径,默认是当前包及子包
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Conditional(MyConditionOnClassImpl.class)
public @interface MyConditionOnClass {

    String className();
}

其中MyConditionOnClassImpl是实现了spring的条件接口Condition,matches返回true的时候spring就会创建该bean,否则就不会创建,而MyConditionOnClass注解只是为了更方便的使用而定义出来的,也就是说以下两种使用方式是一样的,意思都是让spring校验条件成功时才返回,但是有了一层注解的保证,使用起来用户更方便,也更加能看出具体意思

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

    @Bean
    @Conditional(MyConditionOnClassImpl.class)
    public TomcatWebServer tomcatWebServer2() {
        return new TomcatWebServer();
    }

有了这个条件注解后,之前的自动配置类就可以改为如下了

package com.zxc.boot.config;

import com.zxc.boot.condition.MyConditionOnClass;
import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class WebServerAutoConfig implements ZxcAutoConfig {

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

    @Bean
    @MyConditionOnClass(className = "jetty的某个类")
    public JettyServer jettyServer() {
        return new JettyServer();
    }
}

这样就不会有多个WebServer的问题了,除非你引入了多个依赖,这个也是实现自动装配的核心原理,因为MyConditionOnClassImpl的matches方法其实就是对你配置的全类名进行加载,如果加载到类就返回匹配,spring就会去创建,如果加载不到类那就不会返回了,所以你就可以通过引入对应的依赖来解决自动切换的问题,不过有了这个自动配置类以后你还是要交给spring管理,怎么弄呢,就需要以下的配置了

自动配置类加载

        首先,其中一种做法就是直接使用@Import注解,对核心的注解改造,如下

package com.zxc.boot.core;

import com.zxc.boot.config.WebServerAutoConfig;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Configuration标明为配置类
 * ComponentScan扫描包路径,默认是当前包及子包
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(WebServerAutoConfig.class)
public @interface ZxcSpringBoot {
}

  这样就可以了,但是这样会有个问题,如果你有大量的自动配置类需要加入到spring容器中,那么就需要写很多这样的导入,显然是很不方便的,所以下面就需要利用spring提供的另外机制,来解决这个问题

ImportSelector机制

        ImportSelector机制也是spring提供的,用于批量导入bean的接口方法,它的方法定义如下,意思就是说String[]数组是类的全类名,只要你返回,spring就会进行去解析

public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

      基于这种机制,我们就可以提供这么个实现类,如下,把要返回的全类名直接返回即可

package com.zxc.boot.auto;

import com.zxc.boot.config.WebServerAutoConfig;
import org.springframework.context.annotation.ImportSelector;
import org.springframework.core.type.AnnotationMetadata;

/**
 * 返回要放到容器中的全类名
 */
public class ZxcImportAutoSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
       return new String[]{WebServerAutoConfig.class.getName()};
    }
}

然后注解改为这样即可

package com.zxc.boot.core;

import com.zxc.boot.auto.ZxcImportAutoSelector;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Configuration标明为配置类
 * ComponentScan扫描包路径,默认是当前包及子包
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@ComponentScan
@Import(ZxcImportAutoSelector.class)
public @interface ZxcSpringBoot {
}

这样一来,如果有多个自动配置类就可以直接在ZxcImportAutoSelector声明即可了,但是这样还是存在一个问题,如果使用者想进行扩展是很麻烦的,因为有了SPI机制来解决这个问题,下面来看看这个方案是怎么样的

Java SPI机制解决动态化

        Java SPI机制大概就是说可以通过一个配置文件来自动加载所有的实现类,首先,需要提供一个接口,比如这里的

package com.zxc.boot.core;

/**
 * 空接口,用于SPI机制
 */
public interface ZxcAutoConfig {
}

其次让我们的WebServerAutoConfig去实现这个接口

        然后在resources资源下建立 META-INF/services 文件夹,注意,是两个文件夹,先创建META-INF文件夹,然后在META-INF文件夹下面创建一个services文件夹,接着在services创建一个文件,文件名为接口的全类名:com.zxc.boot.core.ZxcAutoConfig,注意,文件没有后缀名的,然后把具体的实现放到文件中即可,如果有多个换行就行了,如下

           这些操作做完以后我们就可以使用对之前的进行改造了,利用 ServiceLoader.load()方法来加载,方法是java spi提供的,它会到指定的文件夹下去加载实现类,也就是META-INF/services,注意这个路径是代码写死的,只能按照这种规范来放文件,如下

package com.zxc.boot.auto;

import com.zxc.boot.core.ZxcAutoConfig;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

/**
 * 返回要放到容器中的全类名
 */
public class ZxcImportAutoSelector implements ImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        List<String> configList = new ArrayList<>();
        Iterator<ZxcAutoConfig> iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
        while (iterator.hasNext()) {
            configList.add(iterator.next().getClass().getName());
        }
        return configList.toArray(new String[0]);
    }
}

        这样一来,我们就不需要在代码中写死配置类全路径了,而且如果用户需要扩展的化它也可

以根据spi的机制,在META-INF/service建立对应的文件,然后提供自己的配置类即可,因为ServiceLoader.load()是会加载所有的文件的,到此,boot的核心机制就写完了,这里还有个问题顺便体现,ImportSelector接口是跟其他bean一起初始化的,假设有这么个情况,用户自己也定义了一个WebServerAutoConfig类,那么有可能会当前我们定义的覆盖掉,也就是存在一个顺序问题,对于这种情况,spring提供了一个子接口,叫DeferredImportSelector,这个接口是ImportSelector的子接口,它的功能是会先加载其他的bean,最后才来加载这个接口下面的bean,从而避免了顺序问题,当然了,如果要为了避免的话,就得在WebServerAutoConfig多添加一些条件判断了,如下

package com.zxc.boot.config;

import com.zxc.boot.condition.MyConditionOnClass;
import com.zxc.boot.core.ZxcAutoConfig;
import com.zxc.boot.server.JettyServer;
import com.zxc.boot.server.TomcatWebServer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@ConditionalOnMissingBean(WebServerAutoConfig.class)
public class WebServerAutoConfig implements ZxcAutoConfig {

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

    @Bean
    @MyConditionOnClass(className = "jetty的某个类")
    public JettyServer jettyServer() {
        return new JettyServer();
    }
}

   其中,@ConditionalOnMissingBean是springboot的注解,意思是说没有WebServerAutoConfig的bean我当前的bean才会生效,假设用户自动定义了我这个就不会生效了,当然了这里只是演示下,没有真正的引入,所以我们最后的importSelect就是如下

       

package com.zxc.boot.auto;

import com.zxc.boot.core.ZxcAutoConfig;
import org.springframework.context.annotation.DeferredImportSelector;
import org.springframework.core.type.AnnotationMetadata;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

/**
 * 返回要放到容器中的全类名
 */
public class ZxcImportAutoSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        List<String> configList = new ArrayList<>();
        Iterator<ZxcAutoConfig> iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
        while (iterator.hasNext()) {
            configList.add(iterator.next().getClass().getName());
        }
        return configList.toArray(new String[0]);
    }
}

到这里,我们自己实现的简易boot就完成了,稍微总结一下,大概有这么几个核心点

1. 从spring容器中getWebServer,为后面的自动装配做好准备

2. 使用spring的condition条件功能实现不同实现类根据依赖不同而自动切换

3. 使用ImportSelector结合@Import注解来动态注入多个自动装配类

4. 使用spi机制来让其他使用者也可以很方便的进行扩展

当然了,上面还涉及很多spring的扩展点,这里主要是讲boot的核心原理,就不细说了,下面我们就来看一下使用,有了上面的支持,使用就非常简单了,跟我们平常依赖springboot的用法是差不多的

使用我们自己的springboot

 首先要引入我们自己的依赖

    <dependencies>
        <!-- 依赖自己开发的springboot -->

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

配置主启动类

package com.zxc.user;

import com.zxc.boot.core.ZxcApplication;
import com.zxc.boot.core.ZxcSpringBoot;

@ZxcSpringBoot
public class UserApplication {

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

然后声明bean并使用

如下

package com.zxc.user.service;

import org.springframework.stereotype.Service;

@Service
public class UserService {

    public String test() {
        return "ZxcTest";
    }
}
package com.zxc.user.controller;

import com.zxc.user.service.UserService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

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

接着启动UserApplication的main方法,然后在你控制台输入

http://localhost:8081/user/test

可以看到如下内容

 到此,就结束了

注:虽然这里还是引用了@Service的spring注解,不过我们是通过zxcBoot项目间接去依赖的,而不是直接依赖的

项目地址

链接:https://share.weiyun.com/umgAPdFn 密码:a6axfv

注:项目名为:zxcSpringBoot.zip

总结

        springboot的核心流程并不复杂,个人觉得只要用心去体会,就能搞懂这个原理,springboot里面的一些技术思想很值得我们去借鉴,这里也建议大家如果看的懂源码的话可以多看看,对我们提升技术还是很有帮助的

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值