01、基础入门-SpringBoot2课程介绍

  1. Spring Boot 2核心技术

  2. Spring Boot 2响应式编程

02、基础入门-Spring生态圈

Spring官网

Spring的生态

覆盖了:

  • web开发
  • 数据访问
  • 安全控制
  • 分布式
  • 消息服务
  • 移动开发
  • 批处理

为什么用SpringBoot

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

SpringBoot优点

  • 创建独立Spring应用

  • 内嵌web服务器

  • 自动starter依赖,简化构建配置

  • 自动配置Spring以及第三方功能

  • 提供生产级别的监控、健康检查及外部化配置

  • 无代码生成、无需编写XML

  • SpringBoot是整合Spring技术栈的一站式框架

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

SpringBoot缺点

  • 人称版本帝,迭代快,需要时刻关注变化
  • 封装太深,内部原理复杂,不容易精通

03、基础入门-SpringBoot的大时代背景

微服务

  • 微服务是一种架构风格
  • 一个应用拆分为一组小型服务
  • 每个服务运行在自己的进程内,也就是可独立部署和升级
  • 服务之间使用轻量级HTTP交互
  • 服务围绕业务功能拆分
  • 可以由全自动部署机制独立部署
  • 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术

分布式

分布式的困难

  • 远程调用
  • 服务发现
  • 负载均衡
  • 服务容错
  • 配置管理
  • 服务监控
  • 链路追踪
  • 日志管理
  • 任务调度

分布式的解决

  • SpringBoot + SpringCloud

04、基础入门-SpringBoot官方文档架构

05、基础入门-SpringBoot-HelloWorld

系统要求

  • Java 8+
  • Maven 3.3+

Maven配置文件

新添内容:

1
2
3
4
5
6
7
8
<mirrors>
<mirror>
<id>alimaven</id>
<mirrorOf>central</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
</mirrors>

HelloWorld项目

需求:浏览发送/hello请求,响应 “Hello,Spring Boot 2”

创建maven工程

引入依赖

1
2
3
4
5
6
7
8
9
10
11
12
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

创建主程序

1
2
3
4
5
6
7
8
9
10
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MainApplication {

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

编写业务

1
2
3
4
5
6
7
8
9
10
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(){
return "Hello, Spring Boot 2!";
}
}

运行&测试

  • 运行MainApplication
  • 浏览器输入http://localhost:8888/hello,将会输出Hello, Spring Boot 2!

设置配置

maven工程的resource文件夹中创建application.properties文件。

1
2
# 设置端口号
server.port=8888

更多配置信息

打包部署

在pom.xml添加

1
2
3
4
5
6
7
8
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

在IDEA的Maven插件上点击运行 clean 、package,把helloworld工程项目打包成jar包,

打包好的jar生成在helloworld工程项目的target文件夹内。

cmd运行java -jar boot-01-helloworld-1.0-SNAPSHOT.jar,就可以运行helloworld工程项目。

将jar包直接部署在目标服务器执行即可。

06、基础入门-SpringBoot-依赖管理特性

  • 父项目做依赖管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--    依赖管理-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>

<!-- 上面项目的父项目如下-->
<!-- 它几乎声明了所有开发中常用的依赖的版本号,自动版本仲裁机制-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
  • 开发导入starter场景启动器
    1. 见到很多 spring-boot-starter-**就表示某种场景
    2. 只要引入starter,这个场景的所有常规需要的依赖都会自动引入
    3. 更多SpringBoot所支持的场景
    4. 见到的 *-spring-boot-starter: 第三方提供的简化开发的场景启动器
1
2
3
4
5
6
7
<!--    所有场景启动器最底层的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
  • 无需关注版本号,自动版本仲裁

    1. 引入依赖默认都可以不写版本
    2. 引入非版本仲裁的jar,要写版本号。
  • 可以修改默认版本号

    1. 查看spring-boot-dependencies里面规定当前依赖的版本用的 key。
    2. 在当前项目里面重写配置,如下面的代码。
1
2
3
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>

IDEA快捷键:

  • ctrl + shift + alt + U:以图的方式显示项目中依赖之间的关系。
  • alt + ins:相当于Eclipse的 Ctrl + N,创建新类,新包等。

07、基础入门-SpringBoot-自动配置特性

  • 自动配好Tomcat
    • 引入Tomcat依赖
    • 配置Tomcat
1
2
3
4
5
6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
  • 自动配好SpringMVC

    • 引入SpringMVC全套组件
    • 自动配好SpringMVC常用组件(功能)
  • 自动配好Web常见功能,如:字符编码问题

    • SpringBoot帮我们配置好了所有web开发的常见场景
1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
//1、返回 IOC容器
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
  • 默认的包结构
    • 主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来
    • 无需以前的包扫描配置
    • 想要改变扫描路径
      • @SpringBootApplication(scanBasePackages=“com.xxxx”)
      • @ComponentScan 指定扫描路径
1
2
3
4
5
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.xxxx")
  • 各种配置拥有默认值
    • 默认配置最终都是映射到某个类上,如:XxxxProperties
    • 配置文件的值最终会绑定某个类上,这个类会在容器中创建对象
  • 按需加载所有自动配置项
    • 非常多的starter
    • 引入了哪些场景这个场景的自动配置才会开启
    • SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面

08、底层注解-@Configuration详解

  • 基本使用
    • Full模式与Lite模式
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
/**
* 1、配置类里面使用@Bean标注在方法上向容器中注册组件,默认是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)(保证每个@Bean方法被调用多少次返回的组件都是单实例的)(默认)
* Lite(proxyBeanMethods = false)(每个@Bean方法被调用多少次返回的组件都是新创建的)
*/
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {

/**
* Full:外部无论对配置类中的这个组件注册方法调用多少次获取的都是之前注册到容器中的单实例对象
* @return
*/
@Bean //向容器中添加组件。以方法名作为组件的id。返回类型就是组件类型。返回的值,就是组件在容器中的实例
public User user01(){
User zhangsan = new User("zhangsan", 18);
//user组件依赖了Pet组件
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}

@Configuration测试代码如下:

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
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {

public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}

//3、从容器中获取组件 true
Pet tom01 = context.getBean("tom", Pet.class);
Pet tom02 = context.getBean("tom", Pet.class);
System.out.println("组件:"+(tom01 == tom02));

//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892
MyConfig config = context.getBean(MyConfig.class);
System.out.println(config);

//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot总会检查这个组件是否在容器中有。
//保持组件单实例
User user = config.user01();
User user1 = config.user01();
System.out.println(user == user1); //false

User user01 = context.getBean("user01", User.class);
Pet tom = context.getBean("tom", Pet.class);

System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}
  • 最佳实战
    • 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
    • 配置 类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(默认)

IDEA快捷键:

  • Alt + Ins:生成getter,setter、构造器等代码。
  • Ctrl + Alt + B:查看类的具体实现代码。

09、底层注解-@Import导入组件

@Bean、@Component、@Controller、@Service、@Repository,它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。

@ComponentScan 在07、基础入门-SpringBoot-自动配置特性有用例。

@Import({User.class, DBHelper.class})向容器中导入这两种类型的组件、默认组件的名字就是全类名

1
2
3
4
@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

//1、返回IOC容器
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

//...

//5、获取组件
String[] beanNamesForType = context.getBeanNamesForType(User.class);

for (String s : beanNamesForType) {
System.out.println(s);
}

DBHelper bean1 = context.getBean(DBHelper.class);
System.out.println(bean1);

10、底层注解-@Conditional条件装配

条件装配:满足Conditional指定的条件,则进行组件注入

@ConditionalOnMissingBean举例说明

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
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "tom")//没有tom名字的Bean时,MyConfig类的Bean才能生效。
public class MyConfig {

@Bean
public User user01(){
User zhangsan = new User("zhangsan", 18);
zhangsan.setPet(tomcatPet());
return zhangsan;
}

@Bean("tom22")
public Pet tomcatPet(){
return new Pet("tomcat");
}
}

public static void main(String[] args) {
//1、返回IOC容器
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

//2、查看容器里面的组件
String[] names = context.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}

boolean tom = context.containsBean("tom");
System.out.println("容器中Tom组件:"+tom);//false

boolean user01 = context.containsBean("user01");
System.out.println("容器中user01组件:"+user01);//true

boolean tom22 = context.containsBean("tom22");
System.out.println("容器中tom22组件:"+tom22);//true

}

11、底层注解-@ImportResource导入Spring配置文件

比如,公司使用bean.xml文件生成配置bean,然而你为了省事,想继续复用bean.xml,@ImportResource粉墨登场。

bean.xml:

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans ...">

<bean id="haha" class="com.lun.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>

<bean id="hehe" class="com.lun.boot.bean.Pet">
<property name="name" value="tomcat"></property>
</bean>
</beans>

使用方法:

1
2
3
@ImportResource("classpath:beans.xml")
public class MyConfig {
}

测试类:

1
2
3
4
5
6
7
8
9
public static void main(String[] args) {
//1、返回 IOC容器
ConfigurableApplicationContext context = SpringApplication.run(MainApplication.class, args);

boolean haha = context.containsBean("haha");
boolean hehe = context.containsBean("hehe");
System.out.println("haha:"+haha); //true
System.out.println("hehe:"+hehe); //true
}

12、底层注解-@ConfigurationProperties配置绑定

如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用

传统方法

1
2
3
4
5
6
7
8
9
10
11
12
13
public class getProperties {
public static void main(String[] args) throws FileNotFoundException, IOException {
Properties pps = new Properties();
pps.load(new FileInputStream("a.properties"));
Enumeration enum1 = pps.propertyNames();//得到配置文件的名字
while(enum1.hasMoreElements()) {
String strKey = (String) enum1.nextElement();
String strValue = pps.getProperty(strKey);
System.out.println(strKey + "=" + strValue);
//封装到JavaBean。
}
}
}

Spring Boot提供的配置绑定

@ConfigurationProperties + @Component

假设有配置文件application.properties

1
2
mycar.brand=BYD
mycar.price=100000

只有在容器中的组件,才会拥有SpringBoot提供的强大功能

1
2
3
4
5
@Component
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}

@EnableConfigurationProperties + @ConfigurationProperties

  1. 开启Car配置绑定功能
  2. 把这个Car这个组件自动注册到容器中
1
2
3
4
@EnableConfigurationProperties(Car.class)
public class MyConfig {
...
}
1
2
3
4
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}

13、自动配置【源码分析】-自动包规则原理

Spring Boot应用的启动类:

1
2
3
4
5
6
7
8
@SpringBootApplication
public class MainApplication {

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

}

分析下@SpringBootApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
...
}

重点分析@SpringBootConfiguration@EnableAutoConfiguration@ComponentScan

@SpringBootConfiguration

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}

@Configuration代表当前是一个配置类。

@ComponentScan

指定扫描哪些Spring注解。

@ComponentScan 在07、基础入门-SpringBoot-自动配置特性有用例。

@EnableAutoConfiguration

1
2
3
4
5
6
7
8
9
10
11
12
13
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

Class<?>[] exclude() default {};

String[] excludeName() default {};
}

重点分析@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class)

@AutoConfigurationPackage

标签名直译为:自动配置包,指定了默认的包规则。

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)//向容器中导入一个组件
public @interface AutoConfigurationPackage {
String[] basePackages() default {};

Class<?>[] basePackageClasses() default {};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
//利用Registrar向容器中导入一系列组件
//将指定的包下的所有组件导入进来,启动类(MainApplication)所在包下。
AutoConfigurationPackages.register(registry, (String[])(new PackageImports(metadata)).getPackageNames().toArray(new String[0]));
}

@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}

}

14、自动配置【源码分析】-初始加载自动配置类

@Import(AutoConfigurationImportSelector.class)

分析AutoConfigurationImportSelector #selectImports方法

  • 利用getAutoConfigurationEntry(annotationMetadata);向容器中批量导入一些组件

  • 调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类

  • SpringBoot 2.x

    • 利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader)得到所有的组件

    • META-INF/spring.factories位置来加载一个文件。

      • 默认扫描我们当前系统中所有META-INF/spring.factories位置的文件

      • spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories

1
2
3
4
5
6
7
# 文件里面写死了spring-boot一启动就要向容器中加载的所有配置类
# spring-boot-autoconfigure-2.3.4.RELEASE.jar/META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...
  • SpringBoot 3.x
1
2
3
4
5
6
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, this.getBeanClassLoader()); //---> 关注点 ✨✨✨
List<String> configurations = importCandidates.getCandidates(); //---> 关注点 ✨✨✨
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring/" + this.autoConfigurationAnnotation.getName() + ".imports. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
Assert.notNull(annotation, "'annotation' must not be null");
ClassLoader classLoaderToUse = decideClassloader(classLoader);
String location = String.format("META-INF/spring/%s.imports", annotation.getName()); //---> 关注点✨✨✨
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location); //---> 关注点✨✨✨
List<String> importCandidates = new ArrayList();

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
importCandidates.addAll(readCandidateConfigurations(url));
}

return new ImportCandidates(importCandidates); //---> 关注点✨✨✨
}
  • 默认扫描META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports位置的文件

虽然我们场景的所有自动配置启动的时候默认全部加载,但是xxxxAutoConfiguration按照条件装配规则(@Conditional),最终会按需配置。

15、修改默认配置

1
2
3
4
5
6
7
8
9
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// @Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
// SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;//向容器中加入了文件上传解析器;
}

SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了以用户的优先

总结

  • SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
  • 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)
  • 生效的配置类就会给容器中装配很多组件
  • 只要容器中有这些组件,相当于这些功能就有了
  • 定制化配置
    • 用户直接自己@Bean替换底层的组件
    • 用户去看这个组件是获取的配置文件什么值就去修改。

xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties

16、最佳实践-SpringBoot应用如何编写

  • 引入场景依赖
  • 查看自动配置了哪些(选做)
    • 自己分析,引入场景对应的自动配置一般都生效了
    • 配置文件中debug=true开启自动配置报告。
      • Negative(不生效)
      • Positive(生效)
  • 是否需要修改
    • 参照文档修改配置项
      • 官方文档
      • 自己分析。xxxxProperties绑定了配置文件的哪些。
    • 自定义加入或者替换组件
      • @Bean、@Component…
    • 自定义器 XXXXXCustomizer;

17、最佳实践-Lombok简化开发

Lombok用标签方式代替构造器、getter/setter、toString()等鸡肋代码。

spring boot已经管理Lombok。引入依赖:

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

IDEA中File->Settings->Plugins,搜索安装Lombok插件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@NoArgsConstructor
//@AllArgsConstructor
@Data
@ToString
@EqualsAndHashCode
public class User {

private String name;
private Integer age;

private Pet pet;

public User(String name,Integer age){
this.name = name;
this.age = age;
}
}

简化日志开发

1
2
3
4
5
6
7
8
9
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}

18、最佳实践-dev-tools

添加依赖:

1
2
3
4
5
6
7
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>

在IDEA中,项目或者页面修改以后:Ctrl+F9。

19、最佳实践-Spring Initailizr

Spring Initailizr是创建Spring Boot工程向导。

在IDEA中,菜单栏New -> Project -> Spring Initailizr。

20、配置文件-yaml的用法

同以前的properties用法

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

非常适合用来做以数据为中心的配置文件

基本语法

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

数据类型

  • 字面量:单个的、不可再分的值。date、boolean、string、number、null
1
k: v
  • 对象:键值对的集合。map、hash、set、object
1
2
3
4
5
6
7
8
9
10
#行内写法:  

k: {k1:v1,k2:v2,k3:v3}

#或

k:
k1: v1
k2: v2
k3: v3
  • 数组:一组按次序排列的值。array、list、queue
1
2
3
4
5
6
7
8
9
10
#行内写法:  

k: [v1,v2,v3]

#或者

k:
- v1
- v2
- v3

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}

@Data
public class Pet {
private String name;
private Double weight;
}

用yaml表示以上对象

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
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
interests: [篮球,游泳]
animal:
- jerry
- mario
score:
english:
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
- {name: tom}
- {name: jerry,weight: 47}
health: [{name: mario,weight: 47}]

21、配置文件-自定义类绑定的配置提示

You can easily generate your own configuration metadata file from items annotated with @ConfigurationProperties by using the spring-boot-configuration-processor jar. The jar includes a Java annotation processor which is invoked as your project is compiled.——link

自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>

<!-- 下面插件作用是工程打包时,不将spring-boot-configuration-processor打进包内,让其只在编码的时候有用 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

22、web场景-web开发简介

Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多场景我们都无需自定义配置)

The auto-configuration adds the following features on top of Spring’s defaults:

  • Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.

    • 内容协商视图解析器和BeanName视图解析器
  • Support for serving static resources, including support for WebJars (covered later in this document)).

    • 静态资源(包括webjars)
  • Automatic registration of Converter, GenericConverter, and Formatter beans.

    • 自动注册 Converter,GenericConverter,Formatter
  • Support for HttpMessageConverters (covered later in this document).

    • 支持 HttpMessageConverters (后来我们配合内容协商理解原理)
  • Automatic registration of MessageCodesResolver (covered later in this document).

    • 自动注册 MessageCodesResolver (国际化用)
  • Static index.html support.

    • 静态index.html 页支持
  • Custom Favicon support (covered later in this document).

    • 自定义 Favicon
  • Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).

    • 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)

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

不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则

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

声明 WebMvcRegistrations 改变默认底层组件

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

使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC

23、web场景-静态资源规则与定制化

静态资源目录

只要静态资源放在类路径下: called /static (or /public or /resources or /META-INF/resources

访问 : 当前项目根路径/ + 静态资源名

原理: 静态映射/**

请求进来,先去找Controller看能不能处理。不能处理的所有请求又都交给静态资源处理器。静态资源也找不到则响应404页面

也可以改变默认的静态资源路径,/static/public,/resources, /META-INF/resources失效

1
2
resources:
static-locations: [classpath:/haha/]

静态资源访问前缀

1
2
3
spring:
mvc:
static-path-pattern: /res/**

当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找

webjar

可用jar方式添加css,js等资源文件,

https://www.webjars.org/

例如,添加jquery

1
2
3
4
5
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>

访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js 后面地址要按照依赖里面的包路径。

24、web场景-welcome与favicon功能

官方文档

欢迎页支持

  • 静态资源路径下 index.html

    • 可以配置静态资源路径
    • 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
1
2
3
4
5
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome page功能失效
resources:
static-locations: [classpath:/haha/]
  • controller能处理/index。

自定义Favicon

指网页标签上的小图标。

favicon.ico 放在静态资源目录下即可。

1
2
3
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效

25、web场景-【源码分析】-静态资源原理

  • SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
  • SpringMVC功能的自动配置类WebMvcAutoConfiguration,生效
1
2
3
4
5
6
7
8
9
10
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public class WebMvcAutoConfiguration {
...
}
  • 给容器中配置的内容:
    • 配置文件的相关属性的绑定:WebMvcProperties=>spring.mvcWebProperties=>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 {
...
}

配置类只有一个有参构造器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//有参构造器所有参数的值都会从容器中确定
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}

资源处理的默认规则

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
...
public class WebMvcAutoConfiguration {
...
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
...
@Override
public void addResourceHandlers(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 = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
...

}
...
}

根据上述代码,我们可以通过配置禁止所有静态资源规则。

1
2
3
spring:
resources:
add-mappings: false #禁用所有静态资源规则

静态资源规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@ConfigurationProperties("spring.web")
public class WebProperties {

...

public static class Resources {

private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/",
"classpath:/resources/", "classpath:/static/", "classpath:/public/" };

/**
* Locations of static resources. Defaults to classpath:[/META-INF/resources/,
* /resources/, /static/, /public/].
*/
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;

...

}
}

欢迎页的处理规则

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
72
73
74
75
76
77
78
......

public class WebMvcAutoConfiguration {

.......

public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {

.......

/**
* 配置并创建一个用于处理欢迎页面请求的处理器映射(WelcomePageHandlerMapping)。
* 该方法通过通用的创建逻辑封装了具体的初始化过程。
*
* @param applicationContext 应用上下文,用于获取模板资源及配置信息
* @param mvcConversionService MVC框架使用的类型转换服务
* @param mvcResourceUrlProvider 资源URL提供者,用于构建静态资源链接
* @return 初始化完成的 WelcomePageHandlerMapping 实例
*/
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
return createWelcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider,
WelcomePageHandlerMapping::new);
}

/**
* 配置并创建一个用于处理不接受欢迎页面内容类型请求的处理器映射(WelcomePageNotAcceptableHandlerMapping)。
*
* @param applicationContext 应用上下文,用于获取模板资源及配置信息
* @param mvcConversionService MVC框架使用的类型转换服务
* @param mvcResourceUrlProvider 资源URL提供者,用于构建静态资源链接
* @return 初始化完成的 WelcomePageNotAcceptableHandlerMapping 实例
*/
@Bean
public WelcomePageNotAcceptableHandlerMapping welcomePageNotAcceptableHandlerMapping(
ApplicationContext applicationContext, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider) {
return createWelcomePageHandlerMapping(applicationContext, mvcConversionService, mvcResourceUrlProvider,
WelcomePageNotAcceptableHandlerMapping::new);
}

/**
* 通用方法:创建欢迎页面相关的 HandlerMapping 实例。
*
* @param applicationContext 应用上下文,用于获取模板资源及配置信息
* @param mvcConversionService MVC框架使用的类型转换服务
* @param mvcResourceUrlProvider 资源URL提供者,用于构建静态资源链接
* @param factory 工厂函数,用于创建特定类型的 HandlerMapping 实例
* @param <T> HandlerMapping 的具体子类类型
* @return 初始化完成的目标类型的 HandlerMapping 实例
*/
private <T extends AbstractUrlHandlerMapping> T createWelcomePageHandlerMapping(
ApplicationContext applicationContext, FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider, WelcomePageHandlerMappingFactory<T> factory) {
// 创建模板可用性提供者,用于检测模板引擎是否就绪
TemplateAvailabilityProviders templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);

// 获取静态资源路径匹配规则
String staticPathPattern = this.mvcProperties.getStaticPathPattern();

// 使用工厂方法创建 HandlerMapping 实例
T handlerMapping = factory.create(templateAvailabilityProviders, applicationContext, getIndexHtmlResource(), staticPathPattern);

// 设置拦截器,用于在请求前后进行预处理或后处理
handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));

// 设置跨域资源共享(CORS)相关配置
handlerMapping.setCorsConfigurations(getCorsConfigurations());

// 返回最终构造好的 HandlerMapping 实例
return handlerMapping;
}


}

}

WelcomePageHandlerMapping的构造方法如下:

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
/**
* 构造函数,用于创建一个 WelcomePageHandlerMapping 实例。
*
* @param templateAvailabilityProviders 提供模板可用性的服务,用于判断是否存在可用的模板引擎。
* @param applicationContext 应用上下文,用于获取和管理 Spring 容器中的 Bean。
* @param indexHtmlResource 表示 index.html 资源文件,用于静态欢迎页面的处理。
* @param staticPathPattern 静态资源路径模式,用于匹配静态资源请求路径。
*/
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
setOrder(2); // 设置当前 HandlerMapping 的优先级为 2。

// 解析欢迎页(可能是静态页面或模板页面)
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);

// 如果解析成功(即存在有效的欢迎页)
if (welcomePage != WelcomePage.UNRESOLVED) {
// 记录日志:添加欢迎页或模板
logger.info(LogMessage.of(() -> (!welcomePage.isTemplated())
? "Adding welcome page: " + indexHtmlResource
: "Adding welcome page template: index"));

// 创建参数化视图控制器
ParameterizableViewController controller = new ParameterizableViewController();
// 设置视图名称(如 index)
controller.setViewName(welcomePage.getViewName());
// 将控制器设置为根处理器
setRootHandler(controller);
}
}

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
final class WelcomePage {

/**
* Value used for an unresolved welcome page.
*/
static final WelcomePage UNRESOLVED = new WelcomePage(null, false);

private final String viewName;

private final boolean templated;

private WelcomePage(String viewName, boolean templated) {
this.viewName = viewName;
this.templated = templated;
}

/**
* Return the view name of the welcome page.
* @return the view name
*/
String getViewName() {
return this.viewName;
}

/**
* Return if the welcome page is from a template.
* @return if the welcome page is templated
*/
boolean isTemplated() {
return this.templated;
}

/**
* Resolve the {@link WelcomePage} to use.
* @param templateAvailabilityProviders the template availability providers
* @param applicationContext the application context
* @param indexHtmlResource the index HTML resource to use or {@code null}
* @param staticPathPattern the static path pattern being used
* @return a resolved {@link WelcomePage} instance or {@link #UNRESOLVED}
*/
static WelcomePage resolve(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
if (indexHtmlResource != null && "/**".equals(staticPathPattern)) {
//要使用欢迎页功能,静态资源访问路径必须是 /**
return new WelcomePage("forward:index.html", false);
}
//检查是否存在名为index的模板
if (templateAvailabilityProviders.getProvider("index", applicationContext) != null) {
return new WelcomePage("index", true);
}
return UNRESOLVED;
}

}

这段代码也解释了web场景-welcome与favicon功能中,如果开发者通过spring.mvc.static-path-pattern 修改了该配置,welcome页面和小图标失效的问题。

代码解读

WelcomePageNotAcceptableHandlerMapping::new 能够作为 createWelcomePageHandlerMapping(...) 方法的参数传入,核心原因是 Java 的函数式编程支持和多态机制的结合。

  1. 类型继承关系(多态)
1
2
3
class WelcomePageHandlerMapping extends AbstractUrlHandlerMapping { ... }

class WelcomePageNotAcceptableHandlerMapping extends AbstractUrlHandlerMapping { ... }

因此,它们都满足泛型约束


  1. 函数式接口适配性

方法定义如下:

1
2
3
4
5
private <T extends AbstractUrlHandlerMapping> T createWelcomePageHandlerMapping(
ApplicationContext applicationContext,
FormattingConversionService mvcConversionService,
ResourceUrlProvider mvcResourceUrlProvider,
WelcomePageHandlerMappingFactory<T> factory)

其中,WelcomePageHandlerMappingFactory<T> 是一个函数式接口,定义了创建 T 类型对象的方法:

1
2
3
4
5
6
7
@FunctionalInterface
interface WelcomePageHandlerMappingFactory<T extends AbstractUrlHandlerMapping> {

T create(TemplateAvailabilityProviders templateAvailabilityProviders, ApplicationContext applicationContext,
Resource indexHtmlResource, String staticPathPattern);

}

