Spring Boot 基础(一)

368 阅读19分钟

一、Spring Boot 简介

1.1 回顾:什么是 Spring

  • Spring 基础
  • Spring MVC 基础
  • Spring 是一个开源框架,2003 年兴起的,一个轻量级的 Java 开发框架,作者:Rod Johnson;
  • 作用:为了解决企业级应用开发的复杂性而创建的,简化开发;

1.2 Spring 是如何简化 Java 开发的

  • 为了降低 Java 开发的复杂性,Spring 采用了以下 4 种关键策略:
    1. 基于 POJO 的轻量级和最小侵入性编程,所有东西都是 bean;
    2. 通过 IOC,依赖注入(DI)和面向接口,实现松耦合;
    3. 基于切面(AOP)和惯例,进行声明式编程;
    4. 通过切面和模版减少样式代码,如:RedisTemplate,xxxTemplate;

1.3 Spring Boot 概述

  • Spring Boot 就是一个 Javaweb 的开发框架,和 SpringMVC 类似,对比其他 Javaweb 框架的好处是,简化开发,约定大于配置
  • you can "just run",能迅速的开发 web 应用,几行代码开发一个 http 接口;
  • Spring Boot 基于 Spring 开发,Spirng Boot 本身并不提供 Spring 框架的核心特性,以及扩展功能,只是用于快速、敏捷地开发新一代基于 Spring 框架的应用程序;
  • Spring Boot 并不是用来替代 Spring 的解决方案,而是和 Spring 框架紧密结合,用于提升 Spring 开发者体验的工具;
  • Spring Boot 以 约定大于配置的核心思想,默认进行了很多设置,多数 Spring Boot 应用,只需要很少的 Spring 配置;

Spring Boot 的主要优点

  • 为所有 Spring 开发者更快的入门;
  • 开箱即用,提供各种默认配置,来简化项目配置;
  • 内嵌式容器,简化 Web 项目;
  • 没有冗余代码生成,和 XML 配置的要求;

二、创建 Spring Boot 项目

2.1 运行环境

  • JDK 17;
  • maven-3.8.4;
  • Spring Boot 2.6.6;
  • IDEA 2021.2;

2.2 项目创建方式

  1. 官网快速构建工具
  • 查看版本:

  • 在线构建,直接生成项目压缩包:

  • 通过 IDEA 导入项目即可;

  1. IDEA 直接构建
  • IDEA 构建 SpringBoot 项目:

  • 选择版本和 Maven 依赖:

项目结构分析

  • 项目创建完成,会自动生成以下文件:

    • 程序的主启动类:Application;

    • 配置文件:application.properties

    • 测试类:ApplicationTests;

    • Maven 配置文件:pom.xml

    • 必须在主程序同级目录下创建包:否则无法找到;

  • Spring Boot 项目的依赖:pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 父依赖 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.study</groupId>
    <artifactId>SpringBoot</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringBoot</name>
    <description>SpringBoot</description>
    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <!--SpringBoot的依赖,都是以spring-boot-starter开头-->
        <!--Web依赖:包含Tomcat、dispatcherServlet、xml等配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--springboot单元测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <!--打jar包插件-->
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 创建 controller:

    • 注意:在同级目录下创建包,否则识别不到
    • 创建类:
    @RestController
    public class Hello {
        // 接口:http://localhost:8080/h1
        @RequestMapping("/h1")
        public String hello() {
            // 调用业务,接收前端的参数
            // 以字符串形式返回
            return "Hello World!";
        }
    }
    
  • 从主程序启动项目,浏览器发起请求:

  • 控制台输出了 Tomcat 访问的端口号;

  • 项目打成 jar 包:

    • 点击 maven 的 package:

    • 生成 jar 包位置:

  • 在 IDEA 中,将项目停止,在命令行中运行测试:

java -jar SpringBoot-0.0.1-SNAPSHOT.jar
  • 启动成功:

  • 浏览器查看结果:

  • 如果遇到测试单元相关错误,可以设置 Maven 配置:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <!--跳过项目运行测试用例-->
        <skipTests>true</skipTests>
    </configuration>
</plugin>

更改 Spring Boot 启动时 banner 图案

  • 在线生成工具:链接
  • 在 resources 目录下,创建 banner.txt
  • 将生成的代码,复制到 banner.txt 即可;

三、运行原理

3.1 自动配置

  1. pom.xml
  • 父依赖:spring-boot-starter-parent
    • 主要是依赖父项目,管理项目的资源过滤及插件;
<!-- 父依赖 -->
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.6</version>
    <!--从父级仓库查找依赖版本-->
    <relativePath/> <!-- lookup parent from repository -->
</parent>
  • 点击进入,发现还有父依赖:spring-boot-dependencies
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.6.6</version>
</parent>
  • 核心依赖spring-boot-dependencies 才是真正管理 Spring Boot 应用,所有依赖版本的地方,是 Spring Boot 的版本控制中心;
  • Spring Boot 导入依赖,默认是不需要写版本,但是,如果导入的包,没有在依赖中进行管理,就需要手动配置版本;
  1. 启动器
  • spring-boot-starter:
<!--
	2.6.6版本,将启动器分别配置到模块中了
	如:spring-boot-starter-web,在模块依赖中导入了这个启动器
-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.6.6</version>
    <scope>compile</scope>
</dependency>
  • springboot-boot-starter-xxx:就是 Spring Boot 的场景启动器;
  • spring-boot-starter-web:导入了 web 模块正常运行,所依赖的组件:
    • Spring Boot 将所有的功能场景,都抽取出来,做成一个个的 starter (启动器);
    • 只需要在项目中引入这些 starter 即可,所有相关的依赖都会导入进来;
    • 需要什么功能,就导入什么样的场景启动器,也可以自定义 starter;
  1. 主启动类
  • Application:
// @SpringBootApplication:标注这个类,是一个springboot的应用
@SpringBootApplication
public class Application {
    // 将springboot应用启动(开始一个服务)
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

注解:@SpringBootApplication;

  • 作用:这个类是 Spring Boot 的主配置类,运行这个类的 main 方法,来启动 Spring Boot 应用;
  • 进入这个注解(下载源码查看),可以看到还有其他注解;
@SpringBootConfiguration // 配置类
@EnableAutoConfiguration // 开启自动配置功能
// 自动扫描
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
	// ...
}
  1. @ComponentScan
  • 这个注解在 Spring 中很重要,对应 XML 配置中的元素;
  • 自动扫描当前主启动类同级的包,并加载符合条件的组件,或者 bean,将这个 bean 定义,加载到 IOC 容器中;
  1. @SpringBootConfiguration
  • Spring Boot 的配置类,标注在某个类上,表示这是一个 Spring Boot 的配置类;
  • 点击进入注解查看:
  • @Configuration:配置类,对应 Spring 的 xml 配置文件;
    • @Component:说明这也是 Spring 的一个组件,负责启动应用;
  1. @EnableAutoConfiguration
  • 开启 自动配置 功能;

  • 以前需要手动配置的内容,现在 Spring Boot 可以自动配置;

  • 通过 @EnableAutoConfiguration 注解,Spring Boot 自动配置才能生效

  • 点击进入注解查看:

  • @AutoConfigurationPackage:自动配置包;

