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=自己的类全限定名

 

原文地址:https://www.cnblogs.com/UpGx/p/15545880.html