Loading

35-SpringBoot

1. Spring 和 SpringBoot

1.1 Spring 能做什么?

Spring 的生态:web 开发、数据访问、安全控制、分布式、消息服务、移动开发、批处理 ...

1.2 Spring5 重要升级

1. 响应式编程

2. 内部源码设计

基于 Java8 的一些新特性,如:接口默认实现。重新设计源码架构。

2. SpringBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run". # 能快速创建出生产级别的Spring应用

2.1 时代背景

2.2 简要说明

  • 背景:J2EE 笨重的开发、繁多的配置、低下的开发效率;复杂的部署流程、第三方技术集成难度大。
  • 解决:Spring Boot → J2EE 一站式解决方案(补充:Spring Cloud → 分布式整体解决方案)
  • Spring Boot 来简化 Spring 应用开发的一个框架,是整个 Spring 技术栈的一个大整合。

  1. Create stand-alone Spring applications # 创建独立 Spring 应用
  2. Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files) # 内嵌 web 服务器
  3. Provide opinionated 'starter' dependencies to simplify your build configuration # 自动 starter 依赖,简化构建配置
  4. Automatically configure Spring and 3rd party libraries whenever possible # 自动配置 Spring 以及第三方功能
  5. Provide production-ready features such as metrics, health checks, and externalized configuration # 提供生产级别的监控、健康检查及外部化配置
  6. Absolutely no code generation and no requirement for XML configuration # 无代码生成、无需编写 XML

3. 环境准备

  • JDK 8:Spring Boot 推荐 JDK 7 及以上 → java version "1.8.0_20"
  • Maven 3.x:maven 3.3 以上版本 → Apache Maven 3.5.0
  • SpringBoot 2.1.6.RELEASE

4. HelloWorld

4.1 创建 Maven 项目

<groupId>cn.edu.nuist</groupId>
<artifactId>springboot-01-helloworld</artifactId>
<version>1.0-SNAPSHOT</version>

4.2 导入 SBoot 相关依赖

<!-- 继承 SpringBoot 官方指定的父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.2.RELEASE</version>
</parent>

<dependencies>
    <!-- spring-boot-starter-xxx:场景启动器,SpringBoot 框架提供了很多的 starter 模块  -->
    <!-- 加入 Web 开发所需要的场景启动器,引用了 SpringMVC、tomcat -->
    <!-- 指定 groupId 和 artifactId 即可,版本已在父工程中定义 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
</dependencies>

4.3 编写主程序

@SpringBootApplication // 将当前类标记为一个 SpringBoot 应用
public class HelloWorldMainApplication {
    // main 方法就是整个 SpringBoot 应用的入口,也就是说要通过运行该方法启动 SpringBoot 应用
    public static void main(String[] args) {
        SpringApplication.run(HelloWorldMainApplication.class, args);
    }
}

4.4 编写控制器

@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello() {
        return "HelloWorld!";
    }
}

4.5 启动主程序进行测试

4.6 简化部署

4.6.1 引入打包插件

<!-- Maven 构建过程相关配置 -->
<build>
    <!-- 构建过程中所需要用到的插件 -->
    <plugins>
        <!-- 这个插件将 SpringBoot 应用打包成一个可执行的 jar 包 -->
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

4.6.2 打成 jar 包

4.6.3 执行 jar 包

复制到桌面,然后通过 java -jar 命令运行。

此时,同样可以访问成功。因为:↓

5. 使用向导快速创建工程

IDE 都支持使用 Spring 的项目创建向导快速创建一个 Spring Boot 项目,创建过程中选择我们需要的模块,向导会联网创建 Spring Boot 项目。默认生成的 Spring Boot 项目中主程序已经生成好了,我们只需要我们自己的逻辑写功能代码即可。

  • static:保存所有的静态资源,如 JS、CSS、images 等
  • templates:保存所有的模板页面(Spring Boot 默认 jar 包使用嵌入式的 Tomcat,是不支持 JSP 的),可以使用模板引擎(Freemarker、Thymeleaf)。
  • application.properties:Spring Boot 应用的配置文件;可以用来修改一些默认设置;

6. HelloWorld 探究