// @Import:Spring底层注解,给容器中导入一个组件
// Registrar.class:将主启动类的所在包,及包下面所有子包里面的所有组件,扫描到Spring容器
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
	// ...
}
  • @Import(AutoConfigurationImportSelector.class) :给容器导入一个组件(选择器);

AutoConfigurationImportSelector.java:

  • 自动配置 导入选择器,查看类源码:
  1. 类中有一个这样的方法:获取候选的配置 核心方法
// 获取候选的配置
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 这里的getSpringFactoriesLoaderFactoryClass()方法
	// 返回的就是启动自动导入配置文件的注解类:EnableAutoConfiguration
    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;
}

  1. 这个方法又调用了 SpringFactoriesLoader 类的静态方法,进入SpringFactoriesLoader 类 loadFactoryNames() 方法:
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoader == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }

    String factoryTypeName = factoryType.getName();
    // 这里又调用了loadSpringFactories方法
    return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
  1. 继续点击查看 loadSpringFactories 方法:SpringFactoriesLoader.java
// SpringFactoriesLoader.java
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
    ClassLoader classLoaderToUse = classLoader;
    if (classLoaderToUse == null) {
        classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
    }
    String factoryTypeName = factoryType.getName();
    // 调用下面的静态方法:loadSpringFactories,加载classLoader:classLoaderToUse
    return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
    Map<String, List<String>> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    result = new HashMap<>();
    try {
        // 枚举遍历url里的资源
        // classLoader.getResources(FACTORIES_RESOURCE_LOCATION) 类加载获得所有的资源
        Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
        // 判断有没有更多的元素,如果有,就放进url里面
        while (urls.hasMoreElements()) {
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 把所有的资源,加载到配置类中
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                String factoryTypeName = ((String) entry.getKey()).trim();
                String[] factoryImplementationNames =
                        StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
                for (String factoryImplementationName : factoryImplementationNames) {
                    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;
}
  1. 发现一个多次出现的文件:spring.factories,IDEA 双击 shift 搜索;
  • META-INF/spring.factories:自动配置的核心文件;

  • spring.factories

    • Initializers:初始化相关;
    • Application Listeners:监听相关;
    • Auto Configuration Import Filters:自动选择导入的包;
    • Auto Configure:自动配置相关;
  • 在配置文件里打开一个配置类:WebMvcAutoConfiguration;

  • 可以看到这些都是 JavaConfig 配置类,而且都注入了一些 Bean;

  • 自动配置的真正实现,是从 classpath 中搜寻所有的 META-INF/spring.factories 配置文件,并将其中对应的 org.springframework.boot.autoconfigure. 包下的配置项,通过反射,实例化为对应标注了 @Configuration 的 JavaConfig 形式的 IOC 容器配置类,然后将这些,都汇总成为一个实例,并加载到 IOC 容器中;

小结

  • 自动配置原理分析:

  • Spring Boot 所有自动配置,都是在启动时,扫描并加载 spring.factories 所有的自动配置类,都在这里面,但不一定生效,要判断条件是否成立,只要导入了对应的 start,就有对应的启动器,有了启动器,自动装配就会生效,然后启动成功;
  • Spring Boot 在启动时,从类路径下的 META-INF/spring.factories 中获取 EnableAutoConfiguration 指定的值;
  • 将这些值,作为自动配置类,导入容器,自动配置类就生效,进行自动配置工作;
  • 整合 JavaEE 的整体解决方案,和自动配置都在 springboot-autoconfigure 这个包下;
  • 它会把所有需要导入的组件,以类名的方式返回,这些组件就会被添加到容器;
  • 容器中会存在非常多的 xxxAutoConfiguration 的文件 (@Bean),就是这些类,给容器中导入了这个场景需要的所有组件,并自动装配,@Configuration,JavaConfig...;
  • 有了自动配置类,免去了手动编写配置,注入功能组件等工作;

3.2 SpringApplication

  • 这个 main 方法,相当于开启了一个服务;
// @SpringBootApplication:标注这个类,是一个springboot的应用
@SpringBootApplication
public class Application {
    // 将springboot应用启动(开始一个服务)
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • SpringApplication.run 分析:
    • 该方法主要分两部分:
      1. SpringApplication 的实例化;
      2. run 方法的执行;

SpringApplication

  • 这个类主要做了以下四件事:
    1. 推断应用的类型是普通的项目,还是 Web 项目;
    2. 查找并加载所有可用初始化器,设置到 initializers 属性中;
    3. 找出所有的应用程序监听器,设置到 listeners 属性中;
    4. 推断并设置 main 方法的定义类,找到运行的主类;
  • 查看构造器:
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	// ...
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = new ArrayList(this.getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = this.deduceMainApplicationClass();
}

run 方法

  • 执行流程图:

四、Spring Boot 配置

4.1 配置文件

  • Spring Boot 使用全局的配置文件,配置文件名称是固定的(二选一):

    • application.properties
      • 语法结构:key=value
    • application.yml推荐):
      • 语法结构:key: 空格 value
  • 配置文件的作用:修改 Spring Boot 自动配置的默认值,因为 Spring Boot 在底层,已经自动配置好了;

  • 在配置文件中,修改 Tomcat 默认启动的端口号:

    server.port=8081
    
  • 启动测试:

4.2 yaml

  • yaml 概述:

    • YAML 是 "YAML Ain't a Markup Language" (YAML 不是一种标记语言)的递归缩写;
    • 在开发的这种语言时,YAML 的意思其实是:"Yet Another Markup Language"(仍是一种标记语言);
    • 这种语言以数据做为中心,而不是以标记语言为重点
  • yaml 和 xml 对比:

    • 传统 xml 配置:

      <server>
      	<port>8081<port>
      </server>
      
    • yaml 配置:

      server:
      	# 冒号后面必须加英文空格
      	prot: 8080
      

yaml 基础语法

  • 说明:语法要求严格;

    • 空格不能省略
    • 以缩进,控制层级关系,只要是左边对齐的一列数据,都是同一个层级的
    • 属性和值的大小写,都是十分敏感的
  • 字面量

    • 普通的值 [ 数字、布尔值、字符串 ];

    • 直接写在后面就可以,字符串,默认不用加上双引号,或者单引号;

      # 冒号后面必须加英文空格
      k: v
      
  • 注意:

    • “ ” 双引号,不会转义 字符串里面的特殊字符,特殊字符会作为本身想表示的意思,如:
      • msg: "Hello \n World" 输出:Hello 换行 World ;
    • '' 单引号,会转义特殊字符,特殊字符,会变成和普通字符一样输出,如:
      • msg: 'Hello \n World' 输出:Hello \n World ;
  1. 对象、Map(键值对):
  • 格式:

    # 对象、Map格式
    k:
    	v1:
    	v2:
    
  • 在对象或 Map 的下一行,写属性和值的关系,注意缩进和空格:

    student:
    	name: zhangsan
    	age: 20
    
  • 行内写法:

    student: {name: zhangsan,age: 20}
    
  1. 数组(List、set)
  • - 值表示数组中的一个元素:

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

    pets: [cat,dog,pig]
    

修改 Spring Boot 的默认端口号

  • application.yaml 配置文件中,添加端口号的参数:

    serve:
    	port: 8082
    

4.3 注入配置文件

  • yaml 可以给实体类直接注入匹配值;

yaml 注入配置文件

  • 在 Spring Boot 项目中的 resources 目录下,新建配置文件 application.yaml
  • 创建实体类:Dog(包必须在主程序的同级目录下)
@Component
public class Dog {
    private String name;
    private Integer age;
    // 有参、无参、get、set、toString
}
  • 回顾 Spring 方式,给 bean 注入属性值:@Value
@Component
public class Dog {
    @Value("大黄狗")
    private String name;
    @Value("3")
    private Integer age;
    // 有参、无参、get、set、toString
}
  • 在 Spring Boot 的测试类下,进行测试:
@SpringBootTest
class SpringBoot02ConfingApplicationTests {
    // 通过自动装配,实现类的自动注入
    @Autowired
    private Dog dog;

    @Test
    void contextLoads() {
        System.out.println(dog);
    }
}
  • 运行测试:@Value 注入成功

  • 创建复杂的实体类:Person

// 组件:注册bean到容器中
@Component
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String, Object> map;
    private List<Object> list;
    private Dog dog;
    // 有参、无参、get、set、toString
}
  • 使用 yaml 配置的方式,进行注入(推荐):application.yaml
# 通过person与实例类进行绑定
person:
  name: user1
  age: 23
  happy: true
  birth: 2000/01/01
  map: {k1: v1,K2: v2}
  list:
    - sport
    - code
    - music
  dog:
    name: 大黄狗
    age: 3
  • 把 yaml 文件中的值,注入到实例类中:@ConfigurationProperties
/*
    @ConfigurationProperties作用:
        将配置文件中,配置的每一个属性的值,映射到这个组件中
        告诉SpringBoot,将本类中的所有属性,和配置文件中相关的配置进行绑定
        参数 prefix = “person”:将配置文件中的person下面的所有属性一一对应
*/
// 组件:注册bean到容器中
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String, Object> map;
    private List<Object> list;
    private Dog dog;
    // 有参、无参、get、set、toString
}
  • IDEA 提示,未配置 Spring Boot 配置注解处理器:

  • 解决方案:在 pom.xml 导入相关依赖后,重启即可;