只要传入的对象符合这个接口的语义(即能创建出 T 类型的实例),就可以作为参数传入。


  1. 构造方法匹配

WelcomePageHandlerMapping::new 可以作为 factory 参数传入的前提是它的构造函数签名与接口定义的 create(…) 方法参数一致

1
2
3
4
5
6
7
8
9
10
11
12
13
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource indexHtmlResource, String staticPathPattern) {
setOrder(2);
WelcomePage welcomePage = WelcomePage.resolve(templateAvailabilityProviders, applicationContext,
indexHtmlResource, staticPathPattern);
if (welcomePage != WelcomePage.UNRESOLVED) {
logger.info(LogMessage.of(() -> (!welcomePage.isTemplated()) ? "Adding welcome page: " + indexHtmlResource
: "Adding welcome page template: index"));
ParameterizableViewController controller = new ParameterizableViewController();
controller.setViewName(welcomePage.getViewName());
setRootHandler(controller);
}
}

该构造函数的参数顺序和类型与 WelcomePageHandlerMappingFactory.create(...) 接口完全匹配

模仿示例:

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
/**
* 思路梳理
* 1.我需要一个方法 可以提供 extends xx类的对象
* 2.具体需要获取实现类由调用者决定,提供一个函数式工厂接口来帮我生产 具体的子类对象
*/
public class DemoClass {

static class Demo {
}

static class Demo1 extends Demo {
}

// 使用泛型 <T extends Demo> 表示该方法可以创建任意继承自 Demo 的子类
private <T extends Demo> T createObject(objectFactory<T> factory) {
return factory.create();
}

// 定义一个函数式接口 objectFactory<T>,将对象创建逻辑交给调用方
@FunctionalInterface
interface objectFactory<T extends Demo> {
T create();
}

public Object getObject() {
return createObject(Demo1::new);
}

}

26、请求处理-【源码分析】-Rest映射及源码解析

请求映射

  • @xxxMapping;

    • @GetMapping
    • @PostMapping
    • @PutMapping
    • @DeleteMapping
  • Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)

    • 以前:
      • /getUser 获取用户
      • /deleteUser 删除用户
      • /editUser 修改用户
      • /saveUser保存用户
    • 现在: /user
      • GET-获取用户
      • DELETE-删除用户
      • PUT-修改用户
      • POST-保存用户
    • 核心Filter;HiddenHttpMethodFilter
  • 用法

    • 开启页面表单的Rest功能
    • 页面 form的属性method=post,隐藏域 _method=put、delete等(如果直接get或post,无需隐藏域)
    • 编写请求映射
1
2
3
4
5
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);

//HandlerMapping:处理器映射。/xxx->>xxxx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<form action="/user" method="get">
<input value="REST-GET提交" type="submit" />
</form>

<form action="/user" method="post">
<input value="REST-POST提交" type="submit" />
</form>

<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>

<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT" />
<input value="REST-PUT提交"type="submit" />
<form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@GetMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-张三";
}

@PostMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-张三";
}

@PutMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-张三";
}

@DeleteMapping("/user")
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-张三";
}
  • Rest原理(表单提交要使用REST的时候)
    • 表单提交会带上_method=PUT
    • 静态资源规则HiddenHttpMethodFilter拦截
      • 请求是否正常,并且是POST
        • 获取到_method的值。
        • 兼容以下请求;PUT.DELETE.PATCH
        • 原生request(post),包装模式requestWrapper重写了getMethod方法,返回的是传入的值。
        • 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requestWrapper的。
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
public class HiddenHttpMethodFilter extends OncePerRequestFilter {

private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));

/** Default method parameter: {@code _method}. */
public static final String DEFAULT_METHOD_PARAM = "_method";

private String methodParam = DEFAULT_METHOD_PARAM;


/**
* Set the parameter name to look for HTTP methods.
* @see #DEFAULT_METHOD_PARAM
*/
public void setMethodParam(String methodParam) {
Assert.hasText(methodParam, "'methodParam' must not be empty");
this.methodParam = methodParam;
}

/**
* 重写 Filter 的 doFilterInternal 方法,实现对 HTTP 方法的包装与替换。
* 主要用于支持如 _method 参数模拟 PUT/PATCH/DELETE 等 HTTP 方法。
*
* @param request 原始的 HTTP 请求对象
* @param response HTTP 响应对象
* @param filterChain 过滤器链,用于继续执行后续过滤器或目标请求处理
* @throws ServletException 如果处理过程中发生 Servlet 异常
* @throws IOException 如果发生 I/O 错误
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

// 默认使用原始请求对象
HttpServletRequest requestToUse = request;

// 判断是否为 POST 请求,并且没有异常属性(即不是错误转发)
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {

// 获取指定的方法参数值(如:_method=PUT)
String paramValue = request.getParameter(this.methodParam);

// 检查参数是否存在且合法
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ROOT);

// 如果方法在允许范围内,则创建一个包装后的请求对象
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}

// 继续执行过滤器链,使用可能被包装过的请求对象
filterChain.doFilter(requestToUse, response);
}



/**
* 一个简单的 HttpServletRequest 包装类,用于修改请求方法(method)。
* 该包装器在调用 getMethod() 方法时返回指定的 HTTP 方法字符串。
*
* <p>通常用于模拟 PUT、PATCH 或 DELETE 等 HTTP 方法,这些方法在 HTML 表单中无法直接发送。</p>
*/
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {

/** 存储要伪装的 HTTP 请求方法(如 "PUT", "DELETE" 等) */
private final String method;

/**
* 构造一个新的请求包装对象,并设置要伪装的 HTTP 方法。
*
* @param request 原始的 HttpServletRequest 对象
* @param method 要伪装成的 HTTP 方法字符串
*/
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}

/**
* 重写 getMethod 方法,返回构造时传入的自定义 HTTP 方法。
*
* @return 伪装后的 HTTP 方法(如 "PUT", "DELETE")
*/
@Override
public String getMethod() {
return this.method;
}
}


}
  • Rest使用客户端工具。
    • 如PostMan可直接发送put、delete等方式请求。

27、请求处理-【源码分析】-怎么改变默认的_method

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {

...

@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}

...
}

@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)意味着在没有HiddenHttpMethodFilter时,才执行hiddenHttpMethodFilter()。因此,我们可以自定义filter,改变默认的_method。例如:

1
2
3
4
5
6
7
8
9
10
@Configuration(proxyBeanMethods = false)
public class WebConfig{
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}

\_method改成_m

1
2
3
4
<form action="/user" method="post">
<input name="_m" type="hidden" value="DELETE"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>

28、请求处理-【源码分析】-请求映射原理

image-20250417205228748

SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet -> doDispatch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 找到当前请求使用哪个Handler(Controller的方法)处理
mappedHandler = getHandler(processedRequest);

//HandlerMapping:处理器映射。/xxx->>xxxx
...
}

getHandler()方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

this.handlerMappings在Debug模式下展现的内容:

image-20250424203251656

其中,RequestMappingHandlerMapping保存了所有@RequestMappinghandler的映射规则。

image-20250417205303175

所有的请求映射都在HandlerMapping中:

  • SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 / 能访问到index.html

  • SpringBoot自动配置了默认 的 RequestMappingHandlerMapping

  • 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。

    • 如果有就找到这个请求对应的handler
    • 如果没有就是下一个 HandlerMapping
  • 我们需要一些自定义的映射处理,也可以向容器中添加HandlerMapping。自定义 HandlerMapping

IDEA快捷键:

  • Crtl + Alt + Shift + U : 以UML的类图展现类有哪些继承类,派生类以及实现哪些接口。

  • Ctrl + Alt + U : 同上,区别在于上条快捷键结果在新页展现,而本条快捷键结果在弹窗展现。

  • Ctrl + H : 以树形方式展现类层次结构图。

29、请求处理-常用参数注解使用

注解:

  • @PathVariable 路径变量
  • @RequestHeader 获取请求头
  • @RequestParam 获取请求参数(指问号后的参数,url?a=1&b=2)
  • @CookieValue 获取Cookie值
  • @RequestAttribute 获取request域属性
  • @RequestBody 获取请求体[POST]
  • @MatrixVariable 矩阵变量
  • @ModelAttribute
    • 当注解在控制器的方法上时,会在该控制器所有请求处理方法执行前自动调用,用于预加载模型数据
    • 当注解在方法的参数上时,用于将请求参数绑定到指定对象,并自动加入模型(Model)中

使用用例:

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
@RestController
public class ParameterTestController {


// car/2/owner/zhangsan
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String name,
@PathVariable Map<String,String> pv,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
@RequestParam Map<String,String> params,
@CookieValue("_ga") String _ga,
@CookieValue("_ga") Cookie cookie){

Map<String,Object> map = new HashMap<>();

// map.put("id",id);
// map.put("name",name);
// map.put("pv",pv);
// map.put("userAgent",userAgent);
// map.put("headers",header);
map.put("age",age);
map.put("inters",inters);
map.put("params",params);
map.put("_ga",_ga);
System.out.println(cookie.getName()+"===>"+cookie.getValue());
return map;
}


@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
}

30、请求处理-@RequestAttribute

用例:

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
@Controller
public class RequestController {

@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success请求
}

@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");

Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}

//@RequestAttribute在这个方法✨✨✨
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");

Map<String,Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");
Object world = request.getAttribute("world");
Object message = request.getAttribute("message");

map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
map.put("hello",hello);
map.put("world",world);
map.put("message",message);

return map;
}
}

31、请求处理-@MatrixVariable与UrlPathHelper

  1. 语法: 请求路径:/cars/sell;low=34;brand=byd,audi,yd

  2. SpringBoot默认是禁用了矩阵变量的功能

    • 手动开启:原理。对于路径的处理。UrlPathHelper的removeSemicolonContent设置为false,让其支持矩阵变量。
  3. 矩阵变量必须有url路径变量才能被解析

手动开启矩阵变量

  • 方法1:实现WebMvcConfigurer接口:
1
2
3
4
5
6
7
8
9
10
11
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {

UrlPathHelper urlPathHelper = new UrlPathHelper();
// 设置为不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
  • 方法2:创建返回WebMvcConfigurerBean:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration(proxyBeanMethods = false)
public class WebConfig{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
}
}

@MatrixVariable的用例

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
@RestController
public class ParameterTestController {

///cars/sell;low=34;brand=byd,audi,yd
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand,
@PathVariable("path") String path){
Map<String,Object> map = new HashMap<>();

map.put("low",low);
map.put("brand",brand);
map.put("path",path);
return map;
}

// /boss/1;age=20/2;age=10

@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge){
Map<String,Object> map = new HashMap<>();

map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;

}

}

32、请求处理-【源码分析】-各种类型参数解析原理

这要从DispatcherServlet开始说起:

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
public class DispatcherServlet extends FrameworkServlet {

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
...
  • HandlerMapping中找到能处理请求的Handler(Controller.method())。
  • 为当前Handler 找一个适配器 HandlerAdapter,用的最多的是RequestMappingHandlerAdapter
  • 适配器执行目标方法并确定方法参数的每一个值。

HandlerAdapter

默认会加载所有HandlerAdapter

detect vt.发现;查明;侦察出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DispatcherServlet extends FrameworkServlet {

/** Detect all HandlerAdapters or just expect "handlerAdapter" bean?. */
private boolean detectAllHandlerAdapters = true;

...

private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;

if (this.detectAllHandlerAdapters) {
// Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
// We keep HandlerAdapters in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
...

有这些HandlerAdapter

image-20250417205325144

执行目标方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class DispatcherServlet extends FrameworkServlet {

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = null;

...

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

...
//---->本节重点!!!!!
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

HandlerAdapter接口实现类RequestMappingHandlerAdapter(主要用来处理@RequestMapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

...

//AbstractHandlerMethodAdapter类的方法,RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.handleInternal(request, response, (HandlerMethod)handler);
}

@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
//handleInternal的核心
mav = invokeHandlerMethod(request, response, handlerMethod);//解释看下节
//...
return mav;
}
}

参数解析器

确定将要执行的目标方法的每一个参数的值是什么;

SpringMVC目标方法能写多少种参数类型。取决于参数解析器argumentResolvers

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {//<-----关注点
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}

...

this.argumentResolversafterPropertiesSet()方法内初始化

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
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

@Nullable
private HandlerMethodArgumentResolverComposite argumentResolvers;

@Override
public void afterPropertiesSet() {
...
if (this.argumentResolvers == null) {//初始化argumentResolvers
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
...
}

//初始化了一堆实现HandlerMethodArgumentResolver接口的类
private List<HandlerMethodArgumentResolver> getDefaultArgumentResolvers() {
List<HandlerMethodArgumentResolver> resolvers = new ArrayList<>(30);

// Annotation-based argument resolution
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), false));
resolvers.add(new RequestParamMapMethodArgumentResolver());
resolvers.add(new PathVariableMethodArgumentResolver());
resolvers.add(new PathVariableMapMethodArgumentResolver());
resolvers.add(new MatrixVariableMethodArgumentResolver());
resolvers.add(new MatrixVariableMapMethodArgumentResolver());
resolvers.add(new ServletModelAttributeMethodProcessor(false));
resolvers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestPartMethodArgumentResolver(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RequestHeaderMethodArgumentResolver(getBeanFactory()));
resolvers.add(new RequestHeaderMapMethodArgumentResolver());
resolvers.add(new ServletCookieValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new ExpressionValueMethodArgumentResolver(getBeanFactory()));
resolvers.add(new SessionAttributeMethodArgumentResolver());
resolvers.add(new RequestAttributeMethodArgumentResolver());

// Type-based argument resolution
resolvers.add(new ServletRequestMethodArgumentResolver());
resolvers.add(new ServletResponseMethodArgumentResolver());
resolvers.add(new HttpEntityMethodProcessor(getMessageConverters(), this.requestResponseBodyAdvice));
resolvers.add(new RedirectAttributesMethodArgumentResolver());
resolvers.add(new ModelMethodProcessor());
resolvers.add(new MapMethodProcessor());
resolvers.add(new ErrorsMethodArgumentResolver());
resolvers.add(new SessionStatusMethodArgumentResolver());
resolvers.add(new UriComponentsBuilderMethodArgumentResolver());
if (KotlinDetector.isKotlinPresent()) {
resolvers.add(new ContinuationHandlerMethodArgumentResolver());
}

// Custom arguments
if (getCustomArgumentResolvers() != null) {
resolvers.addAll(getCustomArgumentResolvers());
}

// Catch-all
resolvers.add(new PrincipalMethodArgumentResolver());
resolvers.add(new RequestParamMethodArgumentResolver(getBeanFactory(), true));
resolvers.add(new ServletModelAttributeMethodProcessor(true));

return resolvers;
}

}

HandlerMethodArgumentResolverComposite类如下:(众多参数解析器argumentResolvers的包装类)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

private final List<HandlerMethodArgumentResolver> argumentResolvers = new ArrayList<>();

...

public HandlerMethodArgumentResolverComposite addResolvers(
@Nullable HandlerMethodArgumentResolver... resolvers) {

if (resolvers != null) {
Collections.addAll(this.argumentResolvers, resolvers);
}
return this;
}

...
}

我们看看HandlerMethodArgumentResolver的源码:

1
2
3
4
5
6
7
8
9
10
11
12
public interface HandlerMethodArgumentResolver {

//当前解析器是否支持解析这种参数
boolean supportsParameter(MethodParameter parameter);

@Nullable
//如果支持,就调用 resolveArgument
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;

}

返回值处理器

ValueHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {//<---关注点
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
...

this.returnValueHandlersafterPropertiesSet()方法内初始化

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
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;

@Override
public void afterPropertiesSet() {

...

if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}

//初始化了一堆的实现HandlerMethodReturnValueHandler接口的
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);

// Single-purpose return value types
handlers.add(new ModelAndViewMethodReturnValueHandler());
handlers.add(new ModelMethodProcessor());
handlers.add(new ViewMethodReturnValueHandler());
handlers.add(new ResponseBodyEmitterReturnValueHandler(getMessageConverters(),
this.reactiveAdapterRegistry, this.taskExecutor, this.contentNegotiationManager));
handlers.add(new StreamingResponseBodyReturnValueHandler());
handlers.add(new HttpEntityMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));
handlers.add(new HttpHeadersReturnValueHandler());
handlers.add(new CallableMethodReturnValueHandler());
handlers.add(new DeferredResultMethodReturnValueHandler());
handlers.add(new AsyncTaskMethodReturnValueHandler(this.beanFactory));

// Annotation-based return value types
handlers.add(new ServletModelAttributeMethodProcessor(false));
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice));

// Multi-purpose return value types
handlers.add(new ViewNameMethodReturnValueHandler());
handlers.add(new MapMethodProcessor());

// Custom return value types
if (getCustomReturnValueHandlers() != null) {
handlers.addAll(getCustomReturnValueHandlers());
}

// Catch-all
if (!CollectionUtils.isEmpty(getModelAndViewResolvers())) {
handlers.add(new ModelAndViewResolverMethodReturnValueHandler(getModelAndViewResolvers()));
}
else {
handlers.add(new ServletModelAttributeMethodProcessor(true));
}

return handlers;
}
}

HandlerMethodReturnValueHandlerComposite类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {

private final List<HandlerMethodReturnValueHandler> returnValueHandlers = new ArrayList<>();

...

public HandlerMethodReturnValueHandlerComposite addHandlers(
@Nullable List<? extends HandlerMethodReturnValueHandler> handlers) {

if (handlers != null) {
this.returnValueHandlers.addAll(handlers);
}
return this;
}

}

HandlerMethodReturnValueHandler接口:

1
2
3
4
5
6
7
8
public interface HandlerMethodReturnValueHandler {

boolean supportsReturnType(MethodParameter returnType);

void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;

}

回顾执行目标方法

1
2
3
4
5
6
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = null;
...
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

RequestMappingHandlerAdapterhandle()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

...

//AbstractHandlerMethodAdapter类的方法,RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return this.handleInternal(request, response, (HandlerMethod)handler);
}

@Override
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
//handleInternal的核心
mav = this.invokeHandlerMethod(request, response, handlerMethod);//解释看下节
//...
return mav;
}
}

RequestMappingHandlerAdapterinvokeHandlerMethod()方法:

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
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean {

protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

...

ServletWebRequest webRequest = asyncWebRequest instanceof ServletWebRequest ? (ServletWebRequest)asyncWebRequest : new ServletWebRequest(request, response);

...

ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}

if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}

...

//关注点:执行目标方法
invocableMethod.invokeAndHandle(webRequest, mavContainer, new Object[0]);
return asyncManager.isConcurrentHandlingStarted() ? null : this.getModelAndView(mavContainer, modelFactory, webRequest);
}

invokeAndHandle()方法如下:

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
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod { 

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
Object returnValue = this.invokeForRequest(webRequest, mavContainer, providedArgs);

...

try {
//returnValue存储起来
this.returnValueHandlers.handleReturnValue(returnValue, this.getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception var6) {
...
}
}


//方法在InvocableHandlerMethod类中,ServletInvocableHandlerMethod类 继承InvocableHandlerMethod类
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//获取方法的参数值
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
...

Object returnValue = this.doInvoke(args);
...

return returnValue;
}



protected Object doInvoke(Object... args) throws Exception {
Method method = this.getBridgedMethod(); //@RequestMapping的方法

try {
if (KotlinDetector.isKotlinReflectPresent()) {
if (KotlinDetector.isSuspendingFunction(method)) {
return this.invokeSuspendingFunction(method, this.getBean(), args);
}

if (KotlinDetector.isKotlinType(method.getDeclaringClass())) {
return InvocableHandlerMethod.KotlinDelegate.invokeFunction(method, this.getBean(), args);
}
}
//通过反射调用 getBean()指@RequestMapping的方法所在类的对象
return method.invoke(this.getBean(), args);
} catch (IllegalArgumentException var8) {
...
} catch (InvocationTargetException var9) {
...
}
}
}

如何确定目标方法每一个参数的值

重点分析ServletInvocableHandlerMethodgetMethodArgumentValues方法

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
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
...

@Nullable//InvocableHandlerMethod类的,ServletInvocableHandlerMethod类继承InvocableHandlerMethod类
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
//获取方法的参数值
Object[] args = this.getMethodArgumentValues(request, mavContainer, providedArgs);
...

Object returnValue = this.doInvoke(args);
...

return returnValue;
}

//本节重点,获取方法的参数值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {
MethodParameter[] parameters = this.getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
} else {
Object[] args = new Object[parameters.length];

for(int i = 0; i < parameters.length; ++i) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] == null) {
//查看resolvers中是否有支持解析当前参数的
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}

try {
//如果存在,就开始解析
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
} catch (Exception var10) {
....
}
}
}

return args;
}
}

this.resolvers的类型为HandlerMethodArgumentResolverComposite(在参数解析器章节提及)

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
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {

....

@Override
public boolean supportsParameter(MethodParameter parameter) {
// 判断是否存在支持该参数类型的参数解析器(HandlerMethodArgumentResolver)
return getArgumentResolver(parameter) != null;
}

/**
* 遍历已注册的 HandlerMethodArgumentResolver,
* 并调用支持当前参数类型的解析器来解析参数值。
* @throws IllegalArgumentException 如果没有找到合适的参数解析器
*/
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter,
@Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
@Nullable WebDataBinderFactory binderFactory) throws Exception {

// 获取支持当前参数的 HandlerMethodArgumentResolver
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);

if (resolver == null) {
// 如果没有找到合适的解析器,抛出异常,提示调用者应先调用 supportsParameter
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}

// 使用找到的解析器来实际解析该参数的值
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

/**
* 查找一个已注册的 HandlerMethodArgumentResolver,
* 它支持传入的方法参数(MethodParameter)类型。
*/
@Nullable
public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {

// 先从缓存中查找是否已经缓存了对应的解析器
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);

if (result == null) {
// 如果缓存中没有,则遍历所有已注册的解析器
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
// 检查当前解析器是否支持该参数类型
if (resolver.supportsParameter(parameter)) {
// 找到后缓存该解析器,避免下次重复查找
result = resolver;
this.argumentResolverCache.put(parameter, result);
break; // 找到即退出循环
}
}
}

// 返回找到的解析器,如果没有找到则返回 null
return result;
}
}

33、请求处理-【源码分析】-Servlet API参数解析原理

  • WebRequest
  • ServletRequest
  • MultipartRequest
  • HttpSession
  • javax.servlet.http.PushBuilder
  • Principal
  • InputStream
  • Reader
  • HttpMethod
  • Locale
  • TimeZone
  • ZoneId

**ServletRequestMethodArgumentResolver**用来处理以上的参数

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
PushBuilder.class.isAssignableFrom(paramType) ||
(Principal.class.isAssignableFrom(paramType) && !parameter.hasParameterAnnotations()) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

Class<?> paramType = parameter.getParameterType();

// WebRequest / NativeWebRequest / ServletWebRequest
if (WebRequest.class.isAssignableFrom(paramType)) {
if (!paramType.isInstance(webRequest)) {
throw new IllegalStateException(
"Current request is not of type [" + paramType.getName() + "]: " + webRequest);
}
return webRequest;
}

// ServletRequest / HttpServletRequest / MultipartRequest / MultipartHttpServletRequest
if (ServletRequest.class.isAssignableFrom(paramType) || MultipartRequest.class.isAssignableFrom(paramType)) {
return resolveNativeRequest(webRequest, paramType);
}

// HttpServletRequest required for all further argument types
return resolveArgument(paramType, resolveNativeRequest(webRequest, HttpServletRequest.class));
}

private <T> T resolveNativeRequest(NativeWebRequest webRequest, Class<T> requiredType) {
T nativeRequest = webRequest.getNativeRequest(requiredType);
if (nativeRequest == null) {
throw new IllegalStateException(
"Current request is not of type [" + requiredType.getName() + "]: " + webRequest);
}
return nativeRequest;
}

@Nullable
private Object resolveArgument(Class<?> paramType, HttpServletRequest request) throws IOException {
if (HttpSession.class.isAssignableFrom(paramType)) {
HttpSession session = request.getSession();
if (session != null && !paramType.isInstance(session)) {
throw new IllegalStateException(
"Current session is not of type [" + paramType.getName() + "]: " + session);
}
return session;
}
else if (PushBuilder.class.isAssignableFrom(paramType)) {
PushBuilder pushBuilder = request.newPushBuilder();
if (pushBuilder != null && !paramType.isInstance(pushBuilder)) {
throw new IllegalStateException(
"Current push builder is not of type [" + paramType.getName() + "]: " + pushBuilder);
}
return pushBuilder;
}
else if (InputStream.class.isAssignableFrom(paramType)) {
InputStream inputStream = request.getInputStream();
if (inputStream != null && !paramType.isInstance(inputStream)) {
throw new IllegalStateException(
"Request input stream is not of type [" + paramType.getName() + "]: " + inputStream);
}
return inputStream;
}
else if (Reader.class.isAssignableFrom(paramType)) {
Reader reader = request.getReader();
if (reader != null && !paramType.isInstance(reader)) {
throw new IllegalStateException(
"Request body reader is not of type [" + paramType.getName() + "]: " + reader);
}
return reader;
}
else if (Principal.class.isAssignableFrom(paramType)) {
Principal userPrincipal = request.getUserPrincipal();
if (userPrincipal != null && !paramType.isInstance(userPrincipal)) {
throw new IllegalStateException(
"Current user principal is not of type [" + paramType.getName() + "]: " + userPrincipal);
}
return userPrincipal;
}
else if (HttpMethod.class == paramType) {
return HttpMethod.valueOf(request.getMethod());
}
else if (Locale.class == paramType) {
return RequestContextUtils.getLocale(request);
}
else if (TimeZone.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone : TimeZone.getDefault());
}
else if (ZoneId.class == paramType) {
TimeZone timeZone = RequestContextUtils.getTimeZone(request);
return (timeZone != null ? timeZone.toZoneId() : ZoneId.systemDefault());
}

// Should never happen...
throw new UnsupportedOperationException("Unknown parameter type: " + paramType.getName());
}

}

用例:

1
2
3
4
5
6
7
8
9
10
11
@Controller
public class RequestController {

@GetMapping("/goto")
public String goToPage(HttpServletRequest request){

request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success请求
}
}

34、请求处理-【源码分析】-Model、Map原理