6.1 依赖管理

<!-- SpringBoot 官方指定的父工程 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.2.RELEASE</version>
</parent>

<!-- 上述工程的父工程 ↓ -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.4.2</version>
</parent>

通过查看这个爷工程,可以发现,是它在真正管理 SpringBoot 应用里面的所有开发中常用的依赖的版本号,即“SpringBoot 的版本仲裁中心”。

如果想要自定义修改版本,只需在自己的 boot 项目中定义同名的 version,重写配置。

<properties>
    <mysql.version>5.6.1</mysql.version>
</properties>

6.2 场景启动器

Starters(场景启动器)are a set of convenient dependency descriptors that you can include in your application.

SpringBoot 将所有的功能场景都抽取出来,做成一个个的 starters(场景启动器),只需要在项目里面引入这些 starter,相关场景所有依赖都会导入进来。简单来说,就是你要用什么功能,就导入什么场景的启动器即可。

每一个 spring-boot-starter-* 就代表一种场景,只要引入该场景的 starter,这个场景的所有常规需要的依赖都会为我们自动引入。以下列出部分 SpringBoot 支持的场景,具体的自己去官网看。

还有一点,下面涂成红色的这个 starter,它不是对应的某个场景的启动器,它是所有 starter 最底层的依赖。

使用场景启动器之后,也就无需关注依赖的版本号们了,底层会自动进行版本仲裁。所以以后我们导入依赖默认是不需要写版本的,但如果导入了在“爷工程”中没版本仲裁的依赖,自然需要声明版本号。

当然也可以修改默认版本号,步骤:① 去 spring-boot-dependencies.pom 中(就是上面那张图)找到依赖版本对应的 key;② 在当前项目里面重写配置。

<!-- 比如我要修改 MySQL 依赖的版本 -->
<properties>
    <mysql.version>5.1.43</mysql.version>
</properties>

如果我们要自定义场景启动器,就别以 spring-boot 起名了,遵循规范:xxx-spring-boot-starter

6.3 自动配置

1. 自动配置 tomcat

没配置过 tomcat,为什么HelloWorld 工程能直接通过 localhost:8080 访问?因为 web-starter 依赖 tomcat-starter。

2. 自动配置 SpringMVC

(1) 引入 SpringMVC 全套组件(spring-web、spring-webmvc)

(2) 自动配好 SpringMVC 常用组件(核心控制器、字符编码过滤器、文件上传解析器、视图解析器 ...)

3. 默认的包扫描规则

主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来,无需以前的〈包扫描配置〉。如果想要改变扫描路径,可通过在主程序类上的注解 @SpringBootApplication(scanBasePackages="xxx") 进行修改(或者使用 ← 的拆分形式:@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 然后在包扫描注解上)。

4. 各项配置拥有默认值

默认配置最终都是映射到某个配置类上,例如文件上传相关的 MultipartProperties。配置文件的值最终会绑定每个对应的配置类对象上,这个对象会在容器中。

5. 按需加载所有自动配置项

虽说有非常多的 starter,但只有引入某个场景,这个场景的自动配置才会开启。

SpringBoot 所有的自动配置功能都在 spring-boot-autoconfigure 包里面(这个 jar 是在 spring-boot-starter 也即 core-starter 中被依赖的)。

7. 底层注解

7.1 @Configuration

@Configuration 注解标记一个类后,这个类成为配置类(容器中会有该配置类的一个实例),加载这个类中的配置和之前使用 XML 配置文件效果等同。

@Component
public @interface Configuration {
    @AliasFor(
        annotation = Component.class
    )
    String value() default "";

    boolean proxyBeanMethods() default true;
}

proxyBeanMethods:是否需要代理 beanMethod

  • Full(proxyBeanMethods = true):保证每个 @Bean 方法被调用多少次返回的组件都是单实例的,且容器中保存的是代理的配置类实例;
  • Lite(proxyBeanMethods = false):每个 @Bean 方法被调用多少次返回的组件都是新创建的,且容器中保存的是原生的配置类实例。

