SpringBoot基础

SpringBoot基础

从Spring到SpringBoot

  • Spring通过IOCAOP实现了企业级的开发框架,虽然组件代码是轻量级的,但是配置文件却是重量级的,Spring Boot则简化Spring应用开发,基于约定大于配置(为大部分配置组件提供了默认配置)的思想,just run 就能创建一个独立的,产品级别的应用

    • SpringBoot框架并不是微服务框架,只是为微服务框架的组件构建提供了一个很好的脚手架
    • SpringBoot提供了J2EE一站式解决方案;Spring Cloud提供了分布式整体解决方案

Spring生态

  • Spring严格来说是一个生态,而非仅仅几个常见的框架,基本上覆盖了以下几个主要场景的开发

    • web开发

      • Spring Framework
    • 数据访问

      • Spring Data
    • 安全控制

      • Spring Security
    • 分布式

      • Spring Cloud
      • Spring Session
      • ..
    • 消息服务

      • Spring AMQP
    • 批处理

      • Spring Batch
      在大型企业中,由于业务复杂、数据量大、数据格式不同、数据交互格式繁杂,并非所有的操作都能通过交互界面进行处理。而有一些操作需要定期读取大批量的数据,然后进行一系列的后续处理。这样的过程就是批处理
    • ...
  • 可以说Spring以一己之力提升了整个Java的开发生态,使得Java语言在主流开发场景中都能大显身手,正如Spring官网的首页图展示的那样

    image-20210924220823740

SpringBoot存在的意义

  • 一般来说,Spring生态的学习路线都是从SSM(Spring、Spring MVC、MyBatis)到SpringBoot,SSM中的配置地狱是降低开发效率的巨大障碍;除此之外,上述说的Spring生态的众多功能的搭建与配合使用同样意味着大量的配置,能否简化配置,甚至做到自动配置(约定大于配置的思想),以提升业务开发效率?SpringBoot应运而生。具体的优势在以下几个方面:

    • 可以创建独立的Spring应用
    • 内嵌Web服务器
    • 基于starter的场景自动配置
    • 自动配置Spring与第三方功能
    • 提供生产级别的监控、健康检查,外部化配置
    • 无代码生成,无需编写XML
  • SpringBoot的缺点

    • 版本迭代快
    • 封装比较深,查看源码难度比较高

SpringBoot2.0

  • 作为SpringBoot的新一代版本,SpringBoot2.0在整个框架结构设计上都有重大升级,主要包括以下两部分:

响应式编程

image-20210924223917397

  • 提出了响应式技术栈,即以异步非阻塞的方式使得整个框架能够以更少的系统资源处理更多的并发请求,对应的底层Web开发框架是Spring WebFlux,而不是传统的同步阻塞式的SpringMVC,这一部分可参考后续对于[Spring WebFlux的学习介绍]()

源码设计调整

  • 完全基于JDK8构建,同时兼容JDK11甚至Java17,基于一些新的Java特性对内部源码进行了重新设计,比如JDK8之前,接口没有默认方法实现的特征,导致接口的实现类不得不实现全部的接口方法,即便只使用其中几个方法,为了解决此问题,Spring中大量使用的适配器模式,使用适配器实现接口方法为空方法,再让子类去重写方法,而在JDK8后,一个default关键字就解决了,实现类无必要实现全部方法,因此大量的adapter就消失了

HelloWorld项目

环境配置

  • 首先按照SpringBoot文档要求保证JDK版本为8或以上版本,Spring Framework版本、maven以及gradle版本参考要求即可

    • 可参考具体版本的SpringBoot文档的Getting Started页面,比如2.6.5版本

项目实现

  1. IDEA创建maven项目
  2. 引入starters

    
      org.springframework.boot
      spring-boot-starter-parent
      2.5.5
    
    
    
      
        org.springframework.boot
        spring-boot-starter-web
      
    
    • 父工程统一管理可能使用到的各个组件的版本
    • 导入Web场景启动器以实现Web应用开发
  3. 创建入口类与Controller

    @SpringBootApplication
    public class MainApplication {
        public static void main(String[] args) {
            SpringApplication.run(MainApplication.class, args);
        }
    }
    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String handleHello() {
            return "Hello SpringBoor 2";
        }
    }
    • 与Spring MVC开发的代码编写一致,最大的不同在于无需添加大量的配置
    • 注意业务代码的包应和入口类在同一个包下,否则无法解析业务代码中的的注解配置
  4. 启动运行

    1. 直接启动入口程序即可开启内置的服务器提供Web服务,完成本地测试
  5. 自定义配置

    server:
      port: 8888
    • 项目类路径(maven工程的resources目录下)添加名为application.yml配置文件,既可以做自由的配置
    • 所有可做的配置参考官方文档
  6. 部署,使用SpringBoot提供的打包为可执行jar包的方式

     
    
      
        
          org.springframework.boot
          spring-boot-maven-plugin
        
      
    
    • 执行mvn package即可获得打包好的jar文件,在目标服务器执行java -jar ${可执行jar包路径}即可运行整个工程
    • 在IDEA中可能显示红色划线错误表示找不到此plugin,应该是IDEA的一个bug

SpringBoot的两个特征

  • 以HelloWorld项目为例研究SpringBoot的两个特性

    • 依赖管理
    • 自动化配置(约定大于配置)

依赖管理

  • 本质上就是使用maven父工程和starter启动器做依赖管理

    • maven父工程做依赖版本管理
    • starter场景启动器做依赖包管理
