SpringBoot3-核心特性

1、SpringBoot3-快速入门

1、简介

1、前置知识

  • Java17

  • Spring、SpringMVC、MyBatis

  • Maven、IDEA

2、环境要求

环境&工具 版本(or later)
SpringBoot 3.1.2+
IDEA 2021.2.1+
Java 17+
Maven 3.5+
Tomcat 10.0+
Servlet 5.0+
GraalVM Community 22.3+
Native Build Tools 0.9.19+

3、SpringBoot是什么

SpringBoot 帮我们简单、快速地创建一个独立的、生产级别的 Spring 应用(说明:SpringBoot底层是Spring)

大多数 SpringBoot 应用只需要编写少量配置即可快速整合 Spring 平台以及第三方技术

特性:

  • 快速创建独立 Spring 应用

    • SSM:导包、写配置、启动运行
  • 直接嵌入Tomcat、Jetty or 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

总结:简化开发,简化配置,简化整合,简化部署,简化监控,简化运维。

2、快速体验

场景:浏览器发送 /hello 请求,返回”Hello,Spring Boot 3!

1、开发流程

1. 创建项目

maven 项目

1
2
3
4
5
6
<!--    所有springboot项目都必须继承自 spring-boot-starter-parent -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.1.12</version>
</parent>

2. 导入场景

场景启动器

1
2
3
4
5
6
7
<dependencies>
<!-- web开发的场景启动器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

3. 主程序

1
2
3
4
5
6
@SpringBootApplication //这是一个SpringBoot应用
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}

4. 业务

1
2
3
4
5
6
7
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "Hello,Spring Boot 3!";
}
}

5. 测试

默认启动访问: http://localhost:8080

6. 打包

1
2
3
4
5
6
7
8
9
<!-- SpringBoot应用打包插件 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

mvn clean package把项目打成可执行的jar包

java -jar demo.jar启动项目

2.、特性小结

1. 简化整合

导入相关的场景,拥有相关的功能。场景启动器

默认支持的所有场景:点击查看

  • 官方提供的场景:命名为:spring-boot-starter-*
  • 第三方提供场景:命名为:*-spring-boot-starter

场景一导入,万物皆就绪

2. 简化开发

无需编写任何配置,直接开发业务

3. 简化配置

application.properties

  • 集中式管理配置。只需要修改这个文件就行 。
  • 配置基本都有默认值
  • 能写的所有配置都在:点击查看

4. 简化部署

打包为可执行的jar包。

linux服务器上有java环境。

5. 简化运维

修改配置(外部放一个application.properties文件)、监控、健康检查。

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父项目没有管理的需要自行声明好
1
2
3
4
5
6
<!-- https://mvnrepository.com/artifact/com.alibaba/druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.16</version>
</dependency>

2. 自动配置机制

1. 初步理解
  • 自动配置的 Tomcat、SpringMVC 等

    • 导入场景,容器中就会自动配置好这个场景的核心组件。
    • 以前:DispatcherServlet、ViewResolver、CharacterEncodingFilter….
    • 现在:自动配置好的这些组件
    • 验证:容器中有了什么组件,就具有什么功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {

//java10: 局部变量类型的自动推断
var ioc = SpringApplication.run(MainApplication.class, args);

//1、获取容器中所有组件的名字
String[] names = ioc.getBeanDefinitionNames();
//2、挨个遍历:
// dispatcherServlet、beanNameViewResolver、characterEncodingFilter、multipartResolver
// SpringBoot把以前配置的核心组件现在都给我们自动配置好了。
for (String name : names) {
System.out.println(name);
}
}
  • 默认的包扫描规则

    • @SpringBootApplication 标注的类就是主程序类
    • SpringBoot只会扫描主程序所在的包及其下面的子包,自动的component-scan功能
    • 自定义扫描路径
      • @SpringBootApplication(scanBasePackages = “com.atguigu”)
      • @ComponentScan("com.xoao") 直接指定扫描的路径
  • 配置默认值

    • 配置文件的所有配置项是和某个类的对象值进行一一绑定的。
    • 绑定了配置文件中每一项值的类: 属性类
    • 比如:
      • ServerProperties绑定了所有Tomcat服务器有关的配置
      • MultipartProperties绑定了所有文件上传相关的配置
      • ….参照官方文档:或者参照 绑定的 属性类
  • 按需加载自动配置

    • 导入场景spring-boot-starter-web
    • 场景启动器除了会导入相关功能依赖,导入一个spring-boot-starter,是所有starterstarter,基础核心starter
    • spring-boot-starter导入了一个包 spring-boot-autoconfigure。包里面都是各种场景的AutoConfiguration自动配置类
    • 虽然全场景的自动配置都在 spring-boot-autoconfigure这个包,但是不是全都开启的。
      • 导入哪个场景就开启哪个自动配置

总结: 导入场景启动器、触发 spring-boot-autoconfigure这个包的自动配置生效、容器中就会具有相关场景的功能

2. 完整流程

思考:

1、SpringBoot怎么实现导一个 starter 、写一些简单配置,应用就能跑起来,我们无需关心整合

2、为什么Tomcat的端口号可以配置在application.properties中,并且Tomcat能启动成功?

3、导入场景后哪些自动配置能生效

自动配置流程细节梳理:

1、导入starter-web:导入了web开发场景

  • 1、场景启动器导入了相关场景的所有依赖:starter-jsonstarter-tomcatspringmvc
  • 2、每个场景启动器都引入了一个spring-boot-starter,核心场景启动器。
  • 3、核心场景启动器引入了spring-boot-autoconfigure包。
  • 4、spring-boot-autoconfigure里面囊括了所有场景的所有配置。
  • 5、只要这个包下的所有类都能生效,那么相当于SpringBoot官方写好的整合功能就生效了。
  • 6、SpringBoot默认却扫描不到 spring-boot-autoconfigure下写好的所有配置类。(这些配置类给我们做了整合操作),默认只扫描主程序所在的包

