Sping Boot启动原理精讲第二讲

前言

上一篇文章 《深度解析Spring Boot以及手写一个 starter》火了之后,由于篇幅的原因讲的不够深,我决定再加更一篇,两篇看下来相信对SpringBoot肯定能够理解了。

image.png

自动装配就是像事务、配置文件加载和解析、aop、缓存、数据源、springmvc等等,我们以前在项目中要加载这些需要配置xml或者写个@Configuration和很多个@Bean,这些都是需要手写的,现在不需要,只需要引入spring-boot-autoconfigure这个jar包即可,这个就是自动装配的魅力。以前要手动搞定的事,现在自动搞定。

本文的议题是run方法启动,SPI机制以及@EnableAutoConfiguration。

run方法启动

我们从SpringApplication.run(SpringBootApp.class,args);方法点进去看下,具体的步骤写在下面源码上。

public ConfigurableApplicationContext run(String... args) {
   StopWatch stopWatch = new StopWatch();
   stopWatch.start();
   DefaultBootstrapContext bootstrapContext = createBootstrapContext();
   ConfigurableApplicationContext context = null;
   configureHeadlessProperty();
   //SPI的方式获取SpringApplicationRunListener实例
   SpringApplicationRunListeners listeners = getRunListeners(args);
   //调用SpringApplicationRunListener的starting()方法
   listeners.starting(bootstrapContext, this.mainApplicationClass);
   try {
      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      //生成Environment对象
      ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
      configureIgnoreBeanInfo(environment);
      //打印banner图
      Banner printedBanner = printBanner(environment);
      //创建springboot的上下文对象AnnotationConfigServletWebServerApplicationContext
      context = createApplicationContext();
      context.setApplicationStartup(this.applicationStartup);
      //初始化上下文对象
      prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
      //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, listeners);
      throw new IllegalStateException(ex);
   }

   try {
     //调用SpringApplicationRunListener的running()方法
      listeners.running(context);
   }
   catch (Throwable ex) {
      handleRunFailure(context, ex, null);
      throw new IllegalStateException(ex);
   }
   return context;
}
复制代码

上面的源码主要干了两件事

  • 完成Spring容器的启动,把需要扫描的类实例化
  • 启动Servlet容器,完成Servlet容器的启动

上面的核心代码就是refreshContext(context),这个context对象就是AnnotationConfigServletWebServerApplicationContext,打个断点启动一下

image.png 最终进到了AbstractApplicationContext类的refresh方法,这个就是spring容器启动的核心方法,所以springboot的底层调的还是spring的源码。

Servlet容器启动,refresh方法往下走,就有一个onRefresh方法 image.png 点到具体的实现类ServletWebServerApplicationContext,这里有个createWebServer方法

image.png

