(二)SpringBoot2.0基础之HelloWorld解析

在这里插入图片描述

知其然 知其所以然

创作不易 求点赞👍 求关注❤️ 求分享👥

絮叨

本文是SpringBoot系列的第二篇文章,本篇主要是对第一篇的HelloWorld程序的解析。也会涉及到SpringBoot的一些注解原理的讲解。

正文

为什么SpringBoot可以使用这么少的配置,只是写个主启动类,在pom文件导入一个springboot依赖,就能启动一个Web项目呢?为什么会这么神奇呢?接下来我们就基于第一篇的HelloWorld程序去分析下SpringBoot为什么可以这么神奇。
在分析之前,先普及一个名词starter(翻译为启动器,开胃小吃)。它可以说是SpringBoot中一个接触最多的一个名词,可以认为starter是一种服务的整合——使得使用某个功能的开发者不需要关注各种依赖库的处理,不需要具体的配置信息,由Spring Boot自动通过classpath路径下的类发现需要的Bean,并织入bean。

POM解析

首先呢,我们分析一下pom文件。在pom文件中我们导入了一个父项目并且依赖了一个SpringBoot-web的starter。

  1. 父项目spring-boot-starter-parent
    <parent>
       <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.5.RELEASE</version>
    </parent>
    
    点进父项目,我们发现父项目又依赖了一个叫做spring-boot-dependencies的项目。
    <parent>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-dependencies</artifactId>
      <version>2.2.5.RELEASE</version>
      <relativePath>../../spring-boot-dependencies</relativePath>
    </parent>
    
    点进这个项目,可以看到它定义了每一个依赖的版本。spring-boot-dependencies这个项目的作用就是管理SpringBoot当前版本的所有依赖的版本。 所以以后我们再导入其他依赖后,只要在SpringBoot的依赖中(基本我们用到的主流依赖都在SpringBoot中集成了),就不需要指定版本号了,它会随着SpringBoot的版本号,自动导入其对应的版本号。如果没有在SpringBoot依赖中的管理,就需要我们自己去指定对应的版本号。
    在这里插入图片描述
  2. 导入的web依赖
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
    我们可以看到,在这里我们并没有指定版本号,因为它在父项目spring-boot-dependencies中声明了版本号,就不需要我们自己去指定了。
    在这里插入图片描述
    接下来我们看下spring-boot-starter-web,首先我们可以给他看成两部分,spring-boot-starterweb
    • spring-boot-starter它是SpringBoot的场景启动器,每种框架都有自己的启动器,它很好的降低了使用框架时的复杂度。
    • web标示着它是web的场景启动器,它会自动导入我们web项目需要用到的依赖。
      在这里插入图片描述在这里插入图片描述
      Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),我们要用什么功能,就导入对应的starter就可以了,Springboot会自动帮我们导入对应的依赖。

