• 手写模拟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. 最终为了让用户更容易使用,提供了最底层的注解

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

    核心注解

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

    1. package com.zxc.boot.core;
    2. import com.zxc.boot.auto.ZxcImportAutoSelector;
    3. import org.springframework.context.annotation.ComponentScan;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.context.annotation.Import;
    6. import java.lang.annotation.ElementType;
    7. import java.lang.annotation.Retention;
    8. import java.lang.annotation.RetentionPolicy;
    9. import java.lang.annotation.Target;
    10. /**
    11. * Configuration标明为配置类
    12. * ComponentScan扫描包路径,默认是当前包及子包
    13. * @Import(ZxcImportAutoSelector.class) 导入了自动配置类所在的路径
    14. */
    15. @Target(ElementType.TYPE)
    16. @Retention(RetentionPolicy.RUNTIME)
    17. @Configuration
    18. @ComponentScan
    19. @Import(ZxcImportAutoSelector.class)
    20. public @interface ZxcSpringBoot {
    21. }

    spring容器创建和tomcat启动

    1. package com.zxc.boot.core;
    2. import com.zxc.boot.server.WebServer;
    3. import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
    4. public class ZxcApplication {
    5. public static void run(Class clazz) {
    6. //创建spring容器
    7. AnnotationConfigWebApplicationContext webApplicationContext = new AnnotationConfigWebApplicationContext();
    8. //注册clazz
    9. webApplicationContext.register(clazz);
    10. //刷新容器
    11. webApplicationContext.refresh();
    12. //从容器获取一个WebServer并启动,这是核心的扩展点位置..
    13. //从容器获取,也是实现自动化切换实现的关键所在,,,
    14. WebServer webServer = getWebServer(webApplicationContext);
    15. webServer.start();
    16. }
    17. public static WebServer getWebServer(AnnotationConfigWebApplicationContext webApplicationContext) {
    18. //这个方法不会初始化数据,有一定提升性能帮助
    19. String[] beanNamesForType = webApplicationContext.getBeanNamesForType(WebServer.class);
    20. if(beanNamesForType.length <= 0) {
    21. throw new RuntimeException("没有WebServer实现类,请检查");
    22. }
    23. if(beanNamesForType.length > 1) {
    24. throw new RuntimeException("存在多个WebServer实现类,请检查");
    25. }
    26. return webApplicationContext.getBean(WebServer.class);
    27. }
    28. }

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

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

    WebServer自动装配类

     

    1. package com.zxc.boot.config;
    2. import com.zxc.boot.core.ZxcAutoConfig;
    3. import com.zxc.boot.server.JettyServer;
    4. import com.zxc.boot.server.TomcatWebServer;
    5. import org.springframework.context.annotation.Bean;
    6. import org.springframework.context.annotation.Configuration;
    7. @Configuration
    8. public class WebServerAutoConfig implements ZxcAutoConfig {
    9. @Bean
    10. public TomcatWebServer tomcatWebServer() {
    11. return new TomcatWebServer();
    12. }
    13. @Bean
    14. public JettyServer jettyServer() {
    15. return new JettyServer();
    16. }
    17. }

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

    条件注解

    1. package com.zxc.boot.condition;
    2. import org.springframework.context.annotation.Condition;
    3. import org.springframework.context.annotation.ConditionContext;
    4. import org.springframework.core.type.AnnotatedTypeMetadata;
    5. import java.util.Map;
    6. public class MyConditionOnClassImpl implements Condition {
    7. @Override
    8. public boolean matches(ConditionContext conditionContext, AnnotatedTypeMetadata annotatedTypeMetadata) {
    9. Map attributes = annotatedTypeMetadata.getAnnotationAttributes(MyConditionOnClass.class.getName());
    10. Object className = attributes.get("className");
    11. try {
    12. conditionContext.getClassLoader().loadClass(className.toString());
    13. return true;
    14. } catch (Exception e) {
    15. return false;
    16. }
    17. }
    18. }
    1. package com.zxc.boot.condition;
    2. import org.springframework.context.annotation.Conditional;
    3. import java.lang.annotation.ElementType;
    4. import java.lang.annotation.Retention;
    5. import java.lang.annotation.RetentionPolicy;
    6. import java.lang.annotation.Target;
    7. /**
    8. * Configuration标明为配置类
    9. * ComponentScan扫描包路径,默认是当前包及子包
    10. */
    11. @Target(ElementType.METHOD)
    12. @Retention(RetentionPolicy.RUNTIME)
    13. @Conditional(MyConditionOnClassImpl.class)
    14. public @interface MyConditionOnClass {
    15. String className();
    16. }

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

    1. @Bean
    2. @MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
    3. public TomcatWebServer tomcatWebServer() {
    4. return new TomcatWebServer();
    5. }
    6. @Bean
    7. @Conditional(MyConditionOnClassImpl.class)
    8. public TomcatWebServer tomcatWebServer2() {
    9. return new TomcatWebServer();
    10. }

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

    1. package com.zxc.boot.config;
    2. import com.zxc.boot.condition.MyConditionOnClass;
    3. import com.zxc.boot.core.ZxcAutoConfig;
    4. import com.zxc.boot.server.JettyServer;
    5. import com.zxc.boot.server.TomcatWebServer;
    6. import org.springframework.context.annotation.Bean;
    7. import org.springframework.context.annotation.Configuration;
    8. @Configuration
    9. public class WebServerAutoConfig implements ZxcAutoConfig {
    10. @Bean
    11. @MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
    12. public TomcatWebServer tomcatWebServer() {
    13. return new TomcatWebServer();
    14. }
    15. @Bean
    16. @MyConditionOnClass(className = "jetty的某个类")
    17. public JettyServer jettyServer() {
    18. return new JettyServer();
    19. }
    20. }

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

    自动配置类加载

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

    1. package com.zxc.boot.core;
    2. import com.zxc.boot.config.WebServerAutoConfig;
    3. import org.springframework.context.annotation.ComponentScan;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.context.annotation.Import;
    6. import java.lang.annotation.ElementType;
    7. import java.lang.annotation.Retention;
    8. import java.lang.annotation.RetentionPolicy;
    9. import java.lang.annotation.Target;
    10. /**
    11. * Configuration标明为配置类
    12. * ComponentScan扫描包路径,默认是当前包及子包
    13. */
    14. @Target(ElementType.TYPE)
    15. @Retention(RetentionPolicy.RUNTIME)
    16. @Configuration
    17. @ComponentScan
    18. @Import(WebServerAutoConfig.class)
    19. public @interface ZxcSpringBoot {
    20. }

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

    ImportSelector机制

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

    1. public interface ImportSelector {
    2. /**
    3. * Select and return the names of which class(es) should be imported based on
    4. * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
    5. */
    6. String[] selectImports(AnnotationMetadata importingClassMetadata);
    7. }

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

    1. package com.zxc.boot.auto;
    2. import com.zxc.boot.config.WebServerAutoConfig;
    3. import org.springframework.context.annotation.ImportSelector;
    4. import org.springframework.core.type.AnnotationMetadata;
    5. /**
    6. * 返回要放到容器中的全类名
    7. */
    8. public class ZxcImportAutoSelector implements ImportSelector {
    9. @Override
    10. public String[] selectImports(AnnotationMetadata annotationMetadata) {
    11. return new String[]{WebServerAutoConfig.class.getName()};
    12. }
    13. }

    然后注解改为这样即可

    1. package com.zxc.boot.core;
    2. import com.zxc.boot.auto.ZxcImportAutoSelector;
    3. import org.springframework.context.annotation.ComponentScan;
    4. import org.springframework.context.annotation.Configuration;
    5. import org.springframework.context.annotation.Import;
    6. import java.lang.annotation.ElementType;
    7. import java.lang.annotation.Retention;
    8. import java.lang.annotation.RetentionPolicy;
    9. import java.lang.annotation.Target;
    10. /**
    11. * Configuration标明为配置类
    12. * ComponentScan扫描包路径,默认是当前包及子包
    13. */
    14. @Target(ElementType.TYPE)
    15. @Retention(RetentionPolicy.RUNTIME)
    16. @Configuration
    17. @ComponentScan
    18. @Import(ZxcImportAutoSelector.class)
    19. public @interface ZxcSpringBoot {
    20. }

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

    Java SPI机制解决动态化

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

    1. package com.zxc.boot.core;
    2. /**
    3. * 空接口,用于SPI机制
    4. */
    5. public interface ZxcAutoConfig {
    6. }

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

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

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

    1. package com.zxc.boot.auto;
    2. import com.zxc.boot.core.ZxcAutoConfig;
    3. import org.springframework.context.annotation.DeferredImportSelector;
    4. import org.springframework.core.type.AnnotationMetadata;
    5. import java.util.ArrayList;
    6. import java.util.Iterator;
    7. import java.util.List;
    8. import java.util.ServiceLoader;
    9. /**
    10. * 返回要放到容器中的全类名
    11. */
    12. public class ZxcImportAutoSelector implements ImportSelector {
    13. @Override
    14. public String[] selectImports(AnnotationMetadata annotationMetadata) {
    15. List configList = new ArrayList<>();
    16. Iterator iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
    17. while (iterator.hasNext()) {
    18. configList.add(iterator.next().getClass().getName());
    19. }
    20. return configList.toArray(new String[0]);
    21. }
    22. }

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

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

    1. package com.zxc.boot.config;
    2. import com.zxc.boot.condition.MyConditionOnClass;
    3. import com.zxc.boot.core.ZxcAutoConfig;
    4. import com.zxc.boot.server.JettyServer;
    5. import com.zxc.boot.server.TomcatWebServer;
    6. import org.springframework.context.annotation.Bean;
    7. import org.springframework.context.annotation.Configuration;
    8. @Configuration
    9. @ConditionalOnMissingBean(WebServerAutoConfig.class)
    10. public class WebServerAutoConfig implements ZxcAutoConfig {
    11. @Bean
    12. @MyConditionOnClass(className = "org.apache.catalina.startup.Tomcat")
    13. public TomcatWebServer tomcatWebServer() {
    14. return new TomcatWebServer();
    15. }
    16. @Bean
    17. @MyConditionOnClass(className = "jetty的某个类")
    18. public JettyServer jettyServer() {
    19. return new JettyServer();
    20. }
    21. }

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

           

    1. package com.zxc.boot.auto;
    2. import com.zxc.boot.core.ZxcAutoConfig;
    3. import org.springframework.context.annotation.DeferredImportSelector;
    4. import org.springframework.core.type.AnnotationMetadata;
    5. import java.util.ArrayList;
    6. import java.util.Iterator;
    7. import java.util.List;
    8. import java.util.ServiceLoader;
    9. /**
    10. * 返回要放到容器中的全类名
    11. */
    12. public class ZxcImportAutoSelector implements DeferredImportSelector {
    13. @Override
    14. public String[] selectImports(AnnotationMetadata annotationMetadata) {
    15. List configList = new ArrayList<>();
    16. Iterator iterator = ServiceLoader.load(ZxcAutoConfig.class).iterator();
    17. while (iterator.hasNext()) {
    18. configList.add(iterator.next().getClass().getName());
    19. }
    20. return configList.toArray(new String[0]);
    21. }
    22. }

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

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

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

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

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

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

    使用我们自己的springboot

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

    1. org.example
    2. zxcBoot
    3. 1.0-SNAPSHOT

    配置主启动类

    1. package com.zxc.user;
    2. import com.zxc.boot.core.ZxcApplication;
    3. import com.zxc.boot.core.ZxcSpringBoot;
    4. @ZxcSpringBoot
    5. public class UserApplication {
    6. public static void main(String[] args) {
    7. ZxcApplication.run(UserApplication.class);
    8. }
    9. }

    然后声明bean并使用

    如下

    1. package com.zxc.user.service;
    2. import org.springframework.stereotype.Service;
    3. @Service
    4. public class UserService {
    5. public String test() {
    6. return "ZxcTest";
    7. }
    8. }
    1. package com.zxc.user.controller;
    2. import com.zxc.user.service.UserService;
    3. import org.springframework.web.bind.annotation.GetMapping;
    4. import org.springframework.web.bind.annotation.RequestMapping;
    5. import org.springframework.web.bind.annotation.RestController;
    6. import javax.annotation.Resource;
    7. @RestController
    8. @RequestMapping("/user")
    9. public class UserController {
    10. @Resource
    11. private UserService userService;
    12. @GetMapping("/test")
    13. public String test() {
    14. return userService.test();
    15. }
    16. }

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

    http://localhost:8081/user/test

    可以看到如下内容

     到此,就结束了

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

    项目地址

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

    注:项目名为:zxcSpringBoot.zip

    总结

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

  • 相关阅读:
    JSP ssm 网上求职管理系统myeclipse开发mysql数据库springMVC模式java编程计算机网页设计
    【HTTP】Cookie 和 Session 详解
    Word自定义模板无法在新建时使用--解决方法
    优秀的测试/开发程序员与普通的程序员对比......
    打包报错JavaScript heap out of memory
    IDEA 搭建 SpringCloud 项目【超详细步骤】
    shell脚本入门到实战(二)--shell输入和格式化输出
    spring boot项目中使用nacos作为配置中心
    LNB基础类型了解
    最详细MySql安装教程
  • 原文地址:https://blog.csdn.net/zxc_user/article/details/127691806