一、Spring Boot 简介
1.1 回顾:什么是 Spring
- Spring 基础
- Spring MVC 基础
- Spring 是一个开源框架,2003 年兴起的,一个轻量级的 Java 开发框架,作者:Rod Johnson;
- 作用:为了解决企业级应用开发的复杂性而创建的,简化开发;
1.2 Spring 是如何简化 Java 开发的
- 为了降低 Java 开发的复杂性,Spring 采用了以下 4 种关键策略:
- 基于 POJO 的轻量级和最小侵入性编程,所有东西都是 bean;
- 通过 IOC,依赖注入(DI)和面向接口,实现松耦合;
- 基于切面(AOP)和惯例,进行声明式编程;
- 通过切面和模版减少样式代码,如: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 项目创建方式
- 官网快速构建工具
-
查看版本:
-
在线构建,直接生成项目压缩包:
-
通过 IDEA 导入项目即可;
- 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 自动配置
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 导入依赖,默认是不需要写版本,但是,如果导入的包,没有在依赖中进行管理,就需要手动配置版本;
- 启动器
- 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;
- 主启动类
- 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 {
// ...
}
- @ComponentScan
- 这个注解在 Spring 中很重要,对应 XML 配置中的元素;
- 自动扫描当前主启动类同级的包,并加载符合条件的组件,或者 bean,将这个 bean 定义,加载到 IOC 容器中;
- @SpringBootConfiguration
- Spring Boot 的配置类,标注在某个类上,表示这是一个 Spring Boot 的配置类;
- 点击进入注解查看:
@Configuration:配置类,对应 Spring 的 xml 配置文件;@Component:说明这也是 Spring 的一个组件,负责启动应用;
- @EnableAutoConfiguration
-
开启 自动配置 功能;
-
以前需要手动配置的内容,现在 Spring Boot 可以自动配置;
-
通过
@EnableAutoConfiguration注解,Spring Boot 自动配置才能生效; -
点击进入注解查看:
-
@AutoConfigurationPackage:自动配置包;
// @Import:Spring底层注解,给容器中导入一个组件
// Registrar.class:将主启动类的所在包,及包下面所有子包里面的所有组件,扫描到Spring容器
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
// ...
}
@Import(AutoConfigurationImportSelector.class):给容器导入一个组件(选择器);
AutoConfigurationImportSelector.java:
- 自动配置 导入选择器,查看类源码:
- 类中有一个这样的方法:获取候选的配置 核心方法;
// 获取候选的配置
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;
}
- 这个方法又调用了 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());
}
- 继续点击查看 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;
}
- 发现一个多次出现的文件:
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分析:- 该方法主要分两部分:
- SpringApplication 的实例化;
- run 方法的执行;
- 该方法主要分两部分:
SpringApplication
- 这个类主要做了以下四件事:
- 推断应用的类型是普通的项目,还是 Web 项目;
- 查找并加载所有可用初始化器,设置到 initializers 属性中;
- 找出所有的应用程序监听器,设置到 listeners 属性中;
- 推断并设置 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 ;
- 对象、Map(键值对):
-
格式:
# 对象、Map格式 k: v1: v2: -
在对象或 Map 的下一行,写属性和值的关系,注意缩进和空格:
student: name: zhangsan age: 20 -
行内写法:
student: {name: zhangsan,age: 20}
- 数组(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文件:
- 在 resources 目录下,创建
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.properties或application.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 启动,会扫描以下位置的
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 环境 |
| @ConditionalOnJndi | JNDI 存在指定项 |
5.4 查看生效自动配置类
-
Spring Boot 的自动配置类,必须在一定的条件下才能生效,可以通过启用 debug=true 属性,查看生效的配置类:
application.properties配置文件方式:
# 开启springboot的调试类 debug=trueapplication.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
-
-
运行测试:
六、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 本质是以 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.properties或application.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 官网
- Github
- Spring 官方文档
- 在
pom.xml中添加启动器:
<!--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 语法
-
可以使用任意的
th:attr来替换 html 中原生属性的值; -
表达式
-
简单的表达:
- 变量表达式:
${...}; - 选择变量表达式:
*{...},和${}在功能上是一样; - 消息表达式:
#{...},获取国际化内容; - 链接 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
@Configurationclass of typeWebMvcConfigurerbut without@EnableWebMvc. -
If you want to provide custom instances of
RequestMappingHandlerMapping,RequestMappingHandlerAdapter, orExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrationsand 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
@Configurationannotated with@EnableWebMvc, or alternatively add your own@Configuration-annotatedDelegatingWebMvcConfigurationas 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 进行扩展配置,只要看见了这个,就应该多留心注意;