复杂参数:

  • Map

  • Model(map、model里面的数据会被放在request的请求域 request.setAttribute)

  • Errors/BindingResult

  • RedirectAttributes( 重定向携带数据)

  • ServletResponse(response)

  • SessionStatus

  • UriComponentsBuilder

  • ServletUriComponentsBuilder

用例:

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
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");

Cookie cookie = new Cookie("c1","v1");
response.addCookie(cookie);
return "forward:/success";
}

@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg,
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");

Map<String,Object> map = new HashMap<>();
Object hello = request.getAttribute("hello");//得出testParam方法赋予的值 world666
Object world = request.getAttribute("world");//得出testParam方法赋予的值 hello666
Object message = request.getAttribute("message");//得出testParam方法赋予的值 HelloWorld

map.put("reqMethod_msg",msg1);
map.put("annotation_msg",msg);
map.put("hello",hello);
map.put("world",world);
map.put("message",message);

return map;
}
  • Map<String,Object> map

  • Model model

  • HttpServletRequest request

上面三类都是可以向request域中放数据,用request.getAttribute()获取

接下来我们看看,Map<String,Object> mapModel model用什么参数处理器

Map<String,Object> map参数用MapMethodProcessor处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

public boolean supportsParameter(MethodParameter parameter) {
Class<?> type = parameter.getParameterType();
return (type.isAssignableFrom(Map.class) || ModelMap.class.isAssignableFrom(type)) && parameter.getParameterAnnotations().length == 0;
}

@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
return mavContainer.getModel();
}

...

}

mavContainer.getModel()如下:

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
public class ModelAndViewContainer {

...

private final ModelMap defaultModel = new BindingAwareModelMap();

@Nullable
private ModelMap redirectModel;

...

public ModelMap getModel() {
if (this.useDefaultModel()) {
return this.defaultModel;
} else {
if (this.redirectModel == null) {
this.redirectModel = new ModelMap();
}

return this.redirectModel;
}
}

private boolean useDefaultModel() {
return !this.redirectModelScenario || this.redirectModel == null && !this.ignoreDefaultModelOnRedirect;
}

...

}

Model modelModelMethodProcessor处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {

public boolean supportsParameter(MethodParameter parameter) {
return Model.class.isAssignableFrom(parameter.getParameterType());
}

@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
Assert.state(mavContainer != null, "ModelAndViewContainer is required for model exposure");
return mavContainer.getModel();
}
...
}

return mavContainer.getModel()这与MapMethodProcessor一致

image-20250531100731308

Model也是另一种意义的Map

接下来看看Map<String,Object> mapModel model值是如何做到用request.getAttribute()获取的

众所周知,所有的数据都放在 ModelAndView 包含要去的页面地址View,还包含Model数据。

先看ModelAndView接下来是如何处理的?

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public class DispatcherServlet extends FrameworkServlet {

......

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
......

try {
ModelAndView mv = null;

......

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

......

}

......

//处理分发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
......

}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

......

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);

......

}

......

}

protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);

View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request);
if (view == null) {
throw new ServletException("Could not resolve view with name '" + mv.getViewName() +
"' in servlet with name '" + getServletName() + "'");
}
}
else {
// No need to lookup: the ModelAndView object contains the actual View object.
view = mv.getView();
if (view == null) {
throw new ServletException("ModelAndView [" + mv + "] neither contains a view name nor a " +
"View object in servlet with name '" + getServletName() + "'");
}
}

if (view instanceof SmartView smartView) {
smartView.resolveNestedViews(this::resolveViewNameInternal, locale);
}

// Delegate to the View object for rendering.
if (logger.isTraceEnabled()) {
logger.trace("Rendering view [" + view + "] ");
}
try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response);
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Error rendering view [" + view + "]", ex);
}
throw ex;
}
}

}

在Debug模式下,viewInternalResourceView类。

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
public class InternalResourceView extends AbstractUrlBasedView {

@Override
//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {

......

Map<String, Object> mergedModel = this.createMergedOutputModel(model, request, response);
this.prepareResponse(request, response);
//看下一个方法实现
this.renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
}

@Override
protected void renderMergedOutputModel(
Map<String, Object> model, HttpServletRequest request, HttpServletResponse response) throws Exception {

// Expose the model object as request attributes.
// 暴露模型作为请求域属性
this.exposeModelAsRequestAttributes(model, request);//<---✨✨✨

// Expose helpers as request attributes, if any.
this.exposeHelpers(request);

// Determine the path for the request dispatcher.
String dispatcherPath = this.prepareForRendering(request, response);

// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = this.getRequestDispatcher(request, dispatcherPath);

...
}

//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
protected void exposeModelAsRequestAttributes(Map<String, Object> model,
HttpServletRequest request) throws Exception {

model.forEach((name, value) -> {
if (value != null) {
request.setAttribute(name, value);
}
else {
request.removeAttribute(name);
}
});
}

}

exposeModelAsRequestAttributes方法看出,Map<String,Object> mapModel model这两种类型数据可以给request域中放数据,用request.getAttribute()获取。

35、请求处理-【源码分析】-自定义参数绑定原理

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
public class ParameterTestController {

/**
* 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定
* @param person
* @return
*/
@PostMapping("/saveuser")
public Person saveuser(Person person){
return person;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* 姓名: <input name="userName"/> <br/>
* 年龄: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 宠物姓名:<input name="pet.name"/><br/>
* 宠物年龄:<input name="pet.age"/>
*/
@Data
public class Person {

private String userName;
private Integer age;
private Date birth;
private Pet pet;

}

@Data
public class Pet {

private String name;
private String age;

}

封装过程用到ServletModelAttributeMethodProcessor

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
72
73
74
75
76
77
78
79
80
81
82
83
84
public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {

@Override
//本方法在ModelAttributeMethodProcessor类
public boolean supportsParameter(MethodParameter parameter) {
return (parameter.hasParameterAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType())));
}

@Override
@Nullable
//本方法在ModelAttributeMethodProcessor类
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

...

String name = ModelFactory.getNameForParameter(parameter);
ModelAttribute ann = (ModelAttribute)parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}

BindingResult bindingResult = null;
Object attribute;

if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
if (attribute == null || ObjectUtils.unwrapOptional(attribute) == null) {
bindingResult = binderFactory.createBinder(webRequest, null, name).getBindingResult();
attribute = wrapAsOptionalIfNecessary(parameter, null);
}
}
else {
try {
// Mainly to allow subclasses alternative to create attribute
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (MethodArgumentNotValidException ex) {
if (isBindExceptionRequired(parameter)) {
throw ex;
}
attribute = wrapAsOptionalIfNecessary(parameter, ex.getTarget());
bindingResult = ex.getBindingResult();
}
}

if (bindingResult == null) {
ResolvableType type = ResolvableType.forMethodParameter(parameter);
//创建web数据绑定器。将请求参数的值绑定到指定的Java Bean中
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name, type);//✨✨✨
if (attribute == null) {
constructAttribute(binder, webRequest);
attribute = wrapAsOptionalIfNecessary(parameter, binder.getTarget());
}

if (!binder.getBindingResult().hasErrors()) {
if (!mavContainer.isBindingDisabled(name)) {
//参数绑定(利用反射获取参数值,注入bean中)
bindRequestParameters(binder, webRequest);//✨✨✨
}
validateIfApplicable(binder, parameter);
}

if (binder.getBindingResult().hasErrors() && this.isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
//获取bind结果
bindingResult = binder.getBindingResult();//✨✨✨
}

// Add resolved attribute and BindingResult at the end of the model
Map<String, Object> bindingResultModel = bindingResult.getModel();
mavContainer.removeAttributes(bindingResultModel);
mavContainer.addAllAttributes(bindingResultModel);

return attribute;

}
}

WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中

在过程当中,用到GenericConversionService:在设置每一个值的时候,找它里面的所有converter哪个可以将这个数据类型(request带来参数的字符串)转换到指定的类型

image-20250531105051194

1
2
3
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
((WebRequestDataBinder) binder).bind(request);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void bind(WebRequest request) {
if (shouldNotBindPropertyValues()) {
return;
}
//获取原生request中的所有键值对
MutablePropertyValues mpvs = new MutablePropertyValues(request.getParameterMap()); //✨✨✨
if (request instanceof NativeWebRequest nativeRequest) {
MultipartRequest multipartRequest = nativeRequest.getNativeRequest(MultipartRequest.class);
if (multipartRequest != null) {
bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
}
else if (StringUtils.startsWithIgnoreCase(
request.getHeader(HttpHeaders.CONTENT_TYPE), MediaType.MULTIPART_FORM_DATA_VALUE)) {
HttpServletRequest servletRequest = nativeRequest.getNativeRequest(HttpServletRequest.class);
if (servletRequest != null && HttpMethod.POST.matches(servletRequest.getMethod())) {
StandardServletPartUtils.bindParts(servletRequest, mpvs, isBindEmptyMultipartFiles());
}
}
}
doBind(mpvs); //✨✨✨
}
1
2
3
4
5
6
7
@Override
protected void doBind(MutablePropertyValues mpvs) {
checkFieldDefaults(mpvs);
checkFieldMarkers(mpvs);
adaptEmptyArrayIndices(mpvs);
super.doBind(mpvs); //✨✨✨
}
1
2
3
4
5
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);//✨✨✨
}
1
2
3
4
5
6
7
8
9
10
11
12
protected void applyPropertyValues(MutablePropertyValues mpvs) {
try {
// Bind request parameters onto target object.
getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); //✨✨✨
}
catch (PropertyBatchUpdateException ex) {
// Use bind error processor to create FieldErrors.
for (PropertyAccessException pae : ex.getPropertyAccessExceptions()) {
getBindingErrorProcessor().processPropertyAccessException(pae, getInternalBindingResult());
}
}
}
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
@Override
//方法在AbstractPropertyAccessor中
public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid)
throws BeansException {

List<PropertyAccessException> propertyAccessExceptions = null;
List<PropertyValue> propertyValues = (pvs instanceof MutablePropertyValues mpvs ?
mpvs.getPropertyValueList() : Arrays.asList(pvs.getPropertyValues()));

if (ignoreUnknown) {
this.suppressNotWritablePropertyException = true;
}
try {
for (PropertyValue pv : propertyValues) {
// setPropertyValue may throw any BeansException, which won't be caught
// here, if there is a critical failure such as no matching field.
// We can attempt to deal only with less serious exceptions.
try {
setPropertyValue(pv); //✨✨✨
}

.......

}
}
}
1
2
3
4
@Override
public void setPropertyValue(PropertyValue pv) throws BeansException {
setPropertyValue(pv.getName(), pv.getValue());
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
//该方法在AbstractNestablePropertyAccessor中
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
AbstractNestablePropertyAccessor nestedPa;
try {
//属性访问器
nestedPa = getPropertyAccessorForPropertyPath(propertyName); //✨✨✨
}
catch (NotReadablePropertyException ex) {
throw new NotWritablePropertyException(getRootClass(), this.nestedPath + propertyName,
"Nested property in path '" + propertyName + "' does not exist", ex);
}
PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName));
//设置属性值
nestedPa.setPropertyValue(tokens, new PropertyValue(propertyName, value));//✨✨✨
}
1
2
3
4
5
6
7
8
protected void setPropertyValue(PropertyTokenHolder tokens, PropertyValue pv) throws BeansException {
if (tokens.keys != null) {
processKeyedProperty(tokens, pv);
}
else {
processLocalProperty(tokens, pv);
}
}
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
private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) {
PropertyHandler ph = getLocalPropertyHandler(tokens.actualName);
if (ph == null || !ph.isWritable()) {
if (pv.isOptional()) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring optional value for property '" + tokens.actualName +
"' - property not found on bean class [" + getRootClass().getName() + "]");
}
return;
}
if (this.suppressNotWritablePropertyException) {
// Optimization for common ignoreUnknown=true scenario since the
// exception would be caught and swallowed higher up anyway...
return;
}
throw createNotWritablePropertyException(tokens.canonicalName);
}

Object oldValue = null;
try {
Object originalValue = pv.getValue();
Object valueToApply = originalValue;
if (!Boolean.FALSE.equals(pv.conversionNecessary)) {
if (pv.isConverted()) {
valueToApply = pv.getConvertedValue();
}
else {
if (isExtractOldValueForEditor() && ph.isReadable()) {
try {
oldValue = ph.getValue();
}
catch (Exception ex) {
if (ex instanceof PrivilegedActionException pae) {
ex = pae.getException();
}
if (logger.isDebugEnabled()) {
logger.debug("Could not read previous value of property '" +
this.nestedPath + tokens.canonicalName + "'", ex);
}
}
}
//属性转换 ✨✨✨
valueToApply = convertForProperty(
tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor());
}
pv.getOriginalPropertyValue().conversionNecessary = (valueToApply != originalValue);
}
ph.setValue(valueToApply);
}

......

}
1
2
3
4
5
6
7
8
@Nullable
//该方法在AbstractNestablePropertyAccessor中
protected Object convertForProperty(
String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td)
throws TypeMismatchException {

return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Nullable
private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue,
@Nullable Object newValue, @Nullable Class<?> requiredType, @Nullable TypeDescriptor td)
throws TypeMismatchException {

Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate");
try {
//同样的设计模式 ✨✨✨
return this.typeConverterDelegate.convertIfNecessary(propertyName, oldValue, newValue, requiredType, td);
}

......

}
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
/**
* 将给定值转换为所需的类型(如果必要),支持多种类型转换机制。
*
* <p>该方法尝试按照优先级顺序进行类型转换:
* 1. 使用注册的 PropertyEditor;
* 2. 使用 ConversionService;
* 3. 使用内置的标准类型转换规则(如数组、集合、Map、Number、String构造器等);
* 4. 最后尝试枚举或 String 构造函数方式。
*
* @param propertyName 属性名(可选,用于日志/异常信息)
* @param oldValue 原始值(可能为 null,用于 PropertyEditor 上下文)
* @param newValue 要转换的新值(可能为 null)
* @param requiredType 目标类型(可能为 null)
* @param typeDescriptor 更丰富的目标类型描述符(可能为 null)
* @param <T> 泛型目标类型
* @return 转换后的值,类型为 T
* @throws IllegalArgumentException 如果无法完成转换
*/
@Nullable
public <T> T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue,
@Nullable Class<T> requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException {

// 查找是否注册了自定义的 PropertyEditor
PropertyEditor editor = this.propertyEditorRegistry.findCustomEditor(requiredType, propertyName);

// 存储转换失败的异常,用于后续 fallback
ConversionFailedException conversionAttemptEx = null;

// 如果没有自定义编辑器,并且配置了 ConversionService,则使用其进行转换
ConversionService conversionService = this.propertyEditorRegistry.getConversionService();
if (editor == null && conversionService != null && newValue != null && typeDescriptor != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) { // 1️⃣✨✨✨
try {
//2️⃣✨✨✨
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
catch (ConversionFailedException ex) {
// 记录异常并 fallback 到默认转换逻辑
conversionAttemptEx = ex;
}
}
}

// 默认将 convertedValue 设为原始值
Object convertedValue = newValue;

// 如果当前值不是目标类型,尝试通过 PropertyEditor 进行转换
if (editor != null || (requiredType != null && !ClassUtils.isAssignableValue(requiredType, convertedValue))) {
// 针对 Collection 类型处理元素级别转换
if (typeDescriptor != null && requiredType != null && Collection.class.isAssignableFrom(requiredType)) {
TypeDescriptor elementTypeDesc = typeDescriptor.getElementTypeDescriptor();
if (elementTypeDesc != null) {
Class<?> elementType = elementTypeDesc.getType();
if (convertedValue instanceof String text) {
if (Class.class == elementType || Enum.class.isAssignableFrom(elementType)) {
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
}
if (editor == null && String.class != elementType) {
editor = findDefaultEditor(elementType.arrayType());
}
}
}
}
if (editor == null) {
editor = findDefaultEditor(requiredType); // 获取默认 PropertyEditor
}
convertedValue = doConvertValue(oldValue, convertedValue, requiredType, editor); // 执行转换
}

boolean standardConversion = false;

if (requiredType != null) {
if (convertedValue != null) {
// 如果目标类型是 Object,则直接返回原值
if (Object.class == requiredType) {
return (T) convertedValue;
}

// 处理数组类型转换
else if (requiredType.isArray()) {
if (convertedValue instanceof String text &&
Enum.class.isAssignableFrom(requiredType.componentType())) {
convertedValue = StringUtils.commaDelimitedListToStringArray(text);
}
return (T) convertToTypedArray(convertedValue, propertyName, requiredType.componentType());
}

// 如果输入是数组,输出需要 Collection 或单个元素
else if (convertedValue.getClass().isArray()) {
if (Collection.class.isAssignableFrom(requiredType)) {
convertedValue = convertToTypedCollection(CollectionUtils.arrayToList(convertedValue),
propertyName, requiredType, typeDescriptor);
standardConversion = true;
}
else if (Array.getLength(convertedValue) == 1) {
convertedValue = Array.get(convertedValue, 0);
standardConversion = true;
}
}

// 输入是 Collection,尝试转换为泛型集合
else if (convertedValue instanceof Collection<?> coll) {
convertedValue = convertToTypedCollection(coll, propertyName, requiredType, typeDescriptor);
standardConversion = true;
}

// 输入是 Map,尝试转换键值对类型
else if (convertedValue instanceof Map<?, ?> map) {
convertedValue = convertToTypedMap(map, propertyName, requiredType, typeDescriptor);
standardConversion = true;
}

// 如果目标类型是 String,且原值是基本类型,则调用 toString()
if (String.class == requiredType && ClassUtils.isPrimitiveOrWrapper(convertedValue.getClass())) {
return (T) convertedValue.toString();
}

// 如果值是字符串但目标类型不匹配,尝试通过 String 构造器创建实例
else if (convertedValue instanceof String text && !requiredType.isInstance(convertedValue)) {
if (conversionAttemptEx == null && !requiredType.isInterface() && !requiredType.isEnum()) {
try {
Constructor<T> strCtor = requiredType.getConstructor(String.class);
return BeanUtils.instantiateClass(strCtor, convertedValue);
}
catch (NoSuchMethodException ex) {
if (logger.isTraceEnabled()) {
logger.trace("No String constructor found on type [" + requiredType.getName() + "]", ex);
}
}
catch (Exception ex) {
if (logger.isDebugEnabled()) {
logger.debug("Construction via String failed for type [" + requiredType.getName() + "]", ex);
}
}
}

// 特殊处理空枚举值
String trimmedValue = text.trim();
if (requiredType.isEnum() && trimmedValue.isEmpty()) {
return null;
}

// 尝试将字符串转换为枚举
convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue);
standardConversion = true;
}

// 数字类型转换
else if (convertedValue instanceof Number num && Number.class.isAssignableFrom(requiredType)) {
convertedValue = NumberUtils.convertNumberToTargetClass(num, (Class<Number>) requiredType);
standardConversion = true;
}
}
else {
// 处理 null 值,例如 Optional 类型
if (requiredType == Optional.class) {
convertedValue = Optional.empty();
}
}

// 检查最终转换结果是否符合目标类型
if (!ClassUtils.isAssignableValue(requiredType, convertedValue)) {
if (conversionAttemptEx != null) {
throw conversionAttemptEx;
}
else if (conversionService != null && typeDescriptor != null) {
TypeDescriptor sourceTypeDesc = TypeDescriptor.forObject(newValue);
if (conversionService.canConvert(sourceTypeDesc, typeDescriptor)) {
return (T) conversionService.convert(newValue, sourceTypeDesc, typeDescriptor);
}
}

// 抛出转换失败异常
StringBuilder msg = new StringBuilder();
msg.append("Cannot convert value of type '").append(ClassUtils.getDescriptiveType(newValue));
msg.append("' to required type '").append(ClassUtils.getQualifiedName(requiredType)).append('\'');
if (propertyName != null) {
msg.append(" for property '").append(propertyName).append('\'');
}
if (editor != null) {
msg.append(": PropertyEditor [").append(editor.getClass().getName()).append(
"] returned inappropriate value of type '").append(
ClassUtils.getDescriptiveType(convertedValue)).append('\'');
throw new IllegalArgumentException(msg.toString());
}
else {
msg.append(": no matching editors or conversion strategy found");
throw new IllegalStateException(msg.toString());
}
}
}

// 如果之前 ConversionService 报错但最终成功,记录 debug 日志
if (conversionAttemptEx != null) {
if (editor == null && !standardConversion && requiredType != null && Object.class != requiredType) {
throw conversionAttemptEx;
}
logger.debug("Original ConversionService attempt failed - ignored since " +
"PropertyEditor based conversion eventually succeeded", conversionAttemptEx);
}

// 返回最终转换后的值
return (T) convertedValue;
}

1
2
3
4
5
6
@Override
//方法在GenericConversionService中1️⃣
public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
return (sourceType == null || getConverter(sourceType, targetType) != null);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Nullable
//与参数解析器 返回值解析器 同样的设计模式1️⃣
protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) {
ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType);
GenericConverter converter = this.converterCache.get(key);
if (converter != null) {
return (converter != NO_MATCH ? converter : null);
}

converter = this.converters.find(sourceType, targetType); //✨✨✨
if (converter == null) {
converter = getDefaultConverter(sourceType, targetType);
}

if (converter != null) {
this.converterCache.put(key, converter);
return converter;
}

this.converterCache.put(key, NO_MATCH);
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Nullable
//1️⃣
public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) {
// Search the full type hierarchy
List<Class<?>> sourceCandidates = getClassHierarchy(sourceType.getType());
List<Class<?>> targetCandidates = getClassHierarchy(targetType.getType());
for (Class<?> sourceCandidate : sourceCandidates) {
for (Class<?> targetCandidate : targetCandidates) {
ConvertiblePair convertiblePair = new ConvertiblePair(sourceCandidate, targetCandidate);
GenericConverter converter = getRegisteredConverter(sourceType, targetType, convertiblePair);
if (converter != null) {
return converter;
}
}
}
return null;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@Override
@Nullable
//2️⃣ 方法在GenericConversionService中
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
// 获取到对应的converter ✨✨✨
GenericConverter converter = getConverter(sourceType, targetType);
if (converter != null) {
// 利用ConversionUtils进行转换 ✨✨✨
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Nullable
public static Object invokeConverter(GenericConverter converter, @Nullable Object source,
TypeDescriptor sourceType, TypeDescriptor targetType) {

try {
// 调用对应convert方法 ✨✨✨
return converter.convert(source, sourceType, targetType);
}
catch (ConversionFailedException ex) {
throw ex;
}
catch (Throwable ex) {
throw new ConversionFailedException(sourceType, targetType, source, ex);
}
}

36、请求处理-【源码分析】-自定义Converter原理

未来我们可以给WebDataBinder里面放自己的Converter;

下面演示将字符串“啊猫,3”转换成Pet对象。

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
//WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {

@Override
public Pet convert(String source) {
// 啊猫,3
if(!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] split = source.split(",");
pet.setName(split[0]);
pet.setAge(Integer.parseInt(split[1]));
return pet;
}
return null;
}
});
}
};
}

37、响应处理-【源码分析】-ReturnValueHandler原理

假设给前端自动返回json数据,需要引入相关的依赖

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
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- web场景自动引入了json场景 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
<!--使用jackson作为底层的json处理依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jdk8</artifactId>
<version>2.18.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.2</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-parameter-names</artifactId>
<version>2.18.2</version>
<scope>compile</scope>
</dependency>

控制层代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class ResponseTestController {

@ResponseBody //利用返回值处理器里面的消息转换器进行处理
@GetMapping(value = "/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setUserName("zhangsan");
return person;
}

}

32、请求处理-【源码分析】-各种类型参数解析原理 - 返回值处理器有讨论ReturnValueHandler。现在直接看看重点:

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
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

// Obtain wrapped response to enforce lifecycle rule from Servlet spec, section 2.3.3.4
response = asyncWebRequest.getNativeResponse(HttpServletResponse.class);

ServletWebRequest webRequest = (asyncWebRequest instanceof ServletWebRequest ?
(ServletWebRequest) asyncWebRequest : new ServletWebRequest(request, response)); //✨✨✨

WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); //✨✨✨
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);//✨✨✨
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);
invocableMethod.setMethodValidator(this.methodValidator);

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
Object[] resultContext = asyncManager.getConcurrentResultContext();
Assert.state(resultContext != null && resultContext.length > 0, "Missing result context");
mavContainer = (ModelAndViewContainer) resultContext[0];
asyncManager.clearConcurrentResult();
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(result, !traceOn);
return "Resume with async result [" + formatted + "]";
});
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

invocableMethod.invokeAndHandle(webRequest, mavContainer); //✨✨✨
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, Object... providedArgs) throws Exception {

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);

......

try {
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}

......

}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {

......

@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); // ⬇️ 1️⃣
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);//⬇️ 2️⃣
}

......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Nullable
//1️⃣
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}

控制器方法上标注了@ResponseBody 注解,即RequestResponseBodyMethodProcessor,它实现HandlerMethodReturnValueHandler接口

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
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {	

......

@Override
//2️⃣
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

if (returnValue instanceof ProblemDetail detail) {
outputMessage.setStatusCode(HttpStatusCode.valueOf(detail.getStatus()));
if (detail.getInstance() == null) {
URI path = URI.create(inputMessage.getServletRequest().getRequestURI());
detail.setInstance(path);
}
invokeErrorResponseInterceptors(detail, null);
}

// Try even with null return value. ResponseBodyAdvice could get involved.
// 使用消息转换器进行写出操作,本方法下一章节介绍:
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}

SpringMVC所支持的返回值类型

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
//1️⃣ModelAndViewMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
if (Collection.class.isAssignableFrom(type)) {
type = returnType.nested().getNestedParameterType();
}
return (ModelAndView.class.isAssignableFrom(type) || FragmentsRendering.class.isAssignableFrom(type));
}
//2️⃣ModelMethodProcessor
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Model.class.isAssignableFrom(returnType.getParameterType());
}
//3️⃣ViewMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return View.class.isAssignableFrom(returnType.getParameterType());
}
//4️⃣ResponseBodyEmitterReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> bodyType = ResponseEntity.class.isAssignableFrom(returnType.getParameterType()) ?
ResolvableType.forMethodParameter(returnType).getGeneric().resolve() :
returnType.getParameterType();

