SpringBoot之自动配置和配置文件

SpringBoot的优点

一、Spring和SpringBoot

1.1、Spring

Spring5的重大升级

响应式编程

内部源码设计

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

1.2、SpringBoot

Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".

能快速创建出生产级别的Spring应用

SpringBoot的优点

  • Create stand-alone Spring applications

    • 创建独立Spring应用
  • Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)

    • 内嵌web服务器
  • Provide opinionated 'starter' dependencies to simplify your build configuration

    • 自动starter依赖,简化构建配置
  • Automatically configure Spring and 3rd party libraries whenever possible

    • 自动配置Spring以及第三方功能
  • Provide production-ready features such as metrics, health checks, and externalized configuration

    • 提供生产级别的监控、健康检查及外部化配置
  • Absolutely no code generation and no requirement for XML configuration

    • 无代码生成、无需编写XML
  • SpringBoot是整合Spring技术栈的一站式框架

    SpringBoot是简化Spring技术栈的快速开发脚手架

  • 最大的特性就是依赖管理自动配置

  • 笔者当前使用环境是SpringBoot5.2+idea2020.3.4+maven3.6.3

二、依赖管理

在idea中选择快速搭建SpringBoot项目,看下pom.xml

2.1、版本锁定

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.5.2</version>
    <relativePath/>
</parent>

在maven中,父工程通常都是用来管理所有的jar包依赖,子工程只需要在使用的时候,引用依赖坐标即可。在父工程中,所有的版本号通过都已经锁定好了。

去看下父工程中干了些什么:

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.5.2</version>
  </parent>

SpringBoot的依赖,这个是什么?

继续跟进去查看下,可以看到是SpringBoot对依赖jar包的版本锁定,如下所示:

<properties>
    <activemq.version>5.16.2</activemq.version>
    <antlr2.version>2.7.7</antlr2.version>
    <appengine-sdk.version>1.9.89</appengine-sdk.version>
    <artemis.version>2.17.0</artemis.version>
    <aspectj.version>1.9.6</aspectj.version>
	..........
    <xml-maven-plugin.version>1.0.2</xml-maven-plugin.version>
    <xmlunit2.version>2.8.2</xmlunit2.version>
  </properties>

子工程使用到的时候,只需要直接添加对应的依赖坐标即可。

这样子做有什么好处?能够解决版本冲突!!如果搭建过SSM的同学一定知道,一定经历过版本号冲突,十分痛苦,但是SpringBoot已经帮我们做好了,他们已经使过了,觉得这样子来做是没有问题的。

2.1.1、修改版本锁定

当然,并非是他们锁定了版本号,我们就无法来进行修改了,我们依然可以来进行修改。

比如说当前的mysql版本是8.0的,但是我们想使用5.x的,我们照葫芦画瓢:

<mysql.version>8.0.25</mysql.version>

可以在我们自己的pom.xml文件中加上如下配置:

<properties>
    <java.version>1.8</java.version>
    <mysql.version>5.1.49</mysql.version>
</properties>

然后再引入mysql对应的坐标:

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

刷新下maven,可以看到对应的坐标就已经更改到了当前的版本中来了。

但是这也是利用了maven的一个特性,就近原则。最近的版本号会覆盖掉原来的版本号。

三、自动配置

3.1、约定大于配置

因为SpringBoot在底层为我们做了大量的工作,但是我们不知道,我们仅仅是知道怎么来使用而已,最常用的是springmvc、mybatis、redis等等默认的配置都给我们做好了。

约定大于配置:在springmvc中我们还需要配置默认的编码字符集,映射路径等等,SpringBoot都帮助我们做好了,即使你没做,但是官方已经约定好了,你不配置,我来帮助你配置好;你要是配置了,我就使用你的。

3.2、目录结构

从官方拷贝下来的目录结构如下所示:

com
 +- example
     +- myapplication
         +- MyApplication.java
         |
         +- customer
         |   +- Customer.java
         |   +- CustomerController.java
         |   +- CustomerService.java
         |   +- CustomerRepository.java
         |
         +- order
             +- Order.java
             +- OrderController.java
             +- OrderService.java
             +- OrderRepository.java

The MyApplication.java file would declare the main method, along with the basic @SpringBootApplication, as follows:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

    public static void main(String[] args) {
        SpringApplication.run(MyApplication.class, args);
    }

}