<!-- 导入配置文件处理器,配置文件进行绑定就会有提示,需要重启 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>
  • 在测试类中进行测试:
@SpringBootTest
class SpringBoot02ConfingApplicationTests {
    // 通过自动装配,实现类的自动注入
    @Autowired
    private Person p1;

    @Test
    void contextLoads() {
        System.out.println(p1);
    }
}
  • 运行结果:所有值全部注入成功;

加载指定配置文件

  • 加载配置文件的方式:

    • @PropertySource:加载指定的配置文件;

    • @configurationProperties:默认从全局配置文件中获取值;

  • 自定义配置文件测试:

    • 在 resources 目录下,创建 person.properties 文件:
name=测试
  • 在 Person 类文件中,指定加载 person.properties 文件:
// 组件:注册bean到容器中
@Component
// @PropertySource:加载指定的配置文件
@PropertySource(value = "classpath:person.properties")
public class Person {
    // Spring EL表达式取出配置文件的值,properties方式需要通过@Value进行赋值
    @Value("${name}")
    private String name;
    // ...
}
  • 运行测试:

配置文件占位符

  • yaml 配置文件,还可以编写占位符,生成随机数:
person:
  name: user${random.uuid} # 随机uuid
  age: ${random.int}  # 随机int
  happy: true
  birth: 2000/01/01
  map: {k1: v1,K2: v2}
  list:
    - sport
    - code
    - music
  dog:
    # 引用person.hello的值,如果不存在就用:后面的值,即 other,然后拼接上 _大黄狗
    name: ${person.Hello:other}_大黄狗
    age: 3

回顾 properties 配置方式

  • yaml 是最简单的方式,Spring Boot 推荐,也开发中最常用的;

  • 其它方式:properties ,使用时,需要注意乱码问题:

    • 编码格式需要设置为 UTF-8;

properties 方式测试:

  • 创建实体类:User
// 组件:注册bean到容器中
@Component
public class User {   
    private String name;   
    private Integer age;   
    private String sex;
    // 有参、无参、get、set、toString    
}
  • 创建配置文件:user.properties
name=测试用户
age=20
sex=男
  • 在 User 类上,加载指定的配置文件,并使用 @Value 进行注入:
// 组件:注册bean到容器中
@Component
// @PropertySource:加载指定的配置文件
@PropertySource(value = "classpath:user.properties")
public class User {
    // 从配置文件中取值
    @Value("${name}")
    private String name;
    // #{SPEL} Spring表达式
    @Value("#{2*9}")
    private Integer age;
    // 字面量
    @Value("女")
    private String sex;
    // 有参、无参、get、set、toString    
}
  • 运行测试:
@SpringBootTest
class SpringBoot02ConfingApplicationTests {
    // 通过自动装配,实现类的自动注入
    @Autowired
    private User user;

    @Test
    void contextLoads() {
        System.out.println(user);
    }
}
  • 运行结果:

对比

@ConfigurationProperties@Value
功能批量注入配置文件中的属性一个个指定
松散绑定(松散语法)支持不支持
SPEL不支持支持
JSR303 数据校验支持不支持
复杂类型封装支持不支持
  • @Value:需要为每个属性,单独注解赋值,每个字段都需要添加;
  • @ConfigurationProperties:只需要写一次即可;
  • 松散绑定:如:last-name 和 lastName 是一样的, - 后面跟着的字母,默认是大写的;
  • JSR303 数据校验:在字段上增加一层过滤器验证,保证数据的合法性;
  • 复杂类型封装:yaml 中可以封装对象, @Value 不支持;