return (bodyType != null && (ResponseBodyEmitter.class.isAssignableFrom(bodyType) ||
this.reactiveHandler.isReactiveType(bodyType)));
}
//5️⃣StreamingResponseBodyReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
if (StreamingResponseBody.class.isAssignableFrom(returnType.getParameterType())) {
return true;
}
else if (ResponseEntity.class.isAssignableFrom(returnType.getParameterType())) {
Class<?> bodyType = ResolvableType.forMethodParameter(returnType).getGeneric().resolve();
return (bodyType != null && StreamingResponseBody.class.isAssignableFrom(bodyType));
}
return false;
}
//6️⃣HttpEntityMethodProcessor
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
return ((HttpEntity.class.isAssignableFrom(type) && !RequestEntity.class.isAssignableFrom(type)) ||
ErrorResponse.class.isAssignableFrom(type) || ProblemDetail.class.isAssignableFrom(type));
}
//7️⃣HttpHeadersReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return HttpHeaders.class.isAssignableFrom(returnType.getParameterType());
}
//8️⃣CallableMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Callable.class.isAssignableFrom(returnType.getParameterType());
}
//9️⃣DeferredResultMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> type = returnType.getParameterType();
return (DeferredResult.class.isAssignableFrom(type) ||
org.springframework.util.concurrent.ListenableFuture.class.isAssignableFrom(type) ||
CompletionStage.class.isAssignableFrom(type));
}
//🔟AsyncTaskMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return WebAsyncTask.class.isAssignableFrom(returnType.getParameterType());
}
//1️⃣1️⃣ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (returnType.hasMethodAnnotation(ModelAttribute.class) ||
(this.annotationNotRequired && !BeanUtils.isSimpleProperty(returnType.getParameterType())));
}
//1️⃣2️⃣RequestResponseBodyMethodProcessor
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) ||
returnType.hasMethodAnnotation(ResponseBody.class));
}
//1️⃣3️⃣ViewNameMethodReturnValueHandler
@Override
public boolean supportsReturnType(MethodParameter returnType) {
Class<?> paramType = returnType.getParameterType();
return (void.class == paramType || CharSequence.class.isAssignableFrom(paramType));
}
//1️⃣4️⃣MapMethodProcessor
@Override
public boolean supportsReturnType(MethodParameter returnType) {
return Map.class.isAssignableFrom(returnType.getParameterType());
}

38、响应处理-【源码分析】-HTTPMessageConverter原理

返回值处理器ReturnValueHandler原理:

  • 返回值处理器判断是否支持这种类型返回值 supportsReturnType

  • 返回值处理器调用 handleReturnValue 进行处理

  • RequestResponseBodyMethodProcessor 可以处理返回值标了@ResponseBody 注解的

    • 利用 MessageConverters 进行处理 将数据写为json
      • 内容协商(浏览器默认会以请求头的方式告诉服务器它能接受什么样的内容类型)

      • 服务器最终根据自己自身的能力,决定生产出什么样内容类型的数据

      • SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter,看谁能处理

        • 得到MappingJackson2HttpMessageConverter可以将对象写为json

        • 利用MappingJackson2HttpMessageConverter将对象转为json再写出去

image-20250507222622767

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* 将控制器方法返回值通过合适的 HttpMessageConverter 写入 HTTP 响应中。
*
* @param value 控制器方法返回值(可能为 null)
* @param returnType 控制器方法的返回类型元信息
* @param inputMessage 当前请求封装对象
* @param outputMessage 当前响应封装对象
* @throws IOException 如果 IO 异常
* @throws HttpMediaTypeNotAcceptableException 如果客户端不接受任何可生产的媒体类型
* @throws HttpMessageNotWritableException 如果没有合适的消息转换器
*/
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

// 定义 body、valueType 和 targetType 用于后续判断和转换
Object body;
Class<?> valueType;
Type targetType;

// 如果返回值是 CharSequence(如 String),则直接使用字符串处理
if (value instanceof CharSequence) {
body = value.toString();
valueType = String.class;
targetType = String.class;
}
else {
// 否则获取返回值的实际类型和泛型目标类型
body = value;
valueType = getReturnValueType(body, returnType);
targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass());
}

// 判断是否是 Resource 类型(如 FileSystemResource、ByteArrayResource 等)
if (isResourceType(value, returnType)) {
// 设置 Accept-Ranges 响应头,支持范围请求
outputMessage.getHeaders().set(HttpHeaders.ACCEPT_RANGES, "bytes");
// 如果存在 Range 请求头,并且状态码是 200,则处理分段响应
if (value != null && inputMessage.getHeaders().getFirst(HttpHeaders.RANGE) != null &&
outputMessage.getServletResponse().getStatus() == 200) {
Resource resource = (Resource) value;
try {
List<HttpRange> httpRanges = inputMessage.getHeaders().getRange();
// 设置状态码为 206 Partial Content
outputMessage.getServletResponse().setStatus(HttpStatus.PARTIAL_CONTENT.value());
// 将资源按 Range 分割成多个 Region
body = HttpRange.toResourceRegions(httpRanges, resource);
valueType = body.getClass();
targetType = RESOURCE_REGION_LIST_TYPE; // 指定目标类型为 List<ResourceRegion>
}
catch (IllegalArgumentException ex) {
// 如果 Range 不合法,设置 416 Requested Range Not Satisfiable
outputMessage.getHeaders().set(HttpHeaders.CONTENT_RANGE, "bytes */" + resource.contentLength());
outputMessage.getServletResponse().setStatus(HttpStatus.REQUESTED_RANGE_NOT_SATISFIABLE.value());
}
}
}

// 初始化 selectedMediaType,表示最终选择的 MediaType
MediaType selectedMediaType = null;
// 获取输出消息中的 Content-Type
MediaType contentType = outputMessage.getHeaders().getContentType();
// 判断是否已经设置了具体的 Content-Type
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();

// 获取客户端 Accept 头指定的可接受 MediaType 列表
List<MediaType> acceptableTypes;
try {
acceptableTypes = getAcceptableMediaTypes(request); //1️⃣✨✨✨
}
catch (HttpMediaTypeNotAcceptableException ex) {
// 如果无法解析 Accept 头,并且当前是错误响应(4xx 或 5xx),则忽略内容体
int series = outputMessage.getServletResponse().getStatus() / 100;
if (body == null || series == 4 || series == 5) {
if (logger.isDebugEnabled()) {
logger.debug("Ignoring error response content (if any). " + ex);
}
return;
}
throw ex;
}

// 获取该返回值类型支持的可生产 MediaType 列表
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);//2️⃣✨✨✨

// 如果没有可用的转换器且 body 不为 null,抛出异常
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}

// 找出 accept 和 produce 兼容的 MediaType 列表
List<MediaType> compatibleMediaTypes = new ArrayList<>();
determineCompatibleMediaTypes(acceptableTypes, producibleTypes, compatibleMediaTypes);//3️⃣✨✨✨

// 对于 ProblemDetail,默认回退到 RFC 9457 格式
if (compatibleMediaTypes.isEmpty() && ProblemDetail.class.isAssignableFrom(valueType)) {
determineCompatibleMediaTypes(this.problemMediaTypes, producibleTypes, compatibleMediaTypes);
}

// 如果兼容列表为空,说明没有匹配的 MediaType
if (compatibleMediaTypes.isEmpty()) {
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
return;
}

// 按照 specificity 排序,优先匹配最具体的类型(如 application/json > application/*)
MimeTypeUtils.sortBySpecificity(compatibleMediaTypes);

// 遍历找到第一个 concrete 的 MediaType(非通配符类型)
for (MediaType mediaType : compatibleMediaTypes) {//✨✨✨
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
// 如果是 application/* 类型,使用 application/octet-stream 作为默认 fallback
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}

if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}

// 如果找到了合适的 MediaType
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue(); // 去除 q=xxx 参数

ResolvableType targetResolvableType = null;

// 遍历所有注册的消息转换器 ✨✨✨
for (HttpMessageConverter converter : this.messageConverters) {
ConverterType converterTypeToUse = null;

// 支持泛型的转换器
if (converter instanceof GenericHttpMessageConverter genericConverter) {
if (genericConverter.canWrite(targetType, valueType, selectedMediaType)) {
converterTypeToUse = ConverterType.GENERIC;
}
}
// Smart 转换器,支持 ResolvableType
else if (converter instanceof SmartHttpMessageConverter smartConverter) {
targetResolvableType = getNestedTypeIfNeeded(ResolvableType.forMethodParameter(returnType));
if (smartConverter.canWrite(targetResolvableType, valueType, selectedMediaType)) {
converterTypeToUse = ConverterType.SMART;
}
}
// 基础转换器
else if (converter.canWrite(valueType, selectedMediaType)){
converterTypeToUse = ConverterType.BASE;
}

// 找到合适的转换器后开始写入响应✨✨✨
if (converterTypeToUse != null) {
// 在写入前允许 Advice 修改 body
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(), inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
// 记录调试日志
LogFormatUtils.traceDebug(logger, traceOn ->
"Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]");
// 添加 Content-Disposition 响应头(如文件下载时)
addContentDispositionHeader(inputMessage, outputMessage);

// 根据不同的转换器类型调用 write 方法✨✨✨ 4️⃣
switch (converterTypeToUse) {
case BASE -> converter.write(body, selectedMediaType, outputMessage);
case GENERIC -> ((GenericHttpMessageConverter) converter).write(body, targetType, selectedMediaType, outputMessage);
case SMART -> ((SmartHttpMessageConverter) converter).write(body, targetResolvableType, selectedMediaType, outputMessage, null);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}

// 如果 body 不为 null,但没有任何转换器能处理它
if (body != null) {
Set<MediaType> producibleMediaTypes =
(Set<MediaType>) inputMessage.getServletRequest()
.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

// 如果 Content-Type 已预设或有明确的 producibleMediaTypes,抛出异常
if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) {
throw new HttpMessageNotWritableException(
"No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'");
}
// 否则抛出 MediaType 不可接受异常
throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass()));
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
 //1️⃣AbstractMessageConverterMethodProcessor
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {

return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}


@Override
//ContentNegotiationManager
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
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
/**
* 2️⃣获取指定返回值类型支持的、可以被写入 HTTP 响应的 MediaType 列表。
*
* <p>该方法会优先使用请求上下文中已设置的 producible media types,
* 如果没有,则遍历所有 HttpMessageConverter 来判断是否支持目标类型的写入,
* 并收集其支持的 MediaType。
*
* @param request 当前 HTTP 请求对象
* @param valueClass 返回值的实际类型(如 String, User, Resource 等)
* @param targetType 返回值的泛型信息(可能为 null)
* @return 支持的 MediaType 列表
*/
protected List<MediaType> getProducibleMediaTypes(
HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) {

// 从请求属性中尝试获取预先配置的 producible media types(由 HandlerMapping 设置)
Set<MediaType> mediaTypes =
(Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

// 如果存在预设的 producible media types,直接返回它们的列表
if (!CollectionUtils.isEmpty(mediaTypes)) {
return new ArrayList<>(mediaTypes);
}

// 创建一个 LinkedHashSet 用于保存最终支持的 MediaType,保留插入顺序
Set<MediaType> result = new LinkedHashSet<>();

// 遍历所有注册的消息转换器(messageConverters)
for (HttpMessageConverter<?> converter : this.messageConverters) {

// 如果是 GenericHttpMessageConverter,并且有 targetType:
// 检查它是否能处理该 targetType + valueClass 的组合
if (converter instanceof GenericHttpMessageConverter<?> genericConverter && targetType != null) {
if (genericConverter.canWrite(targetType, valueClass, null)) {
// 添加该转换器支持的所有 MediaType
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}

// 如果是 SmartHttpMessageConverter 并且有 targetType:
// 使用 ResolvableType 进行更精确的支持性检查
else if (converter instanceof SmartHttpMessageConverter<?> smartConverter && targetType != null) {
ResolvableType resolvableType = ResolvableType.forType(targetType);
if (smartConverter.canWrite(resolvableType, valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}

// 对于普通消息转换器,仅根据类类型判断是否支持
else if (converter.canWrite(valueClass, null)) {
result.addAll(converter.getSupportedMediaTypes(valueClass));
}
}

// 如果没有任何匹配的 MediaType,返回 [MediaType.ALL] 表示默认支持所有类型
// 否则返回实际收集到的 MediaType 列表
return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : new ArrayList<>(result));
}

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
/**
* 3️⃣确定客户端可接受(Accept)的 MediaType 与服务器端可生产(Producible)的 MediaType 的兼容组合,
* 并将最匹配的 MediaType 添加到输出列表中。
*
* <p>该方法会遍历所有 Accept 类型和 Produces 类型的组合,找出它们兼容的类型对,
* 并选择其中更具体的那个 MediaType(如 application/json 比 application/* 更具体)。
*
* @param acceptableTypes 客户端声明可以接受的 MediaType 列表(来自 Accept 请求头)
* @param producibleTypes 服务端支持返回的 MediaType 列表(由消息转换器决定)
* @param mediaTypesToUse 输出参数,用于保存最终匹配的 MediaType 列表
*/
private void determineCompatibleMediaTypes(
List<MediaType> acceptableTypes, List<MediaType> producibleTypes, List<MediaType> mediaTypesToUse) {

// 遍历所有客户端可接受的 MediaType
for (MediaType requestedType : acceptableTypes) {
// 遍历所有服务端可生产的 MediaType
for (MediaType producibleType : producibleTypes) {
// 判断两个 MediaType 是否兼容(isCompatibleWith)
// 例如:text/* 与 text/plain 是兼容的;application/json 与 application/json;charset=UTF-8 也是兼容的
if (requestedType.isCompatibleWith(producibleType)) {
// 如果兼容,则取两者中最具体的 MediaType,并加入结果列表
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
}
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
@Override
//4️⃣
public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType,
HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {

final HttpHeaders headers = outputMessage.getHeaders();
addDefaultHeaders(headers, t, contentType);

if (outputMessage instanceof StreamingHttpOutputMessage streamingOutputMessage) {
streamingOutputMessage.setBody(new StreamingHttpOutputMessage.Body() {
@Override
public void writeTo(OutputStream outputStream) throws IOException {
writeInternal(t, type, new HttpOutputMessage() {
@Override
public OutputStream getBody() {
return outputStream;
}

@Override
public HttpHeaders getHeaders() {
return headers;
}
});
}

@Override
public boolean repeatable() {
return supportsRepeatableWrites(t);
}
});
}
else {
writeInternal(t, type, outputMessage);
outputMessage.getBody().flush();
}
}
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
@Override
protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException {

MediaType contentType = outputMessage.getHeaders().getContentType();
JsonEncoding encoding = getJsonEncoding(contentType);

Class<?> clazz = (object instanceof MappingJacksonValue mappingJacksonValue ?
mappingJacksonValue.getValue().getClass() : object.getClass());
ObjectMapper objectMapper = selectObjectMapper(clazz, contentType);
Assert.state(objectMapper != null, () -> "No ObjectMapper for " + clazz.getName());

OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody());
try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding)) {
writePrefix(generator, object);

Object value = object;
Class<?> serializationView = null;
FilterProvider filters = null;
JavaType javaType = null;

if (object instanceof MappingJacksonValue mappingJacksonValue) {
value = mappingJacksonValue.getValue();
serializationView = mappingJacksonValue.getSerializationView();
filters = mappingJacksonValue.getFilters();
}
if (type != null && TypeUtils.isAssignable(type, value.getClass())) {
javaType = getJavaType(type, null);
}

ObjectWriter objectWriter = (serializationView != null ?
objectMapper.writerWithView(serializationView) : objectMapper.writer());
if (filters != null) {
objectWriter = objectWriter.with(filters);
}
if (javaType != null && (javaType.isContainerType() || javaType.isTypeOrSubTypeOf(Optional.class))) {
objectWriter = objectWriter.forType(javaType);
}
SerializationConfig config = objectWriter.getConfig();
if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) &&
config.isEnabled(SerializationFeature.INDENT_OUTPUT)) {
objectWriter = objectWriter.with(this.ssePrettyPrinter);
}
objectWriter = customizeWriter(objectWriter, javaType, contentType);
objectWriter.writeValue(generator, value); //✨✨✨

writeSuffix(generator, object);
generator.flush();
}
catch (InvalidDefinitionException ex) {
throw new HttpMessageConversionException("Type definition error: " + ex.getType(), ex);
}
catch (JsonProcessingException ex) {
throw new HttpMessageNotWritableException("Could not write JSON: " + ex.getOriginalMessage(), ex);
}
}

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
34
/**
* Strategy interface for converting from and to HTTP requests and responses.
*/
public interface HttpMessageConverter<T> {

/**
* Indicates whether the given class can be read by this converter.
*/
boolean canRead(Class<?> clazz, @Nullable MediaType mediaType);

/**
* Indicates whether the given class can be written by this converter.
*/
boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);

/**
* Return the list of {@link MediaType} objects supported by this converter.
*/
List<MediaType> getSupportedMediaTypes();

/**
* Read an object of the given type from the given input message, and returns it.
*/
T read(Class<? extends T> clazz, HttpInputMessage inputMessage)
throws IOException, HttpMessageNotReadableException;

/**
* Write an given object to the given output message.
*/
void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage)
throws IOException, HttpMessageNotWritableException;

}

HttpMessageConverter: 看是否支持将此Class类型的对象,转为MediaType类型的数据。

例子:Person对象转为JSON,或者JSON转为Person

image-20250519214132543这将用到MappingJackson2HttpMessageConverter

1
2
3
4
5
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {

......

}

关于MappingJackson2HttpMessageConverter的实例化请看下节。

关于HttpMessageConverters的初始化

DispatcherServlet的初始化时会调用initHandlerAdapters(ApplicationContext context)

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
public class DispatcherServlet extends FrameworkServlet {

......

private void initHandlerAdapters(ApplicationContext context) {
this.handlerAdapters = null;

if (this.detectAllHandlerAdapters) {
// Find all HandlerAdapters in the ApplicationContext, including ancestor contexts.
Map<String, HandlerAdapter> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerAdapter.class, true, false); //✨✨✨
if (!matchingBeans.isEmpty()) {
this.handlerAdapters = new ArrayList<>(matchingBeans.values());
// We keep HandlerAdapters in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerAdapters);
}
}
else {
try {
HandlerAdapter ha = context.getBean(HANDLER_ADAPTER_BEAN_NAME, HandlerAdapter.class);
this.handlerAdapters = Collections.singletonList(ha);
}
catch (NoSuchBeanDefinitionException ex) {
// Ignore, we'll add a default HandlerAdapter later.
}
}

// Ensure we have at least some HandlerAdapters, by registering
// default HandlerAdapters if no other adapters are found.
if (this.handlerAdapters == null) {
this.handlerAdapters = getDefaultStrategies(context, HandlerAdapter.class);
if (logger.isTraceEnabled()) {
logger.trace("No HandlerAdapters declared for servlet '" + getServletName() +
"': using default strategies from DispatcherServlet.properties");
}
}
}
}

上述代码会加载ApplicationContext的所有HandlerAdapter,用来处理@RequestMappingRequestMappingHandlerAdapter实现HandlerAdapter接口,RequestMappingHandlerAdapter也被实例化。

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
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

......

@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
initControllerAdviceCache();
initMessageConverters(); //✨✨✨

if (this.argumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.initBinderArgumentResolvers == null) {
List<HandlerMethodArgumentResolver> resolvers = getDefaultInitBinderArgumentResolvers();
this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
}
if (this.returnValueHandlers == null) {
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
if (BEAN_VALIDATION_PRESENT) {
List<HandlerMethodArgumentResolver> resolvers = this.argumentResolvers.getResolvers();
this.methodValidator = HandlerMethodValidator.from(
this.webBindingInitializer, this.parameterNameDiscoverer,
methodParamPredicate(resolvers, ModelAttributeMethodProcessor.class),
methodParamPredicate(resolvers, RequestParamMethodArgumentResolver.class));
}
}
}
1
2
3
4
5
6
7
8
9
private void initMessageConverters() {
if (!this.messageConverters.isEmpty()) {
return;
}
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());

this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}

在构造器中看到一堆HttpMessageConverter。接着,重点查看AllEncompassingFormHttpMessageConverter类:

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {

private static final boolean jaxb2Present;

private static final boolean jackson2Present;

private static final boolean jackson2XmlPresent;

private static final boolean jackson2SmilePresent;

private static final boolean jackson2CborPresent;

private static final boolean jackson2YamlPresent;

private static final boolean gsonPresent;

private static final boolean jsonbPresent;

private static final boolean kotlinSerializationCborPresent;

private static final boolean kotlinSerializationJsonPresent;

private static final boolean kotlinSerializationProtobufPresent;

static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();
jaxb2Present = ClassUtils.isPresent("jakarta.xml.bind.Binder", classLoader);
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);
jackson2CborPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.cbor.CBORFactory", classLoader);
jackson2YamlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.yaml.YAMLFactory", classLoader);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("jakarta.json.bind.Jsonb", classLoader);
kotlinSerializationCborPresent = ClassUtils.isPresent("kotlinx.serialization.cbor.Cbor", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
kotlinSerializationProtobufPresent = ClassUtils.isPresent("kotlinx.serialization.protobuf.ProtoBuf", classLoader);
}


public AllEncompassingFormHttpMessageConverter() {

if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}

if (kotlinSerializationJsonPresent) {
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2Present) { //✨✨✨
addPartConverter(new MappingJackson2HttpMessageConverter());
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}

if (jackson2XmlPresent) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}

if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}

if (jackson2CborPresent) {
addPartConverter(new MappingJackson2CborHttpMessageConverter());
}

if (jackson2YamlPresent) {
addPartConverter(new MappingJackson2YamlHttpMessageConverter());
}

if (kotlinSerializationCborPresent) {
addPartConverter(new KotlinSerializationCborHttpMessageConverter());
}

if (kotlinSerializationProtobufPresent) {
addPartConverter(new KotlinSerializationProtobufHttpMessageConverter());
}
}

}

AllEncompassingFormHttpMessageConverter类构造器看到MappingJackson2HttpMessageConverter类的实例化,AllEncompassingFormHttpMessageConverter包含MappingJackson2HttpMessageConverter

ReturnValueHandler与MappingJackson2HttpMessageConverter关联

再次回顾RequestMappingHandlerAdapter

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {

...
@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//✨✨✨



@Nullable //本方法在AbstractHandlerMethodAdapter
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return handleInternal(request, response, (HandlerMethod)handler);
}

@Nullable
protected ModelAndView handleInternal(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;

......

mav = invokeHandlerMethod(request, response, handlerMethod);

......

return mav;
}

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request, HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = asyncWebRequest instanceof ServletWebRequest ? (ServletWebRequest)asyncWebRequest : new ServletWebRequest(request, response);
WebDataBinderFactory binderFactory = this.getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = this.getModelFactory(handlerMethod, binderFactory);
ServletInvocableHandlerMethod invocableMethod = this.createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}

if (this.returnValueHandlers != null) { //✨✨✨
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}

.......

invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}




public void afterPropertiesSet() {

......

//初始化消息转换器
this.initMessageConverters();

//赋值returnValueHandlers
if (this.returnValueHandlers == null) {
resolvers = this.getDefaultReturnValueHandlers();
this.returnValueHandlers = (new HandlerMethodReturnValueHandlerComposite()).addHandlers(resolvers);
}

......

}

private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList(20);

......

//这里就是 ReturnValueHandler与 MappingJackson2HttpMessageConverter关联 的关键点✨✨✨
//MessageConverters通过传参传进来✨✨✨
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),
this.contentNegotiationManager, this.requestResponseBodyAdvice, this.errorResponseInterceptors));

......

return handlers;
}



public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;
}



//RequestMappingHandlerAdapter构造器已初始化部分messageConverters
private void initMessageConverters() {
if (this.messageConverters.isEmpty()) {
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
}


}

应用中WebMvcAutoConfiguration(底层是WebMvcConfigurationSupport实现)传入更多messageConverters,其中就包含MappingJackson2HttpMessageConverter

39、响应处理-【源码分析】-内容协商原理

根据客户端接收能力不同,返回不同媒体类型的数据。

引入XML依赖:

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

可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json、application/xml)。

Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型。

内容协商原理

  1. 判断当前响应头中是否已经有确定的媒体类型MediaType
  2. 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)(这一步在下一节有详细介绍)
    • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
    • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
  3. 遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person)
  4. 找到支持操作Personconverter,把converter支持的媒体类型统计出来
  5. 客户端需要application/xml,服务端有10种 MediaType
  6. 进行内容协商的最佳匹配媒体类型
  7. 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化

40、响应处理-【源码分析】-基于请求参数的内容协商原理

上一节内容协商原理的第二步:

获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)

  • contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
  • HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
1
2
3
4
5
6
7
8
9
10
11
12
13
//RequestResponseBodyMethodProcessor继承这个类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver implements HandlerMethodReturnValueHandler {

......


//在AbstractMessageConverterMethodArgumentResolver类内
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException {
//内容协商管理器 默认使用基于请求头的策略
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class ContentNegotiationManager implements ContentNegotiationStrategy, MediaTypeFileExtensionResolver {

.......

public ContentNegotiationManager() {
this(new HeaderContentNegotiationStrategy()); //内容协商管理器 默认使用基于请求头的策略
}



@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request) throws HttpMediaTypeNotAcceptableException {
for (ContentNegotiationStrategy strategy : this.strategies) {
List<MediaType> mediaTypes = strategy.resolveMediaTypes(request);
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) {
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}

}
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
//基于请求头的策略
public class HeaderContentNegotiationStrategy implements ContentNegotiationStrategy {

/**
* {@inheritDoc}
* @throws HttpMediaTypeNotAcceptableException if the 'Accept' header cannot be parsed
*/
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest request)
throws HttpMediaTypeNotAcceptableException {

String[] headerValueArray = request.getHeaderValues(HttpHeaders.ACCEPT); //✨✨✨
if (headerValueArray == null) {
return MEDIA_TYPE_ALL_LIST;
}

List<String> headerValues = Arrays.asList(headerValueArray);
try {
List<MediaType> mediaTypes = MediaType.parseMediaTypes(headerValues);
MimeTypeUtils.sortBySpecificity(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException | InvalidMimeTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}

}

开启浏览器参数方式内容协商功能

为了方便内容协商,开启基于请求参数的内容协商功能。

1
2
3
4
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式

image-20250519230322931

内容协商管理器,就会多了一个ParameterContentNegotiationStrategy(由Spring容器注入)

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
public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private String parameterName = "format";

public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
}

public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "'parameterName' is required");
this.parameterName = parameterName;
}

public String getParameterName() {
return this.parameterName;
}