2、 主程序@SpringBootApplication

  • 1、@SpringBootApplication由三个注解组成@SpringBootConfiguration@EnableAutoConfiguratio@ComponentScan

  • 2、SpringBoot默认只能扫描自己主程序所在的包及其下面的子包,扫描不到 spring-boot-autoconfigure包中官方写好的配置类

  • 3、**@EnableAutoConfiguration**:SpringBoot 开启自动配置的核心

      1. 是由@Import(AutoConfigurationImportSelector.class)提供功能:批量给容器中导入组件。
      1. SpringBoot启动会默认加载 142个配置类。
      1. 142个配置类来自于spring-boot-autoconfigureMETA-INF/spring/**org.springframework.boot.autoconfigure.AutoConfiguration**.imports文件指定的
    • 项目启动的时候利用 @Import 批量导入组件机制把 autoconfigure 包下的142 xxxxAutoConfiguration类导入进来(自动配置类
    • 虽然导入了142个自动配置类
  • 4、按需生效:

    • 并不是这142个自动配置类都能生效
    • 每一个自动配置类,都有条件注解@ConditionalOnxxx,只有条件成立,才能生效

3、 xxxxAutoConfiguration 自动配置类

  • 1、给容器中使用@Bean 放一堆组件。
  • 2、每个自动配置类都可能有这个注解@EnableConfigurationProperties(**ServerProperties**.class),用来把配置文件中配的指定前缀的属性值封装到 xxxProperties属性类
  • 3、以Tomcat为例:把服务器的所有配置都是以server开头的。配置都封装到了属性类中。
  • 4、给容器中放的所有组件的一些核心参数,都来自于**xxxProperties****xxxProperties**都是和配置文件绑定。
  • 只需要改配置文件的值,核心组件的底层参数都能修改

4、写业务,全程无需关心各种整合(底层这些整合写好了,而且也生效了)

核心流程总结:

1、导入starter,就会导入autoconfigure包。

2、autoconfigure 包里面 有一个文件 META-INF/spring/**org.springframework.boot.autoconfigure.AutoConfiguration**.imports,里面指定的所有启动要加载的自动配置类

3、@EnableAutoConfiguration 会自动的把上面文件里面写的所有自动配置类都导入进来。xxxAutoConfiguration 是有条件注解进行按需加载

4、xxxAutoConfiguration给容器中导入一堆组件,组件都是从 xxxProperties中提取属性值

5、xxxProperties又是和配置文件进行了绑定

效果:导入starter、修改配置文件,就能修改底层行为。

3. 如何学好SpringBoot

框架的框架、底层基于Spring。能调整每一个场景的底层行为。100%项目一定会用到底层自定义

摄影:

  • 傻瓜:自动配置好。
  • 单反:焦距、光圈、快门、感光度….
  • 傻瓜+单反
  1. 理解自动配置原理

    1. 导入starter –> 生效xxxxAutoConfiguration –> 组件 –> xxxProperties –> 配置文件
  2. 理解其他框架底层

    1. 拦截器
  3. 可以随时定制化任何组件

    1. 配置文件
    2. 自定义组件

普通开发:导入starter,Controller、Service、Mapper、偶尔修改配置文件

高级开发:自定义组件、自定义配置、自定义starter

核心:

  • 这个场景自动配置导入了哪些组件,我们能不能Autowired进来使用
  • 能不能通过修改配置改变组件的一些默认参数
  • 需不需要自己完全定义这个组件
  • 场景定制化

最佳实战

  • 选场景,导入到项目

    • 官方:starter
    • 第三方:去仓库搜
  • 写配置,改配置文件关键项

    • 数据库参数(连接地址、账号密码…)
  • 分析这个场景给我们导入了哪些能用的组件

    • 自动装配这些组件进行后续使用
    • 不满意boot提供的自动配好的默认组件
      • 定制化
        • 改配置
        • 自定义组件

整合redis:

  • 选场景spring-boot-starter-data-redis

    • 场景AutoConfiguration 就是这个场景的自动配置类
  • 写配置:

    • 分析到这个场景的自动配置类开启了哪些属性绑定关系
    • @EnableConfigurationProperties(RedisProperties.class)
    • 修改redis相关的配置
  • 分析组件:

    • 分析到 RedisAutoConfiguration 给容器中放了 StringRedisTemplate
    • 给业务代码中自动装配 StringRedisTemplate
  • 定制化

    • 修改配置文件
    • 自定义组件,自己给容器中放一个 StringRedisTemplate

4、核心技能

1. 常用注解

SpringBoot摒弃XML配置方式,改为全注解驱动

1. 组件注册

@Configuration、**@SpringBootConfiguration**

@Bean、**@Scope**

@Controller、 @Service、@Repository、@Component

@Import

@ComponentScan

步骤:

1、@Configuration 编写一个配置类

2、在配置类中,自定义方法给容器中注册组件。配合@Bean

3、或使用@Import 导入第三方的组件

2. 条件注解

如果注解指定的条件成立,则触发指定行为

@ConditionalOnXxx

@ConditionalOnClass:如果类路径中存在这个类,则触发指定行为

@ConditionalOnMissingClass:如果类路径中不存在这个类,则触发指定行为

@ConditionalOnBean:如果容器中存在这个Bean(组件),则触发指定行为

@ConditionalOnMissingBean:如果容器中不存在这个Bean(组件),则触发指定行为

场景:

  • 如果存在FastsqlException这个类,给容器中放一个Cat组件,名cat01,
  • 否则,就给容器中放一个Dog组件,名dog01
  • 如果系统中有dog01这个组件,就给容器中放一个 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)

3. 属性绑定

@ConfigurationProperties: 声明组件的属性和配置文件哪些前缀开始项进行绑定

@EnableConfigurationProperties:快速注册注解:

  • 场景:SpringBoot默认只扫描自己主程序所在的包。如果导入第三方包,即使组件上标注了 @Component、@ConfigurationProperties 注解,也没用。因为组件都扫描不进来,此时使用这个注解就可以快速进行属性绑定并把组件注册进容器

将容器中任意组件(Bean)的属性值配置文件的配置项的值进行绑定

  • 1、给容器中注册组件(@Component、@Bean)
  • 2、使用@ConfigurationProperties 声明组件和配置文件的哪些配置项进行绑定

2. YAML配置文件

痛点:SpringBoot 集中化管理配置,application.properties

问题:配置多以后难阅读和修改,层级结构辨识度不高

YAML 是 “YAML Ain’t a Markup Language”(YAML 不是一种标记语言)。在开发的这种语言时,YAML 的意思其实是:”Yet Another Markup Language”(是另一种标记语言)。

  • 设计目标,就是方便人类读写
  • 层次分明,更适合做配置文件
  • 使用.yaml.yml作为文件后缀

1. 基本语法

  • 大小写敏感
  • 使用缩进表示层级关系,k: v,使用空格分割k,v
  • 缩进时不允许使用Tab键,只允许使用空格。换行
  • 缩进的空格数目不重要,只要相同层级的元素左侧对齐即可
  • # 表示注释,从这个字符一直到行尾,都会被解析器忽略。

支持的写法:

  • 对象键值对的集合,如:映射(map)/ 哈希(hash) / 字典(dictionary)
  • 数组:一组按次序排列的值,如:序列(sequence) / 列表(list)
  • 纯量:单个的、不可再分的值,如:字符串、数字、bool、日期

2. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
@ConfigurationProperties(prefix = "person") //和配置文件person前缀的所有配置进行绑定
@Data //自动生成JavaBean属性的getter/setter
//@NoArgsConstructor //自动生成无参构造器
//@AllArgsConstructor //自动生成全参构造器
public class Person {
private String name;
private Integer age;
private Date birthDay;
private Boolean like;
private Child child; //嵌套对象
private List<Dog> dogs; //数组(里面是对象)
private Map<String,Cat> cats; //表示Map
}

@Data
public class Dog {
private String name;
private Integer age;
}

@Data
public class Child {
private String name;
private Integer age;
private Date birthDay;
private List<String> text; //数组
}

@Data
public class Cat {
private String name;
private Integer age;
}

properties表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
person.name=张三
person.age=18
person.birthDay=2010/10/12 12:12:12
person.like=true
person.child.name=李四
person.child.age=12
person.child.birthDay=2018/10/12
person.child.text[0]=abc
person.child.text[1]=def
person.dogs[0].name=小黑
person.dogs[0].age=3
person.dogs[1].name=小白
person.dogs[1].age=2
person.cats.c1.name=小蓝
person.cats.c1.age=3
person.cats.c2.name=小灰
person.cats.c2.age=2

yaml表示法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
person:
name: 张三
age: 18
birthDay: 2010/10/10 12:12:12
like: true
child:
name: 李四
age: 20
birthDay: 2018/10/10
text: ["abc","def"]
dogs:
- name: 小黑
age: 3
- name: 小白
age: 2
cats:
c1:
name: 小蓝
age: 3
c2: {name: 小绿,age: 2} #对象也可用{}表示

3. 细节

  • birthDay 推荐写为 birth-day

  • 文本

    • 单引号不会转义【\n 则为普通字符串显示】
    • 双引号会转义【\n会显示为换行符
  • 大文本

    • |开头,大文本写在下层,保留文本格式换行符正确显示
    • >开头,大文本写在下层,折叠换行符
  • 多文档合并

    • 使用---可以把多个yaml文档合并在一个文档中,每个文档区依然认为内容独立

4. 小技巧:lombok

简化JavaBean 开发。自动生成构造器、getter/setter、自动生成Builder模式等

1
2
3
4
5
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>compile</scope>
</dependency>

使用@Data等注解

3. 日志配置

规范:项目开发不要编写System.out.println(),应该用日志记录信息

1. 简介

  1. Spring使用commons-logging作为内部日志,但底层日志实现是开放的。可对接其他日志框架。

    1. 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开始的所有配置。

2. 日志格式

1
2
2023-03-31T13:56:17.511+08:00  INFO 4944 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-03-31T13:56:17.511+08:00 INFO 4944 --- [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.7]

默认输出格式:

  • 时间和日期:毫秒级精度
  • 日志级别:ERROR, WARN, INFO, DEBUG, or TRACE
  • 进程 ID
  • —: 消息分割符
  • 线程名: 使用[]包含
  • Logger 名: 通常是产生日志的类名
  • 消息: 日志记录的内容

注意: logback 没有FATAL级别,对应的是ERROR

默认值:参照:spring-bootadditional-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'

3. 记录日志

1
2
3
Logger logger = LoggerFactory.getLogger(getClass());

或者使用Lombok的@Slf4j注解

4. 日志级别

比较有用的技巧是:

将相关的logger分组在一起,统一配置。SpringBoot 也支持。比如:Tomcat 相关的日志统一设置

1
2
logging.group.tomcat=org.apache.catalina,org.apache.coyote,org.apache.tomcat
logging.level.tomcat=trace

5. 日志分组

SpringBoot 预定义两个组

Name Loggers
web 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

6. 文件输出

SpringBoot 默认只把日志写在控制台,如果想额外记录到文件,可以在application.properties中添加logging.file.name or logging.file.path配置项。

logging.file.name logging.file.path 示例 效果
未指定 未指定 仅控制台输出
指定 未指定 my.log 写入指定文件。可以加路径
未指定 指定 /var/log 写入指定目录,文件名为spring.log
指定 指定 以logging.file.name为准

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).

8. 自定义配置

通常我们配置 application.properties 就够了。当然也可以自定义。比如:

日志系统 自定义
Logback logback-spring.xml, logback-spring.groovy, logback.xml, or logback.groovy
Log4j2 log4j2-spring.xml or log4j2.xml
JDK (Java Util Logging) logging.properties

如果可能,我们建议您在日志配置中使用-spring 变量(例如,logback-spring.xml 而不是 logback.xml)。如果您使用标准配置文件,spring 无法完全控制日志初始化。

最佳实战:自己要写配置,配置文件名加上 xx-spring.xml

9. 切换日志组合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<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格式的配置文件

格式 依赖 文件名
YAML com.fasterxml.jackson.core:jackson-databind + com.fasterxml.jackson.dataformat:jackson-dataformat-yaml log4j2.yaml + log4j2.yml
JSON com.fasterxml.jackson.core:jackson-databind log4j2.json + log4j2.jsn

10. 最佳实战

  1. 导入任何第三方框架,先排除它的日志包,因为Boot底层控制好了日志
  2. 修改 application.properties 配置文件,就可以调整日志的所有行为。如果不够,可以编写日志框架自己的配置文件放在类路径下就行,比如logback-spring.xmllog4j2-spring.xml
  3. 如需对接专业日志系统,也只需要把 logback 记录的日志灌倒 kafka之类的中间件,这和SpringBoot没关系,都是日志框架自己的配置,修改配置文件即可
  4. 业务中使用slf4j-api记录日志。不要再 sout 了

2、SpringBoot3-Web开发

SpringBoot的Web开发能力,由SpringMVC提供。

0. WebMvcAutoConfiguration原理

1. 生效条件

1
2
3
4
5
6
7
8
9
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class }) //在这些自动配置之后
@ConditionalOnWebApplication(type = Type.SERVLET) //如果是web应用就生效,类型SERVLET、REACTIVE 响应式web
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) //容器中没有这个Bean,才生效。默认就是没有
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)//优先级
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
}

2. 效果

  1. 放了两个Filter:

    1. HiddenHttpMethodFilter;页面表单提交Rest请求(GET、POST、PUT、DELETE)
    2. FormContentFilter: 表单内容Filter,GET(数据放URL后面)、POST(数据放请求体)请求可以携带数据,PUT、DELETE 的请求体数据会被忽略
  2. 给容器中放了WebMvcConfigurer组件;给SpringMVC添加各种定制功能

    1. 所有的功能最终会和配置文件进行绑定
    2. WebMvcProperties: spring.mvc配置文件
    3. WebProperties: spring.web配置文件
1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class) //额外导入了其他配置
@EnableConfigurationProperties({ WebMvcProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware{

}

3. WebMvcConfigurer接口

提供了配置SpringMVC底层的所有组件入口

img

4. 静态资源规则源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
//1、
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 = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
  1. 规则一:访问: /webjars/**路径就去 classpath:/META-INF/resources/webjars/下找资源.

    1. maven 导入依赖
  2. 规则二:访问: /**路径就去 静态资源默认的四个位置找资源

    1. classpath:/META-INF/resources/
    2. classpath:/resources/
    3. classpath:/static/
    4. classpath:/public/
  3. 规则三:静态资源默认都有缓存规则的设置

    1. 所有缓存的设置,直接通过配置文件spring.web
    2. cachePeriod: 缓存周期; 多久不用找服务器要新的。 默认没有,以s为单位
    3. cacheControl: HTTP缓存控制;https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Caching
    4. useLastModified:是否使用最后一次修改。配合HTTP Cache规则

如果浏览器访问了一个静态资源 index.js,如果服务这个资源没有发生变化,下次访问的时候就可以直接让浏览器用自己缓存中的东西,而不用给服务器发请求。

1
2
3
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
registration.setUseLastModified(this.resourceProperties.getCache().isUseLastModified());

5. EnableWebMvcConfiguration 源码

1
2
3
4
5
6
7
8
9
//SpringBoot 给容器中放 WebMvcConfigurationSupport 组件。
//我们如果自己放了 WebMvcConfigurationSupport 组件,Boot的WebMvcAutoConfiguration都会失效。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware
{


}
  1. HandlerMapping: 根据请求路径 /a 找那个handler能处理请求

    1. WelcomePageHandlerMapping
      1. 访问 /**路径下的所有请求,都在以前四个静态资源路径下找,欢迎页也一样
      2. index.html:只要静态资源的位置有一个 index.html页面,项目启动默认访问

6. 为什么容器中放一个WebMvcConfigurer就能配置底层行为

  1. WebMvcAutoConfiguration 是一个自动配置类,它里面有一个 EnableWebMvcConfiguration
  2. EnableWebMvcConfiguration继承与 DelegatingWebMvcConfiguration,这两个都生效
  3. DelegatingWebMvcConfiguration利用 DI 把容器中 所有 WebMvcConfigurer 注入进来
  4. 别人调用 DelegatingWebMvcConfiguration 的方法配置底层规则,而它调用所有 WebMvcConfigurer的配置底层方法。

7. WebMvcConfigurationSupport

提供了很多的默认设置。

判断系统中是否有相应的类:如果有,就加入相应的HttpMessageConverter

1
2
3
4
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);

1. Web场景

1. 自动配置

1、整合web场景

1
2
3
4
<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、所有自动配置类如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration
====以下是响应式web场景和现在的没关系======
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveMultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.WebSessionIdResolverAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration
================以上没关系=================
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration

6、绑定了配置文件的一堆配置项

  • 1、SpringMVC的所有配置 spring.mvc
  • 2、Web场景通用配置 spring.web
  • 3、文件上传配置 spring.servlet.multipart
  • 4、服务器的配置 server: 比如:编码方式

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. 静态资源

1. 默认规则

1. 静态资源映射

静态资源映射规则在 WebMvcAutoConfiguration 中进行了定义:

  1. /webjars/** 的所有路径 资源都在 classpath:/META-INF/resources/webjars/

  2. /** 的所有路径 资源都在 classpath:/META-INF/resources/、classpath:/resources/、classpath:/static/、classpath:/public/

  3. 所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值

    1. period: 缓存间隔。 默认 0S;
    2. cacheControl:缓存控制。 默认无;
    3. useLastModified:是否使用lastModified头。 默认 false;

2. 静态资源缓存

如前面所述

  1. 所有静态资源都定义了缓存规则。【浏览器访问过一次,就会缓存一段时间】,但此功能参数无默认值

    1. period: 缓存间隔。 默认 0S;
    2. cacheControl:缓存控制。 默认无;
    3. useLastModified:是否使用lastModified头。 默认 false;

3. 欢迎页

欢迎页规则在 WebMvcAutoConfiguration 中进行了定义:

  1. 静态资源目录下找 index.html
  2. 没有就在 templates下找index模板页

4. Favicon

  1. 在静态资源目录下找 favicon.ico

5. 缓存实验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
server.port=9000

#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则
spring.web.resources.add-mappings=true

#设置缓存
#spring.web.resources.cache.period=3600
##缓存详细合并项控制,覆盖period配置:
## 浏览器第一次请求服务器,服务器告诉浏览器此资源缓存7200秒,7200秒以内的所有此资源访问不用发给服务器请求,7200秒以后发请求给服务器
spring.web.resources.cache.cachecontrol.max-age=7200
#使用资源 last-modified 时间,来对比服务器和浏览器的资源是否相同没有变化。相同返回 304
spring.web.resources.cache.use-last-modified=true

2. 自定义静态资源规则

自定义静态资源路径、自定义缓存规则

1. 配置方式

spring.mvc: 静态资源访问前缀路径

spring.web

  • 静态资源目录
  • 静态资源缓存策略
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#1、spring.web:
# 1.配置国际化的区域信息
# 2.静态资源策略(开启、处理链、缓存)

#开启静态资源映射规则
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

#自定义静态资源文件夹位置
spring.web.resources.static-locations=classpath:/a/,classpath:/b/,classpath:/static/

#2、 spring.mvc
## 2.1. 自定义webjars路径前缀
spring.mvc.webjars-path-pattern=/wj/**
## 2.2. 静态资源访问路径前缀
spring.mvc.static-path-pattern=/static/**

2. 代码方式

  • 容器中只要有一个 WebMvcConfigurer 组件。配置的底层行为都会生效
  • @EnableWebMvc //禁用boot的默认配置
1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration //这是一个配置类
public class MyConfig implements WebMvcConfigurer {


@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
//保留以前规则
//自己写新的规则。
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/a/","classpath:/b/")
.setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Configuration //这是一个配置类,给容器中放一个 WebMvcConfigurer 组件,就能自定义底层
public class MyConfig /*implements WebMvcConfigurer*/ {


@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/a/", "classpath:/b/")
.setCacheControl(CacheControl.maxAge(1180, TimeUnit.SECONDS));
}
};
}

}

