zoukankan      html  css  js  c++  java
  • Spring-SpringBoot启动源码分析和自动配置原理

    Spring-SpringBoot启动源码分析和自动配置原理

    SpringBoot实在是我们的一个福音啊,记得使用Spring搭建项目的时候,很多的配置文件。而SpringBoot可以实现0配置,当然配置都变成了一个个bean了。而且我们都知道,启动一个SpringBoot的项目,重点就在一个main方法。所以下面我们就来分析一下他的【启动源码】,以及他的一个重要的特性【自动装配】的原理。

    SpringBoot启动源码分析

    实际上他的启动主要是干了两件事情,接下来我们就从以下这两个方向去探索他的源码,看看是不是这样。
    • 【Spring容器的启动】,目的把所有需要使用的类进行实例化
    • 【Servlet容器的启动】,目的是接受请求。

    【Spring容器的启动】

    大体流程:

    • 通过spi的方式加载监听类SpringApplicationRunListener,
    • 生成environment对象,
    • 创建SpringBoot的上下文对象AnnotationConfigServletWebServerApplicationContext,
    • 对对象进行初始化,调用对象的refresh方法彻底对Spring进行初始化

    源码分析:

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
        configureHeadlessProperty();
         //SPI的方式获取SpringApplicationRunListener实例
        SpringApplicationRunListeners listeners = getRunListeners(args);
         //这里实际上就是一个触发机制,他内部有一个list去存储所有实现了SpringApplicationRunListener的实例
        listeners.starting();
        try {
           ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //生成Environment对象
           ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
           configureIgnoreBeanInfo(environment);
            //打印banner图,启动的时候,我们经常看见的那个springboot的图标。
           Banner printedBanner = printBanner(environment);
            //创建springboot的上下文对象AnnotationConfigServletWebServerApplicationContext
           context = createApplicationContext();
           exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                 new Class[] { ConfigurableApplicationContext.class }, context);
            // 上面创建了这样一个对象,这里肯定对这个对象进行初始化
           prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            //上面的对象已经初始化了,这里调用SrpingBoot的上下文对象,在这里对Spring容器彻底的进行初始化。
           refreshContext(context);
           afterRefresh(context, applicationArguments);
           stopWatch.stop();
           if (this.logStartupInfo) {
              new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
           }
           listeners.started(context);
           callRunners(context, applicationArguments);
        }
        catch (Throwable ex) {
           handleRunFailure(context, ex, exceptionReporters, listeners);
           throw new IllegalStateException(ex);
        }
    
        try {
           listeners.running(context);
        }
        catch (Throwable ex) {
           handleRunFailure(context, ex, exceptionReporters, null);
           throw new IllegalStateException(ex);
        }
        return context;
        }

    【Servlet容器的启动】

    Tomcat的启动(默认)上面聊到#refresh是核心,所以tomcat也是在这里进行启动的,在#refresh中有个onrefresh的方法,就是在这里对它进行启动的,我们点击去看#onrefresh,发现实际上他是一个钩子方法,这里使用的是一个模板模式,所以我们上面实例化出来的那个子类完成对方法的实现的,下面我们看看这个方法。

    private void createWebServer() {
       WebServer webServer = this.webServer;
       ServletContext servletContext = getServletContext();
       if (webServer == null && servletContext == null) {
           //获取tomcat的对象,我们SpringBoot默认使用的tomcat,使用的是Spi的机制。
          ServletWebServerFactory factory = getWebServerFactory();
           //创建tomcat对象,包括设置一系列参数,并且顺便启动tomcat.
          this.webServer = factory.getWebServer(getSelfInitializer());
          getBeanFactory().registerSingleton("webServerGracefulShutdown",
                new WebServerGracefulShutdownLifecycle(this.webServer));
          getBeanFactory().registerSingleton("webServerStartStop",
                new WebServerStartStopLifecycle(this, this.webServer));
       }
       else if (servletContext != null) {
          try {
             getSelfInitializer().onStartup(servletContext);
          }
          catch (ServletException ex) {
             throw new ApplicationContextException("Cannot initialize servlet context", ex);
          }
       }
       initPropertySources();
    }

    自动装配分析

    他的理念就是:【约定大于配置】,在老的Spring中我们要手动增加一些功能,比如aop,mvc,但是使用它,他会自动把这些东西装配到我们的项目中。那它是如何把这些个功能默认装配到我们的项目中的呢?那我们就要从他的spi机制是如何使用和实现的开始聊起。

    【jdk的spi】

    spi我们在聊shardingSphere实现自己的分片算法的时候就已经聊过。主要就是把实现类的路径放在配置文件中,从而加载到项目中,而不去改变项目的结构,具体看ShardingSphere的那一章节哈。]

    【SpringBoot中的spi】:

    【使用方法】

    • 首先定义几个接口
    • 使用几个类对接口进行实现
    • 在工程的resources下面创建META-INF文件夹,在文件夹下创建spring.factories文件,在文件夹中配置内容如下
      •  com.li.glen.spi.Log=
      •  com.li.glen.spi.Log4j,
      •  com.li.glen.spi.Logback
      • 【com.li.glen.spi.Log】:就是接口的完整限定名,下面的这些就是对接口的实现。

    SpringBoot中提供了相关的接口对这这些实现类进行获取

    • 【loadFactoryNames】:方法获取实现了接口的所有类的名称
      •     @Test
            public void test() {
                List<String> strings = SpringFactoriesLoader.loadFactoryNames(Log.class, ClassUtils.getDefaultClassLoader());
                for (String string : strings) {
                    System.out.println(string);
                }
            }
        View Code
    • 【loadFactories】:方法获取实现了接口的所有类的实例
      •     @Test
            public void test1() {
                List<Log> logs = SpringFactoriesLoader.loadFactories(Log.class, ClassUtils.getDefaultClassLoader());
                for (Log log : logs) {
                    System.out.println(log);
                }
            }
        View Code

    【源码分析】

    【#loadFactoryNames】:

    • 大体流程:
    • 首先去缓存中读取,如果没有走下一步
    • 循环读取META-INF/spring.factories下的内容,并且把他们变成key和value的形式,key就是接口名称,value就是他们的实现类的名称
    • 通过名称去获取value即可
    • 实际上他们两个方法的不同之处就是,一个对这些类进行了实例化,而一些没有实例化,所以这里只分析一个方法。

    【源码解读】:

    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        //首先去缓存中获取
       MultiValueMap<String, String> result = cache.get(classLoader);
       if (result != null) {
          return result;
       }
    
       try {
           //去加载FACTORIES_RESOURCE_LOCATION(META-INF/spring.factories)下的文件
           //注意:这里加载的不仅仅是当前项目下面的这个文件,还有所有项目jar中的这个文件
          Enumeration<URL> urls = (classLoader != null ?
                classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
          result = new LinkedMultiValueMap<>();
           //循环所有的文件
          while (urls.hasMoreElements()) {
             URL url = urls.nextElement();
              //把他们变成resource对象
             UrlResource resource = new UrlResource(url);
              //然后通过Properties读取文件,并且把他们变成一个map
              //key就是咱们的接口名,value就是咱们的实现类名,通过逗号分隔,所以value的内容就变成了一个个list
             Properties properties = PropertiesLoaderUtils.loadProperties(resource);
             for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                   result.add(factoryTypeName, factoryImplementationName.trim());
                }
             }
          }
           //最终给他塞进去所有的spring.factories下路径的文件名称,并且返回
          cache.put(classLoader, result);
          return result;
       }
       catch (IOException ex) {
          throw new IllegalArgumentException("Unable to load factories from location [" +
                FACTORIES_RESOURCE_LOCATION + "]", ex);
       }
    }

    【注意】:在通过api对这些类进行调用的过程中,并没有把这些类放在Spring中,那么什么时候他这些类放在Spring容器中的呢?我们这里就要增加一个接口的知识点【DeferredImportSelector 】通过这个接口,我们就能了解他的装配过程。

    【做法】:我们现在把一个没有任何注解的类,给他加载到Spring容器中,我们知道在Spring中我们想把一个bean交给它进行保存,我们就需要把该类的完整限定名在【org.springframework.context.annotation.ImportSelector#selectImports】进行返回。

    【流程】:

    • 我们定义一个类实现DeferredImportSelector的接口,当外部调用#selectImports进行实例化bean的时候,他首先调用#getImportGroup返回的类中的#proces,然后再调用#selectImports。而我们在#process中调用的是当前类的返回值【也就是说,SpringBoot没有调用我们的方法,而是我们自己把我们的方法放在了SpringBoot会调用的一个group中】,实际上是大家看我们ImportBean的这个类中上有一个@Import注解,Spring会去扫描这个注解,同时执行这个注解后面的类的方法。这样就到了咱们写的类中了。

    【代码】:**我们要实例化的类为DeferredBean,里面是空的,什么都没有。 

    public class DeferredImportSelectorDemo implements DeferredImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata importingClassMetadata) {
            System.out.println("==============DeferredImportSelectorDemo.selectImports===========");
            //如果需要把类实例化,就需要把该类的完整限定名返回
            return new String[]{DeferredBean.class.getName()};
        }
    
        //这里要返回实现了Group接口的类
        @Override
        public Class<? extends Group> getImportGroup() {
            return DeferredImportSelectorGroupDemo.class;
        }
    
        private static class DeferredImportSelectorGroupDemo implements Group {
    
            List<Entry> list = new ArrayList<>();
            //收集需要实例化的类
            @Override
            public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
                System.out.println("==============DeferredImportSelectorGroupDemo.process===========");
                String[] strings = selector.selectImports(metadata);
                for (String string : strings) {
                    list.add(new Entry(metadata,string));
                }
            }
    
            //复杂把process收集的结果返回,返回的类必须包装成entry对象
            @Override
            public Iterable<Entry> selectImports() {
                System.out.println("==============DeferredImportSelectorGroupDemo.selectImports===========");
                return list;
            }
        }
    }

    但是没有地方触发我们上面写的类,所以我们需要去增加一个类,这样我们就完成了一个bean的装配。

    @Component
    @Import(DeferredImportSelectorDemo.class)
    public class ImportBean {
    }

    【SpringBoot自动装配】:

    【整体流程】:

    • 在EnableAutoConfifiguration注解中有一个@Import注解,这个注解中import中有一个类【AutoConfigurationImportSelector】,这个类实现了【DeferredImportSelector】这个接口。Spring会去扫描【@import】注解后的的类【AutoConfigurationImportSelector】从而调用一个内部类(【group】)中的俩个方法
    • 这个类中实现了一个内部接口group。这个内部接口中有一个内部的类。
    • 这个内部类中有两个方法process和selectImports,就是Spring会调用的两个方法。
    • process通过去调用loadFactoryNames# 并且传入【@import】中的类名【EnableAutoConfiguration】作为key去查询autconfig中的spring,factories中已经配置好的类。
    • SpringBoot会在初始Spring容器的时候把这个类进行装配

    【细节】

    上面我们自己装配一个类发现Spring主要是循环类上面的有@import注解的类,然后操作的。接着我们看这个AutoConfigurationImportSelector类。这个类是@EnableAutoConfifiguration注解中@Import进来的类。而EnableAutoConfifiguration是在@SpringBootApplication中一个注解。我们发现,这个类也实现了【DeferredImportSelector】接口,那他肯定和我们上面干的事情是一样的,也就是说它里面肯定有一个内部类,并且有两个方法(#process和#selectImports去收集需要交给Spring去装配的类。

    【源码解析】:我们来看看他这个类中的两个方法。

    // 这里获取需要实例化的类
    @Override
    public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) { Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName())); //这里拿到自动配置的实例 AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector) .getAutoConfigurationEntry(annotationMetadata); this.autoConfigurationEntries.add(autoConfigurationEntry); for (String importClassName : autoConfigurationEntry.getConfigurations()) { this.entries.putIfAbsent(importClassName, annotationMetadata); } }
    //这里返回需要实例化的类,并且包装他们 @Override
    public Iterable<Entry> selectImports() { if (this.autoConfigurationEntries.isEmpty()) { return Collections.emptyList(); } Set<String> allExclusions = this.autoConfigurationEntries.stream() .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet()); Set<String> processedConfigurations = this.autoConfigurationEntries.stream() .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream) .collect(Collectors.toCollection(LinkedHashSet::new)); processedConfigurations.removeAll(allExclusions); return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream() .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName)) .collect(Collectors.toList()); }

    这里就是对包装需要装配的类

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
       if (!isEnabled(annotationMetadata)) {
          return EMPTY_ENTRY;
       }
        //这里其实拿到的是SpringBootApplication对象
       AnnotationAttributes attributes = getAttributes(annotationMetadata);
        //拿到自动配置类
       List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
      //因为有些类我们不需要被装配,那就可以使用exclude注解排除他们,这里就是剔除我们不要的类, configurations
    = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = getConfigurationClassFilter().filter(configurations); fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions); }

    这里和咱们上面聊到的spi机制的代码就是一样的了,但是我们记得他的这个方式是通过类名去获取对应的value数组的,他这里的类的全限定名是EnableAutoConfiguration,

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
             getBeanClassLoader());
       Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
             + "are using a custom packaging, make sure that file is correct.");
       return configurations;
    }

    然后它就会通过这个key去获取value,在这个包下面,有所有的SpringBoot为我们准备的包

     

     至此,SpringBoot把需要的类包装完毕,至于如何把类放在Spring中,这就是Spring需要做的事情了。这个东西还是在我们上面讲到的Spring容器初始化的那里的#refresh中做的。

    那借助上面的源码,既然他自动装配的时候,会把key为EnableAutoConfiguration中的所有类都加载那不是不是意味着我们可以写一个key和他名称一样的,然后value是自己的类放在我们自己的factories中让SpringBoot帮忙呢?答案是肯定的。就像这样,Spring就会帮忙管理我们自己的类了。

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=自己的类全限定名

     

  • 相关阅读:
    Android笔记之AsyncTask
    Android笔记之使用Glide加载网络图片、下载图片
    Android笔记之OnLongClickListener
    Android笔记之ViewModel的使用示例
    分享两款好玩的单机游戏^_^
    Android笔记之Retrofit与RxJava的组合
    15张扑克,让观众心选一张,然后分成3组,每组5张。通过询问观众心选的牌是否在于某组中,最后把选中的牌找出来。
    使用Retrofit发送POST请求提交JSON数据
    Android笔记之引用aar
    不可变类
  • 原文地址:https://www.cnblogs.com/UpGx/p/15545880.html
Copyright © 2011-2022 走看看