@Nullable //✨✨✨
protected String getMediaTypeKey(NativeWebRequest request) {
return request.getParameter(this.getParameterName());
}
}

//以下方法在AbstractMappingContentNegotiationStrategy类
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest) throws HttpMediaTypeNotAcceptableException {
return this.resolveMediaTypeKey(webRequest, this.getMediaTypeKey(webRequest));
}

public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key) throws HttpMediaTypeNotAcceptableException {
if (StringUtils.hasText(key)) {
MediaType mediaType = this.lookupMediaType(key); //✨✨✨
if (mediaType != null) {
this.handleMatch(key, mediaType);
//解析到mediaType,直接返回
return Collections.singletonList(mediaType);
}

mediaType = this.handleNoMatch(webRequest, key);
if (mediaType != null) {
this.addMapping(key, mediaType);
return Collections.singletonList(mediaType);
}
}
//如果还是"*/*",继续执行下个策略
return MEDIA_TYPE_ALL_LIST;
}


}

然后,浏览器地址输入带format参数的URL:

1
2
3
http://localhost:8080/test/person?format=json

http://localhost:8080/test/person?format=xml

这样,后端会根据参数format的值,返回对应json或xml格式的数据。

41、响应处理-【源码分析】-自定义MessageConverter

实现多协议数据兼容。json、xml、x-guigu(这个是自创的)

  1. @ResponseBody 响应数据出去 调用 RequestResponseBodyMethodProcessor 处理

  2. Processor 处理方法返回值。通过 MessageConverter处理

  3. 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)

  4. 内容协商找到最终的 messageConverter

SpringMVC的什么功能,一个入口给容器中添加一个 WebMvcConfigurer

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new GuiguMessageConverter());
}
}
}
}
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

/**
* 自定义的Converter
*/
public class GuiguMessageConverter implements HttpMessageConverter<Person> {

@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}

@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}

/**
* 服务器要统计所有MessageConverter都能写出哪些内容类型
*
* application/x-guigu
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-guigu");
}

@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}

@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
//自定义协议数据的写出
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();


//写出去
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
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
import java.util.Date;

@Controller
public class ResponseTestController {

/**
* 1、浏览器发请求直接返回 xml [application/xml] jacksonXmlConverter
* 2、如果是ajax请求 返回 json [application/json] jacksonJsonConverter
* 3、如果硅谷app发请求,返回自定义协议数据 [appliaction/x-guigu] xxxxConverter
* 属性值1;属性值2;
*
* 步骤:
* 1、添加自定义的MessageConverter进系统底层
* 2、系统底层就会统计出所有MessageConverter能操作哪些类型
* 3、客户端内容协商 [guigu--->guigu]
*
* 作业:如何以参数的方式进行内容协商
* @return
*/
@ResponseBody //利用返回值处理器里面的消息转换器进行处理
@GetMapping(value = "/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setUserName("zhangsan");
return person;
}

}

用Postman发送/test/person(请求头Accept:application/x-guigu),将返回自定义协议数据的写出。

42、响应处理-【源码分析】-浏览器与PostMan内容协商完全适配

假设你想基于自定义请求参数的自定义内容协商功能。

换句话,在地址栏输入http://localhost:8080/test/person?format=gg返回数据,跟http://localhost:8080/test/person且请求头参数Accept:application/x-guigu的返回自定义协议数据的一致。

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
@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer*/ {

//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {

/**
* 自定义内容协商策略
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
//自定义媒体类型
mediaTypes.put("gg",MediaType.parseMediaType("application/x-guigu"));
//指定支持解析哪些参数对应的哪些媒体类型
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// parameterStrategy.setParameterName("ff");

//还需添加请求头处理策略,否则accept:application/json、application/xml则会失效
HeaderContentNegotiationStrategy headeStrategy = new HeaderContentNegotiationStrategy();

configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy));
}
}
}

...

}

日后开发要注意,有可能我们添加的自定义的功能会覆盖默认很多功能,导致一些默认的功能失效。

43、视图解析-Thymeleaf初体验

Thymeleaf官方文档

thymeleaf使用

引入Starter

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

自动配置好thymeleaf

1
2
3
4
5
6
7
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
...
}

自动配好的策略

  1. 所有thymeleaf的配置值都在 ThymeleafProperties
  2. 配好了 ThymeleafViewResolver
  3. 我们只需要直接开发页面
1
2
public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处
public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名

编写一个控制层:

1
2
3
4
5
6
7
8
9
10
@Controller
public class ViewTestController {
@GetMapping("/hello")
public String hello(Model model){
//model中的数据会被放在请求域中 request.setAttribute("a",aa)
model.addAttribute("msg","一定要大力发展工业文化");
model.addAttribute("link","http://www.baidu.com");
return "success";
}
}

/templates/success.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">nice</h1>
<h2>
<a href="www.baidu.com" th:href="${link}">去百度</a> <br/>
<a href="www.google.com" th:href="@{/link}">去百度</a>
</h2>
</body>
</html>
1
2
3
server:
servlet:
context-path: /app #设置应用名

这个设置后,URL要插入/app, 如http://localhost:8080/app/hello.html

基本语法

表达式

表达式名字 语法 用途
变量取值 ${…} 获取请求域、session域、对象等值
选择变量 *{…} 获取上下文对象值
消息 #{…} 获取国际化等值
链接 @{…} 生成链接
片段表达式 ~{…} jsp:include 作用,引入公共页面片段

字面量

  • 文本值: ‘one text’ , ‘Another one!’ ,…
  • 数字: 0 , 34 , 3.0 , 12.3 ,…
  • 布尔值: true , false
  • 空值: null
  • 变量: one,two,… 变量不能有空格

文本操作

  • 字符串拼接: +
  • 变量替换: |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)

特殊操作

  • 无操作: _

设置属性值-th:attr

  • 设置单个值
1
2
3
4
5
6
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
  • 设置多个值
1
2
<img src="../../images/gtvglogo.png"  
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />

官方文档 - 5 Setting Attribute Values

迭代

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

条件运算

1
2
3
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
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>

属性优先级

Order Feature Attributes
1 Fragment inclusion th:insert th:replace
2 Fragment iteration th:each
3 Conditional evaluation th:if th:unless th:switch th:case
4 Local variable definition th:object th:with
5 General attribute modification th:attr th:attrprepend th:attrappend
6 Specific attribute modification th:value th:href th:src ...
7 Text (tag body modification) th:text th:utext
8 Fragment specification th:fragment
9 Fragment removal th:remove

官方文档 - 10 Attribute Precedence

44、web实验-后台管理系统基本功能

项目创建

使用IDEA的Spring Initializr。

  • thymeleaf、
  • web-starter、
  • devtools、
  • lombok

登陆页面

  • /static 放置 css,js等静态资源

  • /templates/login.html 登录页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!-- 要加这玩意thymeleaf才能用 -->

<form class="form-signin" action="index.html" method="post" th:action="@{/login}">

...

<!-- 消息提醒 -->
<label style="color: red" th:text="${msg}"></label>

<input type="text" name="userName" class="form-control" placeholder="User ID" autofocus>
<input type="password" name="password" class="form-control" placeholder="Password">

<button class="btn btn-lg btn-login btn-block" type="submit">
<i class="fa fa-check"></i>
</button>

...

</form>
  • /templates/main.html 主页

thymeleaf内联写法:

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

登录控制层

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
@Controller
public class IndexController {
/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){

return "login";
}

@PostMapping("/login")
public String main(User user, HttpSession session, Model model){ //RedirectAttributes

if(StringUtils.hasLength(user.getUserName()) && "123456".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误");
//回到登录页面
return "login";
}
}

/**
* 去main页面
* @return
*/
@GetMapping("/main.html")
public String mainPage(HttpSession session, Model model){

//最好用拦截器,过滤器
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
return "main";
}else {
//session过期,没有登陆过
//回到登录页面
model.addAttribute("msg","请重新登录");
return "login";
}
}

}

模型

1
2
3
4
5
6
7
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}

45、web实验-抽取公共页面

官方文档 - Template Layout

  • 公共页面/templates/common.html
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org"><!--注意要添加xmlns:th才能添加thymeleaf的标签-->
<head th:fragment="commonheader">
<!--common-->
<link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
<link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">
...
</head>
<body>
<!-- left side start-->
<div id="leftmenu" class="left-side sticky-left-side">
...

<div class="left-side-inner">
...

<!--sidebar nav start-->
<ul class="nav nav-pills nav-stacked custom-nav">
<li><a th:href="@{/main.html}"><i class="fa fa-home"></i> <span>Dashboard</span></a></li>
...
<li class="menu-list nav-active"><a href="#"><i class="fa fa-th-list"></i> <span>Data Tables</span></a>
<ul class="sub-menu-list">
<li><a th:href="@{/basic_table}"> Basic Table</a></li>
<li><a th:href="@{/dynamic_table}"> Advanced Table</a></li>
<li><a th:href="@{/responsive_table}"> Responsive Table</a></li>
<li><a th:href="@{/editable_table}"> Edit Table</a></li>
</ul>
</li>
...
</ul>
<!--sidebar nav end-->
</div>
</div>
<!-- left side end-->


<!-- header section start-->
<div th:fragment="headermenu" class="header-section">

<!--toggle button start-->
<a class="toggle-btn"><i class="fa fa-bars"></i></a>
<!--toggle button end-->
...

</div>
<!-- header section end-->

<div id="commonscript">
<!-- Placed js at the end of the document so the pages load faster -->
<script th:src="@{/js/jquery-1.10.2.min.js}"></script>
<script th:src="@{/js/jquery-ui-1.9.2.custom.min.js}"></script>
<script th:src="@{/js/jquery-migrate-1.2.1.min.js}"></script>
<script th:src="@{/js/bootstrap.min.js}"></script>
<script th:src="@{/js/modernizr.min.js}"></script>
<script th:src="@{/js/jquery.nicescroll.js}"></script>
<!--common scripts for all pages-->
<script th:src="@{/js/scripts.js}"></script>
</div>
</body>
</html>
  • /templates/table/basic_table.html
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
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
<meta name="description" content="">
<meta name="author" content="ThemeBucket">
<link rel="shortcut icon" href="#" type="image/png">

<title>Basic Table</title>
<div th:include="common :: commonheader"> </div><!--将common.html的代码段 插进来-->
</head>

<body class="sticky-header">

<section>
<div th:replace="common :: #leftmenu"></div>

<!-- main content start-->
<div class="main-content" >

<div th:replace="common :: headermenu"></div>
...
</div>
<!-- main content end-->
</section>

<!-- Placed js at the end of the document so the pages load faster -->
<div th:replace="common :: #commonscript"></div>


</body>
</html>

Difference between th:insert and th:replace (and th:include)

46、web实验-遍历数据与页面bug修改

控制层代码:

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);

return "table/dynamic_table";
}

页面代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用户名</th>
<th>密码</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Trident</td>
<td th:text="${user.userName}">Internet</td>
<td >[[${user.password}]]</td>
</tr>
</tbody>
</table>

47、视图解析-【源码分析】-视图解析器与视图

登陆页面

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f4f4;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}

.login-container {
background-color: white;
padding: 20px 30px;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
width: 300px;
}

h2 {
text-align: center;
margin-bottom: 20px;
color: #333;
}

.form-group {
margin-bottom: 15px;
}

label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #555;
}

input[type="text"],
input[type="password"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
box-sizing: border-box;
}

button {
width: 100%;
padding: 10px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}

button:hover {
background-color: #45a049;
}
</style>
</head>
<body>
<div class="login-container">
<h2>用户登录</h2>
<form action="/login" method="post">
<div class="form-group">
<label for="userName">用户名:</label>
<input type="text" id="userName" name="userName" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<h5 th:text="${msg}"></h5>
</div>
<button type="submit">登录</button>
</form>
</div>
</body>
</html>

登陆成功页面

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>编辑个人信息</title>
<style>
.profile-edit-container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: 'Arial', sans-serif;
}

h1 {
text-align: center;
color: #333;
margin-bottom: 30px;
}

.form-section {
margin-bottom: 30px;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
}

h2 {
color: #444;
margin-top: 0;
margin-bottom: 20px;
font-size: 1.2em;
}

.form-group {
margin-bottom: 15px;
}

label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}

input[type="text"],
input[type="email"] {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
}

.avatar-upload,
.photos-upload {
display: flex;
gap: 20px;
align-items: flex-start;
}

.avatar-preview {
width: 120px;
height: 120px;
border-radius: 50%;
overflow: hidden;
border: 2px solid #ddd;
background-color: #eee;
display: flex;
align-items: center;
justify-content: center;
color: #999;
}

.avatar-preview img {
width: 100%;
height: 100%;
object-fit: cover;
}

.photos-preview {
display: flex;
flex-wrap: wrap;
gap: 10px;
min-height: 100px;
flex-grow: 1;
}

.photo-thumbnail {
width: 100px;
height: 100px;
border-radius: 4px;
overflow: hidden;
position: relative;
}

.photo-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}

.remove-photo {
position: absolute;
top: 5px;
right: 5px;
background: rgba(0, 0, 0, 0.5);
color: white;
border: none;
border-radius: 50%;
width: 20px;
height: 20px;
cursor: pointer;
}

.empty-state {
color: #999;
align-self: center;
margin: auto;
}

.upload-button {
display: inline-block;
padding: 8px 15px;
background-color: #4CAF50;
color: white;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.3s;
}

.upload-button:hover {
background-color: #45a049;
}

.hint {
font-size: 0.8em;
color: #666;
margin-top: 5px;
}

.form-actions {
display: flex;
justify-content: center;
margin-top: 20px;
}

.submit-button {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
background-color: #2196F3;
color: white;
}

.submit-button:hover {
background-color: #0b7dda;
}

/* 新增的文件名显示样式 */
.file-info {
margin-top: 10px;
padding: 8px;
background-color: #f0f0f0;
border-radius: 4px;
font-size: 0.9em;
color: #555;
}

.file-info span {
display: block;
margin-bottom: 3px;
word-break: break-all;
}

.upload-controls {
flex-grow: 1;
}
</style>
</head>
<body>
<div class="profile-edit-container">
<h1>编辑个人信息</h1>

<form id="profileForm" action="/upload" method="post" enctype="multipart/form-data">
<!-- 基本信息部分 -->
<div class="form-section">
<h2>基本信息</h2>

<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" required>
</div>

<div class="form-group">
<label for="email">邮箱</label>
<input type="email" id="email" name="email" required>
</div>
</div>

<!-- 头像上传部分 -->
<div class="form-section">
<h2>头像</h2>

<div class="avatar-upload">
<div class="avatar-preview" id="avatarPreviewContainer">
<span>请上传头像</span>
</div>

<div class="upload-controls">
<label for="avatar" class="upload-button">选择头像</label>
<input type="file" id="avatar" name="avatar" accept="image/*" style="display: none;">
<div id="avatarFileInfo" class="file-info" style="display: none;"></div>
<p class="hint">支持 JPG, PNG 格式,大小不超过 2MB</p>
</div>
</div>
</div>

<!-- 生活照上传部分 -->
<div class="form-section">
<h2>生活照</h2>

<div class="photos-upload">
<div class="photos-preview" id="photosPreview">
<div class="empty-state">暂无照片</div>
</div>

<div class="upload-controls">
<label for="photos" class="upload-button">添加照片</label>
<input type="file" id="photos" name="photos" accept="image/*" multiple style="display: none;">
<div id="photosFileInfo" class="file-info" style="display: none;"></div>
<p class="hint">最多可上传 10 张照片,每张不超过 5MB</p>
</div>
</div>
</div>

<!-- 操作按钮 -->
<div class="form-actions">
<button type="submit" class="submit-button">保存更改</button>
</div>
</form>
</div>

<script>
// 头像文件选择处理
document.getElementById('avatar').addEventListener('change', function(e) {
const fileInfoDiv = document.getElementById('avatarFileInfo');
if (this.files.length > 0) {
fileInfoDiv.style.display = 'block';
fileInfoDiv.innerHTML = `<span>已选择: ${this.files[0].name}</span>`;

// 预览图片
const previewContainer = document.getElementById('avatarPreviewContainer');
const file = this.files[0];
if (file) {
const reader = new FileReader();
reader.onload = function(e) {
previewContainer.innerHTML = `<img src="${e.target.result}" alt="头像预览">`;
};
reader.readAsDataURL(file);
}
} else {
fileInfoDiv.style.display = 'none';
document.getElementById('avatarPreviewContainer').innerHTML = '<span>请上传头像</span>';
}
});

// 生活照文件选择处理
document.getElementById('photos').addEventListener('change', function(e) {
const fileInfoDiv = document.getElementById('photosFileInfo');
const previewContainer = document.getElementById('photosPreview');

if (this.files.length > 0) {
fileInfoDiv.style.display = 'block';
let fileNames = '';
for (let i = 0; i < this.files.length; i++) {
fileNames += `<span>${i+1}. ${this.files[i].name}</span>`;
}
fileInfoDiv.innerHTML = fileNames;

// 清空预览区域
previewContainer.innerHTML = '';

// 预览图片
for (let i = 0; i < this.files.length; i++) {
const file = this.files[i];
if (file) {
const reader = new FileReader();
reader.onload = (function(file) {
return function(e) {
const thumbnail = document.createElement('div');
thumbnail.className = 'photo-thumbnail';
thumbnail.innerHTML = `
<img src="${e.target.result}" alt="照片预览">
<button class="remove-photo" data-index="${i}">×</button>
`;
previewContainer.appendChild(thumbnail);
};
})(file);
reader.readAsDataURL(file);
}
}
} else {
fileInfoDiv.style.display = 'none';
previewContainer.innerHTML = '<div class="empty-state">暂无照片</div>';
}
});

// 删除照片按钮事件委托
document.getElementById('photosPreview').addEventListener('click', function(e) {
if (e.target.classList.contains('remove-photo')) {
const index = parseInt(e.target.getAttribute('data-index'));
const filesInput = document.getElementById('photos');

// 创建一个新的FileList
const dt = new DataTransfer();
for (let i = 0; i < filesInput.files.length; i++) {
if (i !== index) {
dt.items.add(filesInput.files[i]);
}
}
filesInput.files = dt.files;

// 重新触发change事件
const event = new Event('change');
filesInput.dispatchEvent(event);
}
});
</script>
</body>
</html>

Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Controller
public class IndexController {
@GetMapping(value = {"/", "/login"})
public String loginPage() {
return "login";
}

@PostMapping("/login")
public String login(User user, Model model) {
if (StrUtil.isNotBlank(user.getUserName()) && user.getPassword().equals("123456")) {
return "redirect:/loginSuccess.html";
}
model.addAttribute("msg", "用户名或密码错误");
return "login";
}
@GetMapping("/loginSuccess.html")
public String loginSuccess() {
return "loginSuccess";
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

.......

invocableMethod.invokeAndHandle(webRequest, mavContainer); ❌❌❌
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest); ❌❌❌
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 // ServletInvocableHandlerMethod
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 调用目标方法获取返回值
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

......

try
// 处理返回结果 ❌❌❌
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}

......

}
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
//HandlerMethodReturnValueHandlerComposite
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);❌
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); ❌❌❌
}

@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
if (handler.supportsReturnType(returnType)) {
return handler; // 这里选择的是 ViewNameMethodReturnValueHandler✨✨✨
}
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//由 ViewNameMethodReturnValueHandler 处理
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

if (returnValue instanceof CharSequence) {
String viewName = returnValue.toString();
mavContainer.setViewName(viewName);
if (isRedirectViewName(viewName)) {
mavContainer.setRedirectModelScenario(true);
}
}
else if (returnValue != null) {
// should not happen
throw new UnsupportedOperationException("Unexpected return type: " +
returnType.getParameterType().getName() + " in method: " + returnType.getMethod());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//回到 RequestMappingHandlerAdapter
private ModelAndView getModelAndView(ModelAndViewContainer mavContainer,
ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception {

modelFactory.updateModel(webRequest, mavContainer);
if (mavContainer.isRequestHandled()) {
return null;
}
ModelMap model = mavContainer.getModel();
ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); ❌❌❌
if (!mavContainer.isViewReference()) {
mav.setView((View) mavContainer.getView());
}
if (model instanceof RedirectAttributes redirectAttributes) {
Map<String, ?> flashAttributes = redirectAttributes.getFlashAttributes();
HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class);
if (request != null) {
RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes);
}
}
return mav;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ModelAndView mav;

......

else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod); ❌❌❌
}

......

return mav;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {

......

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); ❌❌❌

......

applyDefaultViewName(processedRequest, mv); ❌
mappedHandler.applyPostHandle(processedRequest, response, mv);
}

processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); ❌❌❌
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {


// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response); ❌❌❌
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
......
}
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
protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception {
// Determine locale for request and apply it to the response.
Locale locale =
(this.localeResolver != null ? this.localeResolver.resolveLocale(request) : request.getLocale());
response.setLocale(locale);

View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need to resolve the view name.
view = resolveViewName(viewName, mv.getModelInternal(), locale, request); ❌❌❌

......
}

......

try {
if (mv.getStatus() != null) {
request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus());
response.setStatus(mv.getStatus().value());
}
view.render(mv.getModelInternal(), request, response); ❌❌❌
}
......
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Nullable
protected View resolveViewName(String viewName, @Nullable Map<String, Object> model,
Locale locale, HttpServletRequest request) throws Exception {

return resolveViewNameInternal(viewName, locale);
}

@Nullable
private View resolveViewNameInternal(String viewName, Locale locale) throws Exception {
if (this.viewResolvers != null) {
for (ViewResolver viewResolver : this.viewResolvers) {
View view = viewResolver.resolveViewName(viewName, locale);
if (view != null) {
return view;
}
}
}
return null;
}

image-20250603204233231