Maven父工程
  • spring-boot-starter-parent-2.5.5.pom如下所示,可以发现spring-boot-starter-parent设置了默认的Java编译版本是8,因此如果项目使用更高版本的话,需要在项目的pom文件中显式设置这个properties

    org.springframework.boot
    spring-boot-dependencies
    2.5.5

spring-boot-starter-parent
pom
spring-boot-starter-parent
Parent pom providing dependency and plugin management for applications built with Maven

  1.8
  @
  ${java.version}
  ${java.version}
  UTF-8
  UTF-8
  • spring-boot-dependencies-2.5.5.pom
org.springframework.boot
spring-boot-dependencies
2.5.5
pom

    5.16.3
    2.7.7
    1.9.91
    2.17.0
    1.9.7
    3.19.0
    4.0.6
    4.0.3
    3.2.0
    1.10.22
    2.9.2
    4.11.3
    1.5.1
  ....



    
      
        org.apache.activemq
        activemq-amqp
        ${activemq.version}
      
      ...
  
  • 在pom文件中找到最终的父工程,即spring-boot-dependencies,这个父项目中定义了一堆properties,其中规定了几乎所有的主流组件的版本,所以这个spring-boot-dependencies用来管理SpringBoot项目中的依赖版本,是SpringBoot应用的版本管理中心,之后的各种引用依赖就不需要再注明版本号了

    • 实际上利用的是maven的版本继承的机制
  • 上述的机制也叫做SpringBoot中的版本仲裁机制,也是一种约定大于配置的思想的体现,就是事先按照版本匹配的约定做好配置,保持版本的一致,而不需要手动再配置,但是如果有特殊的版本需求或者是其他的不存在与版本管理中心的特殊依赖,则直接在当前的SpringBoot Maven工程显式的指定版本或者依赖即可

    • 例如,如果只是更改版本的话,只需要在项目pom文件中,使用property标签设置版本号即可

      
        5.1.43
      
  • 其他的SpringBoot maven更详细的配置参考
  • 如果使用Gradle搭建项目,参考

    • 以搭建普通Web应用为例,其配置文件如下所示:

      plugins {
          id 'org.springframework.boot' version '2.6.5'
          id 'io.spring.dependency-management' version '1.0.11.RELEASE'
          id 'java'
      }
      
      group = 'xyz.demoli'
      version = '0.0.1-SNAPSHOT'
      sourceCompatibility = '11'
      
      repositories {
          mavenCentral()
      }
      
      dependencies {
          implementation 'org.springframework.boot:spring-boot-starter-web'
          testImplementation 'org.springframework.boot:spring-boot-starter-test'
      }
      
      tasks.named('test') {
          useJUnitPlatform()
      }
      • 可以发现Gradle以Plugin的形式提供了依赖版本管理
场景启动器
  • 因为要开发Web应用所以在HelloWorld项目中引入了一个spring-boot-starter-web依赖,在后续的SpringBoot使用中会用到很多所谓的spring-boot-starter-*依赖,也就是场景启动器依赖
  • 场景启动器的意义在于可以将某应用场景的所有可能用到的依赖打包到一起,实现只需要引入一个启动器依赖就可以引入全部场景依赖的目的,简化依赖导入的流程,以spring-boot-starter-web为例,如下图所示,tomcat、Spring MVC、Spring等相关场景的依赖被统一打包在starter内导入到项目中

    SpringBoot基础_第1张图片

  • 启动器除了打包依赖,当然也负责这些依赖的版本管理。启动器给出了所有依赖的适配版本,如果项目使用默认提供的版本可以同样在pom文件中进行显式定制

    org.springframework.boot
    spring-boot-starter-web
    2.5.5
    
      
        org.springframework.boot
        spring-boot-starter
        2.5.5
        compile
      
      
        org.springframework.boot
        spring-boot-starter-json
        2.5.5
        compile
      
      
        org.springframework.boot
        spring-boot-starter-tomcat
        2.5.5
        compile
      
      
        org.springframework
        spring-web
        5.3.10
        compile
      
      
        org.springframework
        spring-webmvc
        5.3.10
        compile
      
    
  • 场景启动器不仅包含了特定开发场景需要的所有依赖还包含支持该场景的组件默认配置,具体参考下边的自动配置原理部分
  • 可以自定义场景启动器以满足定制化的开发需求,可参考文章[SpringBoot Starter开发]()

    • 一般自己开发的或者是第三方的starter的名称都是*-spring-boot-starter
  • SpringBoot官方支持的starters

自动配置原理

  • 以HelloWorld项目的Web应用开发场景为例
从程序入口看起
  • 入口main方法中的SpringApplicationrun方法是整个程序的实际入口,此方法可实现Spring环境的创建,包括各种Bean的注册以及后续要介绍的各种注解的解析与作用
  • 入口main方法所在的入口类上有一个关键的注解,即@SpringBootApplication被这个注解标注的类是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 {
      @AliasFor(
            annotation = EnableAutoConfiguration.class
        )
        Class[] exclude() default {};
    
        @AliasFor(
            annotation = EnableAutoConfiguration.class
        )
        String[] excludeName() default {};
    
        @AliasFor(
            annotation = ComponentScan.class,
            attribute = "basePackages"
        )
        String[] scanBasePackages() default {};
    
        @AliasFor(
            annotation = ComponentScan.class,
            attribute = "basePackageClasses"
        )
        Class[] scanBasePackageClasses() default {};
    
        @AliasFor(
            annotation = Configuration.class
        )
        boolean proxyBeanMethods() default true;
    }
    • 注意@AliasFor注解的作用是声明被标记的SpringBootApplication注解的属性与@AliasForannotation属性声明的注解的同名属性或者是attribute指定的属性是同一个属性