示例:当设置成 true(即 Full 模式,能解决组件依赖)。外部无论通过配置类实例调用该组件注册方法多少次,SpringBoot 总会先去检查 bean 是否已经加载到容器(保证组件单实例),默认返回的是同一个注册到容器中的单实例 bean。

@Import({Person.class}) // 给容器中自动创建出该类型的组件、默认组件名就是「全类名」
@Configuration(proxyBeanMethods=true)
public class MyConfig {

    @Bean // 默认单例
    public Person person1101() { // 方法名即组件名
        Person p = new Person("LJQ", 22);
        // Person 组件依赖 Pet 组件
        p.setPet(getPet());
        return p;
    }

    @Bean("pet")
    public Pet getPet() {
        return new Pet("oka");
    }
}

// ==========================================================================

@SpringBootApplication
public class MainApplication {
  public static void main(String[] args) {
    ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);
    Person person = context.getBean("person1101", Person.class);
    System.out.println(person.getPet() == context.getBean("pet", Pet.class)); // true
    System.out.println("===");
    String[] beanNames = context.getBeanNamesForType(Person.class);
    for (String beanName : beanNames) {
        System.out.println(beanName); // cn.edu.nuist.boot.bean.Person、person1101
    }
  }
}

Full(true)模式与 Lite(false)模式,如何选择:

  • 配置类组件之间无依赖关系用 Lite 模式加速容器启动过程,减少判断;
  • 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用 Full 模式。

对了,Spring 提供的 @Component、@Controller、@Service、@Repository 还是照常使用。

7.2 @Conditional

条件装配:满足 Conditional 指定的条件,则进行组件注入。

7.3 @ImportResource

如何使用 Java 读取到配置文件中的内容,并且把它封装到 JavaBean 中。

@ImportResource("classpath:beans.xml")
@Configuration
public class MyConfig { ... }

7.4 @ConfigurationProperties*

使用 @ConfigurationProperties(配置绑定)读取到 properties 文件中的内容,并且把它封装到 JavaBean 中。

myCar:
  brand: Mercedes-Benz
  color: black

(1) @ConfigurationProperties + @Component

@Component
@ConfigurationProperties(prefix = "mycar")
public class CarConfig { ... }
// 1. 将 CarConfig 声明为组件 2. 开启 CarConfig 配置绑定功能

(2) @ConfigurationProperties + @EnableConfigurationProperties

@ConfigurationProperties(prefix = "mycar")
public class CarConfig { ... }

// ======================================================

@Configuration
@EnableConfigurationProperties(CarConfig.class)
public class MyConfig { ... }
// 1. 开启 CarConfig 配置绑定功能 2. 把这个 CarConfig 这个组件自动注册到容器中
  • 若只做了在 CarConfig 上添加 @ConfigurationProperties 操作,则 IDEA 会提示:Not registered via @EnableConfigurationProperties, marked as Spring component, or scanned via @ConfigurationPropertiesScan;
  • @ConfigurationProperties(prefix = "mycar") 中的 prefix 必须全部小写!不然会提示:Prefix must be in canonical form。

8. 自动配置原理

8.1 @EnableAutoConfiguration

// 该注解来标注一个主程序类,说明这是一个 SpringBoot 应用
@SpringBootApplication
public class HelloWorldMainApplication {
    public static void main(String[] args) {
        // 启动应用
        SpringApplication.run(HelloWorldMainApplication.class, args);
    }
}

@SpringBootApplication 标注在某个类上说明这个类是 SpringBoot 的主配置类,SpringBoot 就应该运行这个类的 main 方法来启动 SpringBoot 应用。

@SpringBootConfiguration // 底层还是 @Configuration
@EnableAutoConfiguration
@ComponentScan(...)
public @interface SpringBootApplication {...}

@EnableAutoConfiguration 上声明的这两个注解分别代表两个功能。

@AutoConfigurationPackage                        // 1.实现「默认包规则」                ## 8.2
@Import({AutoConfigurationImportSelector.class}) // 2.加载全部配置类(130),但会按需配置  ## 8.3
public @interface EnableAutoConfiguration {}

8.2 默认包规则

@Import({AutoConfigurationPackages.Registrar.class})
public @interface AutoConfigurationPackage {
    String[] basePackages() default {};

