SpringBoot3 全栈指南教程——尚硅谷学习笔记 2023 年
一、Spring Boot 3-核心特性
第 1 章 SpringBoot3-快速入门
1.1 简介
1.1.1 前置知识
- Java17
- Spring、SpringMVC、MyBatis
- Maven、IDEA
1.1.2 环境要求
环境&工具****版本SpringBoot3.1.3+IDEA2022.3.3+Java17+Maven3.8.1+Tomcat10.1.12+Servlet5.0.0+GraalVM Community22.3+Native Build Tools0.9.19+
1.1.3 SpringBoot 是什么
SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用****(说明:SpringBoot 底层是 Spring)。
大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术。
特性:
快速创建
独立 Spring 应用。- SSM:导包、配置、启动运行。- 直接
嵌入
Tomcat、Jetty 或 Undertow(无需部署 war 包)【Servlet 容器】。- Linux、Java、Tomcat、MySQL:war 放到 Tomcat 的 webapps 下。- jar、Java 环境:java -jar。 - 重点:提供可选的
starter
,简化应用整合。- 场景启动器(starter):web、json、邮件、oss(对象存储)、异步、定时任务、缓存…- 导很多包,控制好版本。- 为每一种场景准备了一个依赖:web-starter、mybatis-starter。 - 重点:
按需自动配置
Spring 以及第三方库。- 如果这些场景要使用(生效)。这个场景的所有配置都会自动配置好。- 约定大于配置:每个场景都有很多默认配置。- 自定义:配置文件中修改几项就可以。 - 提供
生产级特性
:如监控指标、健康检查、外部化配置等。- 监控指标、健康检查(k8s)、外部化配置。 - 无代码生成、
无 xml
。
总结:简化开发,简化配置,简化整合,简化部署,简化监控,简化运维。
1.2 快速体验
场景:浏览器发送 /hello 请求,返回"Hello, Spring Boot 3!"
1.2.1 开发流程
1.2.1.1 创建项目
maven 项目
<!-- 所有 Spring Boot 项目都必须继承自 spring-boot-starter-parent --><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.3</version></parent>
1.2.1.2 导入场景
场景启动器
<dependencies><!-- Web 开发的场景启动器 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies>
1.2.1.3 主程序
packagecom.myxh.springboot;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/**
* @author MYXH
* @date 2023/9/11
* @description 启动 SpringBoot 项目的主入口程序
*/// 这是一个 SpringBoot 应用@SpringBootApplicationpublicclassMainApplication{publicstaticvoidmain(String[] args){SpringApplication.run(MainApplication.class, args);}}
1.2.1.4 业务
packagecom.myxh.springboot.controller;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;/**
* @author MYXH
* @date 2023/9/11
*/@RestControllerpublicclassHelloController{@GetMapping("/hello")publicStringhello(){return"Hello, Spring Boot 3!";}}
1.2.1.5 测试
默认启动访问:localhost:8080
1.2.1.6 打包
<build><plugins><!-- SpringBoot 应用打包插件--><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
mvn clean package
把项目打成可执行的 jar 包。
java -jar boot3-01-demo-1.0-SNAPSHOT.jar
启动项目。
1.2.2 特性小结
1.2.2.1 简化整合
导入相关的场景,拥有相关的功能的场景启动器。
- 官方提供的场景:命名为
spring-boot-starter-*
。 - 第三方提供场景:命名为
*-spring-boot-starter
。
场景一导入,万物皆就绪。
1.2.2.2 简化开发
无需编写任何配置,直接开发业务。
1.2.2.3 简化配置
application.properties
:
- 集中式管理配置,只需要修改这个文件就行。
- 配置基本都有默认值。
- 能写的所有配置都在:https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties
1.2.2.4 简化部署
打包为可执行的 jar 包。
Linux 服务器上有 Java 环境。
1.2.2.5 简化运维
修改配置(外部放一个 application.properties 文件)、监控、健康检查…
1.2.2.6 Spring Initializr 创建向导
一键创建好整个项目结构。
1.3 应用分析
1.3.1 依赖管理机制
思考:
1、为什么导入
starter-web
所有相关依赖都导入进来?
- 开发什么场景,导入什么场景启动器。
- maven 依赖传递原则。A-B-C:A 就拥有 B 和 C。
- 导入场景启动器,场景启动器自动把这个场景的所有核心依赖全部导入进来。
2、为什么版本号都不用写?
- 每个 boot 项目都有一个父项目
spring-boot-starter-parent
。 - parent 的父项目是
spring-boot-dependencies
。 - 父项目版本仲裁中心,把所有常见的 jar 的依赖版本都声明好了。
- 比如:
mysql-connector-j
。
3、自定义版本号。
- 利用 maven 的就近原则。- 直接在当前项目
properties
标签中声明父项目用的版本属性的 key。- 直接在导入依赖的时候声明版本。
4、第三方的 jar 包。
- boot 父项目没有管理的需要自行声明好。
<dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.16</version></dependency>
1.3.2 自动配置机制
1.3.2.1 初步理解
- 自动配置的 Tomcat、SpringMVC 等。- 导入场景,容器中就会自动配置好这个场景的核心组件。- 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter…- 现在:自动配置好的这些组件。- 验证:容器中有了什么组件,就具有什么功能。
packagecom.myxh.springboot;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;/*** @author MYXH* @date 2023/9/11* @description 启动 SpringBoot 项目的主入口程序*/// 主程序:com.myxh.springboot// @SpringBootConfiguration// @EnableAutoConfiguration// @ComponentScan("com.myxh.springboot")// @SpringBootApplication(scanBasePackages = "com.myxh.springboot")// 这是一个 SpringBoot 应用@SpringBootApplicationpublicclassMainApplication{publicstaticvoidmain(String[] args){// Java10:局部变量类型的自动推断var ioc =SpringApplication.run(MainApplication.class, args);// 1、获取容器中所有组件的名字String[] beanNames = ioc.getBeanDefinitionNames();// 2、挨个遍历/* dispatcherServlet、beanNameViewResolver、characterEncodingFilter、multipartResolver SpringBoot 把以前配置的核心组件现在都给自动配置好了 */for(String beanName : beanNames){System.out.println("beanName = "+ beanName);}}}
- 默认的包扫描规则。-
@SpringBootApplication
标注的类就是主程序类。- SpringBoot 只会扫描主程序所在的包及其下面的子包,自动的 component-scan 功能。- 自定义扫描路径。- @SpringBootApplication(scanBasePackages = “com.myxh.springboot”)-@ComponentScan("com.myxh.springboot")
直接指定扫描的路径。 - 配置默认值。- 配置文件的所有配置项是和某个类的对象值进行一一绑定的。- 绑定了配置文件中每一项值的类:属性类。- 比如:-
ServerProperties
绑定了所有 Tomcat 服务器有关的配置。-MultipartProperties
绑定了所有文件上传相关的配置。- 参照官方文档 https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#appendix.application-properties.server, 或者参照绑定的属性类。 - 按需加载自动配置。- 导入场景
spring-boot-starter-web
。- 场景启动器除了会导入相关功能依赖,导入一个spring-boot-starter
,是所有starter
的starter
,基础核心 starter。-spring-boot-starter
导入了一个包spring-boot-autoconfigure
。包里面都是各种场景的AutoConfiguration
自动配置类。- 虽然全场景的自动配置都在spring-boot-autoconfigure
这个包,但是不是全都开启的。- 导入哪个场景就开启哪个自动配置。
总结:导入场景启动器、触发
spring-boot-autoconfigure
这个包的自动配置生效、容器中就会具有相关场景的功能。
1.3.2.2 完整流程
思考:
1、SpringBoot 怎么实现导一个 starter、写一些简单配置,应用就能跑起来,无需关心整合。
2、为什么 Tomcat 的端口号可以配置在 application.properties 中,并且 Tomcat 能启动成功?
3、导入场景后哪些自动配置能生效?
自动配置流程细节梳理:
1、导入
starter-web
:导入了 web 开发场景。
- 1、场景启动器导入了相关场景的所有依赖:
starter-json
、starter-tomcat
、springmvc
。 - 2、每个场景启动器都引入了一个
spring-boot-starter
,核心场景启动器。 - 3、核心场景启动器引入了
spring-boot-autoconfigure
包。 - 4、
spring-boot-autoconfigure
里面囊括了所有场景的所有配置。 - 5、只要这个包下的所有类都能生效,那么相当于 SpringBoot 官方写好的整合功能就生效了。
- 6、SpringBoot 默认却扫描不到
spring-boot-autoconfigure
下写好的所有配置类。(这些配置类做了整合操作),默认只扫描主程序所在的包。
2、主程序:
@SpringBootApplication
。
- 1、
@SpringBootApplication
由三个注解组成@SpringBootConfiguration
、@EnableAutoConfiguration
、@ComponentScan
。 - 2、SpringBoot 默认只能扫描自己主程序所在的包及其下面的子包,扫描不到
spring-boot-autoconfigure
包中官方写好的配置类。 - 3、
@EnableAutoConfiguration
:SpringBoot 开启自动配置的核心。- ① 是由@Import(AutoConfigurationImportSelector.class)
提供功能:批量给容器中导入组件。- ② SpringBoot 启动会默认加载 146 个配置类。- ③ 这 146 个配置类来自于spring-boot-autoconfigure
下META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件指定的。- ④ 项目启动的时候利用 @Import 批量导入组件机制把autoconfigure
包下的 146xxxAutoConfiguration
类导入进来(自动配置类)。 - 4、按需生效:- 虽然导入了
146
个自动配置,并不是这146
个自动配置类都能生效。- 每一个自动配置类,都有条件注解@ConditionalOnXxx
,只有条件成立,才能生效。
3、
xxxAutoConfiguration
自动配置类。
- 1、给容器中使用 @Bean 放一堆组件。
- 2、每个自动配置类都可能有这个注解
@EnableConfigurationProperties(ServerProperties.class)
,用来把配置文件中配的指定前缀的属性值封装到xxxProperties
属性类中。 - 3、以 Tomcat 为例:把服务器的所有配置都是以
server
开头的。配置都封装到了属性类中。 - 4、给容器中放的所有组件的一些核心参数,都来自于
xxxProperties
。xxxProperties
都是和配置文件绑定。 - 5、只需要改配置文件的值,核心组件的底层参数都能修改。
4、写业务,全程无需关心各种整合(底层这些整合写好了,而且也生效了)。
核心流程总结:
1、导入
starter
,就会导入
autoconfigure
包。
2、
autoconfigure
包里面 有一个文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
,里面指定的所有启动要加载的自动配置类。
3、@EnableAutoConfiguration 会自动的把上面文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration 是有条件注解进行按需加载。
4、
xxxAutoConfiguration
给容器中导入一堆组件,组件都是从
xxxProperties
中提取属性值。
5、
xxxProperties
又是和配置文件进行了绑定。
效果:导入
starter
、修改配置文件,就能修改底层行为。
1.3.2.3 如何学好 SpringBoot
框架的框架、底层基于 Spring。能调整每一个场景的底层行为。100%项目一定会用到底层自定义。
摄影:
- 傻瓜:自动配置好。
- 单反:焦距、光圈、快门、感光度…
- 傻瓜+单反:
1、理解自动配置原理。
- ① 导入 starter -> 生效 xxxAutoConfiguration -> 组件 -> xxxProperties -> 配置文件。
2、理解其他框架底层。
- ① 拦截器。
3、可以随时定制化任何组件。
- ① 配置文件。
- ② 自定义组件。
普通开发:
导入 starter
,Controller、Service、Mapper、偶尔修改配置文件。
高级开发:自定义组件、自定义配置、自定义 starter。
核心:
- 这个场景自动配置导入了哪些组件,能不能 Autowired 进来使用。
- 能不能通过修改配置改变组件的一些默认参数。
- 需不需要自己完全定义这个组件。
- 场景定制化。
最佳实战:
- 选场景,导入到项目。- 官方:starter。- 第三方:去仓库搜。
- 写配置,改配置文件关键项。- 数据库参数(连接地址、账号密码…)。
- 分析这个场景导入了哪些能用的组件。- 自动装配这些组件进行后续使用。- 不满意 SprngBoot 提供的自动配好的默认组件。- 定制化。- 改配置。- 自定义组件。
整合 redis:
- 选场景:
spring-boot-starter-data-redis
。- 场景 AutoConfiguration 就是这个场景的自动配置类。 - 写配置:- 分析到这个场景的自动配置类开启了哪些属性绑定关系。-
@EnableConfigurationProperties(RedisProperties.class)
。- 修改 redis 相关的配置。 - 分析组件:- 分析到
RedisAutoConfiguration
给容器中放了StringRedisTemplate
。- 给业务代码中自动装配StringRedisTemplate
。 - 定制化:- 修改配置文件。- 自定义组件,自己给容器中放一个
StringRedisTemplate
。
1.4 核心技能
1.4.1 常用注解
SpringBoot 摒弃 XML 配置方式,改为全注解驱动。
1.4.1.1 组件注册
@Configuration、**@SpringBootConfiguration**
@Bean、**@Scope**
@Controller、 @Service、**@Repository、@Component**
@Import
@ComponentScan
步骤:
1、**@Configuration 编写一个配置类**。
2、在配置类中,自定义方法给容器中注册组件。配合@Bean。
3、或使用@Import 导入第三方的组件。
1.4.1.2 条件注解
如果注解指定的条件成立,则触发指定行为。
@ConditionalOnXxx
@ConditionalOnClass:如果类路径中存在这个类,则触发指定行为。
@ConditionalOnMissingClass:如果类路径中不存在这个类,则触发指定行为。
@ConditionalOnBean:如果容器中存在这个 Bean(组件),则触发指定行为。
@ConditionalOnMissingBean:如果容器中不存在这个 Bean(组件),则触发指定行为。
场景:
- 如果存在 FastsqlException 这个类,给容器中放一个 Cat 组件,命名 cat1。
- 否则,就给容器中放一个 Dog 组件,命名 dog1。
- 如果系统中有 dog1 这个组件,就给容器中放一个 User 组件,名 zhangsan。
- 否则,就放一个 User,名叫 lisi。
@ConditionalOnBean(value=组件类型,name=组件名字):判断容器中是否有这个类型的组件,并且名字是指定的值。
@ConditionalOnRepositoryType (org.springframework.boot.autoconfigure.data)
@ConditionalOnDefaultWebSecurity (org.springframework.boot.autoconfigure.security)
@ConditionalOnSingleCandidate (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnWarDeployment (org.springframework.boot.autoconfigure.condition)
@ConditionalOnJndi (org.springframework.boot.autoconfigure.condition)
@ConditionalOnResource (org.springframework.boot.autoconfigure.condition)
@ConditionalOnExpression (org.springframework.boot.autoconfigure.condition)
@ConditionalOnClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnEnabledResourceChain (org.springframework.boot.autoconfigure.web)
@ConditionalOnMissingClass (org.springframework.boot.autoconfigure.condition)
@ConditionalOnNotWebApplication (org.springframework.boot.autoconfigure.condition)
@ConditionalOnProperty (org.springframework.boot.autoconfigure.condition)
@ConditionalOnCloudPlatform (org.springframework.boot.autoconfigure.condition)
@ConditionalOnBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingBean (org.springframework.boot.autoconfigure.condition)
@ConditionalOnMissingFilterBean (org.springframework.boot.autoconfigure.web.servlet)
@Profile (org.springframework.context.annotation)
@ConditionalOnInitializedRestarter (org.springframework.boot.devtools.restart)
@ConditionalOnGraphQlSchema (org.springframework.boot.autoconfigure.graphql)
@ConditionalOnJava (org.springframework.boot.autoconfigure.condition)
1.4.1.3 属性绑定
@ConfigurationProperties:声明组件的属性和配置文件哪些前缀开始项进行绑定。
@EnableConfigurationProperties:快速注册注解:
- 场景:SpringBoot 默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解也没用。因为组件都扫描不进来,此时使用这个注解就可以快速进行属性绑定并把组件注册进容器。
将容器中任意组件(Bean)的属性值和配置文件的配置项的值进行绑定。
1、给容器中注册组件(@Component、@Bean)。
2、使用 @ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定。
1.4.2 YAML 配置文件
痛点:SpringBoot 集中化管理配置,application.properties。
问题:配置多以后难阅读和修改,层级结构辨识度不高。
YAML 是 “YAML Ain’t a Markup Language”(YAML 不是一种标记语言)。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(是另一种标记语言)。
- 设计目标,就是方便人类读写。
- 层次分明,更适合做配置文件。
使用 .yaml 或 .yml 作为文件后缀。
1.4.2.1. 基本语法
- 大小写敏感。
- 使用缩进表示层级关系,k: v,使用空格分割 k, v。
- 缩进时不允许使用 Tab 键,只允许使用空格。
- 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可。
- # 表示注释,从这个字符一直到行尾,都会被解析器忽略。
支持的写法:
- 对象:键值对的集合,例如:映射(map)、 哈希(hash)、 字典(dictionary)。
- 数组:一组按次序排列的值,例如:序列(sequence)、 列表(list)。
- 纯量:单个的、不可再分的值,例如:字符串、数字、bool、日期。
1.4.2.2 示例
packagecom.myxh.springboot.bean;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;importjava.util.Date;importjava.util.List;importjava.util.Map;/**
* @author MYXH
* @date 2023/9/11
*/@Component// 和配置文件 person 前缀的所有配置进行绑定@ConfigurationProperties(prefix ="person")// 自动生成无参构造器@NoArgsConstructor// 自动生成全参构造器@AllArgsConstructor// 自动生成 JavaBean 属性的 getter/setter@DatapublicclassPerson{privateString name;privateInteger age;privateDate birthDay;privateBoolean like;// 嵌套对象privateChild child;// 数组(里面是对象)privateList<Dog> dogs;// MapprivateMap<String,Cat> cats;}
packagecom.myxh.springboot.bean;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.stereotype.Component;importjava.util.Date;importjava.util.List;/**
* @author MYXH
* @date 2023/9/11
*/@Component@NoArgsConstructor@AllArgsConstructor@DatapublicclassChild{privateString name;privateInteger age;privateDate birthDay;// 数组privateList<String> text;}
packagecom.myxh.springboot.bean;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.stereotype.Component;/**
* @author MYXH
* @date 2023/9/11
*/@Component@NoArgsConstructor@AllArgsConstructor@DatapublicclassDog{privateString name;privateInteger age;}
packagecom.myxh.springboot.bean;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.stereotype.Component;/**
* @author MYXH
* @date 2023/9/11
*/@Component@NoArgsConstructor@AllArgsConstructor@DatapublicclassCat{privateString name;privateInteger age;}
properties 表示法。
server.port=8080
spring.servlet.multipart.max-file-size=10MB
# 配置 Redis
spring.data.redis.host=localhost
spring.data.redis.port=6379
# properties 表示复杂对象
person.name=张三
person.age=35
person.birthDay=1988/01/01 00:00:00
person.like=true
person.child.name=李四
person.child.age=12
person.child.birthDay=2011/01/01
person.child.text[0]=hello
person.child.text[1]=world
person.dogs[0].name=小黑
person.dogs[0].age=2
person.dogs[1].name=小白
person.dogs[1].age=1
person.cats.cat1.name=小蓝
person.cats.cat1.age=2
person.cats.cat2.name=小灰
person.cats.cat2.age=1
yaml 表示法。
# 1、k: v,k v 之前是空格区分# 2、属性有层级关系,使用下一行,空两个空格# 3、左侧对齐的代表同一层级的属性---server:port:8080# port: 8081spring:servlet:multipart:max-file-size: 10MB
# 配置 Redisdata:redis:host: localhost
port:6379# 下边是一个单独文档---# yaml 表示复杂对象person:name: 张三
age:35birth-day: 1988/01/01 00:00:00like:truechild:name: 李四
age:12birth-day: 2011/01/01
# text: ["he\nllo",'wor\nld']text:-"he\nllo"-'wor\nld'-|
cats:
cat1:
name: 小蓝
age: 2# 对象也可用 {} 表示cat2:{name: 小灰,age:1}->
cats:
cat1:
name: 小蓝
age: 2# 对象也可用 {} 表示cat2:{name: 小灰,age:1}dogs:# 数组也可用 - 表示-name: 小黑
age:2-name: 小白
age:1cats:cat1:name: 小蓝
age:2# 对象也可用 {} 表示cat2:{name: 小灰,age:1}
1.4.2.3 细节
- birthDay 推荐写为 birth-day。
- 文本:- 单引号不会转义【\n 则为普通字符串显示】。- 双引号会转义【\n 会显示为换行符】。
- 大文本:-
|
开头,大文本写在下层,保留文本格式,换行符正确显示。->
开头,大文本写在下层,折叠换行符。
多文档合并:
- 使用
\-\-\-
可以把多个 yaml 文档合并在一个文档中,每个文档区依然认为内容独立。
1.4.2.4. 小技巧:lombok
简化 JavaBean 开发。自动生成构造器、getter/setter、自动生成 Builder 模式等。
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><scope>compile</scope></dependency>
使用
@Data
等注解。
1.4.3 日志配置
规范:项目开发不要编写
System.out.println()
,应该用日志记录信息。
1.4.3.1 简介
1、Spring 使用
commons-logging
作为内部日志,但底层日志实现是开放的。可对接其他日志框架。
- ① spring5 及以后 commons-logging 被 spring 直接自己实现了。
2、支持
jul
,
log4j2
,
logback
。SpringBoot 提供了默认的控制台输出配置,也可以配置输出为文件。
3、
logback
是默认使用的。
4、虽然日志框架很多,但是不用担心,使用 SpringBoot 的默认配置就能工作的很好。
SpringBoot 怎么把日志默认配置好的。
1、每个
starter
场景,都会导入一个核心场景
spring-boot-starter
。
2、核心场景引入了日志的所用功能
spring-boot-starter-logging
。
3、默认使用了
logback + slf4j
组合作为默认底层日志。
4、
日志是系统一启动就要用
,
xxxAutoConfiguration
是系统启动好了以后放好的组件,后来用的。
5、日志是利用监听器机制配置好的。
ApplicationListener
。
6、日志所有的配置都可以通过修改配置文件实现。以
logging
开始的所有配置。
1.4.3.2 日志格式
2023-09-14 20:24:43.709 INFO 96528 --- [main] o.s.b.w.e.t.TomcatWebServer : Tomcat initialized with port(s): 8080(http)2023-09-14 20:24:43.712 INFO 96528 --- [main] o.a.c.c.AprLifecycleListener : Loaded Apache Tomcat Native library [2.0.5] using APR version [1.7.4].
默认输出格式:
- 时间和日期:毫秒级精度。
- 日志级别:
ERROR
,WARN
,INFO
,DEBUG
,TRACE
。 - 进程 ID。
- ---:消息分割符。
- 线程名:使用[]包含。
- Logger 名:通常是产生日志的类名。
- 消息:日志记录的内容。
注意:logback 没有
FATAL
级别,对应的是
ERROR
。
默认值:参照:
spring-boot
包
additional-spring-configuration-metadata.json
文件。
默认输出格式值:
%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd'T'HH:mm:ss.SSSXXX}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}
。
可修改为:
%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level [%thread] %logger{15} ===> %msg%n
。
1.4.3.3 记录日志
Logger logger =LoggerFactory.getLogger(getClass());
或者使用 Lombok 的@Slf4j 注解。
1.4.3.4 日志级别
- 由低到高:
ALL, TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
。- 只会打印指定级别及以上级别的日志。- ALL:打印所有日志。- TRACE:追踪框架详细流程日志,一般不使用。- DEBUG:开发调试细节日志。- INFO:关键、感兴趣信息日志。- WARN:警告但不是错误的信息日志,比如:版本过时。- ERROR:业务错误日志,比如出现各种异常。- FATAL:致命错误日志,比如 jvm 系统崩溃。- OFF:关闭所有日志记录。 - 不指定级别的所有类,都使用 root 指定的级别作为默认级别。
- SpringBoot 日志默认级别是 INFO。
1、在
application.properties/yaml
中配置
logging.level.<logger-name>=<level>
指定日志级别。
2、
level
可取值范围:
TRACE, DEBUG, INFO, WARN, ERROR, FATAL, OFF
,定义在
LogLevel
类中。
3、root 的
logger-name
叫
root
,可以配置
logging.level.root=warn
,代表所有未指定日志级别都使用 root 的 warn 级别。
1.4.3.5 日志分组
比较有用的技巧是:
将相关的
logger
分组在一起,统一配置。SpringBoot 也支持。比如:Tomcat 相关的日志统一设置。
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace
SpringBoot 预定义两个组。
NameLoggersweb
org.springframework.core.codec
,
org.springframework.http
,
org.springframework.web
,
org.springframework.boot.actuate.endpoint.web
,
org.springframework.boot.web.servlet.ServletContextInitializerBeans
sql
org.springframework.jdbc.core
,
org.hibernate.SQL
,
org.jooq.tools.LoggerListener
1.4.3.6 文件输出
SpringBoot 默认只把日志写在控制台,如果想额外记录到文件,可以在
application.properties
中添加
logging.file.name
或
logging.file.path
配置项。
logging.file.name
logging.file.path
示例效果未指定未指定无仅控制台输出。指定未指定
my.log
写入指定文件。可以
加路径
。未指定指定
./log
写入指定目录,文件名为
spring.log
。指定****指定无以
logging.file.name
为准。
1.4.3.7 文件归档与滚动切割
归档:每天的日志单独存到一个文档中。
切割:每个文件 10MB,超过大小切割成另外一个文件。
1、每天的日志应该独立分割出来存档。如果使用
logback
(SpringBoot 默认整合),可以通过
application.properties/yaml
文件指定日志滚动规则。
2、如果是其他日志系统,需要自行配置(添加
log4j2.xml
或
log4j2-spring.xml
)。
3、支持的滚动规则设置如下。
配置项描述
logging.logback.rollingpolicy.file-name-pattern
日志存档的文件名格式(默认值:
${LOG_FILE}.%d{yyyy-MM-dd}.%i.gz
)。
logging.logback.rollingpolicy.clean-history-on-start
应用启动时是否清除以前存档(默认值:
false
)。
logging.logback.rollingpolicy.max-file-size
存档前,每个日志文件的最大大小(默认值:
10MB
)。
logging.logback.rollingpolicy.total-size-cap
日志文件被删除之前,可以容纳的最大大小(默认值:
0B
)。设置
1GB
则磁盘存储超过 1GB 日志后就会删除旧日志文件。
logging.logback.rollingpolicy.max-history
日志文件保存的最大天数(默认值:
7
)。
1.4.3.8 自定义配置
通常配置
application.properties
就够了。当然也可以自定义。比如:
日志系统自定义Logback
logback-spring.xml
,
logback-spring.groovy
,
logback.xml
,
logback.groovy
Log4j2
log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging)
logging.properties
如果可能,建议在日志配置中使用
-spring
变量(例如,
logback-spring.xml
而不是
logback.xml
)。如果使用标准配置文件,spring 无法完全控制日志初始化。
最佳实战:自己要写配置,配置文件名加上
xxx-spring.xml
。
1.4.3.9 切换日志组合
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- 如果第三方框架使用了其他日志框架,如 jul,可以排除掉这个框架的默认日志 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId><exclusions><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-logging</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-log4j2</artifactId></dependency>
log4j2 支持 yaml 和 json 格式的配置文件。
格式依赖文件名YAMLcom.fasterxml.jackson.core:jackson-databind、com.fasterxml.jackson.dataformat:jackson-dataformat-yamllog4j2.yaml 或 log4j2.ymlJSONcom.fasterxml.jackson.core:jackson-databindlog4j2.json 或 log4j2.jsn
1.4.3.10 最佳实战
1、导入任何第三方框架,先排除它的日志包,因为 Boot 底层控制好了日志。
2、修改
application.properties
配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如
logback-spring.xml
,
log4j2-spring.xml
。
3、如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka 之类的中间件,这和 SpringBoot 没关系,都是日志框架自己的配置,修改配置文件即可。
4、业务中使用 slf4j-api 记录日志,不要再用 System.out.println() 了
第 2 章 SpringBoot3-Web 开发
SpringBoot 的 Web 开发能力,由 SpringMVC 提供。
2.1 WebMvcAutoConfiguration 原理
2.1.1 生效条件
// 在这些自动配置之后@AutoConfiguration(after ={DispatcherServletAutoConfiguration.class,TaskExecutionAutoConfiguration.class,ValidationAutoConfiguration.class})// 如果是 Web 应用就生效,类型有 SERVLET、REACTIVE(响应式 Web)@ConditionalOnWebApplication(type =Type.SERVLET)@ConditionalOnClass({Servlet.class,DispatcherServlet.class,WebMvcConfigurer.class})// 容器中没有这个 Bean,才生效,默认就是没有@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)// 优先级@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE+10)@ImportRuntimeHints(WebResourcesRuntimeHints.class)publicclassWebMvcAutoConfiguration{}
2.1.2 效果
1、放了两个 Filter:
- ①
HiddenHttpMethodFilter
:页面表单提交 Rest 请求(GET、POST、PUT、DELETE)。 - ②
FormContentFilter
:表单内容 Filter,GET(数据放 URL 后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略。
2、给容器中放了
WebMvcConfigurer
组件:给 SpringMVC 添加各种定制功能。
- ① 所有的功能最终会和配置文件进行绑定。
- ② WebMvcProperties:
spring.mvc
配置文件。 - ③ WebProperties:
spring.web
配置文件。
@Configuration(proxyBeanMethods =false)// 额外导入了其他配置@Import(EnableWebMvcConfiguration.class)@EnableConfigurationProperties({WebMvcProperties.class,WebProperties.class})@Order(0)publicstaticclassWebMvcAutoConfigurationAdapterimplementsWebMvcConfigurer,ServletContextAware{}
2.1.3 WebMvcConfigurer 接口
提供了配置 SpringMVC 底层的所有组件入口。
2.1.4 静态资源规则源码
@OverridepublicvoidaddResourceHandlers(ResourceHandlerRegistry registry){if(!this.resourceProperties.isAddMappings()){
logger.debug("Default resource handling disabled");return;}addResourceHandler(registry,this.mvcProperties.getWebjarsPathPattern(),"classpath:/META-INF/resources/webjars/");addResourceHandler(registry,this.mvcProperties.getStaticPathPattern(),(registration)->{
registration.addResourceLocations(this.resourceProperties.getStaticLocations());if(this.servletContext !=null){ServletContextResource resource =newServletContextResource(this.servletContext,SERVLET_LOCATION);
registration.addResourceLocations(resource);}});}
1、规则一:访问
/webjars/**
路径就去
classpath:/META-INF/resources/webjars/
下找资源。
- ① Maven 导入依赖。
2、规则二:访问
/**
路径就去
静态资源默认的四个位置找资源
。
- ①
classpath:/META-INF/resources/
- ②
classpath:/resources/
- ③
classpath:/static/
- ④
classpath:/public/
3、规则三:静态资源默认都有缓存规则的设置。
- ① 所有缓存的设置,直接通过配置文件:
spring.web
。 - ② cachePeriod:缓存周期,多久不用找服务器要新的,默认没有缓存周期,以秒为单位。
- ③ cacheControl:HTTP 缓存控制,https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching 。
- ④ useLastModified:是否使用最后一次修改,配合 HTTP Cache 规则。
如果浏览器访问了一个静态资源 index.js,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());
2.1.5 EnableWebMvcConfiguration 源码
/*
SpringBoot 给容器中放 WebMvcConfigurationSupport 组件
如果自己放了 WebMvcConfigurationSupport 组件,SpringBoot 的 WebMvcAutoConfiguration 都会失效
*/@Configuration(proxyBeanMethods =false)@EnableConfigurationProperties(WebProperties.class)publicstaticclassEnableWebMvcConfigurationextendsDelegatingWebMvcConfigurationimplementsResourceLoaderAware{}
1、
HandlerMapping
:根据请求路径
/xxx
找那个 handler 能处理请求。
- ①
WelcomePageHandlerMapping
:- (1) 访问/\*\*
路径下的所有请求,都在以前四个静态资源路径下找,欢迎页也一样。- (2) 找index.html
:只要静态资源的位置有一个index.html
页面,项目启动默认访问。
2.1.6 为什么容器中放一个 WebMvcConfigurer 就能配置底层行为
1、WebMvcAutoConfiguration 是一个自动配置类,它里面有一个
EnableWebMvcConfiguration
。
2、
EnableWebMvcConfiguration
继承于
DelegatingWebMvcConfiguration
,这两个都生效。
3、
DelegatingWebMvcConfiguration
利用依赖注入把容器中所有
WebMvcConfigurer
注入进来。
4、别人调用
DelegatingWebMvcConfiguration
的方法配置底层规则,而它调用所有
WebMvcConfigurer
的配置底层方法。
2.1.7 WebMvcConfigurationSupport
提供了很多的默认设置。
判断系统中是否有相应的类:如果有,就加入相应的
HttpMessageConverter
jackson2Present =ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)&&ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
jackson2XmlPresent =ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
jackson2SmilePresent =ClassUtils.isPresent("com.fasterxml.jackson.dataformat.smile.SmileFactory", classLoader);
2.2 Web 场景
2.2.1 自动配置
1、整合 web 场景。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>
2、引入了
autoconfigure
功能。
3、
@EnableAutoConfiguration
注解使用
@Import(AutoConfigurationImportSelector.class)
批量导入组件。
4、加载
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中配置的所有组件。
5、所有自动配置类如下。
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfigurationorg.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration// ==============以下是响应式 Web 场景==============org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfigurationorg.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration// ===============================================org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfigurationorg.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
6、绑定了配置文件的一堆配置项。
- ① SpringMVC 的所有配置
spring.mvc
。 - ② Web 场景通用配置
spring.web
。 - ③ 文件上传配置
spring.servlet.multipart
。 - ④ 服务器的配置
server
,比如:编码方式
2.2.2 默认效果
默认配置:
1、包含了
ContentNegotiatingViewResolver
和
BeanNameViewResolver
组件,方便视图解析。
2、默认的静态资源处理机制:静态资源放在
static
文件夹下即可直接访问。
3、自动注册了
Converter
,
GenericConverter
,
Formatter
组件,适配常见数据类型转换和格式化需求。
4、支持
HttpMessageConverters
,可以方便返回
json
等数据类型。
5、注册
MessageCodesResolver
,方便国际化及错误消息处理。
6、支持静态
index.html
。
7、自动使用
ConfigurableWebBindingInitializer
,实现消息处理、数据绑定、类型转化、数据校验等功能。
重要:
- 如果想保持 boot mvc 的默认配置,并且自定义更多的 mvc 配置,比如:interceptors, formatters, view controllers 等。可以使用 @Configuration 注解添加一个 WebMvcConfigurer 类型的配置类,并且不要标注 @EnableWebMvc。
- 如果想保持 boot mvc 的默认配置,但要自定义核心组件实例,比如:RequestMappingHandlerMapping, RequestMappingHandlerAdapter, 或 ExceptionHandlerExceptionResolver,给容器中放一个 WebMvcRegistrations 组件即可。
- 如果想全面接管 Spring MVC,**@Configuration** 标注一个配置类,并加上 @EnableWebMvc 注解,实现 WebMvcConfigurer 接口。
2.3 静态资源
2.3.1 默认规则
2.3.1.1 静态资源映射
静态资源映射规则在
WebMvcAutoConfiguration
中进行了定义:
1、
/webjars/**
的所有路径资源都在
classpath:/META-INF/resources/webjars/
。
2、
/**
的所有路径资源都在
classpath:/META-INF/resources/
、
classpath:/resources/
、
classpath:/static/
、
classpath:/public/
。
3、所有静态资源都定义了
缓存规则
。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值。
- ①
period
:缓存间隔,默认 0 秒。 - ②
cacheControl
:缓存控制,默认无。 - ③
useLastModified
:是否使用 lastModified 头,默认 false。
2.3.1.2 静态资源缓存
如前面所述
1、所有静态资源都定义了
缓存规则
。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值。
- ①
period
:缓存间隔,默认 0 秒。 - ②
cacheControl
:缓存控制,默认无。 - ③
useLastModified
:是否使用 lastModified 头,默认 false。
2.3.1.3 欢迎页
欢迎页规则在
WebMvcAutoConfiguration
中进行了定义:
1、在静态资源目录下找
index.html
模板页。
2、没有就在
templates
下找
index.html
模板页。
2.3.1.4 Favicon
1、在静态资源目录下找
favicon.ico
。
2.3.1.5 缓存实验
server.port=8080
# 1、spring.web:
# ① 配置国际化的区域信息
# ② 静态资源策略(开启、处理链、缓存)
# 开启静态资源映射规则
spring.web.resources.add-mappings=true
# 设置缓存
spring.web.resources.cache.period=3600
# 缓存详细合并项控制,覆盖 period 配置
# 浏览照第一次请求服务器,服务器告诉浏览器此资源缓存 7200 秒,7200 秒以内的所有此资源访问不用发给服务器请求,7200 秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
# 共享缓存
spring.web.resources.cache.cachecontrol.cache-public=true
# 使用资源 last-modified 时间,来对比服务器和浏览照的资源是否相同没有变化,相同返回 304
spring.web.resources.cache.use-last-modified=true
2.3.2 自定义静态资源规则
自定义静态资源路径、自定义缓存规则。
2.3.2.1 配置方式
spring.mvc
:静态资源访问前缀路径。
spring.web
:
- 静态资源目录。
- 静态资源缓存策略。
# 2、spring.mvc
# ① 自定义 webjars 路径前缀
spring.mvc.webjars-path-pattern=/webjars/**
# ② 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**
2.3.2.2 代码方式
容器中只要有一个 WebMvcConfigurer 组件,配置的底层行为都会生效。
@EnableWebMvc,禁用 boot 的默认配置。
packagecom.myxh.springboot.web.config;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.CacheControl;importorg.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;importjava.util.concurrent.TimeUnit;/**
* @author MYXH
* @date 2023/9/18
*/// 禁用 Spring Boot 的默认配置// @EnableWebMvc// 这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层@ConfigurationpublicclassMyConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddResourceHandlers(ResourceHandlerRegistry registry){// 保留默认规则WebMvcConfigurer.super.addResourceHandlers(registry);// 新增自定义规则
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/image/, classpath:/static/").setCacheControl(CacheControl.maxAge(7200,TimeUnit.SECONDS));}}
packagecom.myxh.springboot.web.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.CacheControl;importorg.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;importjava.util.concurrent.TimeUnit;/**
* @author MYXH
* @date 2023/9/18
*/// 禁用 Spring Boot 的默认配置// @EnableWebMvc// 这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层@ConfigurationpublicclassMyConfig{@BeanpublicWebMvcConfigurerwebMvcConfigurer(){returnnewWebMvcConfigurer(){/**
* 配置静态资源
*
* @param registry 注册表
*/@OverridepublicvoidaddResourceHandlers(ResourceHandlerRegistry registry){// 保留默认规则WebMvcConfigurer.super.addResourceHandlers(registry);// 新增自定义规则
registry.addResourceHandler("/static/**").addResourceLocations("classpath:/image/, classpath:/static/").setCacheControl(CacheControl.maxAge(7200,TimeUnit.SECONDS));}};}}
2.4 路径匹配
Spring5.3 之后加入了更多的请求路径匹配的实现策略。
以前只支持 AntPathMatcher 策略, 现在提供了 PathPatternParser 策略,并且可以指定到底使用那种策略。
2.4.1 Ant 风格路径用法
Ant 风格的路径模式语法具有以下规则:
\*
:表示任意数量的字符。?
:表示任意一个字符。\*\*
:表示任意数量的目录。{}
:表示一个命名的模式占位符。[]
:表示字符集合
,例如[a-z]
表示小写字母。
例如:
\*.html
匹配任意名称,扩展名为.html
的文件。- /
folder1/\*/\*.java
匹配在folder1
目录下的任意两级目录下的.java
文件。 /folder2/\*\*/\*.jsp
匹配在folder2
目录下任意目录深度的.jsp
文件。/{type}/{id}.html
匹配任意文件名为{id}.html
,在任意命名的{type}
目录下的文件。
注意:Ant 风格的路径模式语法中的特殊字符需要转义,例如:
- 要匹配文件路径中的星号,则需要转义为
\\\\*
。 - 要匹配文件路径中的问号,则需要转义为
\\\\?
。
2.4.2 模式切换
AntPathMatcher 与 PathPatternParser。
PathPatternParser 在 jmh 基准测试下,有 6
8 倍吞吐量提升,降低 30%40% 空间分配率。PathPatternParser 兼容 AntPathMatcher 语法,并支持更多类型的路径模式。
PathPatternParser “***” 多段匹配的支持仅允许在模式末尾使用*。
packagecom.myxh.springboot.web.controller;importjakarta.servlet.http.HttpServletRequest;importlombok.extern.slf4j.Slf4j;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.PathVariable;importorg.springframework.web.bind.annotation.RestController;/**
* @author MYXH
* @date 2023/9/18
*/@Slf4j@RestControllerpublicclassHelloController{/**
* 默认使用新版 PathPatternParser 进行路径匹配
* 不能匹配 ** 在中间的情况,其他情况和 antPathMatcher语法兼容
*
* @param request 请求
* @param path 路径
* @return uri
*/@GetMapping("/a*/b?/**/{p1:[a-f]+}/**")publicStringhello(HttpServletRequest request,@PathVariable("p1")String path){
log.info("路径变量 p1:{}", path);String uri = request.getRequestURI();return uri;}}
总结:
- 使用默认的路径匹配规则,是由
PathPatternParser
提供的。 - 如果路径中间需要有 **,替换成 ant 风格路径。
2.5 内容协商
一套系统适配多端数据返回。
2.5.1 多端内容适配
2.5.1.1 默认规则
1、SpringBoot 多端内容适配。
- ① 基于请求头内容协商:(默认开启)- (1)客户端向服务端发送请求,携带 HTTP 标准的 Accept 请求头。- [1] Accept:
application/json
、text/xml
、text/yaml
。- [2] 服务端根据客户端请求头期望的数据类型进行动态返回。- ② 基于请求参数内容协商:(需要开启)- [1] 发送请求GET /projects/spring-boot?format=json
。- [2] 匹配到@GetMapping("/projects/spring-boot")
。- [3] 根据参数协商,优先返回 json 类型数据 【需要开启参数匹配设置】。- [4] 发送请求GET /projects/spring-boot?format=xml
,优先返回 xml 类型数据。
2.5.1.2 效果演示
请求同一个接口,可以返回 json 和 xml 不同格式数据。
1、引入支持写出 xml 内容依赖。
<!-- 支持返回 XML 格式数据 --><dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-xml</artifactId></dependency>
2、标注注解。
packagecom.myxh.springboot.web.bean;importcom.fasterxml.jackson.dataformat.xml.annotation.JacksonXmlRootElement;importlombok.AllArgsConstructor;importlombok.Data;importlombok.NoArgsConstructor;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;/**
* @author MYXH
* @date 2023/9/18
*/// 可以写出为 xml 文档@JacksonXmlRootElement@Component// 和配置文件 person 前缀的所有配置进行绑定@ConfigurationProperties(prefix ="user")// 自动生成无参构造器@NoArgsConstructor// 自动生成全参构造器@AllArgsConstructor// 自动生成 JavaBean 属性的 getter/setter@DatapublicclassUser{privateLong id;privateString userName;privateString password;privateInteger age;privateString email;privateString role;}
3、开启基于请求参数的内容协商。
# 开启基于请求参数的内容协商功能,默认参数名:format,默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名,默认是 format
spring.mvc.contentnegotiation.parameter-name=type
4、效果。
2.5.1.3 配置协商规则与支持类型
1、修改内容协商方式。
# 开启基于请求参数的内容协商功能,默认参数名:format,默认此功能不开启
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商时使用的参数名,默认是 format
spring.mvc.contentnegotiation.parameter-name=type
2、大多数 MediaType 都是开箱即用的。也可以自定义内容类型,例如:
# 增加一种新的内容类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml
spring.mvc.contentnegotiation.media-types.yml=text/yml
2.5.2 自定义内容返回
2.5.2.1 增加 yaml 返回支持
导入依赖。
<!-- 支持返回 YAML 格式数据 --><dependency><groupId>com.fasterxml.jackson.dataformat</groupId><artifactId>jackson-dataformat-yaml</artifactId></dependency>
把对象写出成 YAML。
packagecom.myxh.springboot.web.controller;importcom.myxh.springboot.web.bean.User;/**
* @author MYXH
* @date 2023/9/18
*/publicclassHelloController{publicstaticvoidmain(String[] args)throwsJsonProcessingException{User user =newUser();
user.setId(1L);
user.setUserName("MYXH");
user.setPassword("520.ILY!");
user.setAge(21);
user.setEmail("[email protected]");YAMLFactory factory =newYAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);ObjectMapper mapper =newObjectMapper(factory);String userYaml = mapper.writeValueAsString(user);System.out.println("userYaml = "+ userYaml);}}
编写配置。
# 增加一种新的内容类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml
spring.mvc.contentnegotiation.media-types.yml=text/yml
增加
HttpMessageConverter
组件,专门负责把对象写出为 yaml 格式。
packagecom.myxh.springboot.web.config;importcom.myxh.springboot.web.component.MyYamlHttpMessageConverter;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.converter.HttpMessageConverter;importorg.springframework.web.servlet.config.annotation.WebMvcConfigurer;importjava.util.List;/**
* @author MYXH
* @date 2023/9/18
*/// 禁用 Spring Boot 的默认配置// @EnableWebMvc// 这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层@ConfigurationpublicclassMyConfig{@BeanpublicWebMvcConfigurerwebMvcConfigurer(){returnnewWebMvcConfigurer(){/**
* 配置一个能把对象转为 yaml 的 messageConverter
*
* @param converters 最初是转换器的空列表
*/@OverridepublicvoidconfigureMessageConverters(List<HttpMessageConverter<?>> converters){
converters.add(newMyYamlHttpMessageConverter());}};}}
2.5.2.2 思考:如何增加其他
- 配置媒体类型支持:-
spring.mvc.contentnegotiation.media-types.yaml=text/yaml
。 - 编写对应的
HttpMessageConverter
,要告诉 Boot 这个支持的媒体类型。- 按照 HttpMessageConverter 的示例写法。 - 把 MessageConverter 组件加入到底层。- 容器中放一个
WebMvcConfigurer
组件,并配置底层的MessageConverter
。
2.5.2.3 HttpMessageConverter 的示例写法
packagecom.myxh.springboot.web.component;importcom.fasterxml.jackson.databind.ObjectMapper;importcom.fasterxml.jackson.dataformat.yaml.YAMLFactory;importcom.fasterxml.jackson.dataformat.yaml.YAMLGenerator;importorg.springframework.http.HttpInputMessage;importorg.springframework.http.HttpOutputMessage;importorg.springframework.http.MediaType;importorg.springframework.http.converter.AbstractHttpMessageConverter;importorg.springframework.http.converter.HttpMessageNotReadableException;importorg.springframework.http.converter.HttpMessageNotWritableException;importjava.io.IOException;importjava.io.OutputStream;importjava.nio.charset.StandardCharsets;/**
* @author MYXH
* @date 2023/9/18
* @description
* 自定义的 YAML HTTP 消息转换器
* 用于将对象转换为 YAML 格式的内容
*/publicclassMyYamlHttpMessageConverterextendsAbstractHttpMessageConverter<Object>{// 将对象转换为 YAML 的 ObjectMapperprivatefinalObjectMapper objectMapper;publicMyYamlHttpMessageConverter(){// 告诉 SpringBoot 这个 MessageConverter 支持哪种媒体类型super(newMediaType("text","yaml",StandardCharsets.UTF_8),newMediaType("text","yml",StandardCharsets.UTF_8));// 创建 YAMLFactory 并禁用写入文档起始标记YAMLFactory factory =newYAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);this.objectMapper =newObjectMapper(factory);}/**
* 判断该转换器是否支持将指定类型的对象转换为 YAML
*
* @param clazz 要测试支持的类
* @return 如果支持转换,则返回 true;否则返回 false
*/@Overrideprotectedbooleansupports(Class<?> clazz){// 只要是对象类型,不是基本类型和数组类型,都支持return!clazz.isPrimitive()&&!clazz.isArray();}/**
* 从 HTTP 输入消息中读取数据并将其转换为指定类型的对象
* {@code @RequestBody} 把对象怎么读进来
*
* @param clazz 要返回的对象类型
* @param inputMessage 要从中读取的 HTTP 输入消息
* @return 转换后的对象
* @throws IOException IO 异常
* @throws HttpMessageNotReadableException Http 消息不可读异常
*/@OverrideprotectedObjectreadInternal(Class<?> clazz,HttpInputMessage inputMessage)throwsIOException,HttpMessageNotReadableException{return objectMapper.readValue(inputMessage.getBody(), clazz);}/**
* 将指定对象写入 HTTP 输出消息
* {@code @ResponseBody} 把对象怎么写出去
*
* @param methodReturnValue 要写入输出消息的对象
* @param outputMessage 要写入的 HTTP 输出消息
* @throws IOException IO 异常
* @throws HttpMessageNotWritableException Http 消息不可写入异常
*/@OverrideprotectedvoidwriteInternal(Object methodReturnValue,HttpOutputMessage outputMessage)throwsIOException,HttpMessageNotWritableException{// 使用 try-with-resources 语句以自动关闭流try(OutputStream os = outputMessage.getBody()){this.objectMapper.writeValue(os, methodReturnValue);}}}
2.5.3 内容协商原理 HttpMessageConverter
HttpMessageConverter 怎么工作?合适工作?
定制 HttpMessageConverter 来实现多端内容协商。
编写 WebMvcConfigurer 提供的 configureMessageConverters 底层,修改底层的 MessageConverter。
2.5.3.1 @ResponseBody 由 HttpMessageConverter 处理
标注了 @ResponseBody 的返回值 将会由支持它的 HttpMessageConverter 写给浏览器。
1、如果 controller 方法的返回值标注了
@ResponseBody
注解。
- ① 请求进来先来到
DispatcherServlet
的doDispatch()
进行处理。 - ② 找到一个
HandlerAdapter
适配器,利用适配器执行目标方法。 - ③
RequestMappingHandlerAdapter
来执行,调用invokeHandlerMethod()
来执行目标方法。 - ④ 目标方法执行之前,准备好两个东西。- (1)
HandlerMethodArgumentResolver
:参数解析器,确定目标方法每个参数值。- (2)HandlerMethodReturnValueHandler
:返回值处理器,确定目标方法的返回值改怎么处理。 - ⑤
RequestMappingHandlerAdapter
里面的invokeAndHandle()
真正执行目标方法。 - ⑥ 目标方法执行完成,会返回返回值对象。
- ⑦ 找到一个合适的返回值处理器
HandlerMethodReturnValueHandler
。 - ⑧ 最终找到
RequestResponseBodyMethodProcessor
能处理标注了@ResponseBody
注解的方法。 - ⑨
RequestResponseBodyMethodProcessor
调用writeWithMessageConverters
,利用MessageConverter
把返回值写出去。
上面解释:**@ResponseBody** 由 HttpMessageConverter 处理。
2、
HttpMessageConverter
会先进行内容协商。
- ① 遍历所有的
MessageConverter
看谁支持这种内容类型的数据。 - ② 默认
MessageConverter
有以下 10 个。 - ③ 最终因为要
json
所以MappingJackson2HttpMessageConverter
支持写出 json。 - ④ jackson 用
ObjectMapper
把对象写出去。
2.5.3.2 WebMvcAutoConfiguration 提供几种默认 HttpMessageConverters
EnableWebMvcConfiguration
通过addDefaultHttpMessageConverters
添加了默认的MessageConverter
,如下:-ByteArrayHttpMessageConverter
:支持字节数据读写。-StringHttpMessageConverter
:支持字符串读写。-ResourceHttpMessageConverter
:支持资源读写。-ResourceRegionHttpMessageConverter
: 支持分区资源写出。-AllEncompassingFormHttpMessageConverter
:支持表单 xml/json 读写。-MappingJackson2HttpMessageConverter
:支持请求响应体 Json 读写。
默认
MessageConverter
有以下 8 个:
系统提供默认的 MessageConverter 功能有限,仅用于 json 或者普通返回数据。额外增加新的内容协商功能,必须增加新的 HttpMessageConverter。
2.6 模板引擎
由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。
如果需要服务端页面渲染,优先考虑使用模板引擎。
模板引擎页面默认放在 src/main/resources/templates。
SpringBoot 包含以下模板引擎的自动配置。
- FreeMarker
- Groovy
- Thymeleaf
- Mustache
Thymeleaf 官网:https://www.thymeleaf.org/ 。
2.6.1 Thymeleaf 整合
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency>
自动配置原理:
1、开启了
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration
自动配置。
2、属性绑定在
ThymeleafProperties
中,对应配置文件
spring.thymeleaf
内容。
3、所有的模板页面默认在
classpath:/templates/
文件夹下。
4、默认效果。
- ① 所有的模板页面在
classpath:/templates/
下面找。 - ② 找后缀名为
.html
的页面。
2.6.2 基础语法
2.6.2.1 核心用法
th:xxx
:动态渲染指定的 html 标签属性值、或者 th 指令(遍历、判断等)。
th:text
:标签体内文本值渲染。-th:utext
:不会转义,显示为 html 原本的样子。th:属性
:标签指定属性渲染。th:attr
:标签任意属性渲染。th:if
、th:each``````...
:其他 th 指令。- 例如:
<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"/><title>欢迎页</title></head><body><h1>你好,<spanth:utext="${message}"></span></h1><hr/><h2>1、Thymeleaf 基础语法</h2><h3>1.1 th:text:替换标签体的内容,会转义</h3><pth:text="${message}"></p><h3>
1.2 th:utext: 替换标签体的内容,不会转义 html 标签,真正显示为 html
该有的样式
</h3><pth:utext="${message}"></p><h3>1.3 th:任意 html 属性,动态替换任意属性的值</h3><imgth:src="@{${imageUrl}}"src="大户爱.png"style="width:300px;"alt="大户爱"/><h3>1.4 th:attr:任意属性指定</h3><imgth:attr="src=@{${imageUrl}},style=${style}"src="大户爱.png"alt="大户爱"/><h3>1.5 th:其他指令</h3><imgth:src="@{${imageUrl}}"th:style="${style}"th:if="${show}"src="大户爱.png"alt="大户爱"/><h3>1.6 @{} 专门用来取各种路径</h3><imgth:src="@{${imageUrl}}"src="大户爱.png"style="width:300px;"alt="大户爱"/><h3>1.7 字符串转换为大写</h3><pth:text="${#strings.toUpperCase(name)}"></p><h3>1.8 字符串拼接</h3><pth:text="${'前缀'+name+'后缀'}"></p><pth:text="|前缀${name}后缀|"></p></body></html>
表达式
:用来动态取值。
$\{\}
:**变量取值;使用 model 共享给页面的值都直接用 ${}**。@{}
:url 路径。#{}
:国际化消息。~{}
:片段引用。\*{}
:变量选择:需要配合 th:object 绑定对象。
系统工具&内置对象
:详细文档。
param
:请求参数对象。session
:session 对象。application
:application 对象。#execInfo
:模板执行信息。#messages
:国际化消息。#uris
:uri/url 工具。#conversions
:类型转换工具。#dates
:日期工具,是java.util.Date
对象的工具类。#calendars
:类似 #dates,只不过是java.util.Calendar
对象的工具类。#temporals
:JDK8+java.time
API 工具类。#numbers
:数字操作工具。#strings
:字符串操作。#objects
:对象操作。#bools
:bool 操作。#arrays
:array 工具。-#lists
:list 工具。#sets
:set 工具。#maps
:map 工具。#aggregates
:集合聚合工具(sum、avg)。#ids
:id 生成工具。
2.6.2.2 语法示例
表达式:
- 变量取值:
${...}
- url 取值:
@{...}
- 国际化消息:
#{...}
- 变量选择:
\*{...}
- 片段引用:
~{...}
常见:
- 文本:
'one text'
,'another one!'
,… - 数字:
0
,12
,3.0
,15.6
, … - 布尔:
true
、false
- null:
null
- 变量名:
one
,sometext
,main
…
文本操作:
- 拼串:
+
- 文本替换:
| The name is ${name} |
布尔操作:
- 二进制运算:
and
,or
- 取反:
!
,not
比较运算:
- 比较:
<
,>
,<=
,>=
(lt
,gt
,le
,ge
) - 等值运算:
==
,!=
(eq
,ne
)
条件运算:
- if-then:
(if)?(then)
- if-then-else:
(if)?(then):(else)
- default:
(value)?:(defaultValue)
特殊语法:
- 无操作:
\_
所有以上都可以嵌套组合。
2.6.2.3 属性设置
1、
th:href="@{/product/list}"
2、
th:attr="class=${active}"
3、
th:attr="src=@{/images/image.png},title=${logo},alt=#{logo}"
4、
th:checked="${user.active}"
2.6.2.4 遍历
语法:th:each=“元素名,迭代状态 : ${集合}”
<!DOCTYPEhtml><htmllang="en"xmlns:th="http://www.thymeleaf.org"><head><metacharset="UTF-8"/><title>用户列表页</title><linkhref="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css"rel="stylesheet"integrity="sha384-T3c6CoIi6uLrA9TneNEoa7RxnatzjcDSCmG1MXxSR1GAsXEV/Dwwykc2MPK8M2HN"crossorigin="anonymous"/></head><body><!-- 导航区使用公共部分进行替换 --><!-- ~{ 模板名 :: 片段名} --><divth:replace="~{common :: myheader}"></div><divclass="container"><tableclass="table"><thead><tr><thscope="col">序号</th><thscope="col">用户名</th><thscope="col">密码</th><thscope="col">年龄</th><thscope="col">邮箱</th><thscope="col">角色</th><thscope="col">状态信息</th></tr></thead><tbody><trth:each="user, stats : ${userList}"th:object="${user}"><thscope="row"th:text="${user.id}"></th><!-- <td th:text="${user.userName}"></td> --><tdth:text="*{userName}"></td><td>[[${user.password}]]</td><tdth:text="|${user.age}(${user.age >= 18 ? '成年' : '未成年'})|"></td><tdth:if="${#strings.isEmpty(user.email)}"th:text="'联系不上'"></td><tdth:if="${not #strings.isEmpty(user.email)}"th:text="${user.email}"></td><tdth:switch="${user.role}"><buttonth:case="'root'"type="button"class="btn btn-primary">
root 用户
</button><buttonth:case="'admin'"type="button"class="btn btn-secondary">
管理员
</button><buttonth:case="'test'"type="button"class="btn btn-success">
测试员
</button><buttonth:case="'user'"type="button"class="btn btn-light">
用户
</button></td><td>
index(索引):[[${stats.index}]]<br/>
count(计数):[[${stats.count}]]<br/>
size(大小):[[${stats.size}]]<br/>
current(当前对象):[[${stats.current}]]<br/>
even(true)/odd(false)(奇数/偶数):[[${stats.even}]]<br/>
first(第一个):[[${stats.first}]]<br/>
last(最后的):[[${stats.last}]]<br/></td></tr></tbody></table></div><scriptsrc="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"integrity="sha384-C6RzsynM9kWDrMNeT87bh95OGNyZPhcTNXj1NW7RuBCsyN/o0jlpcV8Qyq46cDfL"crossorigin="anonymous"></script></body></html>
iterStat 有以下属性:
- index:当前遍历元素的索引,从 0 开始。
- count:当前遍历元素的索引,从 1 开始。
- size:需要遍历元素的总数量。
- current:当前正在遍历的元素对象。
- even/odd:是否为偶数/奇数行。
- first:是否第一个元素。
- last:是否最后一个元素。
2.6.2.5 判断
th:if
<tdth:text="|${user.age}(${user.age >= 18 ? '成年' : '未成年'})|"></td><tdth:if="${#strings.isEmpty(user.email)}"th:text="'联系不上'"></td><tdth:if="${not #strings.isEmpty(user.email)}"th:text="${user.email}"></td>
th:switch
<tdth:switch="${user.role}"><buttonth:case="'root'"type="button"class="btn btn-primary">
root 用户
</button><buttonth:case="'admin'"type="button"class="btn btn-secondary">
管理员
</button><buttonth:case="'test'"type="button"class="btn btn-success">测试员</button><buttonth:case="'user'"type="button"class="btn btn-light">用户</button></td>
2.6.2.6 属性优先级
- 片段。
- 遍历。
- 判断。
<trth:each="user, stats : ${userList}"th:object="${user}"><thscope="row"th:text="${user.id}"></th><!-- <td th:text="${user.userName}"></td> --><tdth:text="*{userName}"></td></tr>
顺序特性属性1片段包含。
th:insert
、
th:replace
2遍历。
th:each
3判断。
th:if
、
th:unless
、
th:switch
、
th:case
4定义本地变量。
th:object
、
th:with
5通用方式属性修改。
th:attr
、
th:attrprepend
、
th:attrappend
6指定属性修改。
th:value
、
th:href
、
th:src
、
...
7文本值。
th:text
、
th:utext
8片段指定。
th:fragment
9片段移除。
th:remove
2.6.2.7 行内写法
[[...]]
或
[(...)]
。
<trth:each="user, stats : ${userList}"th:object="${user}"><thscope="row"th:text="${user.id}"></th><!-- <td th:text="${user.userName}"></td> --><tdth:text="*{userName}"></td><td>[[${user.password}]]</td></tr>
2.6.2.8 变量选择
<trth:each="user, stats : ${userList}"th:object="${user}"><thscope="row"th:text="${user.id}"></th><tdth:text="*{userName}"></td></tr>
等同于
<trth:each="user, stats : ${userList}"><thscope="row"th:text="${user.id}"></th><tdth:text="${user.userName}"></td></tr>
2.6.2.9 模板布局
- 定义模板:
th:fragment
- 引用模板:
~{templatename::selector}
- 插入模板:
th:insert
、th:replace
<!-- 导航区使用公共部分进行替换 --><!-- ~{ 模板名 :: 片段名} --><divth:replace="~{common :: myheader}"></div>
2.6.2.10 devtools
<!-- 热启动功能 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId></dependency>
修改页面后,
ctrl+F9
刷新效果。
Java 代码的修改,如果
devtools
热启动了,可能会引起一些 bug,难以排查。
2.7 国际化
国际化的自动配置参照
MessageSourceAutoConfiguration
。
实现步骤:
1、Spring Boot 在类路径根下查找
messages
资源绑定文件。文件名为:
messages.properties
。
2、多语言可以定义多个消息文件,命名为
messages\_区域代码.properties
。例如:
- ①
messages.properties
:默认。 - ②
messages_zh_CN.properties
:中文环境。 - ③
messages_en_US.properties
:英语环境。
3、在程序中可以自动注入
MessageSource
组件,获取国际化的配置项值。
4、在页面中可以使用表达式
#{}
获取国际化的配置项值。
packagecom.myxh.springboot.web.controller;importjakarta.servlet.http.HttpServletRequest;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.context.MessageSource;importorg.springframework.stereotype.Controller;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.ResponseBody;importjava.util.Locale;/**
* @author MYXH
* @date 2023/9/18
*///适配服务端清染,前后不分离模式@ControllerpublicclassWelcomeController{// 国际化获取消息的组件@AutowiredMessageSource messageSource;@GetMapping("/message")@ResponseBodypublicStringmessage(HttpServletRequest request){Locale locale = request.getLocale();// 利用代码的方式获取国际化配置文件中指定的配置项的值String login = messageSource.getMessage("login",null, locale);return login;}}
2.8 错误处理
2.8.1 默认机制
错误处理的自动配置都在 ErrorMvcAutoConfiguration 中,两大核心机制:
- 1、SpringBoot 会自适应处理错误,响应页面或 JSON 数据。
- 2、SpringMVC 的错误处理机制依然保留,MVC 处理不了,才会交给 boot 进行处理。
- 发生错误以后,转发给/error 路径,SpringBoot 在底层写好一个 BasicErrorController 的组件,专门处理这个请求。
// 返回 HTML@RequestMapping(produces =MediaType.TEXT_HTML_VALUE)publicModelAndViewerrorHtml(HttpServletRequest request,HttpServletResponse response){HttpStatus status =getStatus(request);Map<String,Object> model =Collections.unmodifiableMap(getErrorAttributes(request,getErrorAttributeOptions(request,MediaType.TEXT_HTML))); response.setStatus(status.value());ModelAndView modelAndView =resolveErrorView(request, response, status, model);return(modelAndView !=null)? modelAndView :newModelAndView("error", model);}//返回 ResponseEntity,JSON@RequestMappingpublicResponseEntity<Map<String,Object>>error(HttpServletRequest request){HttpStatus status =getStatus(request);if(status ==HttpStatus.NO_CONTENT){returnnewResponseEntity<>(status);}Map<String,Object> body =getErrorAttributes(request,getErrorAttributeOptions(request,MediaType.ALL));returnnewResponseEntity<>(body, status);}
- 错误页面是这么解析到的。
// 1、解析错误的自定义视图地址ModelAndView modelAndView =resolveErrorView(request, response, status, model);// 2、如果解析不到错误页面的地址,默认的错误页就是 errorreturn(modelAndView !=null)? modelAndView :newModelAndView("error", model);
- 容器中专门有一个错误视图解析器。
@Bean@ConditionalOnBean(DispatcherServlet.class)@ConditionalOnMissingBean(ErrorViewResolver.class)DefaultErrorViewResolverconventionErrorViewResolver(){returnnewDefaultErrorViewResolver(this.applicationContext,this.resources);}
- SpringBoot 解析自定义错误页的默认规则。
@OverridepublicModelAndViewresolveErrorView(HttpServletRequest request,HttpStatus status,Map<String,Object> model){ModelAndView modelAndView =resolve(String.valueOf(status.value()), model);if(modelAndView ==null&&SERIES_VIEWS.containsKey(status.series())){ modelAndView =resolve(SERIES_VIEWS.get(status.series()), model);}return modelAndView;}privateModelAndViewresolve(String viewName,Map<String,Object> model){String errorViewName ="error/"+ viewName;TemplateAvailabilityProvider provider =this.templateAvailabilityProviders.getProvider(errorViewName,this.applicationContext);if(provider !=null){returnnewModelAndView(errorViewName, model);}returnresolveResource(errorViewName, model);}privateModelAndViewresolveResource(String viewName,Map<String,Object> model){for(String location :this.resources.getStaticLocations()){try{Resource resource =this.applicationContext.getResource(location); resource = resource.createRelative(viewName +".html");if(resource.exists()){returnnewModelAndView(newHtmlResourceView(resource), model);}}catch(Exception ex){}}returnnull;}
- 容器中有一个默认的名为 error 的 view,提供了默认白页功能。
@Bean(name ="error")@ConditionalOnMissingBean(name ="error")publicViewdefaultErrorView(){returnthis.defaultErrorView;}
- 封装了 JSON 格式的错误信息。
@Bean@ConditionalOnMissingBean(value =ErrorAttributes.class, search =SearchStrategy.CURRENT)publicDefaultErrorAttributeserrorAttributes(){returnnewDefaultErrorAttributes();}
规则:
1、解析一个错误页。
- ① 如果发生了 500、404、503、403 这些错误。- (1) 如果有模板引擎,默认在
classpath:/templates/error/精确码.html
。- (2) 如果没有模板引擎,在静态资源文件夹下找精确码.html
。 - ② 如果匹配不到
精确码.html
这些精确的错误页,就去找5xx.html
,4xx.html
模糊匹配。- (1) 如果有模板引擎,默认在classpath:/templates/error/5xx.html
。- (2) 如果没有模板引擎,在静态资源文件夹下找5xx.html
。
2、如果模板引擎路径
templates
下有
error.html
页面,就直接渲染。
2.8.2 自定义错误响应
2.8.2.1 自定义 json 响应
使用 @ControllerAdvice + @ExceptionHandler 进行统一异常处理。
2.8.2.2 自定义页面响应
根据 boot 的错误页面规则,自定义页面模板。
2.8.3 最佳实战
- 前后分离。- 后台发生的所有错误,
@ControllerAdvice + @ExceptionHandler
进行统一异常处理。 - 服务端页面渲染。-
不可预知的一些,HTTP 码表示的服务器或客户端错误
。- 给classpath:/templates/error/
下面,放常用精确的错误码页面。500.html
,404.html
。- 给classpath:/templates/error/
下面,放通用模糊匹配的错误码页面。5xx.html
,4xx.html
。- 发生业务错误。- 核心业务,每一种错误,都应该代码控制,跳转到自己定制的错误页。- 通用业务,classpath:/templates/error.html
页面,显示错误信息。
在 HTML 页面、JSON 数据中,可用的 Model 数据如下。
2.9 嵌入式容器
Servlet 容器:管理、运行 Servlet 组件(Servlet、Filter、Listener)的环境,一般指服务器。
2.9.1 自动配置原理
- SpringBoot 默认嵌入 Tomcat 作为 Servlet 容器。
- 自动配置类是 ServletWebServerFactoryAutoConfiguration,EmbeddedWebServerFactoryCustomizerAutoConfiguration。
- 自动配置类开始分析功能,xxxAutoConfiguration。
@AutoConfiguration@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)@ConditionalOnClass(ServletRequest.class)@ConditionalOnWebApplication(type =Type.SERVLET)@EnableConfigurationProperties(ServerProperties.class)@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class,ServletWebServerFactoryConfiguration.EmbeddedTomcat.class,ServletWebServerFactoryConfiguration.EmbeddedJetty.class,ServletWebServerFactoryConfiguration.EmbeddedUndertow.class})publicclassServletWebServerFactoryAutoConfiguration{}
1、
ServletWebServerFactoryAutoConfiguration
自动配置了嵌入式容器场景。
2、绑定了
ServerProperties
配置类,所有和服务器有关的配置
server
。
3、
ServletWebServerFactoryAutoConfiguration
导入了嵌入式的三大服务器
Tomcat
、
Jetty
、
Undertow
。
- ① 导入
Tomcat
、Jetty
、Undertow
都有条件注解,系统中有这个类才行(也就是导了包)。 - ② 默认
Tomcat
配置生效。给容器中放 TomcatServletWebServerFactory。 - ③ 都给容器中
ServletWebServerFactory
放了一个 Web 服务器工厂(造 Web 服务器的)。 - ④ Web 服务器工厂都有一个功能,
getWebServe
r 获取 Web 服务器。 - ⑤ TomcatServletWebServerFactory 创建了 tomcat。
4、ServletWebServerFactory 什么时候会创建 webServer 出来。
5、
ServletWebServerApplicationContext
ioc 容器,启动的时候会调用创建 Web 服务器。
6、Spring 容器刷新(启动)的时候,会预留一个时机,刷新子容器,
onRefresh()
。
7、refresh() 容器刷新,十二个步骤的刷新,子容器会调用
onRefresh()
。
@OverrideprotectedvoidonRefresh(){super.onRefresh();try{createWebServer();}catch(Throwable ex){thrownewApplicationContextException("Unable to start web server", ex);}}
Web 场景的 Spring 容器启动,在 onRefresh 的时候,会调用创建 Web 服务器的方法。
Web 服务器的创建是通过 WebServerFactory 搞定的。容器中又会根据导了什么包条件注解,启动相关的 服务器配置,默认 EmbeddedTomcat 会给容器中放一个 TomcatServletWebServerFactory,导致项目启动,自动创建出 Tomcat。
2.9.2 自定义
切换服务器。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><exclusions><!-- 排除 Tomcat 依赖项 --><exclusion><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-tomcat</artifactId></exclusion></exclusions></dependency><!-- 改为使用 Jetty --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jetty</artifactId></dependency>
2.9.3 最佳实践
用法:
- 修改
server
下的相关配置就可以修改服务器参数。 - 通过给容器中放一个
ServletWebServerFactory
,来禁用掉 SpringBoot 默认放的服务器工厂,实现自定义嵌入任意服务器。
2.10 全面接管 SpringMVC
- SpringBoot 默认配置好了 SpringMVC 的所有常用特性。
- 如果需要全面接管 SpringMVC 的所有配置并禁用默认配置,仅需要编写一个 WebMvcConfigurer 配置类,并标注 @EnableWebMvc 即可。
- 全手动模式。- @EnableWebMvc : 禁用默认配置。- WebMvcConfigurer 组件:定义 MVC 的底层行为。
2.10.1 WebMvcAutoConfiguration 到底自动配置了哪些规则
SpringMVC 自动配置场景配置了如下所有默认行为。
1、
WebMvcAutoConfiguration
Web 场景的自动配置类。
- ① 支持 RESTful 的 filter:HiddenHttpMethodFilter。
- ② 支持非 POST 请求,请求体携带数据:FormContentFilter。
- ③ 导入
EnableWebMvcConfiguration
:- (1)RequestMappingHandlerAdapter
。- (2)WelcomePageHandlerMapping
:欢迎页功能支持(模板引擎目录、静态资源目录放 index.html),项目访问 / 就默认展示这个页面。- (3)RequestMappingHandlerMapping
:找每个请求由谁处理的映射关系。- (4)ExceptionHandlerExceptionResolver
:默认的异常解析器。- (5)LocaleResolver
:国际化解析器。- (6)ThemeResolver
:主题解析器。- (7)FlashMapManager
:临时数据共享。- (8)FormattingConversionService
:数据格式化、类型转化。- (9)Validator
:数据校验JSR303
提供的数据校验功能。- (10)WebBindingInitializer
:请求参数的封装与绑定。- (11)ContentNegotiationManager
:内容协商管理器。 - ④
WebMvcAutoConfigurationAdapter
配置生效,它是一个WebMvcConfigurer
,定义 mvc 底层组件。- (1) 定义好WebMvcConfigurer
底层组件默认功能,所有功能详见列表。- (2) 视图解析器:InternalResourceViewResolver
。- (3) 视图解析器:BeanNameViewResolver
,视图名(controller 方法的返回值字符串)就是组件名。- (4) 内容协商解析器:ContentNegotiatingViewResolver
。- (5) 请求上下文过滤器:RequestContextFilter
,任意位置直接获取当前请求。- (6) 静态资源链规则。- (7)ProblemDetailsExceptionHandler
:错误详情。- [1] SpringMVC 内部场景异常被它捕获。 - ⑤ 定义了 MVC 默认的底层行为:
WebMvcConfigurer
。
2.10.2 @EnableWebMvc 禁用默认行为
1、
@EnableWebMvc
给容器中导入
DelegatingWebMvcConfiguration
组件,
是
WebMvcConfigurationSupport
。
2、
WebMvcAutoConfiguration
有一个核心的条件注解,
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
,容器中没有
WebMvcConfigurationSupport
,
WebMvcAutoConfiguration
才生效。
3、 @EnableWebMvc 导入
WebMvcConfigurationSupport
导致
WebMvcAutoConfiguration
失效,导致禁用了默认行为。
@EnableWebMVC 禁用了 Mvc 的自动配置。
WebMvcConfigurer 定义 SpringMVC 底层组件的功能类。
2.10.3 WebMvcConfigurer 功能
定义扩展 SpringMVC 底层功能。
提供方法核心参数功能默认addFormattersFormatterRegistry格式化器:支持属性上
@NumberFormat
和
@DatetimeFormat
的数据类型转换。GenericConversionServicegetValidator无数据校验:校验 Controller 上使用
@Valid
标注的参数合法性,需要导入
starter-validator
。无
addInterceptors
InterceptorRegistry拦截器:拦截收到的所有请求。无
configureContentNegotiation
ContentNegotiationConfigurer内容协商:支持多种数据格式返回,需要配合支持这种类型的
HttpMessageConverter
。支持 json。
configureMessageConverters
List<HttpMessageConverter<?>>
消息转换器:标注
@ResponseBody
的返回值会利用
MessageConverter
直接写出去。8 个,支持
byte
,
string
,
multipart
,
resource
,
json
。addViewControllersViewControllerRegistry视图映射:直接将请求路径与物理视图映射,用于无 Java 业务逻辑的直接视图页渲染。
<mvc:view-controller>
configureViewResolversViewResolverRegistry视图解析器:逻辑视图转为物理视图。iewResolverCompositeaddResourceHandlersResourceHandlerRegistry静态资源处理:静态资源路径映射、缓存控制。ResourceHandlerRegistryconfigureDefaultServletHandlingDefaultServletHandlerConfigurer默认 Servlet:可以覆盖 Tomcat 的
DefaultServlet
,让
DispatcherServlet
拦截
/
。无configurePathMatchPathMatchConfigurer路径匹配:自定义 URL 路径匹配,可以自动为所有路径加上指定前缀,比如
/api
。无
configureAsyncSupport
AsyncSupportConfigurer异步支持:
TaskExecutionAutoConfiguration
addCorsMappingsCorsRegistry跨域:无addArgumentResolvers
List<HandlerMethodArgumentResolver>
参数解析器:mvc 默认提供。addReturnValueHandlers
List<HandlerMethodReturnValueHandler>
返回值解析器:mvc 默认提供。configureHandlerExceptionResolversList异常处理器:默认 3 个,ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver。getMessageCodesResolver无消息码解析器:国际化使用无
2.11 最佳实践
SpringBoot 已经默认配置好了 Web 开发场景常用功能,直接使用即可。
2.11.1 三种方式
方式用法效果全自动直接编写控制器逻辑。全部使用自动配置默认效果。半自动注解
@Configuration
- 配置
WebMvcConfigurer
- 配置
,不要标注 注解WebMvcRegistrations
。保留自动配置效果,手动设置部分功能,定义 MVC 底层组件。全手动注解@EnableWebMvc
@Configuration
- 配置
,标注注解WebMvcConfigurer
。禁用自动配置效果,全手动设置。@EnableWebMvc
总结:
给容器中写一个配置类
@Configuration
实现
WebMvcConfigurer
但是不要标注
@EnableWebMvc
注解,实现半自动的效果。
2.11.2 两种模式
1、
前后分离模式
:
@RestController
响应 JSON 数据。
2、
前后不分离模式
:
@Controller
- Thymeleaf 模板引擎。
2.12 Web 新特性
2.12.1 Problemdetails
RFC 7807: https://www.rfc-editor.org/rfc/rfc7807
错误信息返回新格式。
原理:
@Configuration(proxyBeanMethods =false)// 配置一个属性 spring.mvc.problemdetails.enabled=true@ConditionalOnProperty(prefix ="spring.mvc.problemdetails", name ="enabled", havingValue ="true")staticclassProblemDetailsErrorHandlingConfiguration{@Bean@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)ProblemDetailsExceptionHandlerproblemDetailsExceptionHandler(){returnnewProblemDetailsExceptionHandler();}}
1、
ProblemDetailsExceptionHandler
是一个
@ControllerAdvice
集中处理系统异常。
2、处理以下异常。如果系统出现以下异常,会被 SpringBoot 支持以
RFC 7807
规范方式返回错误数据。
@ExceptionHandler({// 请求方式不支持HttpRequestMethodNotSupportedException.class,HttpMediaTypeNotSupportedException.class,HttpMediaTypeNotAcceptableException.class,MissingPathVariableException.class,MissingServletRequestParameterException.class,MissingServletRequestPartException.class,ServletRequestBindingException.class,MethodArgumentNotValidException.class,NoHandlerFoundException.class,AsyncRequestTimeoutException.class,ErrorResponseException.class,ConversionNotSupportedException.class,TypeMismatchException.class,HttpMessageNotReadableException.class,HttpMessageNotWritableException.class,BindException.class})
效果:
默认响应错误的 json。状态码 405。
{"timestamp":"2023-09-18T11:13:05.515+00:00","status":405,"error":"Method Not Allowed","trace":"org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' is not supported\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.handleNoMatch(RequestMappingInfoHandlerMapping.java:265)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:441)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:382)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:126)\r\n\tat org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getHandlerInternal(RequestMappingInfoHandlerMapping.java:68)\r\n\tat org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:505)\r\n\tat org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1275)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1057)\r\n\tat org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:974)\r\n\tat org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1011)\r\n\tat org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:914)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:563)\r\n\tat org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:885)\r\n\tat jakarta.servlet.http.HttpServlet.service(HttpServlet.java:631)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:205)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:100)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:93)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:201)\r\n\tat org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:116)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:174)\r\n\tat org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:149)\r\n\tat org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:166)\r\n\tat org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:90)\r\n\tat org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:493)\r\n\tat org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:115)\r\n\tat org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:93)\r\n\tat org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)\r\n\tat org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:341)\r\n\tat org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:390)\r\n\tat org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:63)\r\n\tat org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:894)\r\n\tat org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1741)\r\n\tat org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:52)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1191)\r\n\tat org.apache.tomcat.util.threads.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:659)\r\n\tat org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)\r\n\tat java.base/java.lang.Thread.run(Thread.java:833)\r\n","message":"Method 'POST' is not supported.","path":"/list"}
开启 ProblemDetails 返回,使用新的 MediaType
Content-Type: application/problem+json
- 额外扩展返回。
2.12.2 函数式 Web
SpringMVC 5.2 以后允许使用函数式的方式,定义 Web 的请求处理流程。
函数式接口。
Web 请求处理的方式:
1、**@Controller + @RequestMapping:耦合式(路由、业务**耦合)。
2、函数式 Web:分离式(路由、业务分离)。
2.12.2.1 场景
场景:User RESTful - CRUD。
- GET /user/1 获取 1 号用户。
- GET /users 获取所有用户。
- POST /user 请求体携带 JSON,新增一个用户。
- PUT /user/1 请求体携带 JSON,修改 1 号用户。
- DELETE /user/1 删除 1 号用户。
2.12.2.2 核心类
- RouterFunction
- RequestPredicate
- ServerRequest
- ServerResponse
2.12.2.3 示例
packagecom.myxh.springboot.web.config;importcom.myxh.springboot.web.biz.UserBizHandler;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.http.MediaType;importorg.springframework.web.servlet.function.RequestPredicates;importorg.springframework.web.servlet.function.RouterFunction;importorg.springframework.web.servlet.function.RouterFunctions;importorg.springframework.web.servlet.function.ServerResponse;/**
* @author MYXH
* @date 2023/9/18
* @description
* 场景:User RESTful - CRUD
* GET /user/1 获取 1 号用户
* GET /users 获取所有用户
* POST /user 请求体携带 JSON,新增一个用户
* PUT /user/1 请求体携带 JSON,修改 1 号用户
* DELETE /user/1 删除 1 号用户
*/@ConfigurationpublicclassWebFunctionConfig{/**
* 函数式 Web
* 1、给容器中放一个 Bean: 类型是 RouterFunction<ServerResponse>,集中所有路由信息
* 2、每个业务准备一个自己的 Handler
* <br>
* 核心四大对象
* 1、RouterFunction:定义路由信息,发什么请求,谁来处理
* 2、RequestPredicate:定义请求,请求谓语,请求方式(GET、POST),请求参数
* 3、ServerRequest:封装请求完整数据
* 4、ServerResponse:封装响应完整数据
*
* @param userBizHandler 用户业务处理程序(userBizHandler 会被自动注入进来)
* @return routerFunction 路由器功能
*/@BeanpublicRouterFunction<ServerResponse>userRouter(UserBizHandler userBizHandler){// 开始定义路由信息RouterFunction<ServerResponse> routerFunction =RouterFunctions.route().GET("/user/{id}",RequestPredicates.accept(MediaType.ALL), userBizHandler::getUser).GET("/users", userBizHandler::getUsers).POST("/user",RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::saveUser).PUT("/user{id}",RequestPredicates.accept(MediaType.APPLICATION_JSON), userBizHandler::updateUser).DELETE("/user{id}", userBizHandler::deleteUser).build();return routerFunction;}}
packagecom.myxh.springboot.web.biz;importcom.myxh.springboot.web.bean.User;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.web.servlet.function.ServerRequest;importorg.springframework.web.servlet.function.ServerResponse;importjava.util.Arrays;importjava.util.List;/**
* @author MYXH
* @date 2023/9/18
* @description 专门处理 User 有关的业务
*/@Slf4j@ServicepublicclassUserBizHandler{@AutowiredprivateUser user;/**
* 查询指定 id 的用户
*
* @param request 请求
* @return response 响应
* @throws Exception 异常
*/publicServerResponsegetUser(ServerRequest request)throwsException{// 业务处理String id = request.pathVariable("id");
user.setId(1L);
user.setUserName("MYXH");
user.setPassword("520.ILY!");
user.setAge(21);
user.setEmail("[email protected]");
log.info("查询 {} 号用户信息成功", id);// 构造响应ServerResponse response =ServerResponse.ok()// 凡是 body 中的对象,就是以前 @ResponseBody 原理,利用 HttpMessageConverter 写出为 json.body(user);return response;}/**
* 获取所有用户
*
* @param request 请求
* @return response 响应
* @throws Exception 异常
*/publicServerResponsegetUsers(ServerRequest request)throwsException{// 业务处理List<User> userList =Arrays.asList(newUser(1L,"MYXH","520.ILY!",21,"[email protected]","root"),newUser(2L,"root","000000",21,"[email protected]","root"),newUser(3L,"admin","123456",21,"[email protected]","admin"),newUser(4L,"test","test",18,"[email protected]","test"),newUser(5L,"张三","123456",18,"","user"));
log.info("查询所有用户信息成功");// 构造响应ServerResponse response =ServerResponse.ok().body(userList);return response;}/**
* 保存用户
*
* @param request 请求
* @return response 响应
* @throws Exception 异常
*/publicServerResponsesaveUser(ServerRequest request)throwsException{// 业务处理// 提取请求体User body = request.body(User.class);
log.info("保存用户信息成功,用户信息:{}", body);// 构造响应ServerResponse response =ServerResponse.ok().build();return response;}/**
* 更新指定 id 的用户
*
* @param request 请求
* @return response 响应
* @throws Exception 异常
*/publicServerResponseupdateUser(ServerRequest request)throwsException{// 业务处理// 提取请求体User body = request.body(User.class);
log.info("更新用户信息成功,用户信息:{}", body);// 构造响应ServerResponse response =ServerResponse.ok().build();return response;}/**
* 删除指定 id 的用户
*
* @param request 请求
* @return response 响应
* @throws Exception 异常
*/publicServerResponsedeleteUser(ServerRequest request)throwsException{// 业务处理String id = request.pathVariable("id");
log.info("删除 {} 号用户信息成功", id);// 构造响应ServerResponse response =ServerResponse.ok().build();return response;}}
第 3 章 SpringBoot3-数据访问
整合 SSM 场景
SpringBoot 整合 Spring、SpringMVC、MyBatis 进行数据访问场景开发。
3.1 创建 SSM 整合项目
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="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>3.1.4</version><relativePath/><!-- lookup parent from repository --></parent><groupId>com.myxh.springboot</groupId><artifactId>boot3-05-ssm</artifactId><version>0.0.1-SNAPSHOT</version><name>boot3-05-ssm</name><description>boot3-05-ssm</description><properties><java.version>17</java.version></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>3.0.2</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><scope>runtime</scope></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter-test</artifactId><version>3.0.2</version><scope>test</scope></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><configuration><excludes><exclude><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId></exclude></excludes></configuration></plugin></plugins></build></project>
3.2 配置数据源
# 1、先配置数据源信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.zaxxer.hikari.HikariDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot
spring.datasource.username=MYXH
spring.datasource.password=520.ILY!
安装 Free MyBatis Tool 或 MyBatisX 插件,生成 Mapper 接口的 xml 文件即可。
3.3 配置 MyBatis
# 2、配置整合 MyBatis
mybatis.mapper-locations=classpath:/mapper/*.xml
# 打开驼峰命名规则
mybatis.configuration.map-underscore-to-camel-case=true
3.4 CRUD 编写
- 编写 Bean。
- 编写 Mapper。
- 使用
Free MyBatis Tool
插件,快速生成 MapperXML。 - 测试 CRUD。
3.5 自动配置原理
SSM 整合总结:
1、导入****mybatis-spring-boot-starter。
2、配置数据源信息。
3、配置 MyBatis 的 Mapper 接口扫描与 xml 映射文件扫描。
4、编写 Bean,Mapper,生成 xml,编写 SQL 进行 CRUD。事务等操作依然和 Spring 中用法一样。
5、效果:
- ① 所有 SQL 写在 xml 中。
- ② 所有 MyBatis 配置写在 application.properties 下面。
jdbc 场景的自动配置
:-mybatis-spring-boot-starter
导入spring-boot-starter-jdbc
,jdbc 是操作数据库的场景。-jdbc
场景的几个自动配置。- org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration- 数据源的自动配置。- 所有和数据源有关的配置都绑定在DataSourceProperties
。- 默认使用HikariDataSource
。- org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration- 给容器中放了JdbcTemplate
操作数据库。- org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration- org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration- 基于 XA 二阶提交协议的分布式事务数据源。- org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration- 支持事务。- 具有的底层能力:数据源、JdbcTemplate
、事务。MyBatisAutoConfiguration
:配置了 MyBatis 的整合流程。-mybatis-spring-boot-starter
导入mybatis-spring-boot-autoconfigure(mybatis 的自动配置包)
。- 默认加载两个自动配置类:- org.mybatis.spring.boot.autoconfigure.MybatisLanguageDriverAutoConfiguration- org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration- 必须在数据源配置好之后才配置。- 给容器中SqlSessionFactory
组件,创建和数据库的一次会话。- 给容器中SqlSessionTemplate
组件,操作数据库。- MyBatis 的所有配置绑定在MybatisProperties
。- 每个 Mapper 接口的代理对象是怎么创建放到容器中,详见 @MapperScan 原理:- 利用@Import(MapperScannerRegistrar.class)
批量给容器中注册组件,解析指定的包路径里面的每一个类,为每一个 Mapper 接口类,创建 Bean 定义信息,注册到容器中。
如何分析哪个场景导入以后,开启了哪些自动配置类。
寻找:classpath:/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中配置的所有值,就是要开启的自动配置类,但是每个类可能有条件注解,基于条件注解判断哪个自动配置类生效了。
3.6 快速定位生效的配置
# 开启调试模式,详细打印开启了哪些自动配置
# Positive(生效的自动配置),Negative(不生效的自动配置)
debug=true
3.7 扩展:整合其他数据源
3.7.1 Druid 数据源
暂不支持 SpringBoot3
- 导入 druid-starter。
- 写配置。
- 分析自动配置了哪些东西,怎么用。
Druid 官网:https://github.com/alibaba/druid
# 数据源基本配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.url=jdbc:mysql://localhost:3306/spring_boot
spring.datasource.username=MYXH
spring.datasource.password=520.ILY!
# 配置 StatFilter 监控
spring.datasource.druid.filter.stat.enabled=true
spring.datasource.druid.filter.stat.db-type=mysql
spring.datasource.druid.filter.stat.log-slow-sql=true
spring.datasource.druid.filter.stat.slow-sql-millis=2000
# 配置 WallFilter 防火墙
spring.datasource.druid.filter.wall.enabled=true
spring.datasource.druid.filter.wall.db-type=mysql
spring.datasource.druid.filter.wall.config.delete-allow=false
spring.datasource.druid.filter.wall.config.drop-table-allow=false
# 配置监控页,内置监控页面的首页是 /druid/index.html
spring.datasource.druid.stat-view-servlet.enabled=true
spring.datasource.druid.stat-view-servlet.login-username=admin
spring.datasource.druid.stat-view-servlet.login-password=admin
spring.datasource.druid.stat-view-servlet.allow=\*
# 其他 Filter 配置不再演示
# 目前为以下 Filter 提供了配置支持,请参考文档或者根据 IDE 提示(spring.datasource.druid.filter.\*)进行配置。
# StatFilter
# WallFilter
# ConfigFilter
# EncodingConvertFilter
# Slf4jLogFilter
# Log4jFilter
# Log4j2Filter
# CommonsLogFilter
3.8 附录:示例数据库
# 创建数据库 spring_bootCREATEDATABASEIFNOTEXISTS spring_boot;# 选择数据库 spring_bootUSE spring_boot;# 创建用户表 t_userCREATETABLE`t_user`(`id`BIGINT(20)NOTNULLAUTO_INCREMENTCOMMENT'编号',`login_name`VARCHAR(200)NULLDEFAULTNULLCOMMENT'用户名称'COLLATE'utf8_general_ci',`nick_name`VARCHAR(200)NULLDEFAULTNULLCOMMENT'用户昵称'COLLATE'utf8_general_ci',`password`VARCHAR(200)NULLDEFAULTNULLCOMMENT'用户密码'COLLATE'utf8_general_ci',PRIMARYKEY(`id`));# 添加用户表 t_user 的数据INSERTINTO`t_user`(`id`,`login_name`,`nick_name`,`password`)VALUES(1,'MYXH','末影小黑xh','520.ILY!'),(2,'root','root 用户','000000'),(3,'admin','管理员','123456'),(4,'test','测试员','test');
第 4 章 SpringBoot3-基础特性
4.1 SpringApplication
4.1.1 自定义 banner
1、类路径添加
banner.txt
或设置
spring.banner.location
就可以定制 banner。
2、推荐网站:链接: Spring Boot banner 在线生成工具,制作下载英文 banner.txt,修改替换 banner.txt 文字实现自定义,个性化启动。
4.1.2 自定义 SpringApplication
packagecom.myxh.springboot.features;importorg.springframework.boot.Banner;importorg.springframework.boot.autoconfigure.SpringBootApplication;/**
* 主程序类
*/@SpringBootApplicationpublicclassBoot306FeaturesApplication{publicstaticvoidmain(String[] args){// 1、SpringApplication:Boot 应用的核心 API 入口// SpringApplication.run(Boot306FeaturesApplication.class, args);// 1.1 自定义 SpringApplication 的底层设置SpringApplication springApplication =newSpringApplication(Boot306FeaturesApplication.class);// 1.2 程序化调整 SpringApplication 的参数,配置文件优先级高于程序化调整的优先级
springApplication.setBannerMode(Banner.Mode.OFF);// 1.3 SpringApplication 运行起来
springApplication.run(args);}}
4.1.3 FluentBuilder API
packagecom.myxh.springboot.features;importorg.springframework.boot.Banner;importorg.springframework.boot.autoconfigure.SpringBootApplication;importorg.springframework.boot.builder.SpringApplicationBuilder;importorg.springframework.context.ConfigurableApplicationContext;/**
* 主程序类
*/@SpringBootApplicationpublicclassBoot306FeaturesApplication{publicstaticvoidmain(String[] args){// 1、SpringApplication:Boot 应用的核心 API 入口// SpringApplication.run(Boot306FeaturesApplication.class, args);// 1.1 自定义 SpringApplication 的底层设置// SpringApplication springApplication = new SpringApplication(Boot306FeaturesApplication.class);// 1.2 程序化调整 SpringApplication 的参数,配置文件优先级高于程序化调整的优先级// springApplication.setBannerMode(Banner.Mode.OFF);// 1.3 SpringApplication 运行起来// springApplication.run(args);// 2、Builder 方式构建 SpringApplication:通过 FluentAPI 进行设置ConfigurableApplicationContext applicationContext =newSpringApplicationBuilder().main(Boot306FeaturesApplication.class).sources(Boot306FeaturesApplication.class).bannerMode(Banner.Mode.OFF).run(args);}}
4.2 Profiles
环境隔离能力,快速切换开发、测试、生产环境。
步骤:
1、标识环境:指定哪些组件、配置在哪个环境生效。
2、切换环境:这个环境对应的所有组件和配置就应该生效。
4.2.1 使用
4.2.1.1 指定环境
- Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效。
- 任何
@Component
,@Configuration
或@ConfigurationProperties
可以使用@Profile
标记,来指定何时被加载。(容器中的组件都可以被@Profile
标记)
4.2.1.2 环境激活
1、配置激活指定环境,配置文件。
# 激活指定的一个或多个环境
spring.profiles.active=dev, test
2、也可以使用命令行激活。
--spring.profiles.active=dev, test
。
3、还可以配置默认环境,不标注 @Profile 的组件永远都存在。
- ① 以前默认环境叫 default。
- ②
spring.profiles.default=dev
。
4、推荐使用激活方式激活指定环境。
4.2.1.3 环境包含
注意:
1、
spring.profiles.active
和
spring.profiles.default
只能用到无 profile 的文件中,如果在
application-dev.properties/yaml
中编写就是无效的。
2、也可以额外添加生效文件,而不是激活替换。比如:
# 包含指定环境,不管激活哪个环境,这个环境都有,总是要生效的环境
spring.profiles.include=dev, test
最佳实战:
- 生效的环境 = 激活的环境/默认环境 + 包含的环境。
- 项目里面这么用。- 基础的配置
mybatis
、log
:写到包含环境中。- 需要动态切换变化的db
、redis
:写到激活的环境中。
4.2.2 Profile 分组
创建
prod
组,指定包含
db
和
mq
配置。
# Profile 分组
spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq
使用
--spring.profiles.active=prod
,就会激活
prod
,
db
,
mq
配置文件。
4.2.3 Profile 配置文件
application-{profile}.properties
可以作为指定环境的配置文件。- 激活这个环境,配置就会生效。最终生效的所有配置是。-
application.properties
:主配置文件,任意时候都生效。-application-{profile}.properties
:指定环境配置文件,激活指定环境生效。
profile 优先级 > application 优先级。
4.3 外部化配置
场景:线上应用如何快速修改配置,并应用最新配置?
SpringBoot 使用配置优先级 + 外部配置简化配置更新、简化运维。
只需要给 jar 应用所在的文件夹放一个 application.properties 最新配置文件,重启项目就能自动应用最新配置。
4.3.1 配置优先级
Spring Boot 允许将配置外部化,以便可以在不同的环境中使用相同的应用程序代码。
可以使用各种外部配置源,包括
Java Properties 文件
、
YAML 文件
、
环境变量
和
命令行参数
。
@Value
可以获取值,也可以用
@ConfigurationProperties
将所有属性绑定到
Java Object
中。
以下是 SpringBoot 属性源加载顺序,后面的会覆盖前面的值,由低到高,高优先级配置覆盖低优先级。
1、默认属性(通过
SpringApplication.setDefaultProperties
指定的)。
2、
@PropertySource
指定加载的配置(需要写在
@Configuration
类上才可生效)。
3、配置文件(application.properties/yml 等)。
4、
RandomValuePropertySource
支持的
random.*
配置(如:@Value(“${random.int}”))。
5、OS 环境变量。
6、Java 系统属性(
System.getProperties()
)。
7、JNDI 属性(来自
java:comp/env
)。
8、ServletContext
初始化参数。
9、ServletConfig
初始化参数。
10、
SPRING_APPLICATION_JSON
属性(内置在环境变量或系统属性中的 JSON)。
11、命令行参数。
12、测试属性。(
@SpringBootTest
进行测试时指定的属性)。
13、测试类
@TestPropertySource
注解。
14、Devtools 设置的全局属性。(
$HOME/.config/spring-boot
)。
结论:配置可以写到很多位置,常见的优先级顺序:
- 命令行 > 配置文件 > springapplication 配置
配置文件优先级如下:(后面覆盖前面)
1、jar 包内的
application.properties/yml
。
2、jar 包内的
application-{profile}.properties/yml
。
3、jar 包外的
application.properties/yml
。
4、jar 包外的
application-{profile}.properties/yml
。
建议:用一种格式的配置文件。如果
.properties
和
.yml
同时存在,则
.properties
优先。
结论:包外 > 包内,同级情况:profile 配置 > application 配置。
所有参数均可由命令行传入,使用
--参数项=参数值
,将会被添加到环境变量中,并优先于
配置文件
。
比如
java -jar app.jar --name="Spring"
,可以使用
@Value("${name}")
获取。
演示场景:
- 包内:application.properties
server.port=8080
。 - 包内:application-dev.properties
server.port=8081
- 包外:application.properties
server.port=8180
- 包外:application-dev.properties
server.port=8181
启动端口?:
命令行
8181
8180
8081
8080
。
4.3.2 外部配置
SpringBoot 应用启动时会自动寻找
application.properties
和
application.yaml
位置,进行加载。顺序如下:(后面覆盖前面)
1、类路径:内部。
- ① 类根路径.
- ② 类下
/config
包。
2、当前路径:项目所在的位置。
- ① 当前路径。
- ② 当前下
/config
子目录。 - ③
/config
目录的直接子目录。
最终效果:优先级由高到低,前面覆盖后面。
- 命令行 > 包外 config 直接子目录 > 包外 config 目录 > 包外根目录 > 包内目录。
- 同级比较:- profile 配置 > 默认配置。- properties 配置 > yaml 配置。
规律:最外层的最优先。
- 命令行 > 所有。
- 包外 > 包内。
- config 目录 > 根目录。
- profile > application。
配置不同就都生效(互补),配置相同高优先级覆盖低优先级。
4.3.3 导入配置
使用
spring.config.import
可以导入额外配置。
# 导入指定的配置
spring.config.import=classpath:/a.properties
# 导入配置优先级低于配置文件的优先级
a=c
无论以上写法的先后顺序,
a.properties
的值总是优先于直接在文件中编写的 a。
4.3.4 属性占位符
配置文件中可以使用
${name:default}
形式取出之前配置过的值。
server.port=8080
my.server.port=我的服务端口是:${server.port},我的用户名是:${my.username:末影小黑xh}
4.4 单元测试 JUnit5
4.4.1 整合
SpringBoot 提供一系列测试工具集及注解方便进行测试。
spring-boot-test
提供核心测试能力,
spring-boot-test-autoconfigure
提供测试的一些自动配置。
只需要导入
spring-boot-starter-test
即可整合测试。
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency>
spring-boot-starter-test
默认提供了以下库供测试使用。
- JUnit 5
- Spring Test
- AssertJ
- Hamcrest
- Mockito
- JSONassert
- JsonPath
4.4.2 测试
4.4.2.1 组件测试
直接
@Autowired
容器中的组件进行测试。
4.4.2.2 注解
JUnit5 的注解与 JUnit4 的注解有所变化。
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test:表示方法是测试方法,但是与 JUnit4 的 @Test 不同,他的职责非常单一不能声明任何属性,拓展的测试将会由 Jupiter 提供额外测试。
- @ParameterizedTest:表示方法是参数化测试,下方会有详细介绍。
- @RepeatedTest:表示方法可重复执行,下方会有详细介绍。
- @DisplayName:为测试类或者测试方法设置展示名称。
- @BeforeEach:表示在每个单元测试之前执行。
- @AfterEach:表示在每个单元测试之后执行。
- @BeforeAll:表示在所有单元测试之前执行。
- @AfterAll:表示在所有单元测试之后执行。
- @Tag:表示单元测试类别,类似于 JUnit4 中的 @Categories。
- @Disabled:表示测试类或测试方法不执行,类似于 JUnit4 中的 @Ignore。
- @Timeout:表示测试方法运行如果超过了指定时间将会返回错误。
- @ExtendWith:为测试类或测试方法提供扩展类引用。
packagecom.myxh.springboot.features;importcom.myxh.springboot.features.service.HelloService;importorg.junit.jupiter.api.*;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;// 具备测试 SpringBoot 应用容器中所有组件的功能,测试类必须在主程序所在的包及其子包@SpringBootTestclassBoot306FeaturesApplicationTests{// 自动注入任意组件即可测试@AutowiredHelloService helloService;@DisplayName("测试 sum() 方法 ")@TestvoidcontextLoads(){Integer sum = helloService.sum(1,2);Assertions.assertEquals(3, sum);}@DisplayName("测试1")@Testpublicvoidtest1(){System.out.println("test1");}// 所有测试方法运行之前先运行这个,只打印一次@BeforeAllstaticvoidinitAll(){System.out.println("hello");}// 每个测试方法运行之前先运行这个,每个方法运行打印一次@BeforeEachvoidinit(){System.out.println("world");}}
4.4.2.3 断言
方法说明assertEquals判断两个对象或两个原始类型是否相等。assertNotEquals判断两个对象或两个原始类型是否不相等。assertSame判断两个对象引用是否指向同一个对象。assertNotSame判断两个对象引用是否指向不同的对象。assertTrue判断给定的布尔值是否为 true。assertFalse判断给定的布尔值是否为 false。assertNull判断给定的对象引用是否为 null。assertNotNull判断给定的对象引用是否不为 null。assertArrayEquals****数组断言。assertAll****组合断言。assertThrows****异常断言。assertTimeout****超时断言。fail****快速失败。
4.4.2.4 嵌套测试
JUnit 5 可以通过 Java 中的内部类和 @Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。
packagecom.myxh.springboot.features;importorg.junit.jupiter.api.BeforeEach;importorg.junit.jupiter.api.DisplayName;importorg.junit.jupiter.api.Nested;importorg.junit.jupiter.api.Test;importorg.springframework.boot.test.context.SpringBootTest;importjava.util.EmptyStackException;importjava.util.Stack;importstaticorg.junit.jupiter.api.Assertions.*;/**
* @author MYXH
* @date 2023/9/23
*/@SpringBootTest@DisplayName("一个堆栈")publicclassHelloTest{Stack<Object> stack;@Test@DisplayName("使用 new Stack() 实例化堆栈")voidisInstantiatedWithNew(){newStack<>();}@Nested@DisplayName("当新建时")classWhenNew{@BeforeEachvoidcreateNewStack(){
stack =newStack<>();}@Test@DisplayName("为空")voidisEmpty(){assertTrue(stack.isEmpty());}@Test@DisplayName("在弹出时抛出 EmptyStackException 异常")voidthrowsExceptionWhenPopped(){assertThrows(EmptyStackException.class, stack::pop);}@Test@DisplayName("在查看时抛出 EmptyStackException 异常")voidthrowsExceptionWhenPeeked(){assertThrows(EmptyStackException.class, stack::peek);}@Nested@DisplayName("在推入元素后")classAfterPushing{String anElement ="一个元素";@BeforeEachvoidpushAnElement(){
stack.push(anElement);}@Test@DisplayName("它不再为空")voidisNotEmpty(){assertFalse(stack.isEmpty());}@Test@DisplayName("当弹出元素并且为空时返回该元素")voidreturnElementWhenPopped(){assertEquals(anElement, stack.pop());assertTrue(stack.isEmpty());}@Test@DisplayName("当查看元素时返回该元素,但仍然保持非空")voidreturnElementWhenPeeked(){assertEquals(anElement, stack.peek());assertFalse(stack.isEmpty());}}}}
4.4.2.5 参数化测试
参数化测试是 JUnit5 很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为单元测试带来许多便利。
利用 @ValueSource 等注解,指定入参,将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。
@ValueSource:为参数化测试指定入参来源,支持八大基础类以及 String 类型,Class 类型。
@NullSource:表示为参数化测试提供一个 null 的入参。
@EnumSource:表示为参数化测试提供一个枚举入参。
@CsvFileSource:表示读取指定 CSV 文件内容作为参数化测试入参。
@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)。
packagecom.myxh.springboot.features;importcom.myxh.springboot.features.service.HelloService;importorg.junit.jupiter.api.*;importorg.junit.jupiter.params.ParameterizedTest;importorg.junit.jupiter.params.provider.MethodSource;importorg.junit.jupiter.params.provider.ValueSource;importorg.junit.platform.commons.util.StringUtils;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importjava.util.stream.Stream;// 具备测试 SpringBoot 应用容器中所有组件的功能,测试类必须在主程序所在的包及其子包@SpringBootTestclassBoot306FeaturesApplicationTests{// 自动注入任意组件即可测试@AutowiredHelloService helloService;@DisplayName("测试 sum() 方法 ")@TestvoidcontextLoads(){Integer sum = helloService.sum(1,2);Assertions.assertEquals(3, sum);}@DisplayName("测试1")@Testpublicvoidtest1(){System.out.println("test1");}// 参数化测试@ParameterizedTest@ValueSource(strings ={"one","two","three"})@DisplayName("参数化测试1")publicvoidparameterizedTest1(String string){System.out.println(string);Assertions.assertTrue(StringUtils.isNotBlank(string));}@ParameterizedTest// 指定方法名,返回值就是测试用的参数@MethodSource("method")@DisplayName("方法来源参数")publicvoidtestWithExplicitLocalMethodSource(String name){System.out.println(name);Assertions.assertNotNull(name);}staticStream<String>method(){// 返回 Stream 则可returnStream.of("apple","banana");}// 所有测试方法运行之前先运行这个,只打印一次@BeforeAllstaticvoidinitAll(){System.out.println("hello");}// 每个测试方法运行之前先运行这个,每个方法运行打印一次@BeforeEachvoidinit(){System.out.println("world");}}
第 5 章 SpringBoot3-核心原理
5.1 事件和监听器
5.1.1 生命周期监听
场景:监听应用的生命周期。
5.1.1.1 监听器 SpringApplicationRunListener
1、自定义
SpringApplicationRunListener
来监听事件。
- ① 编写
SpringApplicationRunListener
实现类。packagecom.myxh.springboot.core.listener;importorg.springframework.boot.ConfigurableBootstrapContext;importorg.springframework.boot.SpringApplicationRunListener;importorg.springframework.context.ConfigurableApplicationContext;importorg.springframework.core.env.ConfigurableEnvironment;importjava.time.Duration;/** * @author MYXH * @date 2023/9/24 * @description * SpringBoot 应用生命周期监听 * <br> * Listener先要从 META-INF/spring.factories 读到 * <br> * 1、引导:利用 BootstrapContext 引导整个项目启动 * starting: 应用开始,SpringApplication 的 run 方法一调用,只要有了 BootstrapContext 就执行 * environmentPrepared: 环境准备好(把启动参数等绑定到环境变量中),但是 ioc 还没有创建(调一次) * 2、启动: * contextPrepared: ioc 容器创建并准备好,但是 sources(主配置类)没加载,并关闭引导上下文,组件都没创建(调一次) * contextLoaded: ioc 容器加载,主配置类加载进去了,但是 ioc 容器还没刷新(bean 没创建) * ----- 截止现在,ioc 容器里面还没造 bean ----- * started: ioc 容器刷新了(所有 bean 造好了),但是 runner 没调用 * ready: ioc 容器刷新了(所有 bean 造好了),所有 runner 调用完了 * 3、运行: * 以上步骤都正确执行,代表容器 running。 */publicclassMyAppListenerimplementsSpringApplicationRunListener{@Overridepublicvoidstarting(ConfigurableBootstrapContext bootstrapContext){System.out.println("--------------- starting() 正在启动 ---------------");}@OverridepublicvoidenvironmentPrepared(ConfigurableBootstrapContext bootstrapContext,ConfigurableEnvironment environment){System.out.println("--------------- environmentPrepared() 环境准备完成 ---------------");}@OverridepublicvoidcontextPrepared(ConfigurableApplicationContext context){System.out.println("--------------- contextPrepared() ioc 容器准备完成 ---------------");}@OverridepublicvoidcontextLoaded(ConfigurableApplicationContext context){System.out.println("--------------- contextLoaded() ioc 容器加载完成 ---------------");}@Overridepublicvoidstarted(ConfigurableApplicationContext context,Duration timeTaken){System.out.println("--------------- started() 启动完成 ---------------");}@Overridepublicvoidready(ConfigurableApplicationContext context,Duration timeTaken){System.out.println("--------------- ready() 准备就绪 ---------------");}@Overridepublicvoidfailed(ConfigurableApplicationContext context,Throwable exception){System.out.println("--------------- failed() 应用启动失败 ---------------");}}``````packagecom.myxh.springboot.core.listener;importorg.springframework.context.ApplicationEvent;importorg.springframework.context.ApplicationListener;/** * @author MYXH * @date 2023/9/24 */publicclassMyListenerimplementsApplicationListener<ApplicationEvent>{@OverridepublicvoidonApplicationEvent(ApplicationEvent event){System.out.println("-------- 事件到达 --------");System.out.println("event = "+ event);System.out.println("------------------------");}}
- ② 在
META-INF/spring.factories
中配置org.springframework.boot.SpringApplicationRunListener=自己的 Listener
,还可以指定一个有参构造器,接受两个参数(SpringApplication application, String[] args)
。org.springframework.boot.SpringApplicationRunListener=\com.myxh.springboot.core.listener.MyAppListenerorg.springframework.context.ApplicationListener=\com.myxh.springboot.core.listener.MyListener
- ③ springboot 在
spring-boot.jar
中配置了默认的 Listener,如下。# RunListenersorg.springframework.boot.SpringApplicationRunListener=\org.springframework.boot.context.event.EventPublishingRunListener
5.1.1.2 生命周期全流程
5.1.2 事件触发时机
5.1.2.1 各种回调监听器
BootstrapRegistryInitializer
:感知特定阶段,感知引导初始化。-META-INF/spring.factories
。- 创建引导上下文bootstrapContext
的时候触发。-application.addBootstrapRegistryInitializer();
- 场景:进行密钥校对授权。- ApplicationContextInitializer:感知特定阶段,感知 ioc 容器初始化。-
META-INF/spring.factories
。-application.addInitializers();
- ApplicationListener:感知全阶段,基于事件机制,感知事件,一旦到了哪个阶段可以做别的事。-
@Bean
或@EventListener
:事件驱动。-SpringApplication.addListeners(...)
或SpringApplicationBuilder.listeners(...)
。-META-INF/spring.factories
。 - SpringApplicationRunListener:感知全阶段生命周期 + 各种阶段都能自定义操作,功能更完善。-
META-INF/spring.factories
。 - ApplicationRunner:感知特定阶段,感知应用就绪 Ready,卡死应用,就不会就绪。-
@Bean
。 - CommandLineRunner:感知特定阶段,感知应用就绪 Ready,卡死应用,就不会就绪。-
@Bean
。
最佳实战:
- 如果想要在项目启动前操作:
BootstrapRegistryInitializer
和ApplicationContextInitializer
。 - 如果想要在项目启动完成后操作:ApplicationRunner 和 CommandLineRunner。
- 如果要干涉生命周期做事:SpringApplicationRunListener。
- 如果想要用事件机制:ApplicationListener。
5.1.2.2 完整触发流程
9 大事件触发顺序和时机:
1、
ApplicationStartingEvent
:应用启动但未做任何事情, 除过注册
listeners
和
initializers
。
2、
ApplicationEnvironmentPreparedEvent
:Environment 准备好,但 context 未创建。
3、
ApplicationContextInitializedEvent
:ApplicationContext 准备好,ApplicationContextInitializers 调用,但是任何 bean 未加载。
4、
ApplicationPreparedEvent
:容器刷新之前,bean 定义信息加载。
5、
ApplicationStartedEvent
:容器刷新完成, runner 未调用。
以下就开始插入了探针机制。
6、
AvailabilityChangeEvent
:
LivenessState.CORRECT
应用存活,存活探针。
7、
ApplicationReadyEvent
:任何 runner 被调用。
8、
AvailabilityChangeEvent
:
ReadinessState.ACCEPTING_TRAFFIC
就绪探针,可以接请求。
9、
ApplicationFailedEvent
:启动出错。
应用事件发送顺序如下:
感知应用是否存活了:可能植物状态,虽然活着但是不能处理请求。
应用是否就绪了:能响应请求,说明确实活的比较好。
5.1.2.3 SpringBoot 事件驱动开发
应用启动过程生命周期事件感知(9 大事件)、应用运行中事件感知(无数种)。
- 事件发布:
ApplicationEventPublisherAware
或注入 ApplicationEventMulticaster
。 - 事件监听:
组件 + @EventListener
。
事件发布者。
packagecom.myxh.springboot.core.event;importorg.springframework.context.ApplicationEvent;importorg.springframework.context.ApplicationEventPublisher;importorg.springframework.context.ApplicationEventPublisherAware;importorg.springframework.stereotype.Service;/**
* @author MYXH
* @date 2023/9/24
* @description 事件是广播出去的,所有监听这个事件的监听器都可以收到
*/@ServicepublicclassEventPublisherimplementsApplicationEventPublisherAware{// 底层发送事件用的组件,SpringBoot 会通过 ApplicationEventPublisherAware 接口自动注入ApplicationEventPublisher applicationEventPublisher;/**
* 所有事件都可以发
*
* @param event 事件
*/publicvoidsendEvent(ApplicationEvent event){// 调用底层 API 发送事件
applicationEventPublisher.publishEvent(event);}/**
* 会被自动调用,把真正发事件的底层组组件给注入进来
*
* @param applicationEventPublisher 此对象要使用的事件发布者
*/@OverridepublicvoidsetApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher){this.applicationEventPublisher = applicationEventPublisher;}}
packagecom.myxh.springboot.core.event;importcom.myxh.springboot.core.entity.UserEntity;importorg.springframework.context.ApplicationEvent;/**
* @author MYXH
* @date 2023/9/24
* @description 登录成功事件,所有事件都推荐继承 ApplicationEvent
*/publicclassLoginSuccessEventextendsApplicationEvent{/**
* 登录成功事件
*
* @param source 事件来源,代表是谁登录成了
*/publicLoginSuccessEvent(UserEntity source){super(source);}}
事件订阅者。
packagecom.myxh.springboot.core.service;importcom.myxh.springboot.core.entity.UserEntity;importcom.myxh.springboot.core.event.LoginSuccessEvent;importorg.springframework.context.event.EventListener;importorg.springframework.core.annotation.Order;importorg.springframework.stereotype.Service;/**
* @author MYXH
* @date 2023/9/24
*/@ServicepublicclassSystemService{@Order(1)@EventListenerpublicvoidonEvent(LoginSuccessEvent event){System.out.println("-------- SystemService 感知到事件 --------");System.out.println("event = "+ event);System.out.println("------------------------");UserEntity source =(UserEntity) event.getSource();recordLog(source.getUsername());}publicvoidrecordLog(String username){System.out.println(username +"登录信息已被记录");}}
packagecom.myxh.springboot.core.service;importcom.myxh.springboot.core.entity.UserEntity;importcom.myxh.springboot.core.event.LoginSuccessEvent;importorg.springframework.context.ApplicationListener;importorg.springframework.core.annotation.Order;importorg.springframework.stereotype.Service;/**
* @author MYXH
* @date 2023/9/24
*/@Order(2)@ServicepublicclassAccountServiceimplementsApplicationListener<LoginSuccessEvent>{publicvoidaddAccountScore(String username){System.out.println(username +" 加了 1 分");}@OverridepublicvoidonApplicationEvent(LoginSuccessEvent event){System.out.println("-------- AccountService 收到事件 --------");UserEntity source =(UserEntity) event.getSource();addAccountScore(source.getUsername());}}
packagecom.myxh.springboot.core.service;importcom.myxh.springboot.core.entity.UserEntity;importcom.myxh.springboot.core.event.LoginSuccessEvent;importorg.springframework.context.event.EventListener;importorg.springframework.core.annotation.Order;importorg.springframework.scheduling.annotation.Async;importorg.springframework.stereotype.Service;/**
* @author MYXH
* @date 2023/9/24
*/@ServicepublicclassCouponService{publicCouponService(){System.out.println("构造器调用");}@Async@Order(3)@EventListenerpublicvoidonEvent(LoginSuccessEvent loginSuccessEvent){System.out.println("-------- CouponService 感知到事件 --------");System.out.println("loginSuccessEvent = "+ loginSuccessEvent);System.out.println("------------------------");UserEntity source =(UserEntity) loginSuccessEvent.getSource();sendCoupon(source.getUsername());}publicvoidsendCoupon(String username){System.out.println(username +" 随机得到了一张优惠券");}}
packagecom.myxh.springboot.core.service;importcom.myxh.springboot.core.event.LoginSuccessEvent;importorg.springframework.context.event.EventListener;importorg.springframework.stereotype.Service;/**
* @author MYXH
* @date 2023/9/24
*/@ServicepublicclassHelloService{@EventListenerpublicvoidonEvent(LoginSuccessEvent event){System.out.println("-------- HelloService 感知到事件 --------");System.out.println("event = "+ event);System.out.println("------------------------");// 调用业务}}
5.2 自动配置原理
5.2.1 入门理解
应用关注的三大核心:场景、配置、组件。
5.2.1.1 自动配置流程
1、导入
starter
。
2、依赖导入
autoconfigure
。
3、寻找类路径下
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件。
4、启动,加载所有自动配置类
xxxAutoConfiguration
。
- ① 给容器中配置功能组件。
- ②
组件参数
绑定到属性类
中,xxxProperties
。 - ③
属性类
和配置文件
前缀项绑定。 - ④
@Contional 派生的条件注解进
行判断是否组件生效。
5、效果:
- ① 修改配置文件,修改底层参数。
- ② 所有场景自动配置好直接使用。
- ③ 可以注入 SpringBoot 配置好的组件随时使用。
5.2.1.2 SPI 机制
- Java 中的 SPI(Service Provider Interface)是一种软件设计模式,用于在应用程序中动态地发现和加载组件。SPI 的思想是,定义一个接口或抽象类,然后通过在 classpath 中定义实现该接口的类来实现对组件的动态发现和加载。
- SPI 的主要目的是解决在应用程序中使用可插拔组件的问题。例如,一个应用程序可能需要使用不同的日志框架或数据库连接池,但是这些组件的选择可能取决于运行时的条件。通过使用 SPI,应用程序可以在运行时发现并加载适当的组件,而无需在代码中硬编码这些组件的实现类。
- 在 Java 中,SPI 的实现方式是通过在 META-INF/services 目录下创建一个以服务接口全限定名为名字的文件,文件中包含实现该服务接口的类的全限定名。当应用程序启动时,Java 的 SPI 机制会自动扫描 classpath 中的这些文件,并根据文件中指定的类名来加载实现类。
- 通过使用 SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性。
在 SpringBoot 中,
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
。
写一段 Java 的 SPI 机制代码。
下面是一个简单的 Java SPI(Service Provider Interface)机制的示例代码,代码中包含了详细的注释说明:
packagecom.example.spi;// 定义服务接口publicinterfaceMyService{voidexecute();}
packagecom.example.spi;// 定义服务提供者 1publicclassMyService1implementsMyService{@Overridepublicvoidexecute(){System.out.println("MyService1 正在执行。");}}
packagecom.example.spi;// 定义服务提供者 2publicclassMyService2implementsMyService{@Overridepublicvoidexecute(){System.out.println("MyService2 正在执行。");}}
packagecom.example.spi;importjava.util.ServiceLoader;// 定义服务加载器publicclassServiceLoader{// 加载并执行服务publicvoidloadAndExecuteServices(){// 使用Java SPI机制加载所有的服务提供者java.util.ServiceLoader<MyService> serviceLoader =java.util.ServiceLoader.load(MyService.class);// 遍历所有的服务提供者,并执行服务for(MyService service : serviceLoader){
service.execute();}}}
packagecom.example.spi;// 测试代码publicclassMain{publicstaticvoidmain(String[] args){ServiceLoader loader =newServiceLoader();
loader.loadAndExecuteServices();}}
上面的代码展示了如何使用 Java SPI 机制来加载并执行服务提供者。其中,
MyService
是服务接口,
MyService1
和
MyService2
是两个具体的服务提供者实现。
ServiceLoader
是服务加载器,它通过
java.util.ServiceLoader
来加载所有的服务提供者,并执行服务的
execute()
方法。最后,在
Main
类的
main()
方法中调用
ServiceLoader
的
loadAndExecuteServices()
方法即可加载并执行服务提供者。
注意:在实际使用 Java SPI 机制时,需要在
src/main/resources/META-INF/services
目录下创建一个以服务接口全限定名命名的文件,文件内容为具体的服务提供者实现类的全限定名。在本示例中,需要创建
src/main/resources/META-INF/services/com.example.MyService
文件,并在其中分别写入
com.example.MyService1
和
com.example.MyService2
。这样,在调用
java.util.ServiceLoader.load()
方法时,就能正确加载到所有的服务提供者实现类。
5.2.1.3 功能开关
- 自动配置:全部都配置好,什么都不用管,自动批量导入。- 项目一启动,SPI 文件中指定的所有都加载。
@EnableXxx
:手动控制哪些功能的开启,手动导入。- 开启 xxx 功能。- 都是利用 @Import 把此功能要用的组件导入进去。
5.2.2 进阶理解
5.2.2.1 @SpringBootApplication
@SpringBootConfiguration。
就是 @Configuration,容器中的组件,配置类,Spring IOC 启动就会加载创建这个类对象。
@EnableAutoConfiguration:开启自动配置。
开启自动配置。
@AutoConfigurationPackage:扫描主程序包,加载自己的组件。
- 利用
@Import(AutoConfigurationPackages.Registrar.class)
想要给容器中导入组件。 - 把主程序所在的包的所有组件导入进来。
- 为什么 SpringBoot 默认只扫描主程序所在的包及其子包。
@Import(AutoConfigurationImportSelector.class):加载所有自动配置类,加载 starter 导入的组件。
List<String> configurations =ImportCandidates.load(AutoConfiguration.class,getBeanClassLoader()).getCandidates();
扫描 SPI 文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports。
@ComponentScan。
组件扫描:排除一些组件(哪些不要)。
排除前面已经扫描进来的配置类、和自动配置类。
@ComponentScan(excludeFilters ={@Filter(type =FilterType.CUSTOM, classes =TypeExcludeFilter.class),@Filter(type =FilterType.CUSTOM, classes =AutoConfigurationExcludeFilter.class)})
5.2.2.2 完整启动加载流程
生命周期启动加载流程。
5.3 自定义 starter
场景:抽取聊天机器人场景,它可以打招呼。
效果:任何项目导入此 starter 都具有打招呼功能,并且问候语中的人名需要可以在配置文件中修改。
- 1、创建
自定义 starter
项目,引入spring-boot-starter
基础依赖。 - 2、编写模块功能,引入模块所有需要的依赖。
- 3、编写
xxxAutoConfiguration
自动配置类,帮其他项目导入这个模块需要的所有组件。 - 4、编写配置文件
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
指定启动需要加载的自动配置。 - 5、其他项目引入即可使用。
5.3.1 业务代码
自定义配置有提示,导入以下依赖重启项目,再写配置文件就有提示。
<!-- 导入配置处理器,配置文件自定义的 properties 配置都会有提示 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency>
packagecom.myxh.springboot.starter.robot.properties;importlombok.Data;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.stereotype.Component;/**
* @author MYXH
* @date 2023/9/24
*/// 此属性类和配置文件指定前缀绑定@ConfigurationProperties(prefix ="robot")@Component@DatapublicclassRobotProperties{privateString name;privateInteger age;privateString email;}
robot.name=MYXH
robot.age=21
[email protected]
5.3.2 基本抽取
- 创建 starter 项目,把公共代码需要的所有依赖导入。
- 把公共代码复制进来。
- 自己写一个
RobotAutoConfiguration
,给容器中导入这个场景需要的所有组件。- 为什么这些组件默认不会扫描进去?- starter 所在的包和引入它的项目的主程序所在的包不是父子层级。packagecom.myxh.springboot.starter.robot;importcom.myxh.springboot.starter.robot.controller.RobotController;importcom.myxh.springboot.starter.robot.properties.RobotProperties;importcom.myxh.springboot.starter.robot.service.RobotService;importorg.springframework.context.annotation.Configuration;importorg.springframework.context.annotation.Import;/** * @author MYXH * @date 2023/9/24 */// 给容器中导入 Robot 功能要用的所有组件@Import({RobotProperties.class,RobotController.class,RobotService.class})@ConfigurationpublicclassRobotAutoConfiguration{/* // 把组件导入到容器中 @Bean public RobotController robotController() { return new RobotController(); } */}
- 别人引用这个
starter
,直接导入这个RobotAutoConfiguration
,就能把这个场景的组件导入进来。 - 功能生效。
- 测试编写配置文件。
5.3.3 使用@EnableXxx 机制
packagecom.myxh.springboot.starter.robot.annotation;importcom.myxh.springboot.starter.robot.RobotAutoConfiguration;importorg.springframework.context.annotation.Import;importjava.lang.annotation.*;/**
* @author MYXH
* @date 2023/9/24
*/@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})@Documented@Import(RobotAutoConfiguration.class)public@interfaceEnableRobot{}
别人引入
starter
需要使用
@EnableRobot
开启功能。
5.3.4 完全自动配置
- 依赖 SpringBoot 的 SPI 机制。
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件中编写好自动配置类的全类名即可。- 项目启动,自动加载自动配置类。
版权归原作者 末影小黑xh 所有, 如有侵权,请联系我们删除。