SpringBoot推荐使用的源代码的结构,我们最好是将启动类放在其他包的外面,这样做的好处是什么?SpringBoot默认扫描的是当前启动类所在的包和所在包的子包。当然,这也是推荐使用,你要是不想遵循这样的结构,那么也可以不这样配置,但是你需要额外的去配置。这里不说这个,因为99%的都是遵循这种默认规则。

3.3、自动配置原理探究

@SpringBootApplication注解探究

首先来研究一下启动类上的注解@SpringBootApplication,下面来分析一下@SpringBootApplication注解的结构示意图,如下所示:

下面来详细分析一下@SpringBootApplication:

/**修饰自定义注解,指定该自定义注解的注解位置,类还是方法,或者属性
*/
@Target(ElementType.TYPE)

/**
被它所注解的注解保留多久,注解的生命周期。可选的参数值在枚举类型 RetentionPolicy 中,一般如果需要在运行时去动态获取注解信息,那只能用 RUNTIME 注解;如果要在编译时进行一些预处理操作,比如生成一些辅助代码(如 ButterKnife),就用 CLASS注解;如果只是做一些检查性的操作,比如 @Override 和 @SuppressWarnings,则可选用 SOURCE 注解。
1、RetentionPolicy.SOURCE:注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃;
2、RetentionPolicy.CLASS:注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期;
3、RetentionPolicy.RUNTIME:注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在;
*/
@Retention(RetentionPolicy.RUNTIME)
/**
将此注解包含在 javadoc 中 ,它代表着此注解会被javadoc工具提取成文档。在doc文档中的内容会因为此注解的信息内容不同而不同。相当与@see(后面可以跟类路径等参数实现链接跳转  Ctrl跳转),@param(注释参数) 等。
*/
@Documented
/**
修饰自定义注解,该自定义注解注解的类,被继承时,子类也会拥有该自定义注解
*/
@Inherited
// 
@SpringBootConfiguration
// 表示当前注解标记的类是一个配置类
@EnableAutoConfiguration
// 扫描包路径
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
@SpringBootConfiguration

相当于Configuration,表明启动类也是一个配置类,可以配置@Bean

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
// 标记了当前注解标记的类是配置类
@Configuration
public @interface SpringBootConfiguration {
    // 表示的是当前标记的属性默认使用的是@Configuration注解中的proxyBeanMethods的值
    // //默认使用CGLIB代理该类
	@AliasFor(annotation = Configuration.class)
	boolean proxyBeanMethods() default true;
}
@EnableAutoConfiguration
@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 扫描包路径

保存自动配置类以供之后的使用,比如给JPA entity扫描器用来扫描开发人员通过注解@Entity定义的entity类。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
//手工注册bean
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage 

对于@Import注解来说,有三种使用方式:

  • 普通类,直接导入到IOC容器管理;
  • ImportBeanDefinitionRegistrar接口实现类,支持手动注册bean;
  • ImportSelector实现,将selectImports方法返回的数组第加载到IOC;

而导入的AutoConfigurationPackages.Registrar类,看下类结构:

可以看到AutoConfigurationPackages.Registrar是实现了ImportBeanDefinitionRegistrar接口,那么在spring中在扫描加载BD的时候会来添加一个配置类到IOC容器中。会调用ImportBeanDefinitionRegistrar.registerBeanDefinitions方法

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

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

首先看下PackageImports的构造方法:

获取得到@AutoConfigurationPackage中写的basePackages和basePackageClasses的路径,这里也就是所谓的容器扫描包路径的位置。

  • 1、如果配置了basePackages和basePackageClasses的路径,那么IOC容器就来进行扫描;
  • 2、如果没有配置,那么就扫描@AutoConfigurationPackage所在类的包路径;
AutoConfigurationImportSelector 自动配置类的加载

首先来看下AutoConfigurationImportSelector 的继承结构图,如下所示:

因为AutoConfigurationImportSelector间接实现了ImportSelector接口,那么肯定会来实现ImportSelector接口中的selectImports方法。而又因为AutoConfigurationImportSelector实现的是DeferredImportSelector接口,这个接口比较特殊。我们可以直接看下Spring中是如何来对这个类来进行筛选的。

直接来到org.springframework.context.annotation.ConfigurationClassParser#parse(Set)方法中来