    Class<?>[] basePackageClasses() default {};
}

导入了一个类型为 AutoConfigurationPackages.Registrar 的组件:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

  @Override
  public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
    register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
  }

  @Override
  public Set<Object> determineImports(AnnotationMetadata metadata) {
    return Collections.singleton(new PackageImports(metadata));
  }

}

由 { debug + registerBeanDefinitions() } 可知为啥能将主配置类所在包及下面所有子包里面的所有组件批量注册到 Spring 容器了。

8.3 自动配置功能组件

AutoConfigurationImportSelector

@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // ===== ↓↓↓↓↓ Step Into ↓↓↓↓↓ =====
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  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);
}

protected List<String> getCandidateConfigurations(
      AnnotationMetadata metadata, AnnotationAttributes attributes) {
  // ===== ↓↓↓↓↓ Step Into ↓↓↓↓↓ =====
  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;
}

SpringFactoriesLoader

public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  ClassLoader classLoaderToUse = classLoader;
  if (classLoaderToUse == null) {
    classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
  }
  // org.springframework.boot.autoconfigure.EnableAutoConfiguration
  String factoryTypeName = factoryType.getName();
  // ===== ↓↓↓↓↓ Step Into ↓↓↓↓↓ =====
  return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
  Map<String, List<String>> result = (Map)cache.get(classLoader);
  if (result != null) {
    return result;
  } else {
    HashMap result = new HashMap();
    try {
      // 会加载所有jar包中 META-INF 下的 spring.factories 文件(如果有的话)
      Enumeration urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);

      while(urls.hasMoreElements()) {
        URL url = (URL)urls.nextElement();
        UrlResource resource = new UrlResource(url);
        Properties properties = PropertiesLoaderUtils.loadProperties(resource);
        Iterator var6 = properties.entrySet().iterator();

        while(var6.hasNext()) {
            Entry<?, ?> entry = (Entry)var6.next();
            String factoryTypeName = ((String)entry.getKey()).trim();
            String[] factoryImplementationNames = StringUtils
                    .commaDelimitedListToStringArray((String)entry.getValue());
            String[] var10 = factoryImplementationNames;
            int var11 = factoryImplementationNames.length;

            for(int var12 = 0; var12 < var11; ++var12) {
                String factoryImplementationName = var10[var12];
                ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                    return new ArrayList();
                })).add(factoryImplementationName.trim());
            }
        }
      }

      result.replaceAll((factoryType, implementations) -> {
          return (List)implementations.stream().distinct().collect(
              Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
      });
      cache.put(classLoader, result);
      return result; // dubug 看看这个 map 中都有啥(见下图)
    } catch (IOException var14) {
        throw new IllegalArgumentException(
            "Unable to load factories from location [META-INF/spring.factories]", var14);
    }
  }
}

SpringBoot 一启动,工厂就要给容器加载所有自动配置类(加载配置类并不代表配置生效),最终会按需配置!即:满足各个配置类中的条件装配注解 @ConditionalOnXxx 的要求,自动配置类才会生效,继而将相关的组件注册到容器中,并自动配置好这些组件。

8.4 举例

(1)DispatcherServletAutoConfiguration

@Bean
// 容器中有 MultipartResolver 类型的组件
@ConditionalOnBean(MultipartResolver.class)
// 容器中没有名为 "multipartResolver" 的组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
// 若给 @Bean 标注的方法增加方法入参,则入参的实际对象将会从容器中去找
public MultipartResolver multipartResolver(MultipartResolver resolver) {
    // Detect if the user has created a MultipartResolver but named it incorrectly
    return resolver;
} // 当返回后,该类型的组件就又叫回了 "multipartResolver"(因为 @Bean 方法名即为默认组件名的缘故)

(2)HttpEncodingAutoConfiguration