private void createWebServer() {
   WebServer webServer = this.webServer;
   ServletContext servletContext = getServletContext();
   if (webServer == null && servletContext == null) {
      StartupStep createWebServer = this.getApplicationStartup().start("spring.boot.webserver.create");
      //这里获取TomcatServletWebServerFactory对象
      ServletWebServerFactory factory = getWebServerFactory();
      createWebServer.tag("factory", factory.getClass().toString());
      //这里new了Tomcat对象,启动了tomcat容器
      this.webServer = factory.getWebServer(getSelfInitializer());
      createWebServer.end();
      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();
}
复制代码

点击factory.getWebServer(getSelfInitializer());进入到tomcat的实现类

image.png 这一段就是在启动tomcat容器并且把项目部署到tomcat中。至此run方法就介绍完了,结合着源码看就是启动Spring容器和Servlet容器。

SPI机制

SPI简介

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文 件夹查找文件,自动加载文件里所定义的类。 这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用 到了SPI机制。我们先通过一个很简单的例子来看下它是怎么用的。 简单来说,SPI是一种扩展机制,核心就是将服务配置化,在核心代码不用改动的前提下,通过加载配置文件中的服 务,然后根据传递的参数来决定到底走什么逻辑,走哪个服务的逻辑。这样就对扩展是开放的,对修改是关闭的。

SpringBoot中的SPI

先在包里建了几个类

image.png

分别看下它们的代码,定义接口

public interface Log {
    void debug();
}
复制代码

定义Log4j实现类

public class Log4j implements Log {
    @Override
    public void debug() {
        System.out.println("-----Log4j");
    }
}
复制代码

定义Logback实现类

public class Logback implements Log {
    @Override
    public void debug() {
        System.out.println("-----Logback");
    }
}
复制代码

定义Slf4j实现类

public class Slf4j implements Log {
    @Override
    public void debug() {
        System.out.println("-----Slf4j");
    }
}
复制代码

在resources文件下META-INF文件下新建一个spring.factories

image.png

里面写的内容,这里的key是类型(可以是接口、注解、抽象类等等),value是实例类

image.png

再写一个测试类

public class SpiTest {

    @Test
    public void test() {
        List<String> strings = SpringFactoriesLoader.loadFactoryNames(Log.class, ClassUtils.getDefaultClassLoader());
        for (String string : strings) {
            System.out.println(string);
        }
    }

    @Test
    public void test1() {
        List<Log> logs = SpringFactoriesLoader.loadFactories(Log.class, ClassUtils.getDefaultClassLoader());
        for (Log log : logs) {
            System.out.println(log);
        }
    }

}
复制代码

执行一下,没问题,全都获取出来了

image.png

image.png

下面我们来分析下SpringFactoriesLoader.loadFactoryNames源码,点进去看到factoryType.getName()是获取类型的名字,这个就是key。

image.png

再点进去看下

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
   //先根据classLoader从缓存中拿,如果能拿到就返回
   Map<String, List<String>> result = cache.get(classLoader);
   if (result != null) {
      return result;
   }

   result = new HashMap<>();
   try {
      //FACTORIES_RESOURCE_LOCATION的值就是 META-INF/spring.factories
      //获取的文件是所有jar包和自己工程里面的所有spring.factories文件
      Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
      //循环所有文件
      while (urls.hasMoreElements()) {
         URL url = urls.nextElement();
         //包装成UrlResource对象
         UrlResource resource = new UrlResource(url);
         //核心代码,把文件包装成properties对象
         Properties properties = PropertiesLoaderUtils.loadProperties(resource);
         //循环properties对象
         for (Map.Entry<?, ?> entry : properties.entrySet()) {
            //拿到key
            String factoryTypeName = ((String) entry.getKey()).trim();
            String[] factoryImplementationNames =
                  StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
            for (String factoryImplementationName : factoryImplementationNames) {
               //把key对应的所有实例加入到list容器中
               result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
                     .add(factoryImplementationName.trim());
            }
         }
      }

      // Replace all lists with unmodifiable lists containing unique elements
      result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
            .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
      //建立缓存
      cache.put(classLoader, result);
   }
   catch (IOException ex) {
      throw new IllegalArgumentException("Unable to load factories from location [" +
            FACTORIES_RESOURCE_LOCATION + "]", ex);
   }
   return result;
}
复制代码

我们看到 Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);这里FACTORIES_RESOURCE_LOCATION的值就是META-INF/spring.factories,所以我们上面做演示的时候,配置是写在这个文件里面的,因为代码里就是从这里读的。

image.png 然后我们看到这里Enumeration是urls,为什么是urls,因为这里获取的是自己项目里的META-INF/spring.factories文件以及引用jar包里的META-INF/spring.factories文件。

我们在result上打个断点,发现加载了46个key value,可是我们在项目里只写了com.jackxu.zhengcheng.spi.Log这三个,剩下的就是在别的jar包里加载的key value。比如我们熟悉的EnableAutoConfiguration,一共有131个,这个就是在spring-boot-autoconfigure包里读取出来的。

image.png 最后SpringFactoriesLoader.loadFactoryNames返回的就是上面截图中com.jackxu.zhengcheng.spi.Log作为key返回的3个list,SpringFactoriesLoader.loadFactories就不分析了,只是调用了loadFactoryNames拿到了接口类型对应的所有类的名称,多了一步反射实例化而已。