public void parse(Set<BeanDefinitionHolder> configCandidates) {
  for (BeanDefinitionHolder holder : configCandidates) {
    BeanDefinition bd = holder.getBeanDefinition();
    try {
      // 省略代码处理
      parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
      catch (Throwable ex) {
        throw new BeanDefinitionStoreException(ex);
      }
    }
    // 记住这个处理!
    this.deferredImportSelectorHandler.process();
  }

看看如何解析DeferredImportSelector接口即可。来到org.springframework.context.annotation.ConfigurationClassParser#processImports方法中来

可以看到一行代码判断:

if (selector instanceof DeferredImportSelector) {
  this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
}

然后来到handle方法中:

public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
  DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(configClass, importSelector);
  if (this.deferredImportSelectors == null) {
    DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
    handler.register(holder);
    handler.processGroupImports();
  }
  else {
    // 当前做的只是添加
    this.deferredImportSelectors.add(holder);
  }
}

但是当返回上一个步骤中去之后,可以看到会来调用DeferredImportSelectorHandler类中的process方法来进行处理。

直接来到:org.springframework.context.annotation.ConfigurationClassParser.DeferredImportSelectorHandler#process

public void process() {
  List<DeferredImportSelectorHolder> deferredImports = this.deferredImportSelectors;
  this.deferredImportSelectors = null;
  try {
    if (deferredImports != null) {
      DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
      deferredImports.sort(DEFERRED_IMPORT_COMPARATOR);
      // 调用register方法进行注册
      deferredImports.forEach(handler::register);
      handler.processGroupImports();
    }
  }
  finally {
    this.deferredImportSelectors = new ArrayList<>();
  }
}

看下这里的注册方法:

public void register(DeferredImportSelectorHolder deferredImport) {
  // AutoConfigurationGroup
  Class<? extends Group> group = deferredImport.getImportSelector().getImportGroup();  
  DeferredImportSelectorGrouping grouping = this.groupings.computeIfAbsent((group != null ? group : deferredImport),
    key -> new DeferredImportSelectorGrouping(createGroup(group)));
  grouping.add(deferredImport);
  this.configurationClasses.put(deferredImport.getConfigurationClass().getMetadata(),
                                deferredImport.getConfigurationClass());
}

然后开始处理:

public void processGroupImports() {
  // AutoConfigurationGroup
  for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
    grouping.getImports().forEach(entry -> {
      ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata());
      try {
        processImports(configurationClass, asSourceClass(configurationClass),
                       asSourceClasses(entry.getImportClassName()), false);
      }
      catch (BeanDefinitionStoreException ex) {
        throw ex;
      }
      catch (Throwable ex) {
        throw new BeanDefinitionStoreException(
          "Failed to process import candidates for configuration class [" +
          configurationClass.getMetadata().getClassName() + "]", ex);
      }
    });
  }
}

在调用grouping.getImports()方法进行遍历的时候,发现调用以下方法:

public Iterable<Group.Entry> getImports() {
  for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
    // 调用group中的方法进行处理。即AutoConfigurationGroup中的process方法
    this.group.process(deferredImport.getConfigurationClass().getMetadata(),deferredImport.getImportSelector());
  }
  return this.group.selectImports();
}

如下所示:那么会执行org.springframework.boot.autoconfigure.AutoConfigurationImportSelector.AutoConfigurationGroup#process方法,所以先来看下这个方法

那么直接看下org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#getAutoConfigurationEntry方法

这里有几个注意点:

  • 1、getCandidateConfigurations方法是利用Spring中的SPI机制,找到META-INF\spring.factories中的文件,但是在META-INF\spring.factories文件中,所有的内容格式都是以KEY-VALUE的格式存在的。但是对于当前来说,只是加载@EnableAutoConfiguration注解对应的全限定类名对应的VALUE,即所谓的自动配置类;
  • 2、checkExcludedClasses方法存在的意义:首先从注解的exclude/excludeName属性中获取排除项,对于获取得到需要排除的配置项从META-INF\spring.factories中@EnableAutoConfiguration注解对应的全限定类名的VALUE来进行查找,对于不属于AutoConfiguration的exclude报错。所以在从注解的exclude/excludeName属性中获取排除项一定要是在META-INF\spring.factories中@EnableAutoConfiguration注解对应的全限定类名的VALUE中的;
  • 3、对于所有的自动配置类来说,并不一定是添加的自动配置都生效的,还需要通过条件过滤器来进行过滤筛选得到能够满足条件的自动配置类到IOC容器中来;