小结

  • 配置 yaml 和配置 properties 都可以获取到值,强烈推荐 yaml
  • 如果在某个业务中,只需要获取配置文件中的某个值,可以使用 @value
  • 如果专门编写了一个 JavaBean,和配置文件进行一一映射,就直接使用 @configurationProperties

4.4 JSR303 数据校验

  • Spring Boot 中可以用 @validated 来校验数据,如果数据异常,则会统一抛出异常,方便异常中心统一处理;
  • 测试:
// 注册bean
@Component
// yaml注入
@ConfigurationProperties(prefix = "login")
// JSR-303 数据校验
@Validated
public class Login {
    @NotEmpty(message = "名字不能为空")
    private String userName;
    private String password;
    @Max(value = 120, message = "年龄最大不能超过120")
    private Integer age;
    // 必须为邮箱格式
    @Email(message = "邮箱格式错误")
    private String email;
    // 有参、无参、get、set、toString
}
  • yaml:
login:
  # 松散绑定,对应:userName
  user-name:
  passwrod: 123456
  age: 120
  email: 123
  • 运行结果:

  • 注意:JSR-303 注解报错

  • 解决方法:添加依赖

<!--JSR-303 数据校验-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
  • JSR-303 常用参数:
// 空检查
@Null       // 验证对象是否为null
@NotNull    // 验证对象是否不为null, 无法查检长度为0的字符串
@NotBlank   // 检查约束字符串是不是Null还有被Trim的长度是否大于0,只对字符串,且会去掉前后空格.
@NotEmpty   // 检查约束元素是否为NULL或者是EMPTY.
    
// Booelan检查
@AssertTrue     // 验证 Boolean 对象是否为 true  
@AssertFalse    // 验证 Boolean 对象是否为 false  
    
// 长度检查
@Size(min=, max=) // 验证对象(Array,Collection,Map,String)长度是否在给定的范围之内  
@Length(min=, max=) // string is between min and max included.

// 日期检查
@Past       // 验证 Date 和 Calendar 对象是否在当前时间之前  
@Future     // 验证 Date 和 Calendar 对象是否在当前时间之后  
@Pattern    // 验证 String 对象是否符合正则表达式的规则

4.5 多环境切换

  • profile 是 Spring 对不同环境,提供不同配置功能的支持,可以通过激活不同的环境版本,实现快速切换环境;

多配置文件

  • 配置文件名,可以是 application-{profile}.properties/yaml, 用来指定多个环境版本,如:application-test.yaml 代表测试环境配置,application-dev.yaml 代表开发环境配置;

  • Spring Boot 默认直接启动的配置文件为 application.propertiesapplication.yaml

  • 如果需要指定环境配置,可以在默认配置文件中,配置需要激活的环境:

    • 方式一:application.properties 配置;
    # 在配置文件中指定使用dev环境,通过设置不同的端口号进行测试
    # 启动SpringBoot,可以看到已经切换到dev下的配置
    spring.profiles.active=dev
    
    • 方式二(推荐):application.yaml 的多文档块;
    # 不同的文档块之间,使用---隔开
    server:
      port: 8081
    spring:
      profiles:
        active: dev
    ---
    server:
      port: 8082
    
    # Spring Boot 2.4以上版本 新的配置方式
    spring:
      config:
        activate:
          on-profile: dev
    ---
    server:
      port: 8083
    
    # 弃用的方式
    spring:
      profiles: prod
    
  • 注意:如果 yaml 和 properties 同时都配置了端口,并且没有激活其他环境,默认会使用 properties 配置文件;

配置文件加载位置

  • 外部加载配置文件的方式行多,选择最常用的即可,在开发的资源文件中进行配置;

  • Spring Boot 2.6.7参考文档

  • Spring Boot 启动,会扫描以下位置的 application.properties 或者 application.yaml 文件作为 Spring Boot 的默认配置文件:

    • 优先级1:项目路径下的 config 文件夹配置文件;
    • 优先级2:项目路径下配置文件;
    • 优先级3:资源路径下的 config 文件夹配置文件;
    • 优先级4(最低):资源路径下配置文件;
    • 优先级由高到底,高优先级的配置,会覆盖低优先级的配置;
    • Spring Boot 会从这四个位置全部加载主配置文件,互补配置;

扩展:指定位置加载配置文件

  • 可以通过 spring.config.location,改变默认的配置文件位置;
  • 项目打包好以后,可以使用命令行参数的形式,在启动项目时,指定配置文件的新位置,这种情况,一般是后期运维做的多,相同配置,外部指定的配置文件,优先级最高
java -jar myproject.jar --spring.config.location=F:/application.yaml

五、自动配置原理

  • 配置文件的配置内容:官网配置文档
  • application.yaml 的配置文件内容,要与 spring.factories 文件中的内容对应;

5.1 分析自动配置原理

  • 以 HttpEncodingAutoConfiguration(Http 编码自动配置)为例,分析自动配置原理:
// Spring Boot 2.6.7
// 表示这是一个配置类,和以前编写的配置文件一样,也可以给容器中添加组件
@Configuration(proxyBeanMethods = false)
/*
	启动指定类的ConfigurationProperties功能:
		进入这个ServerProperties查看,将配置文件中对应的值和ServerProperties绑定起来,
		并把ServerProperties加入到ioc容器中
 */
@EnableConfigurationProperties(ServerProperties.class)
/*
	Spring底层@Conditional注解:
		据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效
		这里的意思是,判断当前应用是否是web应用,如果是,当前配置类生效
*/
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 判断当前项目有没有CharacterEncodingFilter这个类(SpringMVC乱码过滤器)
@ConditionalOnClass(CharacterEncodingFilter.class)
/*
	判断配置文件中是否存在某个配置:server.servlet.encoding
		如果不存在,判断也是成立的
		即使配置文件中,不配置spring.http.encoding.enabled=true,也是默认生效的
 */
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
    // 已经和SpringBoot的配置文件映射了
    private final Encoding properties;

    // 只有一个有参构造器的情况下,参数的值,就会从容器中拿
    public HttpEncodingAutoConfiguration(ServerProperties properties) {
        this.properties = properties.getServlet().getEncoding();
    }
    // 给容器中添加一个组件,这个组件的某些值,需要从properties中获取
    @Bean
    // 判断容器没有这个组件
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
        return filter;
    }
    // ...
}
  • 总结:根据当前不同的条件判断,决定这个配置类是否生效(自动装配的原理);

    • 一但这个配置类生效,就会给容器中添加各种组件;

    • 这些组件的属性,是从对应的 properties 类中获取的,这些类里的每一个属性,又是和配置文件绑定的;

    • 所有在配置文件中能配置的属性,都是在 xxxProperties 类中封装着;

    • 配置文件能配置什么,可以参照某个功能对应的属性类;

      // 从配置文件中,获取指定的值和bean的属性进行绑定
      @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
      public class ServerProperties {
          // ...
      }
      
    • 去配置文件里面试试前缀,看提示:

5.2 自动装配的核心(重点)

  • Spring Boot 启动时,加载大量的自动配置类;

  • 查看需要的功能,有没有写在 Spring Boot 默认的自动配置类中;

  • 再查看这个自动配置类中,到底配置了哪些组件(只要需要的组件,存在其中,就不需要进行手动配置);

  • 在配置文件中,能配置的内容,都存在一个规律:

    • 给容器中自动配置类,添加组件时,会从 properties 类中获取某些属性,只需要在 yaml 等配置文件中,指定这些属性的值,即可实现自定义;

    • xxxAutoConfigurartion:自动配置类(加载默认值),给容器中添加组件;

    • xxxProperties:封装配置文件中相关的属性;

    • yaml 等配置文件,和 xxxProperties 文件绑定,可实现自定义;

5.3 @Conditional 派生注解(了解)

  • 自动配置类,必须在一定的条件下才能生效;
  • @Conditional 派生注解(Spring 注解版,原生的 @Conditional 作用):
    • 作用:必须是 @Conditional 指定的条件成立,才给容器中添加组件,配置文件中的内容才生效:
@Conditional 扩展注解作用(判断是否满足当前指定条件)
@ConditionalOnJava系统的 Java 版本,是否符合要求
@ConditionalOnBean容器中存在指定 Bean
@ConditionalOnMissingBean容器中不存在指定 Bean
@ConditionalOnExpression满足 SpEL 表达式指定
@ConditionalOnClass系统中有指定的类
@ConditionalOnMissingClass系统中没有指定的类
@ConditionalOnSingleCandidate容器中只有一个指定的 Bean,或者这个 Bean 是首选 Bean
@ConditionalOnProperty系统中指定的属性,是否有指定的值
@ConditionalOnResource类路径下,是否存在指定资源文件
@ConditionalOnWebApplication当前是 web 环境
@ConditionalOnNotWebApplication当前不是 web 环境
@ConditionalOnJndiJNDI 存在指定项

5.4 查看生效自动配置类

  • Spring Boot 的自动配置类,必须在一定的条件下才能生效,可以通过启用 debug=true 属性,查看生效的配置类:

    • application.properties 配置文件方式:
    # 开启springboot的调试类
    debug=true
    
    • application.yaml 配置文件方式:
    # 开启springboot的调试类
    debug: true
    
  • 在控制台查看:

  • Positive matches:(自动配置类启用的:正匹配);

  • Negative matches:(没有启动,没有匹配成功的自动配置类:负匹配);

  • Unconditional classes:(没有条件的类);

5.5 自定义 starter(启动器)

  • 启动器模块:是一个空 jar 文件,仅提供辅助性依赖管理,这些依赖可能用于自动装配或者其他类库;
  • 命名规约:
    • 官方命名:
      • spring-boot-starter-xxx;
      • 如:spring-boot-starter-web;
    • 自定义命名:
      • xxx-spring-boot-starter;
      • 如:mybatis-spring-boot-starter;

编写启动器

  • 在 IDEA 中新建一个空项目:spring-boot-starter-diy

  • 创建一个普通 Maven 模块:test-spring-boot-starter

  • 创建一个 Spring Boot 模块:test-spring-boot-starter-autoconfigure

  • 项目基本结构:

  • starter 模块中,导入 autoconfigure 的依赖:

<!-- 启动器 -->
<dependencies>
    <!-- 引入自动配置模块 -->
    <dependency>
        <groupId>com.study</groupId>
        <artifactId>test-spring-boot-starter-autoconfigure</artifactId>
        <version>0.0.1-SNAPSHOT</version>
    </dependency>
</dependencies>
  • 将 autoconfigure 项目下,多余的文件都删掉,pom.xml 中只留下一个 starter,这是所有的启动器基本配置:

  • 在 autoconfigure 项目下,创建一个服务类:HelloService

package com.study;

public class HelloService {
    HelloProperties helloProperties;

    public HelloProperties getHelloProperties() {
        return helloProperties;
    }

    public void setHelloProperties(HelloProperties helloProperties) {
        this.helloProperties = helloProperties;
    }

    public String sayHello(String name) {
        return helloProperties.getPrefix() + name +
                helloProperties.getSuffix();
    }
}
  • 创建配置类:HelloProperties
package com.study;

@ConfigurationProperties(prefix = "test.hello")
public class HelloProperties {
    private String prefix;
    private String suffix;

    public String getPrefix() {
        return prefix;
    }

    public void setPrefix(String prefix) {
        this.prefix = prefix;
    }

    public String getSuffix() {
        return suffix;
    }

    public void setSuffix(String suffix) {
        this.suffix = suffix;
    }
}
  • 创建自动配置类并注入 bean:HelloServiceAutoConfiguration
package com.study;

@Configuration
// web应用生效
@ConditionalOnWebApplication
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
    @Autowired
    HelloProperties helloProperties;

    @Bean
    public HelloService helloService() {
        HelloService service = new HelloService();
        service.setHelloProperties(helloProperties);
        return service;
    }
}
  • 在 resources 下创建:META-INF\spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.study.HelloServiceAutoConfiguration
  • 文件结构:

  • 全部创建完成后,将模块安装到 maven 仓库中:

创建项目测试启动器

  • 创建 Spring Boot 项目(不用添加依赖);
  • 在 pom.xml 文件中,导入自定义的启动器:
<!--导入定义启动器-->
<dependency>
    <groupId>com.study</groupId>
    <artifactId>test-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
  • 创建 controller 包及 HelloController 类,测试自定义的接口:
package com.study.controller;

@RestController
public class HelloController {
    @Autowired
    HelloService helloService;

    @RequestMapping("/hello")
    public String hello() {
        return helloService.sayHello("hello");
    }
}
  • 创建配置文件:

    • 方式一:application.properties

      test.hello.prefix=say
      test.hello.suffix=world
      
    • 方式二:application.yaml

      test:
        hello:
          prefix: say
          suffix: world
      
  • 运行测试:

    image-20220425190537570

六、Spring Boot Web 开发

6.1 静态资源处理

  • 以前的 Web 应用,静态内容一般放在 main 下的 webapp 中;
  • Spring Boot 项目,如何引入前端资源,如:css、js 等文件;

第一种:静态资源映射规则 1

  • Spring Boot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 这个配置类里面,查看相关的配置:
    • addResourceHandlers 添加资源处理:
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
    if (!this.resourceProperties.isAddMappings()) {
        // 已禁用默认资源处理
        logger.debug("Default resource handling disabled");
        return;
    }
    // webjars 配置
    addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
    // 静态资源配置
    addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
        registration.addResourceLocations(this.resourceProperties.getStaticLocations());
        if (this.servletContext != null) {
            ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
            registration.addResourceLocations(resource);
        }
    });
}
  • 查看源代码得出,所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找对应的资源;