视图解析原理流程

  1. 目标方法处理的过程中(阅读DispatcherServlet源码),所有数据都会被放在ModelAndViewContainer里面,其中包括数据和视图地址

  2. 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在ModelAndViewContainer

  3. 任何目标方法执行完成以后都会返回ModelAndView(数据和视图地址)

  4. processDispatchResult()处理派发结果(页面该如何响应)

    • `render(mv, request, response); 进行页面渲染逻辑
      • 根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】

        • 所有的视图解析器尝试是否能根据当前返回值得到View对象

        • 得到了 redirect:/loginSuccess.html --> Thymeleaf new RedirectView()

        • ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象

        • view.render(mv.getModelInternal(), request, response)视图对象调用自定义的render进行页面渲染工作

      • RedirectView 如何渲染【重定向到一个页面】

      • 获取目标url地址

      • response.sendRedirect(encodedURL)

视图解析

  • 返回值以forward:开始 =>new InternalResourceView(forwardUrl) => 转发request.getRequestDispatcher(path).forward(request, response)
  • 返回值以redirect:开始 => new RedirectView() => render就是重定向
  • 返回值是普通字符串:new ThymeleafView() => render

48、拦截器-登录检查与静态资源放行

  1. 编写一个拦截器实现HandlerInterceptor接口

  2. 拦截器注册到容器中(实现WebMvcConfigureraddInterceptors()

  3. 指定拦截规则(注意,如果是拦截所有,【静态资源也会被拦截】

编写一个实现HandlerInterceptor接口的拦截器:

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
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

/**
* 目标方法执行之前
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);

//登录检查逻辑
HttpSession session = request.getSession();

Object loginUser = session.getAttribute("loginUser");

if(loginUser != null){
//放行
return true;
}

//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
request.getRequestDispatcher("/").forward(request,response);
return false;
}

/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}

/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}

拦截器注册到容器中 && 指定拦截规则:

1
2
3
4
5
6
7
8
9
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())//拦截器注册到容器中
.addPathPatterns("/**") //所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**"); //放行的请求
}

49、拦截器-【源码分析】-拦截器的执行时机和原理

  1. 根据当前请求,找到HandlerExecutionChain(可以处理请求的handler以及handler的所有 拦截器)
  2. 先来顺序执行 所有拦截器的 preHandle()方法。
    • 如果当前拦截器preHandle()返回为true。则执行下一个拦截器的preHandle()
    • 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion();
  3. 如果任何一个拦截器返回false,直接跳出不执行目标方法。
  4. 所有拦截器都返回true,才执行目标方法。
  5. 倒序执行所有拦截器的postHandle()方法。
  6. 前面的步骤有任何异常都会直接倒序触发 afterCompletion()
  7. 页面成功渲染完成以后,也会倒序触发 afterCompletion()

image-20250417205507874

DispatcherServlet中涉及到HandlerInterceptor的地方:

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
72
public class DispatcherServlet extends FrameworkServlet {

...

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

.......

//该方法内调用HandlerInterceptor的preHandle() 1️⃣ ✨✨✨
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.2️⃣ ✨✨✨
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

.......

//该方法内调用HandlerInterceptor的postHandle() 3️⃣ ✨✨✨
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
//4️⃣ ✨✨✨ ⬇️⬇️⬇️⬇️⬇️⬇️
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
...
}
}

private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception {

if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
mappedHandler.triggerAfterCompletion(request, response, ex);
}
throw ex;
}

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

...

if (mappedHandler != null) {
//该方法内调用HandlerInterceptor接口的afterCompletion方法
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null); ⬅️⬅️⬅️⬅️⬅️⬅️
}
}


}
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
public class HandlerExecutionChain {

...

boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
for (int i = 0; i < this.interceptorList.size(); i++) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
//HandlerInterceptor的preHandle方法
if (!interceptor.preHandle(request, response, this.handler)) {

triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
return true;
}

void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv)
throws Exception {

for (int i = this.interceptorList.size() - 1; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);

//HandlerInterceptor接口的postHandle方法
interceptor.postHandle(request, response, this.handler, mv);
}
}

void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) {
for (int i = this.interceptorIndex; i >= 0; i--) {
HandlerInterceptor interceptor = this.interceptorList.get(i);
try {
//HandlerInterceptor接口的afterCompletion方法
interceptor.afterCompletion(request, response, this.handler, ex);
}
catch (Throwable ex2) {
logger.error("HandlerInterceptor.afterCompletion threw exception", ex2);
}
}
}


}

50、文件上传-单文件与多文件上传的使用

  • 控制层代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class InformationController {
@PostMapping("/upload")
public Boolean upload(@RequestParam("name") String name,
@RequestParam("email") String email,
@RequestPart("avatar") MultipartFile avatar,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
System.out.println(name);
System.out.println(email);
if (!avatar.isEmpty()) {
avatar.transferTo(new File("/Users/lixingda/IdeaProjects/Rbac" + avatar.getOriginalFilename()));
}
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
photo.transferTo(new File("/Users/lixingda/IdeaProjects/Rbac" + photo.getOriginalFilename()));
}
}
return true;
}
}

文件上传相关的配置类:

  • org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
  • org.springframework.boot.autoconfigure.web.servlet.MultipartProperties

文件大小相关配置项:

1
2
spring.servlet.multipart.max-file-size=50MB
spring.servlet.multipart.max-request-size=100MB

51、文件上传-【源码流程】文件上传参数解析器

文件上传相关的自动配置类MultipartAutoConfiguration

  • 自动配置好StandardServletMultipartResolver 【文件上传解析器】
  • 原理步骤
    • 请求进来使用文件上传解析器判断(multipartResolver.isMultipart(request))并封装(multipartResolver.resolveMultipart(request)),返回MultipartHttpServletRequest文件上传请求
    • 参数解析器来解析请求中的文件内容封装成MultipartFile
    • 将request中的文件信息封装为一个Map;MultiValueMap<String,MultipartFile>
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
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {

private final MultipartProperties multipartProperties;

public MultipartAutoConfiguration(MultipartProperties multipartProperties) {
this.multipartProperties = multipartProperties;
}

@Bean
@ConditionalOnMissingBean({ MultipartConfigElement.class, CommonsMultipartResolver.class })
public MultipartConfigElement multipartConfigElement() {
return this.multipartProperties.createMultipartConfig();
}

@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
//配置好文件上传解析器
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}

}
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
//文件上传解析器
public class StandardServletMultipartResolver implements MultipartResolver {

private boolean resolveLazily = false;

private boolean strictServletCompliance = false;


public void setResolveLazily(boolean resolveLazily) {
this.resolveLazily = resolveLazily;
}

public void setStrictServletCompliance(boolean strictServletCompliance) {
this.strictServletCompliance = strictServletCompliance;
}


@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}

@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

@Override
public void cleanupMultipart(MultipartHttpServletRequest request) {
if (!(request instanceof AbstractMultipartHttpServletRequest abstractMultipartHttpServletRequest) ||
abstractMultipartHttpServletRequest.isResolved()) {

try {
for (Part part : request.getParts()) {
part.delete();
}
}
catch (Throwable ex) {
LogFactory.getLog(getClass()).warn("Failed to perform cleanup of multipart items", ex);
}
}
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private void initMultipartResolver(ApplicationContext context) {
try {
// 配置类中配置的StandardServletMultipartResolver文件上传解析器
this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);
if (logger.isTraceEnabled()) {
logger.trace("Detected " + this.multipartResolver);
}
else if (logger.isDebugEnabled()) {
logger.debug("Detected " + this.multipartResolver.getClass().getSimpleName());
}
}
catch (NoSuchBeanDefinitionException ex) {
// Default is no multipart resolver.
this.multipartResolver = null;
if (logger.isTraceEnabled()) {
logger.trace("No MultipartResolver '" + MULTIPART_RESOLVER_BEAN_NAME + "' declared");
}
}
}
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
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 做预处理,如果有上传文件 就new StandardMultipartHttpServletRequest包装类
processedRequest = checkMultipart(request); //⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️⬇️
multipartRequestParsed = (processedRequest != request);

// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = HttpMethod.GET.matches(method);
if (isGet || HttpMethod.HEAD.matches(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());//✨✨✨✨

.........

}

.........

}

finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
asyncManager.setMultipartRequestParsed(multipartRequestParsed);
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed || asyncManager.isMultipartRequestParsed()) {
cleanupMultipart(processedRequest);
}
}
}
}
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
/**
* 检查并处理当前请求是否为 multipart 请求,如果是则解析为 MultipartHttpServletRequest。
*
* @param request 原始的 HttpServletRequest 请求对象
* @return 处理后的 HttpServletRequest 对象,如果为 multipart 请求则返回 MultipartHttpServletRequest 实例,否则返回原始请求
* @throws MultipartException 如果解析 multipart 请求过程中发生异常且不是在错误处理阶段,则抛出该异常
*/
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { //⬅️⬅️⬅️⬅️⬅️⬅️⬅️⬅️⬅️
// 判断是否存在 multipartResolver,并且请求是 multipart 类型
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {//⬇️⬇️⬇️⬇️
// 检查当前请求是否已经被封装为 MultipartHttpServletRequest(即已经是 multipart 类型)
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
// 如果当前是正常的请求分发类型(DispatcherType.REQUEST),记录 trace 日志说明已由其他组件如 MultipartFilter 解析过
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, for example, by MultipartFilter");
}
}
// 如果已经存在 MultipartException 异常属性,则跳过再次解析以避免干扰错误渲染
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
// 使用 multipartResolver 解析请求并返回解析后的 MultipartHttpServletRequest
return this.multipartResolver.resolveMultipart(request);//⬇️⬇️⬇️⬇️⬇️⬇️
}
catch (MultipartException ex) {
// 如果当前是在处理错误页面的请求(即存在 ERROR_EXCEPTION_ATTRIBUTE 属性)
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// 继续使用原始请求进行后续处理
}
else {
// 非错误处理阶段抛出异常
throw ex;
}
}
}
}
// 默认返回原始请求(如果不是 multipart 请求或解析失败但处于错误处理流程中)
return request;
}

1
2
3
4
5
@Override
public boolean isMultipart(HttpServletRequest request) { //⬅️⬅️⬅️⬅️
return StringUtils.startsWithIgnoreCase(request.getContentType(),
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}
1
2
3
4
@Override
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { //⬅️⬅️⬅️⬅️⬅️⬅️
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}

mv = ha.handle(processedRequest, response, mappedHandler.getHandler())跳到以下的类方法中

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
	@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;

.........

mav = invokeHandlerMethod(request, response, handlerMethod);

.........

return mav;
}

@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}

.........

invocableMethod.invokeAndHandle(webRequest, mavContainer);

.........

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

}
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
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {

.........

public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);

.........

}

@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);//⬇️⬇️⬇️

.........

return doInvoke(args);//反射调用
}

@Nullable
protected Object doInvoke(Object... args) throws Exception {
Method method = getBridgedMethod();
ReflectionUtils.makeAccessible(method);
return method.invoke(getBean(), args);

.........

}

//处理得出multipart参数,准备稍后的反射调用(@PostMapping标记的上传方法)
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception { //⬅️⬅️⬅️

MethodParameter[] parameters = getMethodParameters();

.........

Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//关注点1
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//关注点2
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
...
}
}
return args;
}

}

主角RequestPartMethodArgumentResolver

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
public class RequestPartMethodArgumentResolver extends AbstractMessageConverterMethodArgumentResolver {

//对应上面代码关注点1
@Override
public boolean supportsParameter(MethodParameter parameter) {
//标注@RequestPart的参数
if (parameter.hasParameterAnnotation(RequestPart.class)) {
return true;
}
else {
if (parameter.hasParameterAnnotation(RequestParam.class)) {
return false;
}
return MultipartResolutionDelegate.isMultipartArgument(parameter.nestedIfOptional());
}
}

//对应上面代码关注点2
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest request, @Nullable WebDataBinderFactory binderFactory) throws Exception {

HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class);
Assert.state(servletRequest != null, "No HttpServletRequest");

RequestPart requestPart = parameter.getParameterAnnotation(RequestPart.class);
boolean isRequired = ((requestPart == null || requestPart.required()) && !parameter.isOptional());

String name = getPartName(parameter, requestPart);
parameter = parameter.nestedIfOptional();
Object arg = null;

//封装成MultipartFile类型的对象作参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);//⬇️⬇️⬇️⬇️⬇️
if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) {
arg = mpArg;
}

...

return adaptArgumentIfNecessary(arg, parameter);
}
}
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
/**
* 解析 multipart 请求中的参数,支持 MultipartFile、Part 等类型。
*
* @param name 参数名称
* @param parameter 方法参数元信息
* @param request HTTP 请求对象
* @return 解析得到的参数值,如果无法解析则返回 UNRESOLVABLE 或 null
* @throws Exception 如果解析过程中发生异常
*/
public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request)
throws Exception { //⬅️⬅️⬅️⬅️⬅️

// 从当前请求中获取原生的 MultipartHttpServletRequest 对象(即已被解析过的 multipart 请求)
MultipartHttpServletRequest multipartRequest =
WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class);

// 判断是否为 multipart 请求内容(检查 Content-Type 是否为 multipart/form-data)
boolean isMultipart = (multipartRequest != null || isMultipartContent(request));

// 处理参数类型为 MultipartFile 的情况
if (MultipartFile.class == parameter.getNestedParameterType()) {
// 如果不是 multipart 请求,则不处理,返回 null
if (!isMultipart) {
return null;
}
// 如果 multipartRequest 未初始化,则创建一个 StandardMultipartHttpServletRequest 实例来解析原始请求
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
// 返回指定名称的文件对象
return multipartRequest.getFile(name);
}
// 处理参数类型为 Collection<MultipartFile> 的情况
else if (isMultipartFileCollection(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
// 获取多个文件并返回 List 形式
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files : null);
}
// 处理参数类型为 MultipartFile[] 数组的情况
else if (isMultipartFileArray(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name); //✨✨✨✨✨✨
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
}
// 处理参数类型为 Part(Servlet 3.0 的 Part 接口)的情况
else if (Part.class == parameter.getNestedParameterType()) {
if (!isMultipart) {
return null;
}
// 使用 HttpServletRequest.getPart() 获取单个 Part
return request.getPart(name);
}
// 处理参数类型为 Collection<Part> 的情况
else if (isPartCollection(parameter)) {
if (!isMultipart) {
return null;
}
// 调用辅助方法获取多个 Part 并返回 List
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts : null);
}
// 处理参数类型为 Part[] 数组的情况
else if (isPartArray(parameter)) {
if (!isMultipart) {
return null;
}
List<Part> parts = resolvePartList(request, name);
return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null);
}
// 不支持的参数类型返回 UNRESOLVABLE(表示应由其他 Resolver 处理)
else {
return UNRESOLVABLE;
}
}

  • 当请求是 multipart/form-data 类型,并且被 MultipartResolver 解析之后:
    • MultipartResolver(如 StandardServletMultipartResolver 或 CommonsMultipartResolver)调用 resolveMultipart(request) 方法。
    • 此方法会将原始的 HttpServletRequest 包装成 MultipartHttpServletRequest 实现类的实例(如 StandardMultipartHttpServletRequest)。
    • 在构造该实例的过程中,会解析请求内容,并将上传的文件封装为 MultipartFile 对象,同时填充到 MultiValueMap<String, MultipartFile> 中。
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
public class StandardMultipartHttpServletRequest extends AbstractMultipartHttpServletRequest {

.......

public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing)
throws MultipartException {

super(request);
if (!lazyParsing) {
parseRequest(request); //⬇️⬇️⬇️
}
}

private void parseRequest(HttpServletRequest request) { //⬅️⬅️⬅️
try {
Collection<Part> parts = request.getParts();
this.multipartParameterNames = CollectionUtils.newLinkedHashSet(parts.size());
MultiValueMap<String, MultipartFile> files = new LinkedMultiValueMap<>(parts.size());
for (Part part : parts) {
String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION);
ContentDisposition disposition = ContentDisposition.parse(headerValue);
String filename = disposition.getFilename();
if (filename != null) {
files.add(part.getName(), new StandardMultipartFile(part, filename));
}
else {
this.multipartParameterNames.add(part.getName());
}
}
setMultipartFiles(files); //⬇️⬇️⬇️⬇️
}
catch (Throwable ex) {
handleParseFailure(ex);
}
}
// 改方法在 AbstractMultipartHttpServletRequest类中
protected final void setMultipartFiles(MultiValueMap<String, MultipartFile> multipartFiles) {//⬅️⬅️⬅️⬅️
this.multipartFiles =
new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles));
}
}

52、错误处理-SpringBoot默认错误处理机制

Spring Boot官方文档 - Error Handling

默认规则

  • 默认情况下,Spring Boot提供/error处理所有错误的映射

  • 机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个“whitelabel”错误视图,以HTML格式呈现相同的数据

  • 要对其进行自定义,添加View解析为error

  • 要完全替换默认行为,可以实现 ErrorController 并注册该类型的Bean定义,或添加ErrorAttributes类型的组件以使用现有机制但替换其内容。

  • /templates/error/下的4xx,5xx页面会被自动解析

53、错误处理-【源码分析】底层组件功能分析

  • ErrorMvcAutoConfiguration 自动配置异常处理规则

  • 容器中的组件 类型:DefaultErrorAttributes => id:errorAttributes

    • public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered

      • DefaultErrorAttributes:定义错误页面中可以包含的数据(异常明细,堆栈信息等)
  • 容器中的组件 类型:BasicErrorController=> id:basicErrorController(json+白页 适配响应)

    • 处理默认 /error路径的请求
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
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

.........

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(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 : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
  • 容器中有组件 View => id是 error;(响应默认错误页)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {

private final StaticView defaultErrorView = new StaticView();

@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}

// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}

}
  • 容器中放组件 BeanNameViewResolver(视图解析器) 按照返回的视图名作为组件的id去容器中找View对象。
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
private static final class StaticView implements View {

private static final MediaType TEXT_HTML_UTF8 = new MediaType("text", "html", StandardCharsets.UTF_8);

private static final Log logger = LogFactory.getLog(StaticView.class);

@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
if (response.isCommitted()) {
String message = getMessage(model);
logger.error(message);
return;
}
response.setContentType(TEXT_HTML_UTF8.toString());
StringBuilder builder = new StringBuilder();
Object timestamp = model.get("timestamp");
Object message = model.get("message");
Object trace = model.get("trace");
if (response.getContentType() == null) {
response.setContentType(getContentType());
}
builder.append("<html><body><h1>Whitelabel Error Page</h1>")
.append("<p>This application has no explicit mapping for /error, so you are seeing this as a fallback.</p>")
.append("<div id='created'>")
.append(timestamp)
.append("</div>")
.append("<div>There was an unexpected error (type=")
.append(htmlEscape(model.get("error")))
.append(", status=")
.append(htmlEscape(model.get("status")))
.append(").</div>");
if (message != null) {
builder.append("<div>").append(htmlEscape(message)).append("</div>");
}
if (trace != null) {
builder.append("<div style='white-space:pre-wrap;'>").append(htmlEscape(trace)).append("</div>");
}
builder.append("</body></html>");
response.getWriter().append(builder.toString());
}

private String htmlEscape(Object input) {
return (input != null) ? HtmlUtils.htmlEscape(input.toString()) : null;
}

private String getMessage(Map<String, ?> model) {
Object path = model.get("path");
String message = "Cannot render error page for request [" + path + "]";
if (model.get("message") != null) {
message += " and exception [" + model.get("message") + "]";
}
message += " as the response has already been committed.";
message += " As a result, the response may have the wrong status code.";
return message;
}

@Override
public String getContentType() {
return "text/html";
}

}
  • 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties({ WebProperties.class, WebMvcProperties.class })
static class DefaultErrorViewResolverConfiguration {

private final ApplicationContext applicationContext;

private final Resources resources;

DefaultErrorViewResolverConfiguration(ApplicationContext applicationContext, WebProperties webProperties) {
this.applicationContext = applicationContext;
this.resources = webProperties.getResources();
}

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

}
  • 如果发生异常错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面(主要作用)。

    • error/404、5xx.html
    • 如果想要返回页面,就会找error视图(StaticView默认是一个白页)
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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

private static final Map<Series, String> SERIES_VIEWS;

static {
Map<Series, String> views = new EnumMap<>(Series.class);
views.put(Series.CLIENT_ERROR, "4xx");
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}

private final ApplicationContext applicationContext;

private final Resources resources;

private final TemplateAvailabilityProviders templateAvailabilityProviders;

private int order = Ordered.LOWEST_PRECEDENCE;

/**
* Create a new {@link DefaultErrorViewResolver} instance.
* @param applicationContext the source application context
* @param resources resource properties
* @since 2.4.0
*/
public DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resources) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resources, "Resources must not be null");
this.applicationContext = applicationContext;
this.resources = resources;
this.templateAvailabilityProviders = new TemplateAvailabilityProviders(applicationContext);
}

DefaultErrorViewResolver(ApplicationContext applicationContext, Resources resourceProperties,
TemplateAvailabilityProviders templateAvailabilityProviders) {
Assert.notNull(applicationContext, "ApplicationContext must not be null");
Assert.notNull(resourceProperties, "Resources must not be null");
this.applicationContext = applicationContext;
this.resources = resourceProperties;
this.templateAvailabilityProviders = templateAvailabilityProviders;
}

@Override
public ModelAndView resolveErrorView(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;
}

private ModelAndView resolve(String viewName, Map<String, Object> model) { //✨✨✨✨✨✨
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}

private ModelAndView resolveResource(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()) {
return new ModelAndView(new HtmlResourceView(resource), model);
}
}
catch (Exception ex) {
// Ignore
}
}
return null;
}

@Override
public int getOrder() {
return this.order;
}

public void setOrder(int order) {
this.order = order;
}

/**
* {@link View} backed by an HTML resource.
*/
private static class HtmlResourceView implements View {

private final Resource resource;

HtmlResourceView(Resource resource) {
this.resource = resource;
}

@Override
public String getContentType() {
return MediaType.TEXT_HTML_VALUE;
}

@Override
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
response.setContentType(getContentType());
FileCopyUtils.copy(this.resource.getInputStream(), response.getOutputStream());
}

}

}

54、错误处理-【源码流程】异常处理流程

譬如写一个会抛出异常的控制层:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Slf4j
@RestController
public class HelloController {

@RequestMapping("/hello")
public String handle01(){

int i = 1 / 0;//将会抛出ArithmeticException

log.info("Hello, Spring Boot 2!");
return "Hello, Spring Boot 2!";
}
}

当浏览器发出/hello请求,DispatcherServletdoDispatch()mv = ha.handle(processedRequest, response, mappedHandler.getHandler());将会抛出ArithmeticException

  • 执行目标方法,目标方法运行期间有任何异常都会catch,用dispatchException封装异常

  • 进入视图解析(页面渲染)processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);

  • 处理handler发生的异常,处理完成返回 ModelAndView

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
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {

.........

// Actually invoke the handler.
//1️⃣ 将会抛出ArithmeticException
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
//2️⃣ 将会捕捉ArithmeticException
dispatchException = ex;
}
catch (Throwable err) {

.........

}
//3️⃣ 捕捉后,继续运行
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);⬇️⬇️⬇️⬇️
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {

.......

}
}
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
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {

boolean errorView = false;

if (exception != null) {
if (exception instanceof ModelAndViewDefiningException mavDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = mavDefiningException.getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//✨✨✨✨✨✨ 处理handler发生的异常,处理完成返回 ModelAndView
mv = processHandlerException(request, response, handler, exception); ⬇️⬇️⬇️⬇️⬇️
errorView = (mv != null);
}
}

// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}

if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}

if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
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
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {

// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Reset the response body buffer if the response is not committed already,
// leaving the response headers in place.
try {
response.resetBuffer();
}
catch (IllegalStateException illegalStateException) {
// the response is already committed, leave it to exception handlers anyway
}

// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null;
if (this.handlerExceptionResolvers != null) {
//✨✨✨✨遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
// 若只有系统的自带的异常解析器(没有自定义的),异常还是会抛出
throw ex;
}
1
2
3
4
5
6
7
public interface HandlerExceptionResolver {

@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);

}
  • 系统默认的异常解析器

    • image-20250522222204114

    • DefaultErrorAttributes先来处理异常,它主要功能是把异常信息保存到request域,并且返回null

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {

.......

@Override
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) {
storeErrorAttributes(request, ex);
return null;
}

private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);//✨✨✨ 把异常信息保存到request域
}

.......

}
  • 默认没有任何解析器能处理异常,所以异常会被抛出。

  • 最终底层就会转发/error请求。会被底层的BasicErrorController处理。

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
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(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);
//如果/template/error内没有4**.html或5**.html,
//modelAndView为空,最终还是返回viewName为error的modelAndView
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
}

@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}

.........

}
  • 解析错误视图;遍历所有的errorViewResolver 看谁能解析

  • 默认的DefaultErrorViewResolver,作用是把响应状态码作为错误页的地址,error/500.html

  • 模版引擎最终响应这个页面error/500.html

55、错误处理-【源码流程】几种异常处理原理

  • 自定义错误页

    • error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
  • @ControllerAdvice+@ExceptionHandler处理全局异常;底层是 ExceptionHandlerExceptionResolver 支持的

1
2
3
4
5
6
7
8
9
10
11
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler({ArithmeticException.class,NullPointerException.class}) //处理异常
public String handleArithException(Exception e){

log.error("异常是:{}",e);
return "login"; //视图地址
}
}
  • @ResponseStatus+自定义异常,底层是 ResponseStatusExceptionResolver ,把responseStatus注解的信息组装成ModelAndView返回

    底层调用 response.sendError(statusCode, resolvedReason),向tomcat发送/error错误

1
2
3
4
5
6
7
8
9
10
11
@ResponseStatus(value= HttpStatus.FORBIDDEN,reason = "用户数量太多")
public class UserTooManyException extends RuntimeException {

public UserTooManyException(){

}

public UserTooManyException(String message){
super(message);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Controller
public class TableController {

@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "123444"),
new User("haha", "aaaaa"),
new User("hehe ", "aaddd"));
model.addAttribute("users",users);

if(users.size()>3){
throw new UserTooManyException();//抛出自定义异常
}
return "table/dynamic_table";
}

}
  • Spring抛出的异常如 HttpRequestMethodNotSupportedException等;由DefaultHandlerExceptionResolver处理框架底层异常。

    • response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
  • 自定义实现 HandlerExceptionResolver 处理异常;可以作为默认的全局异常处理规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Order(value= Ordered.HIGHEST_PRECEDENCE)  //优先级,数字越小优先级越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler, Exception ex) {

try {
response.sendError(511,"我喜欢的错误");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
  • ErrorViewResolver 实现自定义处理异常
    • response.sendError(),error请求就会转给controller。
      • 抛出异常没有Resolver能处理,tomcat底层调用response.sendError(),error请求就会转给controller。
      • basicErrorController要去的页面地址是由ErrorViewResolver解析,最底层的异常处理(所有没人处理的异常都由它捕获)
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
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {

...

@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(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 : new ModelAndView("error", model);
}

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}

...

}
1
2
3
4
5
6
@FunctionalInterface
public interface ErrorViewResolver {

ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model);

}

56、原生组件注入-原生注解与Spring方式注入

官方文档 - Servlets, Filters, and listeners

使用原生的注解

1
2
3
4
5
6
7
8
@WebServlet(urlPatterns = "/my")//---> 直接响应,没有经过spring的拦截器
public class MyServlet extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("66666");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@WebFilter(urlPatterns={"/css/*","/images/*"}) //没有拦截my
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}

@Override
public void destroy() {
log.info("MyFilter销毁");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {


@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目初始化完成");
}

@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MySwervletContextListener监听到项目销毁");
}
}

最后还要在主启动类添加注解@ServletComponentScan

1
2
3
4
5
6
7
8
@ServletComponentScan(basePackages = "com.atguigu")// ---> 指定原生servlet组件都放在哪里
@SpringBootApplication(exclude = RedisAutoConfiguration.class)
public class Boot05WebAdminApplication {

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

Spring方式注入

ServletRegistrationBean, FilterRegistrationBean, and ServletListenerRegistrationBean

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
@Configuration(proxyBeanMethods = true)//保证依赖的组件是单实例
public class MyRegistConfig {

@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();

return new ServletRegistrationBean(myServlet,"/my","/my02");
}


@Bean
public FilterRegistrationBean myFilter(){

MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}

@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}

57、原生组件注入-【源码分析】DispatcherServlet注入原理

org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration配置类

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
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
@AutoConfigureAfter(ServletWebServerFactoryAutoConfiguration.class)
public class DispatcherServletAutoConfiguration {

/*
* The bean name for a DispatcherServlet that will be mapped to the root URL "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_BEAN_NAME = "dispatcherServlet";

/*
* The bean name for a ServletRegistrationBean for the DispatcherServlet "/"
*/
public static final String DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME = "dispatcherServletRegistration";

@Configuration(proxyBeanMethods = false)
@Conditional(DefaultDispatcherServletCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
protected static class DispatcherServletConfiguration {

//---> 容器中自动配置DispatcherServlet组件,属性绑定了 WebMvcProperties
@Bean(name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
DispatcherServlet dispatcherServlet = new DispatcherServlet();
dispatcherServlet.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
dispatcherServlet.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
dispatcherServlet.setThrowExceptionIfNoHandlerFound(webMvcProperties.isThrowExceptionIfNoHandlerFound());
dispatcherServlet.setPublishEvents(webMvcProperties.isPublishRequestHandledEvents());
dispatcherServlet.setEnableLoggingRequestDetails(webMvcProperties.isLogRequestDetails());
return dispatcherServlet;
}

@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}

}

@Configuration(proxyBeanMethods = false)
@Conditional(DispatcherServletRegistrationCondition.class)
@ConditionalOnClass(ServletRegistration.class)
@EnableConfigurationProperties(WebMvcProperties.class)
@Import(DispatcherServletConfiguration.class)
protected static class DispatcherServletRegistrationConfiguration {

//---> 通过DispatcherServletRegistrationBean将DispatcherServlet配置进来
@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME)
@ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet,
WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet,
webMvcProperties.getServlet().getPath());//---> dispatcherServlet的拦截路径,默认"/"
registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME);
registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup());
multipartConfig.ifAvailable(registration::setMultipartConfig);
return registration;
}

}

...

}

DispatcherServlet默认映射的是 / 路径,可以通过在配置文件修改spring.mvc.servlet.path

多个servlet都能处理同一层路径,则遵循精确优先原则

image-20250418221705823

58、嵌入式Servlet容器-【源码分析】切换web服务器与定制化

  • 默认支持的WebServer

    • Tomcat, Jetty, or Undertow
    • ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器。
  • 原理

    • SpringBoot应用启动发现当前是Web应用,web场景包-导入tomcat。
    • web应用会创建一个web版的IOC容器 ServletWebServerApplicationContext
    • ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory (Servlet 的web服务器工厂——>Servlet 的web服务器)。
    • SpringBoot底层默认有很多的WebServer工厂(ServletWebServerFactoryConfiguration内创建Bean),如:
      • TomcatServletWebServerFactory
      • JettyServletWebServerFactory
      • UndertowServletWebServerFactory
    • 底层直接会有一个自动配置类 ServletWebServerFactoryAutoConfiguration
    • ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)。
    • ServletWebServerFactoryConfiguration 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
    • TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize——this.tomcat.start();
    • 内嵌服务器,与以前手动把启动服务器相比,改成现在使用代码启动(tomcat核心jar包存在)。

Spring Boot默认使用Tomcat服务器,若需更改其他服务器,则修改工程pom.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion> <!--排除tomcat场景-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency> <!--启用jetty场景-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jetty</artifactId>
</dependency>

官方文档 - Use Another Web Server

定制Servlet容器

  • 实现WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>

    • 把配置文件的值和ServletWebServerFactory进行绑定
  • 修改配置文件 server.xxx

  • 直接自定义 ConfigurableServletWebServerFactory

xxxxxCustomizer:定制化器,可以改变xxxx的默认规则

1
2
3
4
5
6
7
8
9
10
11
12
13
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;

@Component
public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {

@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}

}

59、定制化原理-SpringBoot定制化组件的几种方式(小结)

定制化的常见方式

  • 修改配置文件
  • xxxxxCustomizer
  • 编写自定义的配置类 xxxConfiguration + @Bean替换、增加容器中默认组件,视图解析器
  • Web应用 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能 + @Bean给容器中再扩展一些组件
1
2
3
4
5
6
@Configuration
public class XXXWebConfig implements WebMvcConfigurer{

........

}
  • @EnableWebMvc + WebMvcConfigurer@Bean 可以全面接管SpringMVC,所有规则全部自己重新配置; 实现定制和扩展功能(高级功能,初学者退避三舍)。
    • 原理:
      1. WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类,如静态资源、欢迎页等
      2. 一旦使用@EnableWebMvc ,复合注解会 @Import(DelegatingWebMvcConfiguration.class)
      3. DelegatingWebMvcConfiguration的作用,只保证SpringMVC最基本的使用
        • 把所有系统中的WebMvcConfigurer拿过来,所有功能的定制都是这些WebMvcConfigurer合起来一起生效
        • 自动配置了一些非常底层的组件,如RequestMappingHandlerMapping,这些组件依赖的组件都是从容器中获取
        • public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
      4. WebMvcAutoConfiguration里面的配置要能生效必须@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
      5. @EnableWebMvc 导致了WebMvcAutoConfiguration 没有生效

原理分析套路

场景starter - xxxxAutoConfiguration - 条件判断导入xxx组件 - 绑定xxxProperties - 绑定配置文件项

60、数据访问-数据库场景的自动配置分析与整合测试

导入JDBC场景

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

数据库驱动?

为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下要操作什么数据库。

接着导入数据库驱动包(MySQL为例)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<!--默认版本:-->
<mysql.version>8.0.22</mysql.version>

<!--数据库版本和驱动版本对应-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<version>5.1.49</version>-->
</dependency>

<!--
想要修改版本
1、直接依赖引入具体版本(maven的就近依赖原则)
2、重新声明版本(maven的属性的就近优先原则)
-->
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>

相关数据源配置类

  • DataSourceAutoConfiguration : 数据源的自动配置。

    • 修改数据源相关的配置:spring.datasource

    • 数据库连接池的配置,是自己容器中没有DataSource才自动配置的

    • 底层配置好的连接池是:HikariDataSource

    • 	@Configuration(proxyBeanMethods = false)
      	@Conditional(PooledDataSourceCondition.class)
      	@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
      	@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
      			DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
      			DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
      	protected static class PooledDataSourceConfiguration
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23

      - `DataSourceTransactionManagerAutoConfiguration`: 事务管理器的自动配置。

      - `JdbcTemplateAutoConfiguration`: `JdbcTemplate`的自动配置,可以来对数据库进行CRUD。

      - 可以修改前缀为`spring.jdbc`的配置项来修改`JdbcTemplate`。
      - `@Bean @Primary JdbcTemplate`:Spring容器中有这个`JdbcTemplate`组件,使用`@Autowired`。

      - `JndiDataSourceAutoConfiguration`: JNDI的自动配置。

      - `XADataSourceAutoConfiguration`: 分布式事务相关的。



      ### 修改配置项

      ```yaml
      spring:
      datasource:
      url: jdbc:mysql://localhost:3306/db_account
      username: root
      password: 123456
      driver-class-name: com.mysql.jdbc.Driver