SpringBootConfiguration注解
  • 用来指定作用的入口类是Spring配置类,在包扫描后生效

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Configuration
    public @interface SpringBootConfiguration {
        @AliasFor(
            annotation = Configuration.class
        )
        boolean proxyBeanMethods() default true;
    }
    @SpringBootApplication
    public class SpringBootHelloWorldQuickApplication {
    
        public static void main(String[] args) {
            ConfigurableApplicationContext context = SpringApplication.run(SpringBootHelloWorldQuickApplication.class, args);
            SpringBootHelloWorldQuickApplication application = context.getBean("springBootHelloWorldQuickApplication", SpringBootHelloWorldQuickApplication.class);
            System.out.println(application);
        }
    }
    • 可以从IoC容器中获取主配置类Bean
  • 尽管启动类同时被ComponentScan注解和Configuration注解作用,但是ComponentScan做注解解析时,不会因为启动类被Configuration作用,而重复的去解析启动类上的注解,这一点在ComponentScanAnnotationParser的parse方法中有定义,该方法中添加了一个AbstractTypeHierarchyTraversingFilter该filter的match方法会过滤掉启动类

    public Set parse(AnnotationAttributes componentScan, final String declaringClass) {
        // ...
        ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader);
      // ...
      scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) {
        protected boolean matchClassName(String className) {
          return declaringClass.equals(className);
        }
      });
      return scanner.doScan(StringUtils.toStringArray(basePackages));
    }
ComponentScan注解
  • 默认的,主程序所在的包以及所有的下层级的包都会被扫描解析

    • 也可以使用@SpringBootApplication注解的scanBasePackages属性指定自定义的包扫描位置
    • 或者直接使用@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan三个注解代替@SpringBootApplication,然后再使用@ComponentScan配置包扫描路径即可
  • SpringBootApplication注解中的ComponentScan注解使用了两个自动扫描的排除规则

    @ComponentScan(
        excludeFilters = {@Filter(
        type = FilterType.CUSTOM,
        classes = {TypeExcludeFilter.class}
    ), @Filter(
        type = FilterType.CUSTOM,
        classes = {AutoConfigurationExcludeFilter.class}
    )}
    )
    public @interface SpringBootApplication {
        // ...
    }
    • TypeExcludeFilter主要是做扩展使用,可以通过继承此类实现自定义扫描排除规则
    • AutoConfigurationExcludeFilter 设置不扫描所有的既是配置类又是自动配置类的组件,以避免出现重复注册
    • 参考1
    • 参考2