3. 路径匹配

Spring5.3 之后加入了更多的请求路径匹配的实现策略;

以前只支持 AntPathMatcher 策略, 现在提供了 PathPatternParser 策略。并且可以让我们指定到底使用那种策略。

1. Ant风格路径用法

Ant 风格的路径模式语法具有以下规则:

  • *:表示任意数量的字符。
  • ?:表示任意一个字符
  • :表示任意数量的目录**。
  • {}:表示一个命名的模式占位符
  • []:表示字符集合,例如[a-z]表示小写字母。

例如:

  • *.html配任意名称,扩展名为.html的文件。
  • /folder1/*/*.java配在folder1目录下的任意两级目录下的.java文件。
  • /folder2/**/*.jsp 匹配在folder2目录下任意目录深度的.jsp文件。
  • /{type}/{id}.html 匹配任意文件名为{id}.html,在任意命名的{type}目录下的文件。

注意:Ant 风格的路径模式语法中的特殊字符需要转义,如:

  • 要匹配文件路径中的星号,则需要转义为\\*
  • 要匹配文件路径中的问号,则需要转义为\\?

2. 模式切换

AntPathMatcher 与 PathPatternParser

  • PathPatternParser 在 jmh 基准测试下,有 68 倍吞吐量提升,降低 30%40%空间分配率
  • PathPatternParser 兼容 AntPathMatcher语法,并支持更多类型的路径模式
  • PathPatternParser ** 多段匹配的支持仅允许在模式末尾使用