单元测试数据源

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {

@Autowired
JdbcTemplate jdbcTemplate;

@Test//用@org.junit.Test会报空指针异常,可能跟JUnit新版本有关
void contextLoads() {
// jdbcTemplate.queryForObject("select * from account_tbl")
// jdbcTemplate.queryForList("select * from account_tbl")
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("记录总数:{}",aLong);
}

}

61、数据访问-自定义方式整合druid数据源

Druid官网

Druid是什么

它是数据库连接池,它能够提供强大的监控和扩展功能。

官方文档 - Druid连接池介绍

Spring Boot整合第三方技术的两种方式:

  • 自定义

  • 找starter场景

自定义方式

添加依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>

配置Druid数据源

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

@Bean
@ConfigurationProperties("spring.datasource")//复用配置文件的数据源配置
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();

// druidDataSource.setUrl();
// druidDataSource.setUsername();
// druidDataSource.setPassword();

return druidDataSource;
}
}

更多配置项

配置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
34
35
36
37
38
39
40
41
42
43
44
45
46
@Configuration
public class MyConfig {

@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();

//加入监控和防火墙功能功能
druidDataSource.setFilters("stat,wall");

return druidDataSource;
}

/**
* 配置 druid的监控页功能
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet(){
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean =
new ServletRegistrationBean<>(statViewServlet, "/druid/*");

//监控页账号密码:
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","123456");

return registrationBean;
}

/**
* WebStatFilter 用于采集web-jdbc关联监控的数据。
*/
@Bean
public FilterRegistrationBean webStatFilter(){
WebStatFilter webStatFilter = new WebStatFilter();

FilterRegistrationBean<WebStatFilter> filterRegistrationBean = new FilterRegistrationBean<>(webStatFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/*"));
filterRegistrationBean.addInitParameter("exclusions","*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");

return filterRegistrationBean;
}

}

62、数据访问-druid数据源starter整合方式

官方文档 - Druid Spring Boot Starter

引入依赖

1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>

分析自动配置

  • 扩展配置项 spring.datasource.druid
  • 自动配置类DruidDataSourceAutoConfigure
  • DruidSpringAopConfiguration.class, 监控SpringBean的;配置项:spring.datasource.druid.aop-patterns
  • DruidStatViewServletConfiguration.class, 监控页的配置。spring.datasource.druid.stat-view-servlet默认开启。
  • DruidWebStatFilterConfiguration.class,web监控配置。spring.datasource.druid.web-stat-filter默认开启。
  • DruidFilterConfiguration.class所有Druid的filter的配置:
1
2
3
4
5
6
7
8
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";

配置示例

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
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver

druid:
aop-patterns: com.atguigu.admin.* #监控SpringBean
filters: stat,wall # 底层开启功能,stat(sql监控),wall(防火墙)

stat-view-servlet: # 配置监控页功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false

web-stat-filter: # 监控web
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'


filter:
stat: # 对上面filters里面的stat的详细配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false

63、数据访问-整合MyBatis-配置版

MyBatis的GitHub仓库

MyBatis官方

starter的命名方式

  1. SpringBoot官方的Starter:spring-boot-starter-*
  2. 第三方的: *-spring-boot-starter

引入依赖

1
2
3
4
5
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>

配置模式:

  • 全局配置文件

  • SqlSessionFactory:自动配置好了

  • SqlSession:自动配置了SqlSessionTemplate 组合了SqlSession

  • @Import(AutoConfiguredMapperScannerRegistrar.class)

  • Mapper: 只要我们写的操作MyBatis的接口标准了@Mapper就会被自动扫描进来

1
2
3
4
5
6
7
8
9
10
@EnableConfigurationProperties(MybatisProperties.class) //MyBatis配置项绑定类。
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration{
...
}

@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties{
...
}

配置文件

1
2
3
4
5
6
7
8
9
10
11
spring:
datasource:
username: root
password: 1234
url: jdbc:mysql://localhost:3306/my
driver-class-name: com.mysql.jdbc.Driver

# 配置mybatis规则
mybatis:
config-location: classpath:mybatis/mybatis-config.xml #全局配置文件位置
mapper-locations: classpath:mybatis/*.xml #sql映射文件位置

mybatis-config.xml:

1
2
3
4
5
6
7
8
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

<!-- 由于Spring Boot自动配置缘故,此处不必配置,只用来做做样。-->
</configuration>

Mapper接口

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper">

<select id="getUser" resultType="com.lun.boot.bean.User">
select * from user where id=#{id}
</select>
</mapper>
1
2
3
4
5
6
7
import com.lun.boot.bean.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper {
public User getUser(Integer id);
}

POJO

1
2
3
4
5
6
public class User {
private Integer id;
private String name;

//getters and setters...
}

DB

1
2
3
4
5
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(45) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4;

Controller and Service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Controller
public class UserController {

@Autowired
private UserService userService;

@ResponseBody
@GetMapping("/user/{id}")
public User getUser(@PathVariable("id") Integer id){

return userService.getUser(id);
}

}
1
2
3
4
5
6
7
8
9
10
11
@Service
public class UserService {

@Autowired
private UserMapper userMapper;//IDEA下标红线,可忽视这红线

public User getUser(Integer id){
return userMapper.getUser(id);
}

}

配置private Configuration configuration; 也就是配置mybatis.configuration相关的,就是相当于改mybatis全局配置文件中的值。(也就是说配置了mybatis.configuration,就不需配置mybatis全局配置文件了)

1
2
3
4
5
6
7
# 配置mybatis规则
mybatis:
mapper-locations: classpath:mybatis/mapper/*.xml
# 可以不写全局配置文件,所有全局配置文件的配置都放在configuration配置项中了。
# config-location: classpath:mybatis/mybatis-config.xml
configuration:
map-underscore-to-camel-case: true

小结

  • 导入MyBatis官方Starter。
  • 编写Mapper接口,需@Mapper注解。
  • 编写SQL映射文件并绑定Mapper接口。
  • application.yaml中指定Mapper配置文件的所处位置,以及指定全局配置文件的信息 (建议:配置在mybatis.configuration)。

64、数据访问-整合MyBatis-注解配置混合版

你可以通过Spring Initializr添加MyBatis的Starer。

注解与配置混合搭配,干活不累

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mapper
public interface UserMapper {
public User getUser(Integer id);

@Select("select * from user where id=#{id}")
public User getUser2(Integer id);

public void saveUser(User user);

@Insert("insert into user(`name`) values(#{name})")
@Options(useGeneratedKeys = true, keyProperty = "id")
public void saveUser2(User user);

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lun.boot.mapper.UserMapper">

<select id="getUser" resultType="com.lun.boot.bean.User">
select * from user where id=#{id}
</select>

<insert id="saveUser" useGeneratedKeys="true" keyProperty="id">
insert into user(`name`) values(#{name})
</insert>

</mapper>

最佳实战:

  • 引入mybatis-starter

  • 配置application.yaml中,指定mapper-location位置即可

  • 编写Mapper接口并标注@Mapper注解

  • 简单方法直接注解方式

  • 复杂方法编写mapper.xml进行绑定映射

  • 使用@MapperScan("com.lun.boot.mapper") 简化,Mapper接口就可以不用标注@Mapper注解。

1
2
3
4
5
6
7
8
9
@MapperScan("com.lun.boot.mapper")
@SpringBootApplication
public class MainApplication {

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

}

65、数据访问-整合MyBatisPlus操作数据库

IDEA的MyBatis的插件 - MyBatisX

MyBatisPlus官网

MyBatisPlus官方文档

MyBatisPlus是什么

MyBatis-Plus(简称 MP)是一个 MyBatis的增强工具,在 MyBatis 的基础上只做增强不做改变,为简化开发、提高效率而生。

添加依赖:

1
2
3
4
5
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>

自动配置

  • MybatisPlusAutoConfiguration配置类,MybatisPlusProperties配置项绑定。 mybatis-plus:xxx 就是对mybatis-plus的定制

  • SqlSessionFactory自动配置好,底层是容器中默认的数据源。

  • mapperLocations自动配置好的,有默认值classpath*:/mapper/**/*.xml,这表示任意包的类路径下的所有mapper文件夹下任意路径下的所有xml都是sql映射文件。 建议以后sql映射文件放在 mapper下。

  • 容器中也自动配置好了SqlSessionTemplate

  • @Mapper 标注的接口也会被自动扫描,建议直接 @MapperScan("com.lun.boot.mapper")批量扫描。

  • MyBatisPlus优点之一:只需要我们的Mapper继承MyBatisPlus的BaseMapper 就可以拥有CRUD能力,减轻开发工作。

1
2
3
public interface UserMapper extends BaseMapper<User> {

}

66、数据访问-CRUD实验-数据列表展示

官方文档 - CRUD接口

使用MyBatis Plus提供的IServiceServiceImpl,减轻Service层开发工作。

1
2
3
4
5
6
7
8
9
10
11
import com.lun.hellomybatisplus.model.User;
import com.baomidou.mybatisplus.extension.service.IService;

import java.util.List;

/**
* Service 的CRUD也不用写了
*/
public interface UserService extends IService<User> {
//此处故意为空
}
1
2
3
4
5
6
7
8
9
10
11
12
13
import com.lun.hellomybatisplus.model.User;
import com.lun.hellomybatisplus.mapper.UserMapper;
import com.lun.hellomybatisplus.service.UserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {
//此处故意为空
}

与下一节联合在一起

67、数据访问-CRUD实验-分页数据展示

与下一节联合在一起

68、数据访问-CRUD实验-删除用户完成

添加分页插件:

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
@Configuration
public class MyBatisConfig {


/**
* MybatisPlusInterceptor
* @return
*/
@Bean
public paginationInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false
// paginationInterceptor.setOverflow(false);
// 设置最大单页限制数量,默认 500 条,-1 不受限制
// paginationInterceptor.setLimit(500);
// 开启 count 的 join 优化,只针对部分 left join

//这是分页拦截器
PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor();
paginationInnerInterceptor.setOverflow(true);
paginationInnerInterceptor.setMaxLimit(500L);
mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor);

return mybatisPlusInterceptor;
}
}
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
<table class="display table table-bordered table-striped" id="dynamic-table">
<thead>
<tr>
<th>#</th>
<th>name</th>
<th>age</th>
<th>email</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr class="gradeX" th:each="user: ${users.records}">
<td th:text="${user.id}"></td>
<td>[[${user.name}]]</td>
<td th:text="${user.age}">Win 95+</td>
<td th:text="${user.email}">4</td>
<td>
<a th:href="@{/user/delete/{id}(id=${user.id},pn=${users.current})}"
class="btn btn-danger btn-sm" type="button">删除</a>
</td>
</tr>
</tfoot>
</table>

<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="dynamic-table_info">
当前第[[${users.current}]]页 总计 [[${users.pages}]]页 共[[${users.total}]]条记录
</div>
</div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled"><a href="#">← 前一页</a></li>
<li th:class="${num == users.current?'active':''}"
th:each="num:${#numbers.sequence(1,users.pages)}" >
<a th:href="@{/dynamic_table(pn=${num})}">[[${num}]]</a>
</li>
<li class="next disabled"><a href="#">下一页 → </a></li>
</ul>
</div>
</div>
</div>

#numbers表示methods for formatting numeric objects.link

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
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1")Integer pn,
RedirectAttributes ra){

userService.removeById(id);

ra.addAttribute("pn",pn);
return "redirect:/dynamic_table";
}

@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
//表格内容的遍历

//从数据库中查出user表中的用户进行展示

//构造分页参数
Page<User> page = new Page<>(pn, 2);
//调用page进行分页
Page<User> userPage = userService.page(page, null);

model.addAttribute("users",userPage);

return "table/dynamic_table";
}

69、数据访问-准备阿里云Redis环境

添加依赖

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--导入jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

image-20250421223756035

自动配置

  • RedisAutoConfiguration自动配置类,RedisProperties 属性类 --> spring.redis.xxx是对redis的配置。
  • 连接工厂LettuceConnectionConfigurationJedisConnectionConfiguration是准备好的。
  • 自动注入了RedisTemplate<Object, Object>xxxTemplate
  • 自动注入了StringRedisTemplate,key,value都是String
  • 底层只要我们使用StringRedisTemplateRedisTemplate就可以操作Redis。

外网Redis环境搭建

  1. 阿里云按量付费Redis,其中选择经典网络

  2. 申请Redis的公网连接地址。

  3. 修改白名单,允许0.0.0.0/0访问。

70、数据访问-Redis操作与统计小实验

相关Redis配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
redis:
# url: redis://lfy:Lfy123456@r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com:6379
host: r-bp1nc7reqesxisgxpipd.redis.rds.aliyuncs.com
port: 6379
password: lfy:Lfy123456
client-type: jedis
jedis:
pool:
max-active: 10
# lettuce:# 另一个用来连接redis的java框架
# pool:
# max-active: 10
# min-idle: 5

测试Redis连接:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@SpringBootTest
public class Boot05WebAdminApplicationTests {

@Autowired
StringRedisTemplate redisTemplate;


@Autowired
RedisConnectionFactory redisConnectionFactory;

@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();

operations.set("hello","world");

String hello = operations.get("hello");
System.out.println(hello);

System.out.println(redisConnectionFactory.getClass());
}

}

Redis Desktop Manager:可视化Redis管理软件。

URL统计拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Component
public class RedisUrlCountInterceptor implements HandlerInterceptor {

@Autowired
StringRedisTemplate redisTemplate;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String uri = request.getRequestURI();

//默认每次访问当前uri就会计数+1
redisTemplate.opsForValue().increment(uri);

return true;
}
}

注册URL统计拦截器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
public class AdminWebConfig implements WebMvcConfigurer{

@Autowired
RedisUrlCountInterceptor redisUrlCountInterceptor;


@Override
public void addInterceptors(InterceptorRegistry registry) {

registry.addInterceptor(redisUrlCountInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/js/**","/aa/**");
}
}

Filter、Interceptor 几乎拥有相同的功能?

  • Filter是Servlet定义的原生组件,它的好处是脱离Spring应用也能使用。
  • Interceptor是Spring定义的接口,可以使用Spring的自动装配等功能。

调用Redis内的统计数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Slf4j
@Controller
public class IndexController {

@Autowired
StringRedisTemplate redisTemplate;

@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){

log.info("当前方法是:{}","mainPage");

ValueOperations<String, String> opsForValue =
redisTemplate.opsForValue();

String s = opsForValue.get("/main.html");
String s1 = opsForValue.get("/sql");

model.addAttribute("mainCount",s);
model.addAttribute("sqlCount",s1);

return "main";
}
}

71、单元测试-JUnit5简介

Spring Boot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库

JUnit 5官方文档

作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

  • JUnit Platform: Junit Platform是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。

  • JUnit Jupiter: JUnit Jupiter提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于在Junit Platform上运行。

  • JUnit Vintage: 由于JUint已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit3.x的测试引擎。

注意

  • SpringBoot 2.4 以上版本移除了默认对 Vintage 的依赖。如果需要兼容JUnit4需要自行引入(不能使用JUnit4的功能 @Test)

  • JUnit 5’s Vintage已经从spring-boot-starter-test从移除。如果需要继续兼容Junit4需要自行引入Vintage依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
  • 使用添加JUnit 5,添加对应的starter:
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
  • Spring的JUnit 5的基本单元测试模板(Spring的JUnit4的是@SpringBootTest+@RunWith(SpringRunner.class)):
  • SpringBoot整合Junit以后。
    • 编写测试方法:@Test标注(注意需要使用junit5版本的注解)
    • Junit类具有Spring的功能,@Autowired、比如 @Transactional 标注测试方法,测试完成后自动回滚
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;//注意不是org.junit.Test(这是JUnit4版本的)
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class SpringBootApplicationTests {

@Autowired
private Component component;

@Test
//@Transactional 标注后连接数据库有回滚功能
public void contextLoads() {
Assertions.assertEquals(5, component.getFive());
}
}

72、单元测试-常用测试注解

官方文档 - 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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import org.junit.jupiter.api.*;

@DisplayName("junit5功能测试类")
public class Junit5Test {


@DisplayName("测试displayname注解")
@Test
void testDisplayName() {
System.out.println(1);
System.out.println(jdbcTemplate);
}

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
assertTrue(StringUtils.isPalindrome(candidate));
}


@Disabled
@DisplayName("测试方法2")
@Test
void test2() {
System.out.println(2);
}

@RepeatedTest(5)
@Test
void test3() {
System.out.println(5);
}

/**
* 规定方法超时时间。超出时间测试出异常
*
* @throws InterruptedException
*/
@Timeout(value = 500, unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}


@BeforeEach
void testBeforeEach() {
System.out.println("测试就要开始了...");
}

@AfterEach
void testAfterEach() {
System.out.println("测试结束了...");
}

@BeforeAll
static void testBeforeAll() {
System.out.println("所有测试就要开始了...");
}

@AfterAll
static void testAfterAll() {
System.out.println("所有测试以及结束了...");

}

}

73、单元测试-断言机制

断言Assertion是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。检查业务逻辑返回的数据是否合理。所有的测试运行结束以后,会有一个详细的测试报告。

assert
v. 明确肯定;断言;坚持自己的主张;表现坚定;维护自己的权利(或权威);生效

JUnit 5 内置的断言可以分成如下几个类别:

简单断言

用来对单个值进行简单的验证。如:

方法 说明
assertEquals 判断两个对象或两个原始类型是否相等
assertNotEquals 判断两个对象或两个原始类型是否不相等
assertSame 判断两个对象引用是否指向同一个对象
assertNotSame 判断两个对象引用是否指向不同的对象
assertTrue 判断给定的布尔值是否为 true
assertFalse 判断给定的布尔值是否为 false
assertNull 判断给定的对象引用是否为 null
assertNotNull 判断给定的对象引用是否不为 null
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math"); //前面断言失败,后面的代码都不会执行
assertNotEquals(3, 1 + 1);

assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);

assertFalse(1 > 2);
assertTrue(1 < 2);

assertNull(null);
assertNotNull(new Object());
}

数组断言

通过 assertArrayEquals 方法来判断两个对象或原始类型的数组是否相等。

1
2
3
4
5
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}

组合断言

assertAll()方法接受多个 org.junit.jupiter.api.Executable 函数式接口的实例作为要验证的断言,可以通过 lambda 表达式很容易的提供这些断言。

1
2
3
4
5
6
7
8
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}

异常断言

在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.assertThrows(),配合函数式编程就可以进行使用。

1
2
3
4
5
6
7
@Test
@DisplayName("异常测试")
public void exceptionTest() {
ArithmeticException exception = Assertions.assertThrows(
//扔出断言异常
ArithmeticException.class, () -> System.out.println(1 % 0));
}

超时断言

JUnit5还提供了Assertions.assertTimeout()为测试方法设置了超时时间。

1
2
3
4
5
6
@Test
@DisplayName("超时测试")
public void timeoutTest() {
//如果测试方法时间超过1s将会异常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}

快速失败

通过 fail 方法直接使得测试失败。

1
2
3
4
5
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}

74、单元测试-前置条件

Unit 5 中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言assertions会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行终止

前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@DisplayName("前置条件")
public class AssumptionsTest {
private final String environment = "DEV";

@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}

@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}

assumeTrueassumFalse 确保给定的条件为 truefalse,不满足条件会使得测试执行终止。

assumingThat 的参数是表示条件的布尔值和对应的 Executable 接口的实现对象。只有条件满足时,Executable 对象才会被执行;当条件不满足时,测试执行并不会终止。

75、单元测试-嵌套测试

官方文档 - Nested Tests

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
72
73
@DisplayName("A stack")
class TestingAStackDemo {

Stack<Object> stack;

@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
//嵌套测试情况下,外层的Test不能驱动内层的Before(After)Each/All之类的方法提前/之后运行
}

@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可以驱动外层的Before(After)Each/All之类的方法提前/之后运行
@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());
}
}
}
}

76、单元测试-参数化测试

官方文档 - Parameterized Tests

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

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

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

  • @ValueSource: 为参数化测试指定入参来源,支持八大基础类以及String类型,Class类型
  • @NullSource: 表示为参数化测试提供一个null的入参
  • @EnumSource: 表示为参数化测试提供一个枚举入参
  • @CsvFileSource:表示读取指定CSV文件内容作为参数化测试入参
  • @MethodSource:表示读取指定方法的返回值作为参数化测试入参(注意方法返回需要是一个流)

当然如果参数化测试仅仅只能做到指定普通的入参还达不到让我觉得惊艳的地步。让我真正感到他的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON 文件甚至方法的返回值也可以作为入参。只需要去实现**ArgumentsProvider**接口,任何外部文件都可以作为它的入参。

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");
}

迁移指南

官方文档 - Migrating from JUnit 4

在进行迁移的时候需要注意如下的变化:

  • 注解在 org.junit.jupiter.api 包中,断言在 org.junit.jupiter.api.Assertions 类中,前置条件在 org.junit.jupiter.api.Assumptions 类中。
  • @Before@After 替换成@BeforeEach@AfterEach
  • @BeforeClass@AfterClass 替换成@BeforeAll@AfterAll
  • @Ignore 替换成 @Disabled
  • @Category 替换成 @Tag
  • @RunWith@Rule@ClassRule 替换成@ExtendWith

77、指标监控-SpringBoot Actuator与Endpoint

未来每一个微服务在云上部署以后,我们都需要对其进行监控、追踪、审计、控制等。SpringBoot就抽取了Actuator场景,使得我们每个微服务快速引用即可获得生产级别的应用监控、审计等功能。

官方文档 - Spring Boot Actuator: Production-ready Features

1.x与2.x的不同

  • SpringBoot Actuator 1.x

    • 支持SpringMVC
    • 基于继承方式进行扩展
    • 层级Metrics配置
    • 自定义Metrics收集
    • 默认较少的安全策略
  • SpringBoot Actuator 2.x

    • 支持SpringMVC、JAX-RS以及Webflux
    • 注解驱动进行扩展
    • 层级&名称空间Metrics
    • 底层使用MicroMeter,强大、便捷默认丰富的安全策略