EnableAutoConfiguration注解
  • EnableAutoConfiguration注解顾名思义用来开启starter提供的自动配置(自动配置的功能是以自动配置类的形式作为Bean进行注册后提供),该注解本质上还是合成注解

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import({AutoConfigurationImportSelector.class})
    public @interface EnableAutoConfiguration {
        String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
    
        Class[] exclude() default {};
    
        String[] excludeName() default {};
    }
    • @AutoConfigurationPackage注解

      @Target({ElementType.TYPE})
      @Retention(RetentionPolicy.RUNTIME)
      @Documented
      @Inherited
      @Import({Registrar.class})
      public @interface AutoConfigurationPackage {
      }
      • Import注解作用的Registar类实现了ImportBeanDefinitionRegistrar接口,因此实际上会调用其registerBeanDefinitions方法

        static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
          Registrar() {
          }
          // 此方法被调用
          public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
          }
        
          public Set determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
          }
        }
        
        public static void register(BeanDefinitionRegistry registry, String... packageNames) {
          if (registry.containsBeanDefinition(BEAN)) {
            BeanDefinition beanDefinition = registry.getBeanDefinition(BEAN);
            ConstructorArgumentValues constructorArguments = beanDefinition.getConstructorArgumentValues();
            constructorArguments.addIndexedArgumentValue(0, addBasePackages(constructorArguments, packageNames));
          } else {
            GenericBeanDefinition beanDefinition = new GenericBeanDefinition();
            beanDefinition.setBeanClass(AutoConfigurationPackages.BasePackages.class);
            beanDefinition.getConstructorArgumentValues().addIndexedArgumentValue(0, packageNames);
            beanDefinition.setRole(2);
            registry.registerBeanDefinition(BEAN, beanDefinition);
          }
        
        } 
               
        • 通过断点分析可得,实际上是将当前注解所标注的类(即入口类)所在包的路径封装到 org.springframework.boot.autoconfigure.AutoConfigurationPackages.BasePackages类型的Bean进行注册
      • @Import({AutoConfigurationImportSelector.class}),因为AutoConfigurationImportSelector类实现了DeferredImportSelector接口,所以该类会被作为配置类解析的最后一步,ConfigurationClassParser类的parse方法会调用AutoConfigurationImportSelector类的process方法

        public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
            // ...
        }
        public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
          //...
          // 核心方法 getAutoConfigurationEntry
          AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(this.getAutoConfigurationMetadata(), annotationMetadata);
          //...
        }
        protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
          //...
          // 核心方法是getCandidateConfigurations 通过该方法可以获取要注册的所有配置类集合,后续的操作都是对此配置类集合的
          List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
          // 对自动配置类集合做一些调整
          configurations = this.removeDuplicates(configurations);
          Set 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);
        }
        }
         protected List getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
           // 关键方法为loadFactoryNames
           List 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 loadFactoryNames(Class factoryType, @Nullable ClassLoader classLoader) {
          // org.springframework.boot.autoconfigure.EnableAutoConfiguration
          String factoryTypeName = factoryType.getName();
          // 关键方法 loadSpringFactories
          return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
        }
        private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) {
          // ...
          try {
            Enumeration urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            // ...
        }
        • 通过查看源码可知,实际上会扫描org.springframework.boot:spring-boot-autoconfigure包下的META-INF/spring.factories文件,会读取文件中的org.springframework.boot.autoconfigure.EnableAutoConfiguration项下的所有自动配置类

          • 本质上是扫描类路径下所有包内的META-INF/spring.factories文件,但是starter组件包一起其他依赖包内都没有META-INF/spring.factories文件,因此在自定义starter时,为了使自定义的默认配置生效,也需要向starter中照猫画虎的添加这么一个文件才行
        • getAutoConfigurationEntry方法对从getCandidateConfigurations中获得的所有自动配置类的集合进行了一定的修改,主要包括

          • 去重
          • 排除掉@EnableAutoConfiguration注解中的exclude和excludeName属性声明的配置类

            • getExclusions方法
          • 排除掉被条件装配注解作用的自动配置类中的不符合条件的部分

            • filter方法
            • 可参考下边的@Condition注解的原理部分
      • 自动配置类
        • SpringBoot中所有的默认配置都被集中管理在一个项目中,即spring-boot-autoconfigure
        • 所有的SpringBoot的官方场景启动器都会有spring-boot-starter这个依赖,这个依赖的又有一个依赖就是spring-boot-autoconfigure。查看其源码,可以发现有各种场景下的默认配置类,一般这些默认配置类都会以**AutoConfiguration命名自动配置类在满足特定的条件后被注册到Spring容器中,则其自动配置可以在SpringBoot项目中生效

          SpringBoot基础_第2张图片

          • spring-boot-autoconfigure项目除了提供各个场景下的自动配置类,还提供了对应的配置类供用户实现自定义配置,以提升灵活性。application.yaml配置文件中的可用配置都会映射到配置类**Properties中,**AutoConfiguration自动配置类会尝试加载注册到**Properties中的用户自定义配置,使其生效(如果有的话)

            // 以server.port=9000为例 对应的配置类是 org.springframework.boot.autoconfigure.web.ServerProperties
            @ConfigurationProperties(
                prefix = "server",
                ignoreUnknownFields = true
            )
            public class ServerProperties {
                private Integer port;
                private InetAddress address;
                //...
                  public void setPort(Integer port) {
                    this.port = port;
                }
                  //..
            }
            • 使用的关键注解是@ConfigurationProperties
        配置的按需加载
        • 前边说到META-INF/spring.factories中定义的自动配置类不会全部都被注册生效,只会在特定条件下生效,这种按条件注册的功能依赖于下边的一系列注解

          // 以批处理的自动配置类为例
          @Configuration(
              proxyBeanMethods = false
          )
          @ConditionalOnClass({JobLauncher.class, DataSource.class})
          @AutoConfigureAfter({HibernateJpaAutoConfiguration.class})
          @ConditionalOnBean({JobLauncher.class})
          @EnableConfigurationProperties({BatchProperties.class})
          @Import({BatchConfigurerConfiguration.class, DatabaseInitializationDependencyConfigurer.class})
          public class BatchAutoConfiguration {
            //...
          }
          • 关键注解是@ConditionalOnClass@ConditionalOnBean,顾名思义,其要求注解属性中的类被导入后,才会使得BatchAutoConfiguration的配置生效,而JobLauncher等类只有在批处理的场景启动器依赖导入或者是相关的主要第三方依赖导入后才会被导入到工程中,由此完成自动配置的按需加载
        @Conditional系列注解
        @Conditional扩展注解 作用(判断是否满足当前指定条件)
        @ConditionalOnJava 系统的java版本是否符合要求
        @ConditionalOnBean 容器中存在指定Bean
        @ConditionalOnMissingBean 容器中不存在指定Bean
        @ConditionalOnExpression 满足SpEL表达式指定
        @ConditionalOnClass 系统中有指定的类
        @ConditionalOnMissingClass 系统中没有指定的类
        @ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者这个Bean是首选Bean
        @ConditionalOnProperty 系统中指定的属性是否有指定的值
        @ConditionalOnResource 类路径下是否存在指定资源文件
        @ConditionalOnWebApplication 当前是web环境
        @ConditionalOnNotWebApplication 当前不是web环境
        @ConditionalOnJndi JNDI存在指定项
        • 上边列举的都是可以直接拿来用的条件装配的注解,即@Conditional的扩展注解,@Conditional注解本身的使用就相对繁琐了,需要自己定义判断逻辑

          @Component
          // 根据OnSmtpEnvCondition中的条件判断是否注册SmtpMailService
          @Conditional(OnSmtpEnvCondition.class)
          public class SmtpMailService implements MailService {
              ...
          }
          // 实现Condition接口
          public class OnSmtpEnvCondition implements Condition {
              public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
                  return "true".equalsIgnoreCase(System.getenv("smtp"));
              }
          }
          • OnSmtpEnvCondition的条件是存在环境变量smtp,值为true。这样就可以通过环境变量来控制是否创建SmtpMailService
        • 这个系列的依赖可以作用在方法上,与@Bean一起使用,决定该方法的返回对象是否要注册到容器中,也可以作用在类上,与@Configuration一起使用,决定该配置类整体是否生效
        • ConditionalOnClass注解为例,本质上起作用的是OnClassCondition类,而该类实际上实现了AutoConfigurationImportFilter接口,通过其match方法判断条件是否生效,而match方法判断是否生效的过程则是在前文提到的AutoConfigurationImportSelector类的filter方法中实现的

          @Target({ElementType.TYPE, ElementType.METHOD})
          @Retention(RetentionPolicy.RUNTIME)
          @Documented
          @Conditional({OnClassCondition.class})
          public @interface ConditionalOnClass {
              Class[] value() default {};
          
              String[] name() default {};
          }
          class OnClassCondition extends FilteringSpringBootCondition {
              // ...
          }
          public interface AutoConfigurationImportFilter {
              boolean[] match(String[] autoConfigurationClasses, AutoConfigurationMetadata autoConfigurationMetadata);
          }
        @Profile注解
        • 使用该注解定义不同环境下的条件装配

          • dev 开发环境
          • test 测试环境
          • prod 生产环境
          @Configuration
          @ComponentScan
          public class AppConfig {
              @Bean
              @Profile("!test")
              ZoneId createZoneId() {
                  return ZoneId.systemDefault();
              }
          
              @Bean
              @Profile("test")
              ZoneId createZoneIdForTest() {
                  return ZoneId.of("America/New_York");
              }
          }
          • 如果当前的Profile设置为test,则Spring容器会调用createZoneIdForTest()创建ZoneId,否则,调用createZoneId()创建ZoneId
        • 在运行程序时,加上JVM参数-Dspring.profiles.active=test就可以指定以test环境启动,也可以指定多个环境-Dspring.profiles.active=test,master
        补充
        • 用户定制化配置的几种方式

          • 直接使用@Configuration + @Bean替换底层的组件

            • 底层大量使用了@ConditionOnMissingBean以优先使用用户注册的Bean
            • 推荐首先查看文档或者去大致看看源码的配置要点
          • 通过看文档或者直接查看自动配置中的配置文件类,对应的向系统配置文件application.yaml中添加自定义配置
          • 使用组件提供的customizer类或者configurer类

            • 通过查看文档或资料获得相关知识
        • 除了..AutoConfiguration系列的自动配置类与..Properties系列的配置文件类,SpringBoot中还有两种类型的类即:

          • ..Configurer是SpringBoot中的用于用户扩展配置的接口或者类,例如WebMvcConfigurer
          • ..Customizer用来进行定制配置,例如WebServerFactoryCustomizer
          • 参考[使用SpringBoot进行Web开发的文章]()
        • 语法提示,通过添加下边的插件提供配置文件中的关联提示的功能

          
            
              
                org.springframework.boot
                spring-boot-maven-plugin
                
                  
                    org.springframework.boot
                    spring-boot-configuration-processor
                  
                
              
            
          
          
          
            org.springframework.boot
            spring-boot-configuration-processor
            true
          
          • 同时注意在打包插件中配置打包时不包含该依赖,因为只是开发时要用到,没必要将其打包
        最佳实践
        • 根据需求引入官方或者第三方提供的starter

        • 一般都需要对场景做自定义配置,可以通过查官方的配置文档,或者直接从源码层面查看配置项

        • trick:在配置文件中使用debug=true以debug模式运行,控制台会有自动配置报告,可以看到生效的自动配置类与未生效的自动配置类以及其未生效的原因

          • Positive matches:这个栏目下是生效的自动配置类Negative matches:这个栏目下是未生效的自动配置类,以及未生效的原因(这一点可以用来做自定义配置失败的debug)
          • 如果引入了日志框架,可以将日志打印等径设置为debug,效果是类似的,当然更推荐上边的方式
        • 使用lombok优化Bean开发,SpringBoot已经管理了其版本,因此直接引入maven依赖即可,然后在IDEA中安装lombok插件以优化使用体验

          • @Data 等价于@Getter@Setter组合
          • @Getter
          • @Setter
          • @ToString
          • @NoArgsConstructor
          • @AllArgsConstructor
          • @EqualsAndHashCode
          • @Log4j2

            • 直接为当前类注入一个org.apache.logging.log4j.Logger类型的名为log的属性,用来做日志打印
            • 需要注意的是必须首先添加log4j2的依赖
        • 使用devtools热更新功能,可以添加maven依赖也可以在创建项目时在Spring initializer中指定

          
            org.springframework.boot
            spring-boot-devtools
            runtime
            true
          
          • 此时只需rebuild就可以使更改生效,其工作机制实质上就是监听classpath下的文件的变动,一旦监听到变动就重启服务器加载变动
          • 除了可以监听classpath路径下的文件的变化,还可以监听自定义的目录下的文件变化,以达到文件变动后,服务器重新启动的目的,参照codesheep-SpringBoot热部署加持
        • 推荐使用Spring Initializer创建项目(IDEA自带)可以通过勾选的方式做一系列场景配置,生成的项目自动引入相关依赖以及写好主配置类,以及特定场景的对应的文件结构,比如Web场景下会创建类路径下的static文件夹和template文件夹等等

        SpringBoot配置文件

        配置文件

        • SpringBoot默认使用两种配置文件

          • application.properties
          • application.yml

        Yaml语法

        • 介绍一下yaml的基本语法

          • 使用缩进表示层级关系
          • 缩进时不允许使用tab键,只允许使用空格
          • 缩进的空格数不重要,只要相同层级的元素左侧对其即可
          • 大小写敏感
          • 表示数据使用k: v的形式展示 冒号后边必须要有一个空格
        • 数据类型

          • 字面量:字符串,数字,bool,date

            • 注意字符串类型的值,不需要加引号,如果加的话,''""分别对应着对其中的字符串中的转义字符进行字符串输出和转义输出

              • 当字符串中有特殊字符时,务必加引号
          • 对象,Map

            • 对象还是用键值对的形式来表示

              friends:
                  lastName: zhangsan
                  age: 20
            • 行内写法

              friends: {lastName: zhangsan,age: 20}
              • 注意行内写法中的对象属性冒号后边也得有空格
          • 数组

            • -表示数组中的一个元素

              pets:
               - cat
               - dog
               - pig
            • 行内写法

              pets: [cat,dog,pig]
          • 配置文件占位符

            server:
              port: 8081
            
            bean:
              # 随机数的拼接
              lastName: hello${random.uuid}
              age: ${random.int}
              boss: false
              birth: 1997/12/10
              maps: {k1: v1,k2: v2}
              lists: [lisi,zhaoliu]
              dog:
                # 使用:指定当属性不存在时的默认值
                name: ${bean.Name:lee}_dog
                # 复用前边的设置
                lastName: ${bean.lastName:gee}
                age: 2
            • 随机数占位符

              • random.value
              • random.int
              • random.int(int max)
              • random.long
            • 两种类型的配置文件都支持占位符的功能

        Profile

        • Profile是Spring对多环境的支持,程序开发的时候有几个环节 开发-测试-部署,三个场景有三个环境,不同的环境下配置文件也可能不同,可以通过激活,指定参数等方式快速切换环境,同时自动切换指定场景的配置文件

          • 为每一个环境指定一个配置文件,每一个配置文件对应一个profile,命名格式为application-{profile}.properties 例如:application-dev.properties application-prod.properties
          • 当工程中有多个配置文件的时候,默认加载的是不带profile的配置文件也就是application.properties/application.yml
        yaml文档块
        • 对于properties类型的配置文件来说,需要定义多个配置文件对应不同的profile,但是对于yaml配置文件来说,可以把多个profile通过文档块的形式写到一个文件中
        • 所谓的文档块就是在yml配置文件中用---符号分割为多个块,每一个块就对应着一个profile的配置文件

          server:
            port: 8081
          spring:
            profiles:
              active: prod
          ---
          server:
            port: 8082
          spring:
            profiles: dev
          ---
          server:
            port: 8083
          spring:
            profiles: prod
          • 非默认配置文件文档块使用spring.profiles声明文档块属于哪个环境
        环境切换方式
        • 默认的不指定profile的配置文件中进行激活

          spring:
            profiles:
              active: dev #profile名字
        • JVM参数

          • 在idea的edit configuration中配置vm arguments -Dspring.profiles.active=dev
          • 优先级高于配置文件
        • 命令行
          • 在idea的edit configuration中配置program arguments --spring.profiles.active=dev
          • 这种配置方法优先级最高
        默认配置文件优先级
        • 所有位置的默认文件都会被加载,高优先级配置的内容会覆盖低优先级配置的内容,不重复的内容则是补充内容

          • root:./config/application.yaml
          • root:./application.yaml
          • classpath:/config/application.yaml
          • classpath:/application.yaml
        • 参考
        外部配置文件
        • 所谓使用外部配置文件指的是对SpringBoot Application进行部署的时候可以指定包外部的配置文件,来替代内部的配置,以提高配置的自由度(配置修改后不必重新打包)
        • 按照优先级排列

          • 命令行中直接指定配置参数:java -jar ***.jar --server.port=8090 优先级最高
          • 命令行中指定配置文件:可以使用--spring.config.location=...指定配置文件的位置
          • jar包外部的application-{profile}.properties或者application.yml(带spring.profile)配置文件

            • 带profile的优先(profile的指定方式就是前边说的那三种)
            • jar包外指的是与jar包同目录的配置文件,会自动执行加载,不用配置其余命令行参数
          • jar包外部的application.properties或者application.yml(不带spring.profile)配置文件
          • jar包内部的application-{profile}.properties或者application.yml(带spring.profile)配置文件
          • jar包内部的application.properties或者application.yml(不带spring.profile)配置文件

            • 如果同时出现则application.properties优先
        • 参考

        SpringBoot事件监听

        • 所谓的事件监听就是指SprigBoot为应用启动的不同时期提供了可以监听的钩子组件,允许应用执行到特定的阶段时,执行注册好的钩子函数
        • 注册钩子函数的方式就是实现SpringBoot提供的特定的接口,然后注册成为Spring IoC中的组件或者是在当前应用的类路径下的META-INF文件夹下的spring.factories文件中进行配置,具体的看下边的例子

        几个常用的事件回调机制

        • 前三个需要配置在spring.factories中,后两个需要注册成为IoC组件

          • org.springframework.context.ApplicationContextInitializer

            public interface ApplicationContextInitializer {
                void initialize(C var1);
            }
            public class HelloApplicationContextInitializer implements ApplicationContextInitializer {
                @Override
                public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
                    System.out.println("ApplicationContextInitializer initialize");
                }
            }
            org.springframework.context.ApplicationContextInitializer=\
            com.springboot.springboot.listenerAndRunner.HelloApplicationContextInitializer
          • org.springframework.boot.SpringApplicationRunListener

            public interface SpringApplicationRunListener {
                default void starting() {
                }
            
                default void environmentPrepared(ConfigurableEnvironment environment) {
                }
            
                default void contextPrepared(ConfigurableApplicationContext context) {
                }
            
                default void contextLoaded(ConfigurableApplicationContext context) {
                }
            
                default void started(ConfigurableApplicationContext context) {
                }
            
                default void running(ConfigurableApplicationContext context) {
                }
            
                default void failed(ConfigurableApplicationContext context, Throwable exception) {
                }
            }
            public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {
                /**
                 * 需要提供此构造函数,否则报错
                 *
                 * @param application SpringApplication
                 * @param args        参数
                 */
                public HelloSpringApplicationRunListener(SpringApplication application, String[] args) {
                }
            
                @Override
                public void starting() {
                    System.out.println("SpringApplicationRunListener starting");
                }
            
                @Override
                public void environmentPrepared(ConfigurableEnvironment environment) {
                    System.out.println("SpringApplicationRunListener environmentPrepared " + environment.getSystemProperties().get("os.name"));
                }
            
                @Override
                public void contextPrepared(ConfigurableApplicationContext context) {
                    System.out.println("SpringApplicationRunListener contextPrepared");
                }
            
                @Override
                public void contextLoaded(ConfigurableApplicationContext context) {
                    System.out.println("SpringApplicationRunListener contextLoaded");
                }
            
                @Override
                public void started(ConfigurableApplicationContext context) {
                    System.out.println("SpringApplicationRunListener started");
                }
            
                @Override
                public void running(ConfigurableApplicationContext context) {
                    System.out.println("SpringApplicationRunListener running");
                }
            
                @Override
                public void failed(ConfigurableApplicationContext context, Throwable exception) {
                    System.out.println("SpringApplicationRunListener failed");
                }
            }
            org.springframework.boot.SpringApplicationRunListener=\
            com.springboot.springboot.listenerAndRunner.HelloSpringApplicationRunListener
          • org.springframework.context.ApplicationListener

            @FunctionalInterface
            public interface ApplicationListener extends EventListener {
                void onApplicationEvent(E var1);
            }
            public class HelloApplicationListener implements ApplicationListener {
            
                @Override
                public void onApplicationEvent(ApplicationPreparedEvent applicationPreparedEvent) {
                    System.out.println(applicationPreparedEvent.getTimestamp());
                }
            }
            org.springframework.context.ApplicationListener=\
            com.springboot.springboot.listenerAndRunner.HelloApplicationListener
          • org.springframework.boot.ApplicationRunner

            @FunctionalInterface
            public interface ApplicationRunner {
                void run(ApplicationArguments args) throws Exception;
            }
            @Component
            public class HelloApplicationRunner implements ApplicationRunner {
                @Override
                public void run(ApplicationArguments args) throws Exception {
                    System.out.println("ApplicationRunner run " + args);
                }
            }
          • org.springframework.boot.CommandLineRunner

            @FunctionalInterface
            public interface CommandLineRunner {
                void run(String... args) throws Exception;
            }
            @Component
            public class HelloCommandLineRunner  implements CommandLineRunner {
                @Override
                public void run(String... args) throws Exception {
                    System.out.println("CommandLineRunner run " + Arrays.toString(args));
                }
            }

        从启动流程分析回调机制

        • 在SpringApplication的run方法上打断点分析启动流程

          1. 创建SpringApplication对象 org.springframework.boot.SpringApplication#100

            public SpringApplication(Class... primarySources) {
              this((ResourceLoader)null, primarySources);
            }
            
            public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
              this.sources = new LinkedHashSet();
              this.bannerMode = Mode.CONSOLE;
              this.logStartupInfo = true;
              this.addCommandLineProperties = true;
              this.addConversionService = true;
              this.headless = true;
              this.registerShutdownHook = true;
              this.additionalProfiles = new HashSet();
              this.isCustomEnvironment = false;
              this.lazyInitialization = false;
              this.resourceLoader = resourceLoader;
              // 必须提供非空的主配置类,否则无法启动
              Assert.notNull(primarySources, "PrimarySources must not be null");
              // 保存主配置类
              this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
              // 判断web应用的类型,注意这里并不是默认就是web应用,实际上其内部会判断是不是web应用,如果不是会返回NONE,是web应用的话,也要区分是哪种类型 1. REACTIVE 2,SERVLET
              this.webApplicationType = WebApplicationType.deduceFromClasspath();
              // 从类路径下的META-INF/spring.factories中获得org.springframework.context.ApplicationContextInitializer 并保存起来
              this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
              //  同样的从类路径下的META-INF/spring.factories中获得配置的org.springframework.context.ApplicationListener 并保存起来
              this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
              // 从多个配置类中找到含有main方法的入口类
              this.mainApplicationClass = this.deduceMainApplicationClass();
            }
            • 程序入口的main方法未必一定在主配置类上,因此在知道主配置类的前提下寻找含有main方法的入口类并不冲突
            • 应用默认加载的ApplicationContextInitializer如下,红色标记为自定义

              SpringBoot基础_第3张图片

            • 应用默认加载的ApplicationListener如下,红色标记为自定义

              SpringBoot基础_第4张图片

          2. 执行run方法 org.springframework.boot.SpringApplication#138

            public ConfigurableApplicationContext run(String... args) {
              StopWatch stopWatch = new StopWatch();
              stopWatch.start();
              // 声明一个ioc容器
              ConfigurableApplicationContext context = null;
              Collection exceptionReporters = new ArrayList();
              this.configureHeadlessProperty();
              // 获取org.springframework.boot.SpringApplicationRunListener
              // 从类路径的META-INF的spring.factories中获得配置好的runlisteners
              SpringApplicationRunListeners listeners = this.getRunListeners(args);
              // 回调执行所有SpringApplicationRunListener的starting方法
              listeners.starting();
            
              Collection exceptionReporters;
              try {
                // 封装命令行参数
                ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                // 准备环境,创建环境并回调执行SpringApplicationRunListener的environmentPrepared方法
                ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
                this.configureIgnoreBeanInfo(environment);
                // 打印spring的图标
                Banner printedBanner = this.printBanner(environment);
                // 创建ioc容器,并且根据前边的判断的web应用的类型来创建并返回不同类型的容器
                context = this.createApplicationContext();
                exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
                // 准备上下文,将environment环境存储到容器中,并回调执行所有的保存的ApplicationContextInitializer的initialize方法, 执行SpringApplicationRunListeners的contextPrepared方法,在prepareContext函数执行的最后执行SpringApplicationRunListeners的contextLoaded方法
                this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
                // 刷新环境 IOC容器初始化
                // 如果是web应用的话,还会创建Tomcat容器,并启动Tomcat容器
                // 扫描,创建加载 所有的组件(配置类,组件,自动配置都会在这里生效)
                this.refreshContext(context);
                // 空方法,可能只是为了与前几个版本进行兼容吧
                this.afterRefresh(context, applicationArguments);
                stopWatch.stop();
                if (this.logStartupInfo) {
                  (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
                }
                // 回调执行所有SpringApplicationRunListeners的started方法
                listeners.started(context);
                // 从IOC容器中获得ApplicationRunner,和CommandLineRunner,并先后进行回调其run方法
                this.callRunners(context, applicationArguments);
              } catch (Throwable var10) {
                this.handleRunFailure(context, var10, exceptionReporters, listeners);
                throw new IllegalStateException(var10);
              }
            
              try {
                // 回调执行所有SpringApplicationRunListeners的running方法
                listeners.running(context);
                // 整个springboot应用启动完成以后返回ioc容器
                return context;
              } catch (Throwable var9) {
                this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
                throw new IllegalStateException(var9);
              }
            }
            • 需要注意,注册自定义的SpringApplicationRunListener时,除了实现SpringApplicationRunListener之外,还必须要提供特定结构的构造函数,否则会创建实例失败,这是因为在getRunListeners源码中是这样创建SpringApplicationRunListener实例的

              private SpringApplicationRunListeners getRunListeners(String[] args) {
                Class[] types = new Class[]{SpringApplication.class, String[].class};
                // getSpringFactoriesInstances方法提供了this与args两个构造函数的参数
                return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
              }
              
              private  Collection getSpringFactoriesInstances(Class type, Class[] parameterTypes, Object... args) {
                ClassLoader classLoader = this.getClassLoader();
                Set names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
                List instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
                AnnotationAwareOrderComparator.sort(instances);
                return instances;
              }
              
              private  List createSpringFactoriesInstances(Class type, Class[] parameterTypes, ClassLoader classLoader, Object[] args, Set names) {
                List instances = new ArrayList(names.size());
                Iterator var7 = names.iterator();
              
                while(var7.hasNext()) {
                  String name = (String)var7.next();
              
                  try {
                    Class instanceClass = ClassUtils.forName(name, classLoader);
                    Assert.isAssignable(type, instanceClass);
                    // 获取特定结构的构造函数以初始化,否则报错
                    Constructor constructor = instanceClass.getDeclaredConstructor(parameterTypes);
                    T instance = BeanUtils.instantiateClass(constructor, args);
                    instances.add(instance);
                  } catch (Throwable var12) {
                    throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, var12);
                  }
                }
              
                return instances;
              }

        总结执行时机

        1. 进入静态run方法
        2. 创建SpringApplication实例
        3. SpringApplication的实例run方法执行,声明IoC容器
        4. 执行SpringApplicationRunListener 的 starting方法
        5. 执行SpringApplicationRunListener 的 environmentPrepared方法
        6. 打印SpringBoot图标,创建IoC容器
        7. 执行ApplicationContextInitializer 的 initialize方法
        8. 执行SpringApplicationRunListener 的 contextPrepared方法
        9. 执行SpringApplicationRunListener 的 contextLoaded方法
        10. 执行refreshContext方法,初始化IoC容器,如果是Servlet Web应用,会根据配置创建对应的Servlet容器,比如Tomcat,并启动,除此之外,会扫描,创建,加载所有的组件(也包括自动配置的生效,即对应的一系列注解的生效)
        11. 执行SpringApplicationRunListener 的 started方法
        12. 执行ApplicationRunner 的 run方法
        13. 执行CommandLineRunner 的 run方法
        14. 执行SpringApplicationRunListener 的 running方法
        15. 至于 ApplicationListener,根据其泛型中声明的事件不同,其onApplicationEvent方法的执行时间也不同,具体情况具体分析

        参考

        1. SpringBoot官方文档
        2. SpringBoot更新日志
        3. B站视频资料

          1. 配套文档