1
2
3
4
5
6
7
8
9
@GetMapping("/a*/b?/{p1:[a-f]+}")
public String hello(HttpServletRequest request,
@PathVariable("p1") String path) {

log.info("路径变量p1: {}", path);
//获取请求路径
String uri = request.getRequestURI();
return uri;
}

总结:

  • 使用默认的路径匹配规则,是由 PathPatternParser 提供的
  • 如果路径中间需要有 **,替换成ant风格路径
1
2
3
4
# 改变路径匹配策略:
# ant_path_matcher 老版策略;
# path_pattern_parser 新版策略;
spring.mvc.pathmatch.matching-strategy=ant_path_matcher

4. 内容协商

一套系统适配多端数据返回

1. 多端内容适配

1. 默认规则

  1. SpringBoot 多端内容适配

    1. 基于请求头内容协商:(默认开启)
      1. 客户端向服务端发送请求,携带HTTP标准的Accept请求头
        1. Accept: application/jsontext/xmltext/yaml
        2. 服务端根据客户端请求头期望的数据类型进行动态返回
    1. 基于 请求参数 内容协商:(需要开启)
      1. 发送请求 GET /projects/spring-boot?format=json
      2. 匹配到 @GetMapping("/projects/spring-boot")
      3. 根据参数协商,优先返回 json 类型数据【需要开启参数匹配设置
      4. 发送请求 GET /projects/spring-boot?format=xml,优先返回 xml 类型数据

2. 效果演示

请求同一个接口,可以返回json和xml不同格式数据

  1. 引入支持写出xml内容依赖

    1
    2
    3
    4
    <dependency>
    <groupId>com.fasterxml.jackson.dataformat</groupId>
    <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
  2. 标注注解

    1
    2
    3
    4
    5
    6
    7
    8
    @JacksonXmlRootElement  // 可以写出为xml文档
    @Data
    public class Person {
    private Long id;
    private String userName;
    private String email;
    private Integer age;
    }
  3. 开启基于请求参数的内容协商

    1
    2
    3
    4
    # 开启基于请求参数的内容协商功能。 默认参数名:format。 默认此功能不开启
    spring.mvc.contentnegotiation.favor-parameter=true
    # 指定内容协商时使用的参数名。默认是 format
    spring.mvc.contentnegotiation.parameter-name=type
  4. 效果

3. 配置协商规则与支持类型

  1. 修改内容协商方式

    1
    2
    3
    4
    #使用参数进行内容协商
    spring.mvc.contentnegotiation.favor-parameter=true
    #自定义参数名,默认为format
    spring.mvc.contentnegotiation.parameter-name=myparam
  2. 大多数 MediaType 都是开箱即用的。也可以自定义内容类型,如:

    1
    spring.mvc.contentnegotiation.media-types.yaml=text/yaml

2. 自定义内容返回

1. 增加yaml返回支持

导入依赖

1
2
3
4
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-yaml</artifactId>
</dependency>

把对象写出成YAML

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person();
person.setId(1L);
person.setUserName("张三");
person.setEmail("aaa@qq.com");
person.setAge(18);

YAMLFactory factory = new YAMLFactory().disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
ObjectMapper mapper = new ObjectMapper(factory);

String s = mapper.writeValueAsString(person);
System.out.println(s);
}

编写配置

1
2
#新增一种媒体类型
spring.mvc.contentnegotiation.media-types.yaml=text/yaml

增加HttpMessageConverter组件,专门负责把对象写出为yaml格式

1
2
3
4
5
6
7
8
9
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override //配置一个能把对象转为yaml的messageConverter
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MyYamlHttpMessageConverter());
}
};
}

2. 思考:如何增加其他

  • 配置媒体类型支持:

    • spring.mvc.contentnegotiation.media-types.yaml=text/yaml
  • 编写对应的HttpMessageConverter,要告诉Boot这个支持的媒体类型

    • 按照3的示例
  • 把MessageConverter组件加入到底层

    • 容器中放一个WebMvcConfigurer 组件,并配置底层的MessageConverter

3. HttpMessageConverter的示例写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class MyYamlHttpMessageConverter extends AbstractHttpMessageConverter<Object> {

private ObjectMapper objectMapper = null; //把对象转成yaml

public MyYamlHttpMessageConverter(){
//告诉SpringBoot这个MessageConverter支持哪种媒体类型 //媒体类型
super(new MediaType("text", "yaml", Charset.forName("UTF-8")));
YAMLFactory factory = new YAMLFactory()
.disable(YAMLGenerator.Feature.WRITE_DOC_START_MARKER);
this.objectMapper = new ObjectMapper(factory);
}

@Override
protected boolean supports(Class<?> clazz) {
//只要是对象类型,不是基本类型
return true;
}

@Override //@RequestBody
protected Object readInternal(Class<?> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override //@ResponseBody 把对象怎么写出去
protected void writeInternal(Object methodReturnValue, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

//try-with写法,自动关流
try(OutputStream os = outputMessage.getBody()){
this.objectMapper.writeValue(os,methodReturnValue);
}

}
}

3. 内容协商原理-HttpMessageConverter

  • HttpMessageConverter 怎么工作?合适工作?
  • 定制 HttpMessageConverter 来实现多端内容协商
  • 编写WebMvcConfigurer提供的configureMessageConverters底层,修改底层的MessageConverter

1. @ResponseBodyHttpMessageConverter处理

标注了@ResponseBody的返回值 将会由支持它的 HttpMessageConverter写给浏览器

  1. 如果controller方法的返回值标注了 @ResponseBody 注解

    1. 请求进来先来到DispatcherServletdoDispatch()进行处理
    2. 找到一个 HandlerAdapter 适配器。利用适配器执行目标方法
    3. RequestMappingHandlerAdapter来执行,调用invokeHandlerMethod()来执行目标方法
    4. 目标方法执行之前,准备好两个东西
      1. HandlerMethodArgumentResolver:参数解析器,确定目标方法每个参数值
      2. HandlerMethodReturnValueHandler:返回值处理器,确定目标方法的返回值改怎么处理
    1. RequestMappingHandlerAdapter 里面的invokeAndHandle()真正执行目标方法
    2. 目标方法执行完成,会返回返回值对象
    3. 找到一个合适的返回值处理器 HandlerMethodReturnValueHandler
    4. 最终找到 RequestResponseBodyMethodProcessor能处理 标注了 @ResponseBody注解的方法
    5. RequestResponseBodyMethodProcessor 调用writeWithMessageConverters ,利用MessageConverter把返回值写出去

上面解释:@ResponseBodyHttpMessageConverter处理

  1. HttpMessageConverter先进行内容协商

    1. 遍历所有的MessageConverter看谁支持这种内容类型的数据
    2. 默认MessageConverter有以下
    3. 最终因为要json所以MappingJackson2HttpMessageConverter支持写出json
    4. jackson用ObjectMapper把对象写出去

2. WebMvcAutoConfiguration提供几种默认HttpMessageConverters

  • EnableWebMvcConfiguration通过 addDefaultHttpMessageConverters添加了默认的MessageConverter;如下:

    • ByteArrayHttpMessageConverter: 支持字节数据读写
    • StringHttpMessageConverter: 支持字符串读写
    • ResourceHttpMessageConverter:支持资源读写
    • ResourceRegionHttpMessageConverter: 支持分区资源写出
    • AllEncompassingFormHttpMessageConverter:支持表单xml/json读写
    • MappingJackson2HttpMessageConverter: 支持请求响应体Json读写

默认8个:

系统提供默认的MessageConverter 功能有限,仅用于json或者普通返回数据。额外增加新的内容协商功能,必须增加新的HttpMessageConverter

5. 模板引擎

  • 由于 SpringBoot 使用了嵌入式 Servlet 容器。所以 JSP 默认是不能使用的。
  • 如果需要服务端页面渲染,优先考虑使用 模板引擎

模板引擎页面默认放在 src/main/resources/templates

SpringBoot 包含以下模板引擎的自动配置

  • FreeMarker
  • Groovy
  • Thymeleaf
  • Mustache

Thymeleaf官网https://www.thymeleaf.org/

1
2
3
4
5
6
7
8
9
10
11
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Good Thymes Virtual Grocery</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/gtvg.css}" />
</head>
<body>
<p th:text="#{home.welcome}">Welcome to our grocery store!</p>
</body
</html>