如何使用

  • 添加依赖:
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
  • 访问http://localhost:8080/actuator/**
  • 暴露所有监控信息为HTTP。
1
2
3
4
5
6
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' #以web方式暴露

actuator

英 [ˈæktjʊeɪtə] 美 [ˈæktjuˌeɪtər]

n. 致动(促动,激励,调节)器;传动(装置,机构);拖动装置;马达;操作机构;执行机构(元件);(电磁铁)螺线管;操纵装置(阀门);调速控制器;往复运动油(气)缸;作动筒

metric

英 [ˈmetrɪk] 美 [ˈmetrɪk]

adj. 米制的;公制的;按公制制作的;用公制测量的

n. 度量标准;[数学]度量;诗体;韵文;诗韵

78、指标监控-常使用的端点及开启与禁用

常使用的端点

ID 描述
auditevents 暴露当前应用程序的审核事件信息。需要一个AuditEventRepository组件
beans 显示应用程序中所有Spring Bean的完整列表。
caches 暴露可用的缓存。
conditions 显示自动配置的所有条件信息,包括匹配或不匹配的原因。
configprops 显示所有@ConfigurationProperties
env 暴露Spring的属性ConfigurableEnvironment
flyway 显示已应用的所有Flyway数据库迁移。 需要一个或多个Flyway组件。
health 显示应用程序运行状况信息。
httptrace 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HttpTraceRepository组件。
info 显示应用程序信息。
integrationgraph 显示Spring integrationgraph 。需要依赖spring-integration-core
loggers 显示和修改应用程序中日志的配置。
liquibase 显示已应用的所有Liquibase数据库迁移。需要一个或多个Liquibase组件。
metrics 显示当前应用程序的“指标”信息。
mappings 显示所有@RequestMapping路径列表。
scheduledtasks 显示应用程序中的计划任务。
sessions 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用Spring Session的基于Servlet的Web应用程序。
shutdown 使应用程序正常关闭。默认禁用。
startup 显示由ApplicationStartup收集的启动步骤数据。需要使用SpringApplication进行配置BufferingApplicationStartup
threaddump 执行线程转储。

如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:

ID 描述
heapdump 返回hprof堆转储文件。
jolokia 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core
logfile 返回日志文件的内容(如果已设置logging.file.namelogging.file.path属性)。支持使用HTTPRange标头来检索部分日志文件的内容。
prometheus 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus

其中最常用的Endpoint:

  • Health:监控状况
  • Metrics:运行时指标
  • Loggers:日志记录

Health Endpoint

健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。

重要的几点:

  • health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告。
  • 很多的健康检查默认已经自动配置好了,比如:数据库、redis等。
  • 可以很容易的添加自定义的健康检查机制。

Metrics Endpoint

提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到:

  • 通过Metrics对接多种监控系统。
  • 简化核心Metrics开发。
  • 添加自定义Metrics或者扩展已有Metrics。

开启与禁用Endpoints

  • 默认所有的Endpoint除shutdown都是开启的。
  • 需要开启或者禁用某个Endpoint。配置模式为management.endpoint.<endpointName>.enabled = true
1
2
3
4
management:
endpoint:
beans:
enabled: true
  • 或者禁用所有的Endpoint然后手动开启指定的Endpoint。
1
2
3
4
5
6
7
8
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true

暴露Endpoints

支持的暴露方式

  • HTTP:默认只暴露health和info。
  • JMX:默认暴露所有Endpoint。
  • 除了health和info,剩下的Endpoint都应该进行保护访问。如果引入Spring Security,则会默认配置安全访问规则。
ID JMX Web
auditevents Yes No
beans Yes No
caches Yes No
conditions Yes No
configprops Yes No
env Yes No
flyway Yes No
health Yes Yes
heapdump N/A No
httptrace Yes No
info Yes Yes
integrationgraph Yes No
jolokia N/A No
logfile N/A No
loggers Yes No
liquibase Yes No
metrics Yes No
mappings Yes No
prometheus N/A No
scheduledtasks Yes No
sessions Yes No
shutdown Yes No
startup Yes No
threaddump Yes No

若要更改公开的Endpoint,请配置以下的包含和排除属性:

Property Default
management.endpoints.jmx.exposure.exclude
management.endpoints.jmx.exposure.include *
management.endpoints.web.exposure.exclude
management.endpoints.web.exposure.include info, health

官方文档 - Exposing Endpoints

79、指标监控-定制Endpoint

定制 Health 信息

1
2
3
4
management:
health:
enabled: true
show-details: always #总是显示详细信息。可显示每个模块的状态信息

通过实现HealthIndicator 接口,或继承MyComHealthIndicator 类。

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
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
public class MyHealthIndicator implements HealthIndicator {

@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}

}

/*
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
*/
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
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {

/**
* 真实的检查方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb。 获取连接进行测试
Map<String,Object> map = new HashMap<>();
// 检查完成
if(1 == 2){
// builder.up(); //健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","连接超时");
map.put("ms",3000);
}


builder.withDetail("code",100)
.withDetails(map);

}
}

定制info信息

常用两种方式:

  • 编写配置文件
1
2
3
4
5
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以获取maven的pom文件值
mavenProjectVersion: @project.version@
  • 编写InfoContributor
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.Collections;

import org.springframework.boot.actuate.info.Info;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.stereotype.Component;

@Component
public class ExampleInfoContributor implements InfoContributor {

@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}

}

http://localhost:8080/actuator/info 会输出以上方式返回的所有info信息

定制Metrics信息

Spring Boot支持的metrics

增加定制Metrics:

1
2
3
4
5
6
7
8
9
10
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}

public void hello() {
counter.increment();
}
}
1
2
3
4
5
//也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}

定制Endpoint

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Component
@Endpoint(id = "container")
public class DockerEndpoint {

@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}

@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}

}

场景:

  • 开发ReadinessEndpoint来管理程序是否就绪。
  • 开发LivenessEndpoint来管理程序是否存活。

80、指标监控-Boot Admin Server

官方Github

官方文档

可视化指标监控

What is Spring Boot Admin?

codecentric’s Spring Boot Admin is a community project to manage and monitor your Spring Boot ® applications. The applications register with our Spring Boot Admin Client (via HTTP) or are discovered using Spring Cloud ® (e.g. Eureka, Consul). The UI is just a Vue.js application on top of the Spring Boot Actuator endpoints.

开始使用方法

81、高级特性-Profile环境切换

为了方便多环境适配,Spring Boot简化了profile功能。

  • 默认配置文件application.yaml任何时候都会加载。
  • 指定环境配置文件application-{env}.yamlenv通常替代为test
  • 激活指定环境
    • 配置文件激活:spring.profiles.active=prod
    • 命令行激活:java -jar xxx.jar --spring.profiles.active=prod --person.name=haha(修改配置文件的任意值,命令行优先
  • 默认配置与环境配置同时生效
  • 同名配置项,profile配置优先

@Profile条件装配功能

1
2
3
4
5
6
7
@Data
@Component
@ConfigurationProperties("person")//在配置文件中配置
public class Person{
private String name;
private Integer age;
}

application.properties

1
2
3
person: 
name: lxd
age: 18
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
public interface Person {

String getName();
Integer getAge();

}

@Profile("test")//加载application-test.yaml里的
@Component
@ConfigurationProperties("person")
@Data
public class Worker implements Person {

private String name;
private Integer age;
}

@Profile(value = {"prod","default"})//加载application-prod.yaml里的
@Component
@ConfigurationProperties("person")
@Data
public class Boss implements Person {

private String name;
private Integer age;
}

application-test.yaml

1
2
3
4
5
person:
name: test-张三

server:
port: 7000

application-prod.yaml

1
2
3
4
5
person:
name: prod-张三

server:
port: 8000

application.properties

1
2
# 激活prod配置文件
spring.profiles.active=prod
1
2
3
4
5
6
7
8
@Autowired
private Person person;

@GetMapping("/")
public String hello(){
//激活了prod,则返回Boss;激活了test,则返回Worker
return person.getClass().toString();
}

@Profile还可以修饰在方法上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Color {
}

@Configuration
public class MyConfig {

@Profile("prod")
@Bean
public Color red(){
return new Color();
}

@Profile("test")
@Bean
public Color green(){
return new Color();
}
}

可以激活一组:

1
2
3
4
spring.profiles.active=production

spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq

82、高级特性-配置加载优先级

外部化配置

官方文档 - Externalized Configuration

Spring Boot uses a very particular PropertySource order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones)(1优先级最低,14优先级最高):

  1. Default properties (specified by setting SpringApplication.setDefaultProperties).
  2. @PropertySource annotations on your @Configuration classes. Please note that such property sources are not added to the Environment until the application context is being refreshed. This is too late to configure certain properties such as logging.* and spring.main.* which are read before refresh begins.
  3. Config data (such as application.properties files)
  4. A RandomValuePropertySource that has properties only in random.*.
  5. OS environment variables.
  6. Java System properties (System.getProperties()).
  7. JNDI attributes from java:comp/env.
  8. ServletContext init parameters.
  9. ServletConfig init parameters.
  10. Properties from SPRING_APPLICATION_JSON (inline JSON embedded in an environment variable or system property).
  11. Command line arguments.
  12. properties attribute on your tests. Available on @SpringBootTest and the test annotations for testing a particular slice of your application.
  13. @TestPropertySource annotations on your tests.
  14. Devtools global settings properties in the $HOME/.config/spring-boot directory when devtools is active.
1
2
3
4
5
6
7
8
9
10
11
12
import org.springframework.stereotype.*;
import org.springframework.beans.factory.annotation.*;

@Component
public class MyBean {

@Value("${name}")//以这种方式可以获得配置值
private String name;

// ...

}
  • 外部配置源
    • Java属性文件。
    • YAML文件。
    • 环境变量。
    • 命令行参数。
  • 配置文件查找位置
    1. classpath 根路径。
    2. classpath 根路径下config目录。
    3. jar包当前目录。
    4. jar包当前目录的config目录。
    5. /config子目录的直接子目录。
  • 配置文件加载顺序:
    1. 当前jar包内部的application.propertiesapplication.yml
    2. 当前jar包内部的application-{profile}.propertiesapplication-{profile}.yml
    3. 引用的外部jar包的application.propertiesapplication.yml
    4. 引用的外部jar包的application-{profile}.propertiesapplication-{profile}.yml
  • 指定环境优先,外部优先,后面的可以覆盖前面的同名配置项。

83、高级特性-自定义starter细节

starter启动原理

  • starter的pom.xml引入autoconfigure依赖
graph LR
A[starter] -->B[autoconfigure]
B --> C[spring-boot-starter]
  • autoconfigure包中配置使用META-INF/spring.factoriesEnableAutoConfiguration的值,使得项目启动加载指定的自动配置类

  • 编写自动配置类 xxxAutoConfiguration -> xxxxProperties

    • @Configuration
    • @Conditional
    • @EnableConfigurationProperties
    • @Bean
  • 引入starter — xxxAutoConfiguration — 容器中放入组件 ---- 绑定xxxProperties ---- 配置项

自定义starter

  • 目标:创建HelloService的自定义starter。

  • 创建两个工程,分别命名为hello-spring-boot-starter(普通Maven工程),hello-spring-boot-starter-autoconfigure(需用用到Spring Initializr创建的Maven工程)。

  • hello-spring-boot-starter无需编写什么代码,只需让该工程引入hello-spring-boot-starter-autoconfigure依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>

</project>
  • hello-spring-boot-starter-autoconfigure的pom.xml如下:
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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>hello-spring-boot-starter-autoconfigure</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
</dependencies>
</project>
  • 创建4个文件:
    • com/lun/hello/auto/HelloServiceAutoConfiguration
    • com/lun/hello/bean/HelloProperties
    • com/lun/hello/service/HelloService
    • src/main/resources/META-INF/spring.factories
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import com.lun.hello.bean.HelloProperties;
import com.lun.hello.service.HelloService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(HelloProperties.class)//默认HelloProperties放在容器中
public class HelloServiceAutoConfiguration {

@Bean
@ConditionalOnMissingBean(HelloService.class)
public HelloService helloService(){
return new HelloService();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import org.springframework.boot.context.properties.ConfigurationProperties;

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

public String getPrefix() {
return prefix;
}

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

public String getSuffix() {
return suffix;
}

public void setSuffix(String suffix) {
this.suffix = suffix;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.lun.hello.bean.HelloProperties;
import org.springframework.beans.factory.annotation.Autowired;


/**
* 默认不要放在容器中
*/
public class HelloService {

@Autowired
private HelloProperties helloProperties;

public String sayHello(String userName){
return helloProperties.getPrefix() + ": " + userName + " > " + helloProperties.getSuffix();
}
}
1
2
3
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.lun.hello.auto.HelloServiceAutoConfiguration
  • 用maven插件,将两工程install到本地。

  • 接下来,测试使用自定义starter,用Spring Initializr创建名为hello-spring-boot-starter-test工程,引入hello-spring-boot-starter依赖,其pom.xml如下:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter-test</artifactId>
<version>1.0.0-SNAPSHOT</version>
<name>hello-spring-boot-starter-test</name>
<description>Demo project for Spring Boot</description>

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

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!-- 引入`hello-spring-boot-starter`依赖 -->
<dependency>
<groupId>com.lun</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

  • 添加配置文件application.properties
1
2
hello.prefix=hello
hello.suffix=666
  • 添加单元测试类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import com.lun.hello.service.HelloService;//来自自定义starter
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class HelloSpringBootStarterTestApplicationTests {

@Autowired
private HelloService helloService;

@Test
void contextLoads() {
// System.out.println(helloService.sayHello("lun"));
Assertions.assertEquals("hello: lun > 666", helloService.sayHello("lun"));
}

}

84、原理解析-SpringApplication创建初始化流程

SpringBoot启动过程

  • 创建 SpringApplication

    - 保存一些信息,判定当前应用的类型
    
    • bootstrapRegistryInitializersspring.factories BootstrapRegistryInitializer

      • List bootstrapRegistryInitializers
    • ApplicationContextInitializerspring.factoriesApplicationContextInitializer

      • List<ApplicationContextInitializer<?>> initializers
    • ApplicationListener spring.factories ApplicationListener

      - List<ApplicationListener<?>> listeners
      
  • 运行 SpringApplication

    • Startup startup = Startup.create();

      • 记录应用的启动时间
    • 创建引导上下文(Context环境) createBootstrapContext()

      • 获取到所有之前的 bootstrapRegistryInitializers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
    • 让当前应用进入headless模式。java.awt.headless

    • 获取所有 RunListener(运行监听器)【为了方便所有Listener进行事件感知】

      • getSpringFactoriesInstances 去 spring.factoriesSpringApplicationRunListener
    • 遍历 SpringApplicationRunListener 调用 starting 方法

      • 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting
    • 保存命令行参数 ApplicationArguments

    • 准备环境 prepareEnvironment()

      • 返回或者创建基础环境信息对象 StandardServletEnvironment
      • 配置环境信息对象
        • 读取所有的配置源的配置属性值
      • 绑定环境信息
      • 监听器调用 listener.environmentPrepared() 通知所有的监听器当前环境准备完成
    • printBanner 打印banner

    • 创建IOC容器 createApplicationContext()

      • 根据项目类型(Servlet)创建容器
      • 当前会创建 AnnotationConfigServletWebServerApplicationContext
    • 准备 ApplicationContext IOC容器的基本信息 prepareContext()

      • 保存环境信息

      • IOC容器的后置处理流程

      • 应用初始化器 applyInitializers

        • 遍历所有的 ApplicationContextInitializer。调用 initialize。来对IOC容器进行初始化扩展功能
      • 遍历所有的 listeners 调用 contextPrepared。EventPublishRunListener;通知所有的监听器contextPrepared

      • 所有的监听器 调用 contextLoaded。通知所有的监听器 contextLoaded

    • 刷新IOC容器。refreshContext

      • 创建容器中的所有组件(Spring注解)
    • 容器刷新完成后工作 afterRefresh

    • 所有监听器 调用 listeners.started(context); 通知所有的监听器 started

    • 调用所有runners callRunners()

      • 获取容器中的 ApplicationRunner
      • 获取容器中的 CommandLineRunner
      • 合并所有runner并且按照 @Order 进行排序
      • 遍历所有的runner 调用 run 方法
    • 如果以上过程出现异常

      • 调用Listener 的 failed
    • 调用所有监听器的 ready 方法 listeners.ready(context); 通知所有的监听器 ready

    • running如果有问题。继续通知 failed。调用所有 Listener 的 failed;通知所有的监听器 failed

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
// 构造函数,创建SpringApplication实例,用于启动Spring应用
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 设置资源加载器,用于加载资源文件(如配置文件)
this.resourceLoader = resourceLoader;

// 校验primarySources不能为空,即主配置类不能为null
Assert.notNull(primarySources, "PrimarySources must not be null");

// 将传入的多个主配置类封装成Set集合,确保唯一性,主要用于Spring Boot自动配置
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));

// 推断当前应用的Web应用类型(REACTIVE、SERVLET或NONE),根据类路径判断
this.properties.setWebApplicationType(WebApplicationType.deduceFromClasspath());

// 从spring.factories中加载BootstrapRegistryInitializer的实现类,用于初始化BootstrapRegistry
this.bootstrapRegistryInitializers = new ArrayList<>(
getSpringFactoriesInstances(BootstrapRegistryInitializer.class));

// 从spring.factories中获取ApplicationContextInitializer的实例,并设置到当前SpringApplication中
// ApplicationContextInitializer用于在容器刷新之前进行一些初始化操作
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

// 从spring.factories中获取ApplicationListener的实例,并设置到当前SpringApplication中
// ApplicationListener用于监听Spring Boot应用生命周期事件
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));

// 推断出main方法所在的主程序类,用于后续运行时确定入口类
this.mainApplicationClass = deduceMainApplicationClass();

}

85、原理解析-SpringBoot完整启动过程

继续上一节,接着讨论return new SpringApplication(primarySources).run(args)run方法

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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
public class SpringApplication {

...

public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();//开始计时器
stopWatch.start();//开始计时

//1.
//创建引导上下文(Context环境)createBootstrapContext()
//获取到所有之前的 bootstrappers 挨个执行 intitialize() 来完成对引导启动器上下文环境设置
DefaultBootstrapContext bootstrapContext = createBootstrapContext();

//2.到最后该方法会返回这context
ConfigurableApplicationContext context = null;

//3.让当前应用进入headless模式
configureHeadlessProperty();

//4.获取所有 RunListener(运行监听器),为了方便所有Listener进行事件感知
SpringApplicationRunListeners listeners = getRunListeners(args);

//5. 遍历 SpringApplicationRunListener 调用 starting 方法;
// 相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
//6.保存命令行参数 ApplicationArguments
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

//7.准备环境
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
configureIgnoreBeanInfo(environment);

/*打印标志
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.4.2)
*/
Banner printedBanner = printBanner(environment);

// 创建IOC容器(createApplicationContext())
// 根据项目类型webApplicationType(NONE,SERVLET,REACTIVE)创建容器,
// 当前会创建 AnnotationConfigServletWebServerApplicationContext
context = createApplicationContext();
context.setApplicationStartup(this.applicationStartup);

//8.准备ApplicationContext IOC容器的基本信息
prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
//9.刷新IOC容器,创建容器中的所有组件,Spring框架的内容
refreshContext(context);
//该方法没内容,大概为将来填入
afterRefresh(context, applicationArguments);
stopWatch.stop();//停止计时
if (this.logStartupInfo) {//this.logStartupInfo默认是true
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
//10.
listeners.started(context);

//11.调用所有runners
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
//13.
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}

try {
//12.
listeners.running(context);
}
catch (Throwable ex) {
//13.
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}

//1.
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrappers.forEach((initializer) -> initializer.intitialize(bootstrapContext));
return bootstrapContext;
}

//3.
private void configureHeadlessProperty() {
//this.headless默认为true
System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS,
System.getProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
}

private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";

//4.
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
//getSpringFactoriesInstances 去 spring.factories 找 SpringApplicationRunListener
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
this.applicationStartup);
}

//7.准备环境
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
//返回或者创建基础环境信息对象,如:StandardServletEnvironment, StandardReactiveWebEnvironment
ConfigurableEnvironment environment = getOrCreateEnvironment();
//配置环境信息对象,读取所有的配置源的配置属性值。
configureEnvironment(environment, applicationArguments.getSourceArgs());
//绑定环境信息
ConfigurationPropertySources.attach(environment);
//7.1 通知所有的监听器当前环境准备完成
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
configureAdditionalProfiles(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}

//8.
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
//保存环境信息
context.setEnvironment(environment);
//IOC容器的后置处理流程
postProcessApplicationContext(context);
//应用初始化器
applyInitializers(context);
//8.1 遍历所有的 listener 调用 contextPrepared。
//EventPublishRunListenr通知所有的监听器contextPrepared
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
if (this.lazyInitialization) {
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
//8.2
listeners.contextLoaded(context);
}

//11.调用所有runners
private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();

//获取容器中的 ApplicationRunner
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
//获取容器中的 CommandLineRunner
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
//合并所有runner并且按照@Order进行排序
AnnotationAwareOrderComparator.sort(runners);
//遍历所有的runner。调用 run 方法
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}

//13.
private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
SpringApplicationRunListeners listeners) {
try {
try {
handleExitCode(context, exception);
if (listeners != null) {
//14.
listeners.failed(context, exception);
}
}
finally {
reportFailure(getExceptionReporters(context), exception);
if (context != null) {
context.close();
}
}
}
catch (Exception ex) {
logger.warn("Unable to close ApplicationContext", ex);
}
ReflectionUtils.rethrowRuntimeException(exception);
}

...
}
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
//2. new SpringApplication(primarySources).run(args) 最后返回的接口类型
public interface ConfigurableApplicationContext extends ApplicationContext, Lifecycle, Closeable {
String CONFIG_LOCATION_DELIMITERS = ",; \t\n";
String CONVERSION_SERVICE_BEAN_NAME = "conversionService";
String LOAD_TIME_WEAVER_BEAN_NAME = "loadTimeWeaver";
String ENVIRONMENT_BEAN_NAME = "environment";
String SYSTEM_PROPERTIES_BEAN_NAME = "systemProperties";
String SYSTEM_ENVIRONMENT_BEAN_NAME = "systemEnvironment";
String APPLICATION_STARTUP_BEAN_NAME = "applicationStartup";
String SHUTDOWN_HOOK_THREAD_NAME = "SpringContextShutdownHook";

void setId(String var1);

void setParent(@Nullable ApplicationContext var1);

void setEnvironment(ConfigurableEnvironment var1);

ConfigurableEnvironment getEnvironment();

void setApplicationStartup(ApplicationStartup var1);

ApplicationStartup getApplicationStartup();

void addBeanFactoryPostProcessor(BeanFactoryPostProcessor var1);

void addApplicationListener(ApplicationListener<?> var1);

void setClassLoader(ClassLoader var1);

void addProtocolResolver(ProtocolResolver var1);

void refresh() throws BeansException, IllegalStateException;

void registerShutdownHook();

void close();

boolean isActive();

ConfigurableListableBeanFactory getBeanFactory() throws IllegalStateException;
}
1
2
3
4
5
#4.
#spring.factories
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
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
72
73
74
class SpringApplicationRunListeners {

private final Log log;

private final List<SpringApplicationRunListener> listeners;

private final ApplicationStartup applicationStartup;

SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners,
ApplicationStartup applicationStartup) {
this.log = log;
this.listeners = new ArrayList<>(listeners);
this.applicationStartup = applicationStartup;
}

//5.遍历 SpringApplicationRunListener 调用 starting 方法;
//相当于通知所有感兴趣系统正在启动过程的人,项目正在 starting。
void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
(step) -> {
if (mainApplicationClass != null) {
step.tag("mainApplicationClass", mainApplicationClass.getName());
}
});
}

//7.1
void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
doWithListeners("spring.boot.application.environment-prepared",
(listener) -> listener.environmentPrepared(bootstrapContext, environment));
}

//8.1
void contextPrepared(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-prepared", (listener) -> listener.contextPrepared(context));
}

//8.2
void contextLoaded(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.context-loaded", (listener) -> listener.contextLoaded(context));
}

//10.
void started(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.started", (listener) -> listener.started(context));
}

//12.
void running(ConfigurableApplicationContext context) {
doWithListeners("spring.boot.application.running", (listener) -> listener.running(context));
}

//14.
void failed(ConfigurableApplicationContext context, Throwable exception) {
doWithListeners("spring.boot.application.failed",
(listener) -> callFailedListener(listener, context, exception), (step) -> {
step.tag("exception", exception.getClass().toString());
step.tag("message", exception.getMessage());
});
}

private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,
Consumer<StartupStep> stepAction) {
StartupStep step = this.applicationStartup.start(stepName);
this.listeners.forEach(listenerAction);
if (stepAction != null) {
stepAction.accept(step);
}
step.end();
}

...

}

86、原理解析-自定义事件监听组件

MyApplicationContextInitializer.java

1
2
3
4
5
6
7
8
9
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;

public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer ....initialize.... ");
}
}

MyApplicationListener.java

1
2
3
4
5
6
7
8
9
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationListener;

public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener.....onApplicationEvent...");
}
}

MyApplicationRunner.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;


@Order(1)
@Component//放入容器
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner...run...");
}
}

MyCommandLineRunner.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* 应用启动做一个一次性事情
*/
@Order(2)
@Component//放入容器
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner....run....");
}
}

MySpringApplicationRunListener.java

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
import org.springframework.boot.ConfigurableBootstrapContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringApplicationRunListener;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.env.ConfigurableEnvironment;

public class MySpringApplicationRunListener implements SpringApplicationRunListener {

private SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application, String[] args){
this.application = application;
}

@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener....starting....");

}


@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener....environmentPrepared....");
}


@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextPrepared....");

}

@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....contextLoaded....");
}

@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....started....");
}

@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener....running....");
}

@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener....failed....");
}
}

注册MyApplicationContextInitializerMyApplicationListenerMySpringApplicationRunListener:

resources / META-INF / spring.factories:

1
2
3
4
5
6
7
8
org.springframework.context.ApplicationContextInitializer=\
com.lun.boot.listener.MyApplicationContextInitializer

org.springframework.context.ApplicationListener=\
com.lun.boot.listener.MyApplicationListener

org.springframework.boot.SpringApplicationRunListener=\
com.lun.boot.listener.MySpringApplicationRunListener