四、配置文件

在配置文件中可以配置我们最常用的配置信息。

在java早期的时候,我们最常用的配置信息是写到XXX.properties文件中,然后通过流来进行读取到程序中去,然后取值等等一系列操作。如下:

properties---------->流----------------->设置到对应的载体上

当然SpringBoot也做好了,SpringBoot推荐使用yaml/yml文件来进行使用。

我们最常见的使用有两种方式:

  • 1、绑定到java类上;
  • 2、直接在对应的属性上使用@Value注解来赋值;

4.1、yaml介绍

YAML 是 "YAML Ain't Markup Language"(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言)。

4.1.1、语法

  • key: value;kv之间有空格
  • 大小写敏感
  • 使用缩进表示层级关系
  • 缩进不允许使用tab,只允许空格
  • 缩进的空格数不重要,只要相同层级的元素左对齐即可
  • '#'表示注释
  • 字符串无需加引号,如果要加,''与""表示字符串内容 会被 转义/不转义

4.1.2、数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null

k: v

  • 对象:键值对的集合。map、hash、set、object

行内写法: k: {k1:v1,k2:v2,k3:v3}

k:
k1: v1
k2: v2
k3: v3

  • 数组:一组按次序排列的值。array、list、queue

行内写法: k: [v1,v2,v3]

或者

k:

  • v1
  • v2
  • v3

4.2、配置项和java类绑定

所以将配置类中的属性绑定到java类上,有两种方式:

第一种使用方式:直接标注在java类上

@Component
@ConfigurationProperties(prefix = "user")

第二种:一个标注在类上,一个标注在启动类上

@ConfigurationProperties(prefix = "user")
@EnableConfigurationProperties(value = User.class)

第一种使用方式

@Data
public class Phone {
    private String brand;
    private Double price;
    private String address;
    private List param;
}
@Data
// 想要使用SpringBoot的配置文件和java类绑定,首先要称为可用的组件
@Component 
// 当前类想要和配置文件中哪个配置项前缀匹配上
@ConfigurationProperties(prefix = "user") 
public class User {
    private Integer id;
    private String username;
    private List hobbies;
    private Integer[] integers;
    private Set set;
    private Map map;
    private Phone phone;
}

对应yaml文件:

user:
  id: 4273
  username: guang
  hobbies:
    - basketball
    - pingpang
    - rap
  integers: [1,2,3,4]
  set:
    - 1
    - 2
    - 3
  map: {username: guang,password: guang}

  phone:
    brand: apple
    price: 12345.6
    address: 广东省深圳市
    param:
      - size
      - weight

注意:配置文件中的属性名称要和java类中的属性名称保持一致。

最后看下输出java类,如下所示:

User(id=4273, username=guang, hobbies=[basketball, pingpang, rap], integers=[1, 2, 3, 4], set=[1, 2, 3], map={username=guang, password=guang}, phone=Phone(brand=apple, price=12345.6, address=\u5E7F\u4E1C\u7701\u6DF1\u5733\u5E02, param=[size, weight]))

第二种使用方式

如果是我们自己的类,那么可以使用上面那种配置方式。但是还有一种情况,我们尝尝需要使用到了第三方的类,但是我们是引入进来的,我们无法给其添加@ConfigurationProperties注解,但是我们有另外一种操作:

@Data
@ConfigurationProperties(prefix = "user")
public class User {
    private Integer id;
    private String username;
    private List hobbies;
    private Integer[] integers;
    private Set set;
    private Map map;
    private Phone phone;
}

在启动类上加上:

@SpringBootApplication
// 两个作用:1、注册成bean;2、属性绑定
@EnableConfigurationProperties(value = User.class)
public class HelloworldApplication {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(HelloworldApplication.class, args);
        User user = run.getBean( User.class);
        System.out.println(user);
    }
}

参考B站雷神的SpringBoot,对应的文档链接:https://www.yuque.com/atguigu/SpringBoot/rmxq85

posted @   雩娄的木子  阅读(133)  评论(0编辑  收藏  举报
编辑推荐:
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
阅读排行:
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构
点击右上角即可分享
微信分享提示