1. Thymeleaf整合

1
2
3
4
<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. 默认效果

    1. 所有的模板页面在 classpath:/templates/下面找
    2. 找后缀名为.html的页面

2. 基础语法

1. 核心用法

th:xxx动态渲染指定的 html 标签属性值、或者th指令(遍历、判断等)

  • th:text:标签体内文本值渲染

    • th:utext:不会转义,显示为html原本的样子。
  • th:属性:标签指定属性渲染

  • th:attr:标签任意属性渲染

  • th:if``th:each``...:其他th指令

  • 例如:

    1
    2
    3
    4
    <p th:text="${content}">原内容</p>
    <a th:href="${url}">登录</a>
    <img src="../../images/gtvglogo.png"
    th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

表达式 :用来动态取值

  • **${}**:变量取值;使用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. 语法示例

表达式:

  • 变量取值:${...}
  • url 取值:@{...}
  • 国际化消息:#{...}
  • 变量选择:*{...}
  • 片段引用: ~{...}

常见:

  • 文本: 'one text','another one!',...
  • 数字: 0,34,3.0,12.3,...
  • 布尔:true、false
  • null: null
  • 变量名: one,sometext,main...

文本操作:

  • 拼串: +
  • 文本替换:| The name is ${name} |

布尔操作:

  • 二进制运算: and,or
  • 取反:!,not

比较运算:

  • 比较:>,<,<=,>=(gt,lt,ge,le)
  • 等值运算:==,!=(eq,ne)

条件运算:

  • if-then: (if)?(then)
  • if-then-else: (if)?(then):(else)
  • default: (value)?:(defaultValue)

特殊语法:

  • 无操作:_

所有以上都可以嵌套组合

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

3. 属性设置

  1. th:href=”@{/product/list}”
  2. th:attr=”class=${active}”
  3. th:attr=”src=@{/images/gtvglogo.png},title=${logo},alt=#{logo}”
  4. th:checked=”${user.active}”
1
2
3
4
<p th:text="${content}">原内容</p>
<a th:href="${url}">登录</a>
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

4. 遍历

语法: th:each="元素名,迭代状态 : ${集合}"

1
2
3
4
5
6
7
8
9
10
11
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>

iterStat 有以下属性:

  • index:当前遍历元素的索引,从0开始
  • count:当前遍历元素的索引,从1开始
  • size:需要遍历元素的总数量
  • current:当前正在遍历的元素对象
  • even/odd:是否偶数/奇数行
  • first:是否第一个元素
  • last:是否最后一个元素

5. 判断

th:if

1
2
3
4
5
<a
href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}"
>view</a

th:switch

1
2
3
4
5
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>

6. 属性优先级

  • 片段
  • 遍历
  • 判断
1
2
3
<ul>
<li th:each="item : ${items}" th:text="${item.description}">Item description here...</li>
</ul>
Order Feature Attributes
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

7. 行内写法

[[...]] or [(...)]

1
<p>Hello, [[${session.user.name}]]!</p>

8. 变量选择