@EnableAutoConfiguration

在第一篇文章中介绍了@SpringBootApplication是一个组合注解,它包含了@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan这三个注解。这里来介绍@EnableAutoConfiguration注解。

image.png @EnableAutoConfiguration注解上面又有一个核心注解@Import,在第一篇文章中介绍过它的功能,可以引入三种类型的对象:

  1. 普通 Bean 或者带有 @Configuration 的配置文件
  2. 实现 ImportSelector 接口进行动态注入
  3. 实现 ImportBeanDefinitionRegistrar 接口进行动态注入

我们这里用的就是第二种,动态注入,下面来介绍一下它的写法。新建三个类。

image.png

这个就是实现DeferredImportSelector接口的固定写法,这么写了spring才能获取到需要实例化的类。

/**
 * @author jack xu
 */
public class DeferredImportSelectorDemo implements DeferredImportSelector {

    //返回需要实例化的类,这个方法spirng是不会直接调的,这里是给下面process方法调的
    @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;
    }

    //内部类,实现Group
    private static class DeferredImportSelectorGroupDemo implements Group {

        List<Entry> list = new ArrayList<>();


        //spring会调这个方法,收集需要实例化的类
        @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));
            }
        }

        //spring会调这个方法,返回给spring容器进行实例化
        @Override
        public Iterable<Entry> selectImports() {
            System.out.println("==============DeferredImportSelectorGroupDemo.selectImports===========");
            return list;
        }
    }
}
复制代码

需要实例的bean

public class DeferredBean {

    @PostConstruct
    public void init() {
        System.out.println("DeferredBean,我是通过Import注解加载进来的");
    }
    
}
复制代码

通过Import导进来,spring会扫描收集Import注解里面需要实例化的类。

@Component
@Import(DeferredImportSelectorDemo.class)
public class ImportBean {
}
复制代码

启动执行一下,代码里打印的日志全显示出来了,证明spring确实是走的这一套逻辑。

image.png

下面看下AutoConfigurationImportSelector,确实也是这个套路,实现了DeferredImportSelector接口

image.png

返回了实现Group接口的类 image.png

实现了process方法,selectImports方法 image.png

那process方法里我们猜也能够猜到,它的功能是收集需要交给spring容器需要实例化的类,点进去 getAutoConfigurationEntry方法看下。

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return EMPTY_ENTRY;
   }
   //获取@SpringBootApplication的配置属性
   AnnotationAttributes attributes = getAttributes(annotationMetadata);
   //获取候选的所有类的名称
   List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
   configurations = removeDuplicates(configurations);
   //从注解配置属性中获取需要排除的类
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   //从候选的类中删除需要排除的类
   configurations.removeAll(exclusions);
   //SPI的扩展,获取过滤器实例对候选的类过滤
   configurations = getConfigurationClassFilter().filter(configurations);
   fireAutoConfigurationImportEvents(configurations, exclusions);
   //把候选的所有类包装成AutoConfigurationEntry对象
   return new AutoConfigurationEntry(configurations, exclusions);
}
复制代码

那么怎么收集就是通过上面说的SPI的方式,点进getCandidateConfigurations方法看下,SpringFactoriesLoader.loadFactoryNames,获取了131个类型,和上面演示SPI的时候数量一样。

image.png

其实就是收集spring.factories文件中以@EnableAutoConfiguration类型为key的所有的类,然后把这些类交给spring去实例化,而这些类就是我们说的aop、事务、缓存、mvc等功能的支持类,从而达到自动装配。

后记

最后在演示一下自动装配,写一个需要注入的类Person类

image.png

在META-INF/spring.factories下加上org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.jackxu.zhengcheng.spi.Person这句话 image.png 启动一下,Person类就被注进来了

image.png 而DeferredBean是通过@Import注解加载进来的,所以我们想把一个类加载到Spring容器的时候,又多了两种方式,看完这篇文章原理相信大家也都明白了,最后感谢收看,如果你喜欢请点一个赞!

猜你喜欢

转载自juejin.im/post/7056338149655445512