@ConfigurationProperties(
    prefix = "server",
    ignoreUnknownFields = true
)
public class ServerProperties {
    private Integer port;
    private InetAddress address;
    @NestedConfigurationProperty
    private final ErrorProperties error = new ErrorProperties();
    private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
    private String serverHeader;
    private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8L);
    private Shutdown shutdown;
    @NestedConfigurationProperty
    private Ssl ssl;
    @NestedConfigurationProperty
    private final Compression compression;
    @NestedConfigurationProperty
    private final Http2 http2;
    private final ServerProperties.Servlet servlet;
    private final ServerProperties.Tomcat tomcat;
    private final ServerProperties.Jetty jetty;
    private final ServerProperties.Netty netty;
    private final ServerProperties.Undertow undertow;

    // ...

}

// =========================================================

@Configuration(
    proxyBeanMethods = false
)
@EnableConfigurationProperties({ServerProperties.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "server.servlet.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(
                org.springframework.boot.web.servlet.server.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(
                org.springframework.boot.web.servlet.server.Encoding.Type.RESPONSE));
        return filter;
    }
}
  • 自动配置类上标 @EnableConfigurationProperties 注解,封装配置信息的 Properties 上标 @ConfigurationProperties 注解。
  • XxxAutoConfiguration 是和一个 XxxProperties 关联的(因为自动配置类中的组件就是通过 XxxProperties 中的值设置组件的默认行为),而 XxxProperties 又是和核心配置文件绑定的。所以,改核心配置文件就能改掉自动配置类注册的组件的默认行为。
  • XxxAutoConfiguration → @ConditionalOnXxx 按需加载组件 → 组件从 XxxProperties 拿值进行初始化 → application.yml

(3)application.yml 追加配置:debug: true

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   AopAutoConfiguration matched:
      - @ConditionalOnProperty (spring.aop.auto=true) matched (OnPropertyCondition)

   AopAutoConfiguration.ClassProxyingConfiguration matched:
      - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
      - @ConditionalOnProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)

   DispatcherServletAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet' (OnClassCondition)
      - found 'session' scope (OnWebApplicationCondition)

   DispatcherServletAutoConfiguration.DispatcherServletConfiguration matched:
      - @ConditionalOnClass found required class 'javax.servlet.ServletRegistration' (OnClassCondition)
      - Default DispatcherServlet did not find dispatcher servlet beans (DispatcherServletAutoConfiguration.DefaultDispatcherServletCondition)

   MultipartAutoConfiguration#multipartResolver matched:
      - @ConditionalOnMissingBean (types: org.springframework.web.multipart.MultipartResolver; SearchStrategy: all) did not find any beans (OnBeanCondition)

   ...

Negative matches:
-----------------

   ActiveMQAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

   AopAutoConfiguration.AspectJAutoProxyingConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'org.aspectj.weaver.Advice' (OnClassCondition)

   ArtemisAutoConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required class 'javax.jms.ConnectionFactory' (OnClassCondition)

   ...

8.5 withSPI

在 Spring Boot 中,自动配置是一种“根据类路径中的库和依赖项来自动配置 Spring 应用程序上下文”的机制。自动配置是使用 Spring 的 SPI(服务提供者接口)机制实现的

SPI 是一种设计模式,允许通过第三方代码扩展软件库,而无需修改库。在 Java 中,SPI 是使用 java.util.ServiceLoader 类实现的。支持 SPI 的库定义了一个或多个接口,并在其 JAR 文件中提供了一个名为 META-INF/services/<interface-name> 的文件,该文件列出了接口实现的完全限定类名。

在 Spring Boot 中,自动配置通过扫描实现特定接口或具有特定注释的类的类路径来工作。这些类然后用于配置应用程序上下文。例如,如果包含 JDBC 驱动程序的库位于类路径中,Spring Boot 将使用该驱动程序自动配置 DataSource Bean。

为了实现自动配置,Spring Boot 启动模块通常包含 Spring 提供的 SPI 接口的实现。这些接口包括 org.springframework.boot.autoconfigure.condition.Condition,用于指定应用自动配置必须满足的条件,以及 org.springframework.boot.autoconfigure.EnableAutoConfiguration,用于为特定模块启用自动配置。

通过使用 SPI 和自动配置,Spring Boot 能够提供一种流线型和自定义的方法来配置 Spring 应用程序,同时仍然允许定制和可扩展性。

posted @ 2021-09-14 21:34  tree6x7  阅读(42)  评论(0编辑  收藏  举报