1
2
3
4
5
<div th:object="${session.user}">
<p>Name: <span th:text="*{firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="*{lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="*{nationality}">Saturn</span>.</p>
</div>

等同于

1
2
3
4
5
<div>
<p>Name: <span th:text="${session.user.firstName}">Sebastian</span>.</p>
<p>Surname: <span th:text="${session.user.lastName}">Pepper</span>.</p>
<p>Nationality: <span th:text="${session.user.nationality}">Saturn</span>.</p>
</div

9. 模板布局

  • 定义模板: th:fragment
  • 引用模板:~{templatename::selector}
  • 插入模板:th:insertth:replace
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<footer th:fragment="copy">&copy; 2011 The Good Thymes Virtual Grocery</footer>

<body>
<div th:insert="~{footer :: copy}"></div>
<div th:replace="~{footer :: copy}"></div>
</body>
<body>
结果:
<body>
<div>
<footer>&copy; 2011 The Good Thymes Virtual Grocery</footer>
</div>

<footer>&copy; 2011 The Good Thymes Virtual Grocery</footer>
</body>
</body>

10. devtools

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>

修改页面后;ctrl+F9刷新效果;

java代码的修改,如果devtools热启动了,可能会引起一些bug,难以排查

6. 国际化

国际化的自动配置参照MessageSourceAutoConfiguration

实现步骤

  1. Spring Boot 在类路径根下查找messages资源绑定文件。文件名为:messages.properties

  2. 多语言可以定义多个消息文件,命名为messages_区域代码.properties。如:

    1. messages.properties:默认
    2. messages_zh_CN.properties:中文环境
    3. messages_en_US.properties:英语环境
  3. 程序中可以自动注入 MessageSource组件,获取国际化的配置项值

  4. 页面中可以使用表达式 #{}获取国际化的配置项值

1
2
3
4
5
6
7
8
9
10
11
@Autowired  //国际化取消息用的组件
MessageSource messageSource;
@GetMapping("/haha")
public String haha(HttpServletRequest request){

Locale locale = request.getLocale();
//利用代码的方式获取国际化配置文件中指定的配置项的值
String login = messageSource.getMessage("login", null, locale);
return login;
}

7. 错误处理

1. 默认机制

错误处理的自动配置都在ErrorMvcAutoConfiguration中,两大核心机制:

    1. SpringBoot 会自适应 处理错误响应页面JSON数据
    1. SpringMVC的错误处理机制依然保留,MVC处理不了,才会交给boot进行处理

2. 自定义错误响应

1. 自定义json响应

使用@ControllerAdvice + @ExceptionHandler 进行统一异常处理

2. 自定义页面响应

根据boot的错误页面规则,自定义页面模板

  1. 最佳实战
  • 前后分离

    • 后台发生的所有错误,@ControllerAdvice + @ExceptionHandler进行统一异常处理。
  • 服务端页面渲染

    • 不可预知的一些,HTTP码表示的服务器或客户端错误
      • classpath:/templates/error/下面,放常用精确的错误码页面。500.html404.html
      • classpath:/templates/error/下面,放通用模糊匹配的错误码页面。 5xx.html4xx.html
    • 发生业务错误
      • 核心业务,每一种错误,都应该代码控制,跳转到自己定制的错误页
      • 通用业务classpath:/templates/error.html页面,显示错误信息

页面,JSON,可用的Model数据如下

8. 嵌入式容器

Servlet容器:管理、运行Servlet组件(Servlet、Filter、Listener)的环境,一般指服务器

1. 自动配置原理

  • SpringBoot 默认嵌入Tomcat作为Servlet容器。
  • 自动配置类是ServletWebServerFactoryAutoConfigurationEmbeddedWebServerFactory,CustomizerAutoConfiguration`
  • 自动配置类开始分析功能。xxxxAutoConfiguration
1
2
3
4
5
6
7
8
9
10
11
12
@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 })
public class ServletWebServerFactoryAutoConfiguration {

}
  1. ServletWebServerFactoryAutoConfiguration 自动配置了嵌入式容器场景

  2. 绑定了ServerProperties配置类,所有和服务器有关的配置 server

  3. ServletWebServerFactoryAutoConfiguration 导入了 嵌入式的三大服务器 TomcatJettyUndertow

    1. 导入 TomcatJettyUndertow 都有条件注解。系统中有这个类才行(也就是导了包)
    2. 默认 Tomcat配置生效。给容器中放 TomcatServletWebServerFactory
    3. 都给容器中 ServletWebServerFactory放了一个 web服务器工厂(造web服务器的)
    4. web服务器工厂 都有一个功能,getWebServer获取web服务器
    5. TomcatServletWebServerFactory 创建了 tomcat。
  4. ServletWebServerFactory 什么时候会创建 webServer出来。

  5. ServletWebServerApplicationContextioc容器,启动的时候会调用创建web服务器

  6. Spring容器刷新(启动)的时候,会预留一个时机,刷新子容器。onRefresh()

  7. refresh() 容器刷新 十二大步的刷新子容器会调用 onRefresh()

1
2
3
4
5
6
7
8
9
10
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}

Web场景的Spring容器启动,在onRefresh的时候,会调用创建web服务器的方法。

Web服务器的创建是通过WebServerFactory搞定的。容器中又会根据导了什么包条件注解,启动相关的 服务器配置,默认EmbeddedTomcat会给容器中放一个 TomcatServletWebServerFactory,导致项目启动,自动创建出Tomcat。

2. 自定义

切换服务器;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<properties>
<servlet-api.version>3.1.0</servlet-api.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<!-- Exclude the Tomcat dependency -->
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- Use Jetty instead -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

3. 最佳实践

用法:

  • 修改server下的相关配置就可以修改服务器参数
  • 通过给容器中放一个**ServletWebServerFactory**,来禁用掉SpringBoot默认放的服务器工厂,实现自定义嵌入任意服务器

9. 全面接管SpringMVC

  • SpringBoot 默认配置好了 SpringMVC 的所有常用特性。

  • 如果我们需要全面接管SpringMVC的所有配置并禁用默认配置,仅需要编写一个WebMvcConfigurer配置类,并标注 @EnableWebMvc 即可

  • 全手动模式

    • @EnableWebMvc : 禁用默认配置
    • **WebMvcConfigurer**件:定义MVC的底层行为

1. WebMvcAutoConfiguration 到底自动配置了哪些规则

SpringMVC自动配置场景给我们配置了如下所有默认行为

  1. WebMvcAutoConfigurationweb场景的自动配置类

    1. 支持RESTful的filter:HiddenHttpMethodFilter
    2. 支持非POST请求,请求体携带数据:FormContentFilter
    3. 导入**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:内容协商管理器
    1. **WebMvcAutoConfigurationAdapter**配置生效,它是一个WebMvcConfigurer,定义mvc底层组件
      1. 定义好 WebMvcConfigurer 底层组件默认功能;所有功能详见列表
      2. 视图解析器:InternalResourceViewResolver
      3. 视图解析器:BeanNameViewResolver,视图名(controller方法的返回值字符串)就是组件名
      4. 内容协商解析器:ContentNegotiatingViewResolver
      5. 请求上下文过滤器:RequestContextFilter: 任意位置直接获取当前请求
      6. 静态资源链规则
      7. ProblemDetailsExceptionHandler:错误详情
        1. SpringMVC内部场景异常被它捕获:
    1. 定义了MVC默认的底层行为: WebMvcConfigurer

2. @EnableWebMvc 禁用默认行为

  1. @EnableWebMvc给容器中导入 DelegatingWebMvcConfiguration组件,

​ 他是 WebMvcConfigurationSupport

  1. WebMvcAutoConfiguration有一个核心的条件注解, @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),容器中没有WebMvcConfigurationSupportWebMvcAutoConfiguration才生效.
  2. @EnableWebMvc 导入 WebMvcConfigurationSupport 导致 WebMvcAutoConfiguration 失效。导致禁用了默认行为
  • @EnableWebMVC 禁用了 Mvc的自动配置
  • WebMvcConfigurer 定义SpringMVC底层组件的功能类

3. WebMvcConfigurer 功能

定义扩展SpringMVC底层功能

提供方法 核心参数 功能 默认
addFormatters FormatterRegistry 格式化器:支持属性上@NumberFormat和@DatetimeFormat的数据类型转换 GenericConversionService
getValidator 数据校验:校验 Controller 上使用@Valid标注的参数合法性。需要导入starter-validator
addInterceptors InterceptorRegistry 拦截器:拦截收到的所有请求
configureContentNegotiation ContentNegotiationConfigurer 内容协商:支持多种数据格式返回。需要配合支持这种类型的HttpMessageConverter 支持 json
configureMessageConverters List<HttpMessageConverter<?>> 消息转换器:标注@ResponseBody的返回值会利用MessageConverter直接写出去 8 个,支持byte,string,multipart,resource,json
addViewControllers ViewControllerRegistry 视图映射:直接将请求路径与物理视图映射。用于无 java 业务逻辑的直接视图页渲染 mvc:view-controller
configureViewResolvers ViewResolverRegistry 视图解析器:逻辑视图转为物理视图 ViewResolverComposite
addResourceHandlers ResourceHandlerRegistry 静态资源处理:静态资源路径映射、缓存控制 ResourceHandlerRegistry
configureDefaultServletHandling DefaultServletHandlerConfigurer 默认 Servlet:可以覆盖 Tomcat 的DefaultServlet。让DispatcherServlet拦截/
configurePathMatch PathMatchConfigurer 路径匹配:自定义 URL 路径匹配。可以自动为所有路径加上指定前缀,比如 /api
configureAsyncSupport AsyncSupportConfigurer 异步支持 TaskExecutionAutoConfiguration
addCorsMappings CorsRegistry 跨域
addArgumentResolvers List 参数解析器 mvc 默认提供
addReturnValueHandlers List 返回值解析器 mvc 默认提供
configureHandlerExceptionResolvers List 异常处理器 默认 3 个 ExceptionHandlerExceptionResolver ResponseStatusExceptionResolver DefaultHandlerExceptionResolver
getMessageCodesResolver 消息码解析器:国际化使用

10. 最佳实践

SpringBoot 已经默认配置好了Web开发场景常用功能。我们直接使用即可。

三种方式

方式 用法 效果
全自动 直接编写控制器逻辑 全部使用自动配置默认效果
手自一体 @Configuration + 配置 WebMvcConfigurer+ 配置 WebMvcRegistrations 不要标注 @**EnableWebMvc** 保留自动配置效果 手动设置部分功能 定义MVC底层组件
全手动 @Configuration + 配置**WebMvcConfigurer** 标注 @**EnableWebMvc** 禁用自动配置效果 全手动设置

总结:

给容器中写一个配置类**@Configuration**实现 **WebMvcConfigurer**但是不要标注 **@EnableWebMvc**注解,实现手自一体的效果。

两种模式

1、前后分离模式@RestController 响应JSON数据

2、前后不分离模式:@Controller + Thymeleaf模板引擎

11. Web新特性

1. Problemdetails

RFC 7807: https://www.rfc-editor.org/rfc/rfc7807

错误信息返回新格式

原理

1
2
3
4
5
6
7
8
9
10
11
12
@Configuration(proxyBeanMethods = false)
//配置过一个属性 spring.mvc.problemdetails.enabled=true
@ConditionalOnProperty(prefix = "spring.mvc.problemdetails", name = "enabled", havingValue = "true")
static class ProblemDetailsErrorHandlingConfiguration {

@Bean
@ConditionalOnMissingBean(ResponseEntityExceptionHandler.class)
ProblemDetailsExceptionHandler problemDetailsExceptionHandler() {
return new ProblemDetailsExceptionHandler();
}

}
  1. ProblemDetailsExceptionHandler 是一个 @ControllerAdvice集中处理系统异常
  2. 处理以下异常。如果系统出现以下异常,会被SpringBoot支持以 RFC 7807规范方式返回错误数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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

1
2
3
4
5
6
7
8
{
"timestamp": "2023-04-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+ 额外扩展返回

1
2
3
4
5
6
7
{
"type": "about:blank",
"title": "Method Not Allowed",
"status": 405,
"detail": "Method 'POST' is not supported.",
"instance": "/list"
}

2. 函数式Web

SpringMVC 5.2 以后 允许我们使用函数式的方式,定义Web的请求处理流程

函数式接口

Web请求处理的方式:

  1. @Controller + @RequestMapping耦合式路由业务耦合)
  2. 函数式Web:分离式(路由、业务分离)

1. 场景

场景:User RESTful - CRUD

  • GET /user/1 获取1号用户
  • GET /users 获取所有用户
  • POST /user 请求体携带JSON,新增一个用户
  • PUT /user/1 请求体携带JSON,修改1号用户
  • DELETE /user/1 删除1号用户

2. 核心类

  • RouterFunction
  • RequestPredicate
  • ServerRequest
  • ServerResponse

3. 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.function.RequestPredicate;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import static org.springframework.web.servlet.function.RequestPredicates.accept;
import static org.springframework.web.servlet.function.RouterFunctions.route;

@Configuration(proxyBeanMethods = false)
public class MyRoutingConfiguration {

private static final RequestPredicate ACCEPT_JSON = accept(MediaType.APPLICATION_JSON);

@Bean
public RouterFunction<ServerResponse> routerFunction(MyUserHandler userHandler) {
return route()
.GET("/{user}", ACCEPT_JSON, userHandler::getUser)
.GET("/{user}/customers", ACCEPT_JSON, userHandler::getUserCustomers)
.DELETE("/{user}", ACCEPT_JSON, userHandler::deleteUser)
.build();
}

}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.function.ServerRequest;
import org.springframework.web.servlet.function.ServerResponse;

@Component
public class MyUserHandler {

public ServerResponse getUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}

public ServerResponse getUserCustomers(ServerRequest request) {
...
return ServerResponse.ok().build();
}

public ServerResponse deleteUser(ServerRequest request) {
...
return ServerResponse.ok().build();
}

}

3、SpringBoot3-数据访问

整合SSM场景

SpringBoot 整合 SpringSpringMVCMyBatis 进行数据访问场景开发

1. 创建SSM整合项目

1
2
3
4
5
6
7
8
9
10
11
<!-- https://mvnrepository.com/artifact/org.mybatis.spring.boot/mybatis-spring-boot-starter -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

2. 配置数据源

1
2
3
4
5
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.zaxxer.hikari.HikariDataSource

安装MyBatisX 插件,帮我们生成Mapper接口的xml文件即可

3. 配置MyBatis

1
2
3
4
#指定mapper映射文件位置
mybatis.mapper-locations=classpath:/mapper/*.xml
#参数项调整
mybatis.configuration.map-underscore-to-camel-case=true

4. CRUD编写

  • 编写Bean
  • 编写Mapper
  • 使用mybatisx插件,快速生成MapperXML
  • 测试CRUD

5. 自动配置原理

SSM整合总结:

  1. 导入 mybatis-spring-boot-starter

  2. 配置数据源信息

  3. 配置mybatis的**mapper接口扫描****xml映射文件扫描**

  4. 编写bean,mapper,生成xml,编写sql 进行crud。事务等操作依然和Spring中用法一样

  5. 效果:

    1. 所有sql写在xml中
    2. 所有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文件中配置的所有值,就是要开启的自动配置类,但是每个类可能有条件注解,基于条件注解判断哪个自动配置类生效了。

6. 快速定位生效的配置

1
2
3
#开启调试模式,详细打印开启了哪些自动配置
debug=true
# Positive(生效的自动配置) Negative(不生效的自动配置)

7. 扩展:整合其他数据源

1. Druid 数据源

暂不支持 SpringBoot3

  • 导入druid-starter
  • 写配置
  • 分析自动配置了哪些东西,怎么用

Druid官网:https://github.com/alibaba/druid

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#数据源基本配置
spring.datasource.url=jdbc:mysql://192.168.200.100:3306/demo
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource

# 配置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

附录:示例数据库

1
2
3
4
5
6
7
8
9
CREATE TABLE `t_user`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '编号',
`login_name` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户名称' COLLATE 'utf8_general_ci',
`nick_name` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户昵称' COLLATE 'utf8_general_ci',
`passwd` VARCHAR(200) NULL DEFAULT NULL COMMENT '用户密码' COLLATE 'utf8_general_ci',
PRIMARY KEY (`id`)
);
insert into t_user(login_name, nick_name, passwd) VALUES ('zhangsan','张三','123456');

4、SpringBoot3-基础特性

1. SpringApplication

1.1. 自定义 banner

  1. 类路径添加banner.txt或设置spring.banner.location就可以定制 banner
  2. 推荐网站:Spring Boot banner 在线生成工具,制作下载英文 banner.txt,修改替换 banner.txt 文字实现自定义,个性化启动 banner-bootschool.net

1.2. 自定义 SpringApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.Banner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication application = new SpringApplication(MyApplication.class);
application.setBannerMode(Banner.Mode.OFF);
application.run(args);
}

}

1.3. FluentBuilder API

1
2
3
4
5
new SpringApplicationBuilder()
.sources(Parent.class)
.child(Application.class)
.bannerMode(Banner.Mode.OFF)
.run(args);

2. Profiles

环境隔离能力;快速切换开发、测试、生产环境

步骤:

  1. 标识环境:指定哪些组件、配置在哪个环境生效
  2. 切换环境:这个环境对应的所有组件和配置就应该生效

2.1. 使用

2.1.1 指定环境

  • Spring Profiles 提供一种隔离配置的方式,使其仅在特定环境生效;
  • 任何@Component, @Configuration 或 @ConfigurationProperties 可以使用 @Profile 标记,来指定何时被加载。【容器中的组件都可以被 @Profile标记】

2.1.2 环境激活

  1. 配置激活指定环境; 配置文件

    1
    spring.profiles.active=production,hsqldb
  2. 也可以使用命令行激活。–spring.profiles.active=dev,hsqldb

  3. 还可以配置默认环境; 不标注@Profile 的组件永远都存在。

    1. 以前默认环境叫default
    2. spring.profiles.default=test
  4. 推荐使用激活方式激活指定环境

2.1.3 环境包含

注意:

  1. spring.profiles.active 和spring.profiles.default 只能用到 无 profile 的文件中,如果在application-dev.yaml中编写就是无效的

  2. 也可以额外添加生效文件,而不是激活替换。比如:

    1
    2
    spring.profiles.include[0]=common
    spring.profiles.include[1]=local

最佳实战:

  • 生效的环境 = 激活的环境/默认环境 + 包含的环境

  • 项目里面这么用

    • 基础的配置mybatislogxxx:写到包含环境中
    • 需要动态切换变化的 dbredis:写到激活的环境中

2.2. Profile 分组

创建prod组,指定包含db和mq配置

1
2
spring.profiles.group.prod[0]=db
spring.profiles.group.prod[1]=mq

使用--spring.profiles.active=prod ,就会激活proddbmq配置文件

2.3. Profile 配置文件

  • application-{profile}.properties可以作为指定环境的配置文件

  • 激活这个环境,配置就会生效。最终生效的所有配置

    • application.properties:主配置文件,任意时候都生效
    • application-{profile}.properties:指定环境配置文件,激活指定环境生效

profile优先级 > application

3. 外部化配置

场景:线上应用如何快速修改配置,并应用最新配置

  • SpringBoot 使用 配置优先级 + 外部配置 简化配置更新、简化运维。
  • 只需要给jar应用所在的文件夹放一个application.properties最新配置文件,重启项目就能自动应用最新配置

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=8000
  • 包内: application-dev.properties server.port=9000
  • 包外: application.properties server.port=8001
  • 包外: application-dev.properties server.port=9001

启动端口?:命令行 > 9001 > 8001 > 9000 > 8000

3.2. 外部配置

SpringBoot 应用启动时会自动寻找application.properties和application.yaml位置,进行加载。顺序如下:(后面覆盖前面

  1. 类路径: 内部

    1. 类根路径
    2. 类下/config包
  2. 当前路径(项目所在的位置)

    1. 当前路径
    2. 当前下/config子目录
    3. /config目录的直接子目录

最终效果:优先级由高到低,前面覆盖后面

  • 命令行 > 包外config直接子目录 > 包外config目录 > 包外根目录 > 包内目录

  • 同级比较:

    • profile配置 > 默认配置
    • properties配置 > yaml配置

规律:最外层的最优先。

  • 命令行 > 所有
  • 包外 > 包内
  • config目录 > 根目录
  • profile > application

配置不同就都生效(互补),配置相同高优先级覆盖低优先级

3.3. 导入配置

使用spring.config.import可以导入额外配置

1
2
spring.config.import=my.properties
my.property=value

无论以上写法的先后顺序,my.properties的值总是优先于直接在文件中编写的my.property

3.4. 属性占位符

配置文件中可以使用 ${name:default}形式取出之前配置过的值。

1
2
app.name=MyApp
app.description=${app.name} is a Spring Boot application written by ${username:Unknown}

4. 单元测试-JUnit5

4.1. 整合

SpringBoot 提供一系列测试工具集及注解方便我们进行测试。

spring-boot-test提供核心测试能力,spring-boot-test-autoconfigure 提供测试的一些自动配置。

我们只需要导入spring-boot-starter-test 即可整合测试

1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

spring-boot-starter-test 默认提供了以下库供我们测试使用

4.2 测试

4.2.0 组件测试

直接@Autowired容器中的组件进行测试

4.2.1 注解

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 :**为测试类或测试方法提供扩展类引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import static org.junit.jupiter.api.Assertions.fail;
import static org.junit.jupiter.api.Assumptions.assumeTrue;

import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;

class StandardTests {

@BeforeAll
static void initAll() {
}

@BeforeEach
void init() {
}

@DisplayName("😱")
@Test
void succeedingTest() {
}

@Test
void failingTest() {
fail("a failing test");
}

@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}

@Test
void abortedTest() {
assumeTrue("abc".contains("Z"));
fail("test should have been aborted");
}

@AfterEach
void tearDown() {
}

@AfterAll
static void tearDownAll() {
}

}

4.2.2 断言

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
assertArrayEquals 数组断言
assertAll 组合断言
assertThrows 异常断言
assertTimeout 超时断言
fail 快速失败

4.2.3 嵌套测试

JUnit 5 可以通过 Java 中的内部类和@Nested 注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和@AfterEach 注解,而且嵌套的层次没有限制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}

@Nested
@DisplayName("when new")
class WhenNew {

@BeforeEach
void createNewStack() {
stack = new Stack<>();
}

@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}

@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}

@Nested
@DisplayName("after pushing an element")
class AfterPushing {

String anElement = "an element";

@BeforeEach
void pushAnElement() {
stack.push(anElement);
}

@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}

@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}

@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}

4.2.4 参数化测试

参数化测试是JUnit5很重要的一个新特性,它使得用不同的参数多次运行测试成为了可能,也为我们的单元测试带来许多便利。

利用**@ValueSource**等注解,指定入参,我们将可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就新增一个单元测试,省去了很多冗余代码。

@ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型

@NullSource: 表示为参数化测试提供一个null的入参

@EnumSource: 表示为参数化测试提供一个枚举入参

@CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参

@MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("参数化测试1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}


@ParameterizedTest
@MethodSource("method") //指定方法名
@DisplayName("方法来源参数")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}

static Stream<String> method() {
return Stream.of("apple", "banana");
}

5、SpringBoot3-核心原理

1. 事件和监听器

1. 生命周期监听

场景:监听应用生命周期

1. 监听器-SpringApplicationRunListener

  1. 自定义SpringApplicationRunListener监听事件

    1. 编写SpringApplicationRunListener 实现类

    2. META-INF/spring.factories 中配置 org.springframework.boot.SpringApplicationRunListener=自己的Listener,还可以指定一个有参构造器,接受两个参数(SpringApplication application, String[] args)

    3. springboot 在spring-boot.jar中配置了默认的 Listener,如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* Listener先要从 META-INF/spring.factories 读到
*
* 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。
*/

2. 生命周期全流程

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

最佳实战:

  • 如果项目启动前做事: BootstrapRegistryInitializerApplicationContextInitializer
  • 如果想要在项目启动完成后做事:**ApplicationRunner** **CommandLineRunner**
  • 如果要干涉生命周期做事:**SpringApplicationRunListener**
  • 如果想要用事件机制:**ApplicationListener**

2. 完整触发流程

**9大事件**触发顺序&时机

  1. ApplicationStartingEvent:应用启动但未做任何事情, 除过注册listeners and initializers.
  2. ApplicationEnvironmentPreparedEvent: Environment 准备好,但context 未创建.
  3. ApplicationContextInitializedEvent: ApplicationContext 准备好,ApplicationContextInitializers 调用,但是任何bean未加载
  4. ApplicationPreparedEvent: 容器刷新之前,bean定义信息加载
  5. ApplicationStartedEvent: 容器刷新完成, runner未调用

=========以下就开始插入了探针机制============

  1. AvailabilityChangeEventLivenessState.CORRECT应用存活; 存活探针
  2. ApplicationReadyEvent: 任何runner被调用
  3. AvailabilityChangeEventReadinessState.ACCEPTING_TRAFFIC就绪探针,可以接请求
  4. ApplicationFailedEvent :启动出错

应用事件发送顺序如下:

3. SpringBoot 事件驱动开发

应用启动过程生命周期事件感知(9大事件)应用运行中事件感知(无数种)

  • 事件发布ApplicationEventPublisherAware注入:ApplicationEventMulticaster
  • 事件监听组件 + @EventListener

事件发布者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Service
public class EventPublisher implements ApplicationEventPublisherAware {

/**
* 底层发送事件用的组件,SpringBoot会通过ApplicationEventPublisherAware接口自动注入给我们
* 事件是广播出去的。所有监听这个事件的监听器都可以收到
*/
ApplicationEventPublisher applicationEventPublisher;

/**
* 所有事件都可以发
* @param event
*/
public void sendEvent(ApplicationEvent event) {
//调用底层API发送事件
applicationEventPublisher.publishEvent(event);
}

/**
* 会被自动调用,把真正发事件的底层组组件给我们注入进来
* @param applicationEventPublisher event publisher to be used by this object
*/
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.applicationEventPublisher = applicationEventPublisher;
}
}

事件订阅者

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Service
public class CouponService {

@Order(1)
@EventListener
public void onEvent(LoginSuccessEvent loginSuccessEvent){
System.out.println("===== CouponService ====感知到事件"+loginSuccessEvent);
UserEntity source = (UserEntity) loginSuccessEvent.getSource();
sendCoupon(source.getUsername());
}

public void sendCoupon(String username){
System.out.println(username + " 随机得到了一张优惠券");
}
}

2. 自动配置原理

1. 入门理解

应用关注的三大核心场景配置组件

1. 自动配置流程

  1. 导入starter

  2. 依赖导入autoconfigure

  3. 寻找类路径下 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports文件

  4. 启动,加载所有 自动配置类 xxxAutoConfiguration

    1. 给容器中配置功能组件
    2. 组件参数绑定到 属性类中。xxxProperties
    3. 属性类配置文件前缀项绑定
    4. @Contional派生的条件注解进行判断是否组件生效
  5. 效果:

    1. 修改配置文件,修改底层参数
    2. 所有场景自动配置好直接使用
    3. 可以注入SpringBoot配置好的组件随时使用

2. SPI机制

  • Java中的SPI(Service Provider Interface)是一种软件设计模式,用于在应用程序中动态地发现和加载组件。****SPI的思想是,定义一个接口或抽象类,然后通过在classpath中定义实现该接口的类来实现对组件的动态发现和加载。
  • SPI的主要目的是解决在应用程序中使用可插拔组件的问题。例如,一个应用程序可能需要使用不同的日志框架或数据库连接池,但是这些组件的选择可能取决于运行时的条件。通过使用SPI,应用程序可以在运行时发现并加载适当的组件,而无需在代码中硬编码这些组件的实现类。
  • 在Java中,SPI的实现方式是通过在META-INF/services目录下创建一个以服务接口全限定名为名字的文件,文件中包含实现该服务接口的类的全限定名。当应用程序启动时,Java的SPI机制会自动扫描classpath中的这些文件,并根据文件中指定的类名来加载实现类。
  • 通过使用SPI,应用程序可以实现更灵活、可扩展的架构,同时也可以避免硬编码依赖关系和增加代码的可维护性。

以上回答来自ChatGPT-3.5

在SpringBoot中,META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

3. 功能开关

  • 自动配置:全部都配置好,什么都不用管。 自动批量导入

    • 项目一启动,spi文件中指定的所有都加载。
  • @EnableXxxx:手动控制哪些功能的开启; 手动导入。

    • 开启xxx功能
    • 都是利用 @Import 把此功能要用的组件导入进去

2. 进阶理解

1. @SpringBootApplication

@SpringBootConfiguration

就是: @Configuration ,容器中的组件,配置类。spring ioc启动就会加载创建这个类对象

@EnableAutoConfiguration:开启自动配置

开启自动配置

@AutoConfigurationPackage:扫描主程序包:加载自己的组件
  • 利用 @Import(AutoConfigurationPackages.Registrar.class) 想要给容器中导入组件。
  • 把主程序所在的的所有组件导入进来。
  • 为什么SpringBoot默认只扫描主程序所在的包及其子包
@Import(AutoConfigurationImportSelector.class):加载所有自动配置类:加载starter导入的组件
1
List<String> configurations = ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).getCandidates();

扫描SPI文件:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

@ComponentScan

组件扫描:排除一些组件(哪些不要)

排除前面已经扫描进来的配置类、和自动配置类

1
2
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

2. 完整启动加载流程

3. 自定义starter

场景:抽取聊天机器人场景,它可以打招呼

效果:任何项目导入此starter都具有打招呼功能,并且问候语中的人名需要可以在配置文件中修改

    1. 创建自定义starter项目,引入spring-boot-starter基础依赖
    1. 编写模块功能,引入模块所有需要的依赖。
    1. 编写xxxAutoConfiguration自动配置类,帮其他项目导入这个模块需要的所有组件
    1. 编写配置文件META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports指定启动需要加载的自动配置
    1. 其他项目引入即可使用

1. 业务代码

自定义配置有提示。导入以下依赖重启项目,再写配置文件就有提示

1
2
3
4
5
6
7
8
9
@ConfigurationProperties(prefix = "robot")  //此属性类和配置文件指定前缀绑定
@Component
@Data
public class RobotProperties {

private String name;
private String age;
private String email;
}
1
2
3
4
5
6
<!--        导入配置处理器,配置文件自定义的properties配置都会有提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

2. 基本抽取

  • 创建starter项目,把公共代码需要的所有依赖导入

  • 把公共代码复制进来

  • 自己写一个 RobotAutoConfiguration,给容器中导入这个场景需要的所有组件

    • 为什么这些组件默认不会扫描进去?
    • starter所在的包和 引入它的项目的主程序所在的包不是父子层级
  • 别人引用这个starter,直接导入这个 RobotAutoConfiguration,就能把这个场景的组件导入进来

  • 功能生效。

  • 测试编写配置文件

3. 使用@EnableXxx机制

1
2
3
4
5
6
7
8
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import(RobotAutoConfiguration.class)
public @interface EnableRobot {


}

别人引入starter需要使用 @EnableRobot开启功能

4. 完全自动配置

  • 依赖SpringBoot的SPI机制
  • META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中编写好我们自动配置类的全类名即可
  • 项目启动,自动加载我们的自动配置类

SpringBoot3-核心特性
https://xiaoyu72.com/articles/b9b2b255/
Author
XiaoYu
Posted on
August 29, 2023
Updated on
August 29, 2023
Licensed under