WebJars

  • WebJars 官网

  • WebJars 本质是以 jar 包的方式,引入静态资源;

  • 示例:使用 jQuery,只要引入 jQuery 对应版本的 pom 依赖即可;

    <dependency>
        <groupId>org.webjars.npm</groupId>
        <artifactId>jquery</artifactId>
        <version>3.6.0</version>
    </dependency>
    
  • 查看 WebJars 目录结构:

  • 访问:只要是静态资源,Spring Boot 就会去对应的路径,寻找资源,http://localhost:8080/webjars/jquery/3.6.0/dist/jquery.js

第二种:静态资源映射规则 2

  • 在项目中,使用自己的静态资源,该如何导入;
  • 在 staticPathPattern 中,发现第二种映射规则: /**,访问当前的项目任意资源,它会去找 ResourceProperties 这个类,查看源码:
// 进入方法
public String getStaticPathPattern() {
    return this.staticPathPattern;
}

// WebProperties.java
// 找到对应的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

// 找到路径
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};
  • ResourceProperties 可以设置和静态资源有关的参数,这里指向了它会去寻找资源的文件夹,即上面数组的内容;
  • 结论:以下四个目录,存放的静态资源,可以被识别;
    • classpath:/META-INF/resources/
    • classpath:/resources/:优先级1(最高)一般存放上传的文件;
    • classpath:/static/:优先级 2(默认),一般存放图片;
    • classpath:/public/:优先级 3,一般存放公共资源;
  • 在 resources 根目录下,新建对应的文件夹,都可以存放静态文件,如:访问 http://localhost:8080/1.js ,会在这些文件夹中,寻找对应的静态资源文件;

第三种:自定义静态资源路径

  • 可以通过配置文件,指定静态资源文件夹,需要在 application.propertiesapplication.yaml 中配置:
# application.properties
spring.web.resources.static-locations=classpath:/coding

# application.yaml
spring:
  web:
    resources:
      static-locations: classpath:/coding
  • 注意:一旦自定义了静态文件夹的路径,原来的自动配置就会失效;

6.2 首页及图标定制

首页

  • 在 WebMvcAutoConfiguration 类,查看首页的相关配置:
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(
        ApplicationContext applicationContext,
        FormattingConversionService mvcConversionService,
        ResourceUrlProvider mvcResourceUrlProvider) {
    WelcomePageHandlerMapping welcomePageHandlerMapping =
            new WelcomePageHandlerMapping(
            new TemplateAvailabilityProviders(applicationContext),
                    // getWelcomePage():获得欢迎页
                    applicationContext, getWelcomePage(),
            this.mvcProperties.getStaticPathPattern());
    welcomePageHandlerMapping.setInterceptors(getInterceptors(
            mvcConversionService, mvcResourceUrlProvider));
    welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
    return welcomePageHandlerMapping;
}
  • 查看 getWelcomePage()
private Resource getWelcomePage() {
    // getStaticLocations():点进去查看,指向的就是静态资源目录,所以location为静态资源目录
    for (String location : this.resourceProperties.getStaticLocations()) {
        Resource indexHtml = getIndexHtml(location);
        if (indexHtml != null) {
            return indexHtml;
        }
    }
    ServletContext servletContext = getServletContext();
    if (servletContext != null) {
        return getIndexHtml(new ServletContextResource(servletContext, SERVLET_LOCATION));
    }
    return null;
}

private Resource getIndexHtml(String location) {
    return getIndexHtml(this.resourceLoader.getResource(location));
}

// 欢迎页为location下的index.html,也就是任意静态资源目录下的index.html(按优先级选择)
private Resource getIndexHtml(Resource location) {
    try {
        Resource resource = location.createRelative("index.html");
        if (resource.exists() && (resource.getURL() != null)) {
            return resource;
        }
    }
    catch (Exception ex) {
    }
    return null;
}
  • 静态资源文件夹下的所有 index.html 页面,被 /** 映射,访问 http://localhost:8080/ ,就会找静态资源文件夹下的 index.html

  • 测试:在上面的 3 个目录中,任意一个位置,创建 index.html

网站图标

  • 在 Spring Boot 2.2.X 以后的版本中,去掉了默认的自动配置,需要在页面中,自行引用 favicon.ico 图标:

    <head>
        <meta charset="UTF-8">
        <title>首页</title>
        <link rel="icon" href="images/favicon.ico" type="image/x-icon"/>
    </head>
    
  • 在静态资源目录下,放置自定义 favicon.ico 图标,清除浏览器缓存,刷新网页即可;

6.3 模板引擎 Thymeleaf

模板引擎

  • Spring Boot 默认不支持 jsp,推荐使用模板引擎;

  • 模板引擎:使用户界面与业务数据(内容)分离,可以生成特定格式的文档,如:网站的模板引擎,会生成一个标准的 HTML 文档;

  • 原理:把模板和后台封装的数据,提交给模板引擎,模板引擎,将指定模板内容(字符串)中的特定标记(子字符串)替换,生成需要的业务数据展示出去,如:网页;

  • Spring Boot 推荐 Thymeleaf,其实 jsp 就是一个模板引擎,还有 freemarker 等;

引入 Thymeleaf

<!--Thymeleaf 启动器-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • Maven 会自动下载 jar 包:

Thymeleaf 分析

  • 按照 Spring Boot 的自动配置原理,查找 ThymeleafProperties 自动配置类,看下 Thymeleaf 的自动配置规则:
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {

	private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
	// 默认的前缀
	public static final String DEFAULT_PREFIX = "classpath:/templates/";
	// 默认的后缀
	public static final String DEFAULT_SUFFIX = ".html";
}
  • 看到默认的前缀和后缀,只需要把 html 页面,放在类路径下的templates 下就可以自动渲染;
  • 使用 Thymeleaf 不需要进行配置,只需要将 html 页面,放在指定的文件夹下即可;

测试:

  • 创建测试类:TestController
@Controller
public class TestController {
    @RequestMapping("/t1")
    // classpath:/templates/test.html
    public String test(){
        return "test";
    }
}
  • 在 templates 目录下,创建 test.html 页面:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <p>Thymeleaf 测试</p>
</body>
</html>
  • 运行测试:

Thymeleaf 语法

  • 测试:后端数据,在页面中展示;
    • 修改上例的测试请求,后端增加数据传输;
@Controller
public class TestController {
    @RequestMapping("/t1")
    // classpath:/templates/test.html
    public String test(Model model){
        // 向前端传递数据
        model.addAttribute("msg","Hello,Thymeleaf");
        return "test";
    }
}
  • 使用 Thymeleaf,需要在 html 文件中,导入命名空间约束,以方便提示;
<html xmlns:th="http://www.thymeleaf.org">
  • 修改前端页面:test.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!--th:text就是将div中的内容,设置为指定的值,和Vue一样-->
    <div th:text="${msg}"></div>
</body>
</html>
  • 运行测试:

th 语法

表达式

  • 参考官方文档 #4

  • 简单的表达:

    • 变量表达式: ${...}
    • 选择变量表达式: *{...} ,和 ${} 在功能上是一样;
    • 消息表达式: #{...} ,获取国际化内容;
    • 链接 URL 表达式: @{...},定义 URL;
    • 片段表达式: ~{...},片段引用;
  • 字面量

    • 字面量: 'one text', 'Another one!',… ;
    • 数字: 0, 34, 3.0, 12.3,… ;
    • 布尔值: true, false
    • 空: null
    • 文字标记: one, sometext, main,… ;
  • 文本操作:

    • 字符串连接: +
    • 字面替换: |The name is ${name}|
  • 算术运算:

    • 二元运算符: +, -, *, /, %
    • 减号(一元运算符): -
  • 布尔运算:

    • 二元运算符: and, or
    • 布尔否定(一元运算符): !, not
  • 比较运算:

    • 比较: >, <, >=, <= ( gt, lt, ge, le) ;
    • 等式运算符: ==, != ( eq, ne) ;
  • 条件运算符:三元运算符

    • 如果-那么: (if) ? (then)
    • 如果-那么-否则: (if) ? (then) : (else)
    • 默认: (value) ?: (defaultvalue)
  • 特殊:

    • 无操作: _
  • 所有这些功能,都可以组合和嵌套:

'User is of type ' + (${user.isAdmin()} ? 'Administrator' : (${user.type} ?: 'Unknown'))

实例:Thymeleaf 测试

  • 在之前的 Controller 中,增加测试接口:
@RequestMapping("/t2")
public String test2(Map<String, Object> map) {
    map.put("msg", "<h1>hello</h1>");
    map.put("users", Arrays.asList("测试1", "测试2"));
    //classpath:/templates/test.html
    return "test";
}
  • 测试页面:text.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <!--th:text就是将div中的内容,设置为指定的值,和Vue一样-->
    <div th:text="${msg}"></div>
    <!--th:utext 不转义-->
    <div th:utext="${msg}"></div>
    <!--遍历数据-->
    <!--th:each每次遍历都会生成当前这个标签-->
    <p th:each="user:${users}" th:text="${user}"></p>
    <hr>
    <p>
        <!--行内写法(不推荐):官网#12-->
        <span th:each="user:${users}">[[${user}]]</span>
    </p>
</body>
</html>
  • 运行测试:

6.4 MVC 自动配置原理

  • 分析 Spring Boot 对的 SpringMVC 做了哪些配置,如何扩展和定制;

    • 途径一:源码分析;
    • 途径二:查看 官方文档

  • 希望保留 Spring Boot MVC 功能,并且希望添加其他 MVC 配置(拦截器、格式化程序、视图控制器和其他功能),则可以添加自己的 @configuration 类,类型为 webmvcconfiguer,但不能添加 @EnableWebMvc

  • 如果想提供自定义实例 RequestMappingHandlerMapping, RequestMappingHandlerAdapter,或 ExceptionHandlerExceptionResolver,并且保留 Spring Boot MVC 自定义,可以声明 WebMvcRegistrations 实例,来提供此类组件;

  • 如果想完全控制 Spring MVC,可以添加自己的 @Configuration,并用 @EnableWebMvc 进行注释;

ContentNegotiatingViewResolver 内容协商视图解析器

  • 自动配置了 ViewResolver,就是 SpringMVC 的视图解析器,根据方法的返回值,取得视图对象(View),然后由视图对象,决定如何渲染(转发,重定向);
  • 查看源码,找到 WebMvcAutoConfiguration 类,然后搜索 ContentNegotiatingViewResolver,找到如下方法:
@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
    ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
    resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
    // ContentNegotiatingViewResolver使用所有其他视图解析器来定位视图,因此它应该具有较高的优先级
    resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return resolver;
}
  • 点进 ContentNegotiatingViewResolver 类查看,找到对应的解析视图代码:
// 默认的视图解析器
@Override
// @Nullable:即参数可为null
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
    RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
    List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
    if (requestedMediaTypes != null) {
        // 获取候选的视图对象
        List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
        // 选择一个最适合的视图对象,然后把这个对象返回
        View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
        if (bestView != null) {
            return bestView;
        }
    }

    String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
            " given " + requestedMediaTypes.toString() : "";

    if (this.useNotAcceptableStatusCode) {
        if (logger.isDebugEnabled()) {
            logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
        }
        return NOT_ACCEPTABLE_VIEW;
    }
    else {
        logger.debug("View remains unresolved" + mediaTypeInfo);
        return null;
    }
}
  • 进入候选视图的方法 getCandidateViews 查看:
// 获取候选的视图
private List<View> getCandidateViews(String viewName, Locale locale, List<MediaType> requestedMediaTypes)
			throws Exception {

    List<View> candidateViews = new ArrayList<>();
    if (this.viewResolvers != null) {
        Assert.state(this.contentNegotiationManager != null, "No ContentNegotiationManager set");
        // 遍历所有的视图解析器
        for (ViewResolver viewResolver : this.viewResolvers) {
            // 封装成一个对象
            View view = viewResolver.resolveViewName(viewName, locale);
            if (view != null) {
                // 添加到候选的视图
                candidateViews.add(view);
            }
            for (MediaType requestedMediaType : requestedMediaTypes) {
                List<String> extensions = this.contentNegotiationManager.resolveFileExtensions(requestedMediaType);
                for (String extension : extensions) {
                    String viewNameWithExtension = viewName + '.' + extension;
                    view = viewResolver.resolveViewName(viewNameWithExtension, locale);
                    if (view != null) {
                        candidateViews.add(view);
                    }
                }
            }
        }
    }
    if (!CollectionUtils.isEmpty(this.defaultViews)) {
        candidateViews.addAll(this.defaultViews);
    }
    // 返回视图
    return candidateViews;
}
  • 结论:ContentNegotiatingViewResolver 这个视图解析器,就是用来组合所有的视图解析器的;

  • 再去分析下组合逻辑,看到有个属性 viewResolvers,看看它是在哪里进行赋值的:

@Override
protected void initServletContext(ServletContext servletContext) {
    // 从beanFactory工具中,获取容器中的所有视图解析器
	// ViewRescolver.class 组合所有的视图解析器
    Collection<ViewResolver> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(obtainApplicationContext(), ViewResolver.class).values();
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList<>(matchingBeans.size());
        for (ViewResolver viewResolver : matchingBeans) {
            if (this != viewResolver) {
                this.viewResolvers.add(viewResolver);
            }
        }
    }
    else {
        for (int i = 0; i < this.viewResolvers.size(); i++) {
            ViewResolver vr = this.viewResolvers.get(i);
            if (matchingBeans.contains(vr)) {
                continue;
            }
            String name = vr.getClass().getName() + i;
            obtainApplicationContext().getAutowireCapableBeanFactory().initializeBean(vr, name);
        }

    }
    AnnotationAwareOrderComparator.sort(this.viewResolvers);
    this.cnmFactoryBean.setServletContext(servletContext);
}
  • 结论:在容器中去找视图解析器,然后进行组合;
  • 推测:在容器中自定义视图解析器,这个类会自动的将它组合进来;

测试:自定义视图解析器

  • 自定义一个视图解析器:
    • 主程序同级目录下,创建配置文件夹 config 及配置类;
// 配置类:扩展SpringMvc,需要实现WebMvcConfigurer接口
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    // 注册Bean:JavaConfig的配置方式
    @Bean
    public ViewResolver myViewResolver() {
        return new MyViewResolver();
    }

    // 静态内部类:自定义视图解析器,需要实现ViewResolver接口
    private static class MyViewResolver implements ViewResolver {

        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
}
  • 查看视图解析器,是否起作用:

    • 通过给 DispatcherServlet 类中的 doDispatch 方法,加断点,进行调试,因为,所有的请求都会走到这个方法中

  • 启动的项目主程序,随便访问一个页面,查看 Debug 信息:

    • 找到 this;

    • 打开视图解析器,可以看到自定义的视图解析器;

  • 如果想要使用自定义的功能,只需要按照规则,给容器中添加自定义组件,Spring Boot 会进行自动装配;

转换器和格式化器

  • 在 WebMvcAutoConfiguration 类中,找到格式化转换器:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
    Format format = this.mvcProperties.getFormat();
    // 拿到配置文件中的格式化规则
    WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
            .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
    addFormatters(conversionService);
    return conversionService;
}
  • 点进 getFormat()
public Format getFormat() {
    return this.format;
}
  • 查看 Format:
public static class Format {

    /**
     * Date format to use, for example 'dd/MM/yyyy'.
     */
    private String date;

    /**
     * Time format to use, for example 'HH:mm:ss'.
     */
    private String time;

    /**
     * Date-time format to use, for example 'yyyy-MM-dd HH:mm:ss'.
     */
    private String dateTime;

    public String getDate() {
        return this.date;
    }

    public void setDate(String date) {
        this.date = date;
    }

    public String getTime() {
        return this.time;
    }

    public void setTime(String time) {
        this.time = time;
    }

    public String getDateTime() {
        return this.dateTime;
    }

    public void setDateTime(String dateTime) {
        this.dateTime = dateTime;
    }
}
  • application.properties 文件中,可以进行自动配置:
# 默认格式
spring.mvc.format.date=dd/MM/yyyy
  • 如果配置了自定义格式化方式,就会注册到 Bean 中生效,可以在配置文件中,配置日期格式化的规则;

修改 Spring Boot 的默认配置

  • Spring Boot 在自动配置组件时,先看容器中,有没有用户自定义的配置(用户自己配置 @bean),如果有,就使用用户配置的,如果没有,就用自动配置的;
  • 如果组件可以存在多个,比如:视图解析器,就将用户配置的和默认配置组合起来;

扩展使用 Spring MVC

  • 官方文档

  • If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.

  • If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.

  • 扩展 Spring MVC 要做的,就是编写一个 @Configuration 注解类,并且类型要为 WebMvcConfigurer(实现该类),注意:不能标注 @EnableWebMvc 注解

  • 在 config 包下,创建测试类:

// 可以使用自定义类,扩展MVC的功能(官方建议)
// 应为类型要求为WebMvcConfigurer,所以需要实现其接口
@Configuration
public class MyMvcConfig2 implements WebMvcConfigurer {

    // 视图跳转
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // 请求路径:/c1   跳转页面:test.html
        registry.addViewController("/c1").setViewName("test");
    }
}
  • 启动主程序,运行测试:

  • 如果要扩展 SpringMVC,官方推荐使用这种方式,既可以保留 Spring Boot 的所有自动配置,也可以使用扩展的配置;