主启动类解析

  • @SpringBootApplication标注在类上,标明这是一个主配置类,告诉程序这是一个SpringBoot应用。这是一个SpringBoot的约定(SpringBoot的核心理念就是约定优于配置),只要是标注了这个注解,SpringBoot就要运行这个类下面的main方法去启动这个SpringBoot应用。
    下面我们看一下这个注解都干了什么:
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @SpringBootConfiguration
    @EnableAutoConfiguration
    @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
    )
    public @interface SpringBootApplication {
    
    我们可以看到@SpringBootApplication是一个组合注解,包括@SpringBootConfiguration,@EnableAutoConfiguration和@ComponentScan
    • @SpringBootConfiguration标注当前类是一个SpringBoot配置类。就像之前我们在xml中去配置一样,只不过在这里我们用类去代替xml文件。
      @SpringBootConfiguration底层被@Configuration注解标注,表明这是一个配置类,它会被@ComponentScan扫描到,也可以说被@SpringBootConfiguration标注的类是容器中的一个组件。

      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Configuration
      public @interface SpringBootConfiguration {
      
    • @EnableAutoConfiguration开启自动配置。SpringBoot根据我们添加的jar包来配置项目的默认配置。以前我们导入jar要自己配置一些属性,而现在SpringBoot帮我们把这些配置好了。
      那SpringBoot是怎么帮我们去配置的呢,请大家继续往下看

      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @AutoConfigurationPackage
      @Import({AutoConfigurationImportSelector.class})
      public @interface EnableAutoConfiguration {
      
      • @AutoConfigurationPackage:自动配置包。
        @Target({ElementType.TYPE})
        @Retention(RetentionPolicy.RUNTIME)
        @Documented
        @Inherited
        @Import({Registrar.class})
        public @interface AutoConfigurationPackage {
        
        • @Import({Registrar.class}):Spring的底层注解@Import,给容器中导入一个Registrar.class组件。该组件将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器。
          static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports{
              Registrar() {
              }
              public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
              	// registry就是保存要注册到容器中的组件的Bean定义.也就是保存主配置类的所在包及下面所有子包里面的所有Bean定义和一些默认的组件。
              	// (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()就是主配置类的所在包及下面所有子包的路径
                  AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
              }
              public Set<Object> determineImports(AnnotationMetadata metadata) {
                  return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
              }
          }
          
          在这里插入图片描述
          所以这也就是为什么主启动类要写在包外面的原因:因为@SpringBootApplication只会扫描@SpringBootApplication注解标记类包下及其子包的类(特定注解标记,比如说@Controller,@Service,@Component,@Configuration和@Bean注解等等)纳入到spring容器,如果我们定义的Bean不在@SpringBootApplication注解标记类相同包下及其子包的类,所以需要我们去配置一下扫包路径。
      • @Import({AutoConfigurationImportSelector.class}):Spring的底层注解@Import,给容器中导入一个AutoConfigurationImportSelector.class组件。该组件是我们要导入哪些组件的选择器。它会将所有需要导入的组件以全类名的方式存到一个AutoConfigurationImportSelector.AutoConfigurationEntry类型的内部类中返回。之后这些组件就会被添加到spring容器中。
        我们在AutoConfigurationImportSelector.getAutoConfigurationEntry()方法上添加一个断点并以debug方式启动主启动类。
        package org.springframework.boot.autoconfigure;
        
        public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
            protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
            	// annotationMetadata是注解的元信息,包括注解的类的全路径名
                if (!this.isEnabled(annotationMetadata)) {
                    return EMPTY_ENTRY;
                } else {
                    AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
                    // 根据标注类的元信息和属性得到一个configurations的List集合,这个configurations保存的就是我们在容器中要导入的组件。
                    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
                    // 接下来都是对configurations的一些处理,比如去重、去掉一些不包括的组件等等,最终返回一个AutoConfigurationImportSelector.AutoConfigurationEntry类型的对象。
                    configurations = this.removeDuplicates(configurations);
                    Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
                    this.checkExcludedClasses(configurations, exclusions);
                    configurations.removeAll(exclusions);
                    configurations = this.filter(configurations, autoConfigurationMetadata);
                    this.fireAutoConfigurationImportEvents(configurations, exclusions);
                    return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
                }
            }
        }
        
        在这里插入图片描述
        在这里插入图片描述
        也就是说AutoConfigurationImportSelector给容器中导入非常多的自动配置类,就是给容器中导入这个场景需要的所有组件,并配置好这些组件。我们也可以看到他的所有自动配置都是叫XxxxAutoConfiguration,这也是SpringBoot的一个约定,我的自动配置类都是叫XxxxAutoConfiguration。以后我们要是找某场景的自动配置,就可以搜XxxxxAutoConfiguration就可以找到了。
        由上面代码可以知道最重要的就是这一行,之后都是对这个结果进行处理的。接下来我们看看这个方法到底做了什么。
        List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        
        protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        	// 这个configurations实际由这个方法得到,这个方法有两个参数,第一个参数是EnableAutoConfiguration对象,第二个参数是类加载器。
            List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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;
        }
        
        public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
            String factoryTypeName = factoryType.getName();
            // 调用loadSpringFactories()方法,参数是classLoader。
            // 后面的getOrDefault()方法是把loadSpringFactories()方法的返回结果进行判断,
            // 如果Map集合中有这个factoryTypeName指定的key时,就使用这个key值,返回key对应的value。
            // 如果没有就返回默认值Collections.emptyList()
            // 把结果转换成List返回。
            return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
        }
        
        private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
           MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
           if (result != null) {
               return result;
           } else {
               try {
               	   // 从类路径或系统资源路径下的META-INF/spring.factories中获取资源
                   Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                   LinkedMultiValueMap result = new LinkedMultiValueMap();
        
                   while(urls.hasMoreElements()) {
                   	   // 循环上面获取到的资源,转化成Properties资源
                       URL url = (URL)urls.nextElement();
                       UrlResource resource = new UrlResource(url);
                       Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                       // 转化成Set集合遍历
                       Iterator var6 = properties.entrySet().iterator();
        
                       while(var6.hasNext()) {
                       	   // 得到key和value,对value做一些处理后,把key和value保存到LinkedMultiValueMap对象中返回
                           Entry<?, ?> entry = (Entry)var6.next();
                           String factoryTypeName = ((String)entry.getKey()).trim();
                           String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                           int var10 = var9.length;
        
                           for(int var11 = 0; var11 < var10; ++var11) {
                               String factoryImplementationName = var9[var11];
                               result.add(factoryTypeName, factoryImplementationName.trim());
                           }
                       }
                   }
        
                   cache.put(classLoader, result);
                   return result;
               } catch (IOException var13) {
                   throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
               }
           }
        }
        
        由此,我们可以知道,SpringFactoriesLoader.loadFactoryNames()方法得作用是从类路径或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中。
        那它导入了什么呢?我们去看一下。在spring.factories中配置的这些XxxxAutoConfiguration自动配置类正好是我们在上面debug中看到的那些。
        在这里插入图片描述
        在这里插入图片描述
        那自动配置帮我们配置了什么呢?我们看以下WebMvcAutoConfiguration。它帮我们自动配置了视图解析器。如果没有自动配置,我们都要自己去配置视图解析器。还给我们自动配置了很多的Bean。大家可以自己看一下。
        在这里插入图片描述
        总结一下:Spring Boot在启动的时候从类路径下或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作。以前我们需要自己配置的东西,自动配置类都帮我们。为什么SpringBoot会有那么神奇的效果,会需要那么少的配置,就是因为导入的这些配置类会自动帮我们配置。
    • @ComponentScan扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。

总结

本篇我们基于第一篇的HelloWorld程序对SpringBoot可以简单方便快速的搭建一个Web项目的解析。
从pom文件和主启动类进行了分析。pom文件中我们依赖SpringBoot的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。
主启动类我们基于@SpringBootApplication注解进行分析,因为包含@SpringBootConfiguration注解,所以这是一个配置类,它会被@ComponentScan扫描到。因为包含@EnableAutoConfiguration注解,所以会把主配置类的所在包及下面所有子包里面的所有组件扫描到Spring容器。所以SpringBoot在启动的时候从类路径下或系统资源路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的配置类,将这些值作为自动配置类导入到容器中,我们就不需要像以前一样去配置。因为包含@ComponentScan,所以它会扫描当前包及其子包下被@Component,@Controller,@Service,@Repository注解标记的类并纳入到spring容器中进行管理。

想要了解SpringBoot的更多细节,请看下回分解。

如果本篇博客有任何错误,请批评指教,不胜感激 !
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值