分析原理

  • WebMvcAutoConfiguration 是 SpringMVC 的自动配置类,里面有一个内部类:WebMvcAutoConfigurationAdapter;
  • 这个类上有一个注解,在做其他自动配置时会导入:
// 自动配置类
@Import(EnableWebMvcConfiguration.class)
  • 点进 EnableWebMvcConfiguration 类查看,它继承了一个父类:DelegatingWebMvcConfiguration;
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {

	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
	// 从容器中获取所有的webmvcConfigurer
	@Autowired(required = false)
	public void setConfigurers(List<WebMvcConfigurer> configurers) {
		if (!CollectionUtils.isEmpty(configurers)) {
			this.configurers.addWebMvcConfigurers(configurers);
		}
	}
	// ...
}
  • 这个类中,去查找 viewController 当做参考:
@Override
protected void addViewControllers(ViewControllerRegistry registry) {
    this.configurers.addViewControllers(registry);
}
  • 进入 addViewControllers 方法查看:
@Override
public void addViewControllers(ViewControllerRegistry registry) {
    // 遍历所有的WebMvcConfigurer相关配置一起调用,包括默认配置的自定义配置
们配置的
    for (WebMvcConfigurer delegate : this.delegates) {
        delegate.addViewControllers(registry);
    }
}
  • 结论:所有的 WebMvcConfiguration 都会被作用,不止 Spring 默认的配置类,自定义的配置类也会被调用;

全面接管 SpringMVC

  • If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.

  • 全面接管:即 Spring Boot 对 SpringMVC 的自动配置不需要了,所有都是自己去配置;

  • 只需要在自定义配置类中,加上 @EnableWebMvc 注解即可;

  • 在开发中,不推荐使用全面接管 SpringMVC;

@EnableWebMvc 分析

  • 查看 @EnableWebMvc 源码:
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
}
  • 进入 DelegatingWebMvcConfiguration:
    • 它继承了一个父类 WebMvcConfigurationSupport;
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
	// ...
}
  • 回顾 WebMvcAutoConfiguration 自动配置类:
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 这个注解的意思就是:容器中没有这个组件的时候,这个自动配置类才生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
		ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
	// ...
}

总结:

  • @EnableWebMvc 将 WebMvcConfigurationSupport 组件导入进来了,所以自动配置失效;
  • 在 Spring Boot 中会有非常多的 xxxConfiguration 进行扩展配置,只要看见了这个,就应该多留心注意;