基础入门
01、基础入门-SpringBoot2课程介绍
-
Spring Boot 2核心技术
-
Spring Boot 2响应式编程
- 学习要求
-熟悉Spring基础
-熟悉Maven使用 - 环境要求
- Java8及以上
- Maven 3.3及以上
- 学习资料
02、基础入门-Spring生态圈
Spring能做什么
Spring的能力
Spring的生态圈
生态圈:官网的project下多项技术组合,称为spring生态圈
以前学的spring,就是springframeWork
覆盖了:
- web开发
- 数据访问
- 安全控制
- 分布式
- 消息服务
- 移动开发
- 批处理
- …
Spring5重大升级
-
响应式编程
-
Servlet Stack 原始的技术栈开发
-
Reactive Stack 响应式开发(WebFlux) :占用少量的内存,开发更多功能
-
内部源码设计
基于Java8的一些新特性,如:接口默认实现。重新设计源码架构。
- 接口的默认实现(default关键字):比如现在有一个接口,拥有五个方法,分别是: 1,2,3,4,5
- 设计出一个适配器的中间站(设计模式),它的功能就是将接口的五个方法全部实现但实现为空方法,即没有任务逻辑
- 由后面的类来继承这个适配器,你想用那个方法就重写那个方法。缺点:占用了类的继承位置、
- 由java8以后,存在了默认实现的功能,可以不用适配器这种设计模式,节省出继承的位置
为什么用SpringBoot
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can “just run”.link
能快速创建出生产级别的Spring应用。
SpringBoot优点
-
Create stand-alone Spring applications
- 创建独立Spring应用
-
Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- 内嵌web服务器
-
Provide opinionated ‘starter’ dependencies to simplify your build configuration
- 自动starter依赖,简化构建配置
- 自动整合各种场景的依赖版本对应(启动器依赖)
- 自动starter依赖,简化构建配置
-
Automatically configure Spring and 3rd party libraries whenever possible
- 自动配置Spring以及第三方功能(自动装配)
-
Provide production-ready features such as metrics, health checks, and externalized configuration
- 提供生产级别的监控、健康检查及外部化配置
-
Absolutely no code generation and no requirement for XML configuration
- 无代码生成、无需编写XML
-
SpringBoot是整合Spring技术栈的一站式框架
-
SpringBoot是简化Spring技术栈的快速开发脚手架
-
spring生态圈整合技术过于麻烦,springboot框架帮我们做了这件事
SpringBoot缺点
- 人称版本帝,迭代快,需要时刻关注变化
- 封装太深,内部原理复杂,不容易精通
03、基础入门-SpringBoot的大时代背景
微服务
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
分布式
分布式的困难
- 远程调用
- 服务发现
- 负载均衡
- 服务容错
- 配置管理
- 服务监控
- 链路追踪
- 日志管理
- 任务调度
- …
分布式的解决
- SpringBoot + SpringCloud
做成响应式数据流(springCloudDataFlow)
云原生
原生应用如何上云。 Cloud Native
上云的困难
- 服务自愈
- 弹性伸缩 高峰流量时,多增加几台服务器分担压力
- 服务隔离
- 自动化部署
- 灰度发布
- 流量治理 限制不同的服务器接收流量多少
- …
上云的解决
04、基础入门-SpringBoot官方文档架构
官网文档架构 RD
05、基础入门-SpringBoot-HelloWorld
系统要求
- Java 8
- Maven 3.3+
- IntelliJ IDEA 2019.1.2
Maven配置文件
新添内容:setting.xml
<mirrors>
<mirror>
<id>nexus-aliyun</id>
<mirrorOf>central</mirrorOf>
<name>Nexus aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public</url>
</mirror>
</mirrors>
<profiles>
<profile>
<id>jdk-1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
</properties>
</profile>
</profiles>
HelloWorld项目
需求:浏览发送/hello请求,响应 “Hello,Spring Boot 2”
创建maven工程
引入依赖
// 父项目管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.4.RELEASE</version>
</parent>
//boot启动依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
创建主程序
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);
}
}
编写业务
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
类 (以前maven项目需要整理部署tomcat) - 浏览器输入
http://localhost:8888/hello
,将会输出Hello, Spring Boot 2!
。
设置配置
maven工程的resource文件夹中创建application.properties文件。
# 设置端口号
server.port=8888
打包部署
在pom.xml添加
<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-依赖管理特性
- 父项目做依赖管理,每一个springboot都有一个父项目(自动版本仲裁机制)
依赖管理
<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>
它几乎$声明了所有开发中常用的依赖的版本号$,自动版本仲裁机制
<artifactId>spring-boot-dependencies</artifactId>点进去就能看见,以后自己就不用写版本号
- 开发导入starter场景启动器
-
见到很多 spring-boot-starter-xx* : *就某种场景
-
只要引入starter,这个场景的所有常规需要的依赖我们都自动引入
-
见到的 *xx-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
-
所有场景启动器最底层的依赖,如下:
-
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
-
无需关注版本号,自动版本仲裁
- 引入依赖默认都可以不写版本
- 引入非版本仲裁的jar,要写版本号。
-
可以修改默认版本号
- 查看spring-boot-dependencies里面规定当前依赖的版本 用的 key 如 properties下的mysql.version。
- 在当前项目里面重写配置,如下面的代码。
在当前pom.xml中
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
IDEA快捷键:
ctrl + shift + alt + U
:以图的方式显示项目中依赖之间的关系。alt + ins
:相当于Eclipse的 Ctrl + N,创建新类,新包等。
07、基础入门-SpringBoot-自动配置特性
- 自动配好Tomcat
- 引入Tomcat依赖。
- 配置Tomcat
<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常见功能,如:字符编码问题,前端控制器DispatchServlet
- SpringBoot帮我们配置好了所有web开发的常见场景
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
}
- 默认的包结构
-
主程序(MainApplication)所在包及其下面的所有子包里面的组件都会被默认扫描进来
为什么?因为@SpringBootApplication
中有这个注解 -
@ComponentScan( excludeFilters = {@Filter( type = FilterType.CUSTOM, classes = {TypeExcludeFilter.class} ), @Filter( type = FilterType.CUSTOM, classes = {AutoConfigurationExcludeFilter.class} )} )
-
无需以前的包扫描配置
-
想要改变扫描路径,比如不满足上面默认扫描的条件,就需要自己设置
-
@SpringBootApplication(scanBasePackages=“com.lun”)
-
@ComponentScan (指定扫描路径)
-
@SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.lun")
-
- 各种配置拥有默认值
- 默认配置最终都是映射到某个类上,如:
MultipartProperties
- 配置文件的值最终会绑定每个类上(xxxProperties,xxxautoConfiguration),这个类会在容器中创建对象
- 按需加载所有自动配置项
- 非常多的starter
- 引入了哪些场景(依赖)这个场景的自动配置才会开启(为了节省资源)
- SpringBoot所有的自动配置功能都在 spring-boot-autoconfigure 包里面
- 尽管Springboot中有很多自动配置,但不是全部都生效
- …
08、底层注解-@Configuration详解
- 基本使用
- Full模式与Lite模式
- 示例
/**
* 1、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例的
* 2、配置类本身也是组件
* 3、proxyBeanMethods:代理bean的方法
* Full(proxyBeanMethods = true)(保证每个@Bean方法被调用多少次返回的组件都是单实例的)(默认)
* Lite(proxyBeanMethods = false)(每个@Bean方法被调用多少次返回的组件都是新创建的)
*/
// 这个在spring讲过
@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") // 以tom 作为组件id
public Pet tomcatPet(){
return new Pet("tomcat");
}
}
@Configuration测试代码如下:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan("com.atguigu.boot")
public class MainApplication {
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
//3、从容器中获取组件
Pet tom01 = run.getBean("tom", Pet.class);
Pet tom02 = run.getBean("tom", Pet.class);
System.out.println("组件:"+(tom01 == tom02));
//4、com.atguigu.boot.config.MyConfig$$EnhancerBySpringCGLIB$$51f1e1ca@1654a892 使用了cglib动态代理
MyConfig bean = run.getBean(MyConfig.class); // 加载配置类
System.out.println(bean);
//如果@Configuration(proxyBeanMethods = true)代理对象调用方法。SpringBoot¥总会¥检查这个组件是否在容器中有。
//保持组件单实例
User user = bean.user01(); // 调用方法也是从容器中拿取
User user1 = bean.user01();
System.out.println(user == user1);
User user01 = run.getBean("user01", User.class);
Pet tom = run.getBean("tom", Pet.class);
// 组件依赖:检测对象是否一样
System.out.println("用户的宠物:"+(user01.getPet() == tom));
}
}
- 最佳实战(有无组件依赖)
- 配置 类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
- 配置 类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式(默认)
- 其底层使用了 Component 注解
lite 英 [laɪt] 美 [laɪt]
adj. 低热量的,清淡的(light的一种拼写方法);类似…的劣质品
IDEA快捷键:
Alt + Ins
:生成getter,setter、构造器等代码。Ctrl + Alt + B 《=》 ctrl +鼠标左键
:查看类的具体实现代码。
09、底层注解-@Import导入组件
@Bean、@Component、@Controller、@Service、@Repository,它们是Spring的基本标签,在Spring Boot中并未改变它们原来的功能。
@ComponentScan 在07、基础入门-SpringBoot-自动配置特性有用例。
@Import({User.class, DBHelper.class})给容器中自动创建出这两个类型的组件、默认组件的名字就是全类名
@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
}
测试类:
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//...
//5、获取组件
String[] beanNamesForType = run.getBeanNamesForType(User.class);
for (String s : beanNamesForType) {
System.out.println(s);
}
DBHelper bean1 = run.getBean(DBHelper.class); // import导入
System.out.println(bean1);
10、底层注解-@Conditional条件装配
条件装配:满足Conditional指定的条件,则进行组件注入
这个注解可以加在方法上,类上。类上注解的优先级要高于方法上加注解
@Conditional({OnClassCondition.class})
public @interface ConditionalOnClass { // 可以使用两个属性, 一种是name名字id String类型;一种是 value Class类型
Class<?>[] value() default {};
String[] name() default {};
}
用@ConditionalOnMissingBean举例说明
@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 run = SpringApplication.run(MainApplication.class, args);
//2、查看容器里面的组件
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
boolean tom = run.containsBean("tom");
System.out.println("容器中Tom组件:"+tom);//false
boolean user01 = run.containsBean("user01");
System.out.println("容器中user01组件:"+user01);//true
boolean tom22 = run.containsBean("tom22");
System.out.println("容器中tom22组件:"+tom22);//true
}
11、底层注解-@ImportResource导入Spring配置文件
比如,公司使用bean.xml文件生成配置bean,然而你为了省事,想继续复用bean.xml,@ImportResource
注解登场。仍然是放在MyConfig
类上
bean.xml: (在spring中是使用ClassPathXmlApplication解析的这文件;在springmvc中使用web.xml注册这个文件)
<?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>
使用方法:
@ImportResource("classpath:beans.xml")
public class MyConfig {
...
}
测试类:
public static void main(String[] args) {
//1、返回我们IOC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
boolean haha = run.containsBean("haha");
boolean hehe = run.containsBean("hehe");
System.out.println("haha:"+haha);//true
System.out.println("hehe:"+hehe);//true
}
12、底层注解-@ConfigurationProperties配置绑定
如何使用Java读取到properties文件中的内容,并且把它封装到JavaBean中,以供随时使用
传统方法:
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
mycar.brand=BYD
mycar.price=100000
只有在容器中的组件,才会拥有SpringBoot提供的强大功能
@Component("car1") // 将car注入容器
@ConfigurationProperties(prefix = "mycar") // 将car的属性和配置文件绑定
public class Car {
...
}
可以使用controller访问json,也可以使用IoC容器直接获取bean
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
Car car1 = run.getBean("car1", Car.class);
System.out.println(car1);
Spring Boot另一种配置配置绑定:(在属性配置类中确定绑定)
@EnableConfigurationProperties + @ConfigurationProperties
- 开启Car配置绑定功能
- 把这个Car这个组件自动注册到容器中
- 这种写法不用在实体类的头上加
@Component
注解,因为如果这个实体类是第三方库里的类,我们是没有办法为其添加注解的
@Configuration(proxyBeanMethods=false)
@EnableConfigurationProperties(Car.class)
public class MyConfig {
...
}
@ConfigurationProperties(prefix = "mycar")
public class Car {
...
}
注解 @Component 或 @EnableConfigurationProperties(xxx.class) 这两注解比写一个
13、自动配置【源码分析】-自动包规则原理
Spring Boot应用的启动类:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
分析下**@SpringBootApplication
**注解
@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
@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
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage // 核心注解
@Import(AutoConfigurationImportSelector.class) // 核心注解 导入组件AutoConfigurationImportSelector
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
重点分析@AutoConfigurationPackage
,@Import(AutoConfigurationImportSelector.class)
。
@AutoConfigurationPackage 核心注解一
标签名直译为:自动配置包,指定了默认的包规则。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)// $核心注解 给容器中导入一个组件()
public @interface AutoConfigurationPackage {
String[] basePackages() default {};
Class<?>[] basePackageClasses() default {};
}
-
核心导入的
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { Registrar() { } public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 注册器,注解元数据 AutoConfigurationPackages.register(registry, (String[])(new AutoConfigurationPackages.PackageImports(metadata)).getPackageNames().toArray(new String[0])); } public Set<Object> determineImports(AnnotationMetadata metadata) { // 注解元数据 return Collections.singleton(new AutoConfigurationPackages.PackageImports(metadata)); } }
作用如下两点
-
利用Registrar给容器中导入一系列组件
-
将指定的一个包下的所有组件导入进来。MainApplication所在包下。
14、自动配置【源码分析】-初始加载自动配置类
@Import(AutoConfigurationImportSelector.class) 核心注解二
- 利用方法
public String[] selectImports(AnnotationMetadata annotationMetadata)
中的getAutoConfigurationEntry(annotationMetadata);
给容器中批量导入一些组件 - 调用 方法
getAutoConfigurationEntry(annotationMetadata)
中的List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)
获取到所有需要导入到容器中的配置类
然后再从configurations(里面有127个组件)中去除重复的配置,移除在排除列表中的配置,再操作后封装后返回。 - 利用工厂加载
Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader);
来自方法getCandidateConfigurations
得到所有的组件 - 从
META-INF/spring.factories
位置【FACTORIES_RESOURCE_LOCATION】来加载一个文件。- 默认扫描我们当前系统里面所有
META-INF/spring.factories
位置的文件,写死了加载的组件 spring-boot-autoconfigure-2.3.4.RELEASE.jar
包里面也有META-INF/spring.factories
- 默认扫描我们当前系统里面所有
# 文件里面写死了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,\
...
虽然我们127个场景的所有自动配置启动的时候默认全部加载,但是xxxxAutoConfiguration
自动加载类中会按照条件装配规则(@Conditional
)是否满足,最终会按需配置。
如AopAutoConfiguration
类:
@Configuration(
proxyBeanMethods = false
)
@ConditionalOnProperty( //扫描配置文件,即propertie或者yaml,是否有以下配置
prefix = "spring.aop",
name = "auto",
havingValue = "true",
matchIfMissing = true // 就算没有前提条件,也当作配置了即生效
)
// 上述的条件限制了它的自动装配
public class AopAutoConfiguration {
public AopAutoConfiguration() {
}
...
}
15、自动配置【源码分析】-自动配置流程
以DispatcherServletAutoConfiguration
的内部类**DispatcherServletConfiguration
**为例子:
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE) // 配置顺序
@Configuration(proxyBeanMethods = false) // 这是一个配置类
@ConditionalOnWebApplication(type = Type.SERVLET) // 判断当前是不是一个原声的servlet的web服务
@ConditionalOnClass(DispatcherServlet.class) // 是否存在DispatcherServlet的类,因为下面需要new一个前端控制器
@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) // 1.开启这个类,跟配置文件的绑定作用 ; 2.将WebMvcProperties组件放入容器中
protected static class DispatcherServletConfiguration {
// 注册dispatcherServlet组件 (底层帮我们配置好了)
/*
自己new一个,并且返回,值从配置文件(WebMvcProperties绑定了配置文件)中拿到
*/
//注册multipartResolver 文件上传解析器
@Bean
@ConditionalOnBean(MultipartResolver.class)//容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中不存在一个名叫 multipartResolver(常量名MULTIPART_RESOLVER_BEAN_NAME) 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了参数,这个参数的值,spring会自动去容器中找到一个MultipartResolver类型的bean
return resolver;
}
}
-
@EnableConfigurationProperties(WebMvcProperties.class)
关联上配置文件 - multipartResolver方法的作用就是,防止你配置文件上传解析器时,命名不叫multipartResolver,进行捕获(通过上面的ConditionOnMissingBean),将其传入方法并返回,这时,它的名字就被改变成规范的名字multipartResolver
- SpringBoot默认会在底层配好所有的组件,但是如果用户自己配置了以用户的优先(在HttpEncodingAutoConfiguration也有体现,
@ConditionalOnMissingBean
:如果用户自己配置了,我就不生效)。但是会规范化命名
@ConfigurationProperties(prefix = "spring.mvc") // 配置文件中写的前缀
public class WebMvcProperties { // 这个类,有些类似我们做封装数据的类,即pojo/model
/**
* Formatting strategy for message codes. For instance, `PREFIX_ERROR_CODE`.
*/
private DefaultMessageCodesResolver.Format messageCodesResolverFormat;
/**
* Locale to use. By default, this locale is overridden by the "Accept-Language"
* header.
*/
private Locale locale;
/**
* Define how the locale should be resolved.
*/
private LocaleResolver localeResolver = LocaleResolver.ACCEPT_HEADER;
private final Format format = new Format();
/**
* Whether to dispatch TRACE requests to the FrameworkServlet doService method.
*/
private boolean dispatchTraceRequest = false;
........等等配置属性
类似的还有 HttpEncodingAutoConfiguration网络编码的自动配置,自动配置类对中文乱码处理,同样像上面那样分析
总结:
- SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration,根据 springautoconfig.jar中的metinf/spring.properties中扫描有哪些组件
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。(xxxxProperties里面读取,xxxProperties和配置文件进行了绑定)
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 以前spring,springmvc中这些组件都要自己配置
- 定制化配置
- 用户直接自己@Bean替换底层的组件(替换组件类型)
- 用户去看这个组件是获取的配置文件什么值就去修改。(替换组件内容)
xxxxxAutoConfiguration —> 组件 —> xxxxProperties里面拿值 ----> application.properties/application.yaml
16、最佳实践-SpringBoot应用如何编写
- 引入场景依赖
- 查看自动配置了哪些
- 自己分析,引入场景(依赖)对应的自动配置一般都生效了
- 配置文件中**
debug=true
**开启自动配置报告。- Negative(不生效)
- Positive(生效)
- 是否需要修改
- 参照文档修改配置项
- 官方文档
- 自己分析。xxxxProperties绑定了配置文件的哪些。
- 自定义加入或者替换组件
- @Bean、@Component…
- 自定义器 XXXXXCustomizer todo:后期再讲
- 参照文档修改配置项
17、最佳实践-Lombok简化开发
Lombok用标签方式代替构造器、getter/setter、toString()等鸡肋代码。
小组开发时一人使用lombok,其他的人都要用Lombok
spring boot已经管理Lombok。引入依赖:
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
IDEA中File->Settings->Plugins,搜索安装Lombok插件。
简化日志开发
@Slf4j // 注入日志信息记录仪 log
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handle01(@RequestParam("name") String name){
log.info("请求进来了....");
return "Hello, Spring Boot 2!"+"你好:"+name;
}
}
18、最佳实践-dev-tools
Spring Boot includes an additional set of tools that can make the application development experience a little more pleasant. The
spring-boot-devtools
module can be included in any project to provide additional development-time features.——linkApplications that use
spring-boot-devtools
automatically restart whenever files on the classpath change. This can be a useful feature when working in an IDE, as it gives a very fast feedback loop for code changes. By default, any entry on the classpath that points to a directory is monitored for changes. Note that certain resources, such as static assets and view templates, do not need to restart the application.——linkTriggering a restart
As DevTools monitors classpath resources, the only way to trigger a restart is to update the classpath. The way in which you cause the classpath to be updated depends on the IDE that you are using:
- In Eclipse, saving a modified file causes the classpath to be updated and triggers a restart.
- In IntelliJ IDEA, building the project (
Build -> Build Project
)(shortcut: Ctrl+F9) has the same effect.
添加依赖:
<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用法,但优先级不如properties
非常适合用来做以数据为中心的配置文件。yaml注重的应是数据本身,而不是以标记语言为重点
基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- '#'表示注释
- 字符串无需加引号,如果要加,单引号’'、双引号""表示字符串内容会被 不转义、转义
- “zhang \n san” ==> 控制台输出为 zhang (回车下一行) san
- ‘zhang \n san’ ==> 控制台输出为 zhang \n san
- 即转移字符在双引号中生效,单引号中不生效
数据类型
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
- 对象:键值对的集合。map、hash、set、object
#行内写法 json:
k: {k1:v1,k2:v2,k3:v3}
#或
k:
k1: v1
k2: v2
k3: v3
- 数组:一组按次序排列的值。array、list、queue
#行内写法:
k: [v1,v2,v3]
#或者
k:
- v1
- v2
- v3
实例
@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; //map
private Set<Double> salarys; //set
private Map<String, List<Pet>> allPets; //大map
}
@Data
public class Pet {
private String name;
private Double weight;
}
用yaml表示以上对象
person:
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
# 对象写法
pet:
name: tomcat
weight: 23.4
# 数组和列表
interests: [篮球,游泳]
animal:
- jerry
- mario
# map<String ,Object>
score:
english: # 键
first: 30
second: 40
third: 50
math: [131,140,148]
chinese: {first: 128,second: 136}
salarys: [3999,4999.98,5999.99]
# salarys:
# -3999
#
-4999
allPets:
sick: # 键
- name: tom # -代表这两个是组成一个列表
weight: 30
- {name: jerry,weight: 47} # {k:v,.....} 对象写法
health: [{name: mario,weight: 47}] # 列表的另一种写法
21、配置文件-自定义类绑定的配置提示
当我们配置上述属性时,idea不会提供属性名的提示,而其本身的配置就会提示。那我们该怎么做,也有提示呢?
自定义的类和配置文件绑定一般没有提示。若要提示,添加如下依赖:
<!--配置进程,提示-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!-- 下面插件作用是工程打包时,不将spring-boot-configuration-processor打进包内,让其只在编码的时候有用
在dependency中加入scope标签指定其声明周期也可以
-->
<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开发简介
springboot帮忙整合了很多springMVC的配置,当然我们也可以自定义
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
andBeanNameViewResolver
beans.- 内容协商视图解析器和BeanName视图解析器
-
Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包括webjars)
-
Automatic registration of
Converter
,GenericConverter
, andFormatter
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上) @DataBinder
- 自动使用
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 typeWebMvcConfigurer
but without@EnableWebMvc
.不用@EnableWebMvc注解。使用
@Configuration
+WebMvcConfigurer
自定义规则
If you want to provide custom instances of
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of typeWebMvcRegistrations
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
-annotatedDelegatingWebMvcConfiguration
as described in the Javadoc of@EnableWebMvc
.使用
@EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
23、web场景-静态资源规则与定制化
静态资源目录
只要静态资源放在类路径下: /static(or
/publicor
/resourcesor
/META-INF/resources` <!-四个静态资源存放位置—>
访问 : 当前项目根路径/ + 静态资源名
原理: 静态映射 /**。
前端请求进来 (寻找处理顺序)
- 先去找Controller看能不能处理
- 不能处理的所有请求(因为静态映射为 /** )又都交给静态资源处理器
- 静态资源也找不到则响应404页面。
也可以改变默认的静态资源路径就会,使用如下的方法,但会导致/static
,/public
,/resources
, /META-INF/resources
失效
spring:
web:
resources:
static-locations: [classpath:/haha/] # 当设置了这配置后,只有在当前haha文件夹下的静态资源,才能结合之前的static-path-pattern配置访问,其他的目录都不行
静态资源访问前缀
spring:
mvc:
static-path-pattern: /res/** # 原先是 /**
当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹resources下找
webjar
可用jar方式(依赖方式)添加css,js等资源文件
例如,添加jquery
<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。
- 可以配置静态资源路径
- 但是不可以配置静态资源的访问前缀
static-path-pattern
,否则导致 index.html不能被默认访问
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致welcome 欢迎页功能失效 (封装不灵活点)
resources:
static-locations: [classpath:/haha/] #如果配置了这个,讲欢迎页放在这下面
- controller能处理/index。
自定义Favicon
指网页标签上的小图标。
favicon.ico 放在静态资源目录下即可。
spring:
# mvc:
# static-path-pattern: /res/** 这个会导致 Favicon 功能失效
如果加载不出来有可能是你之前访问过这个服务器,浏览器对这个请求已经 有缓存,需要清除浏览的缓存 Ctrl +f5
可以使用配置类,配置资源绑定,重写addHandlerResources方法
而使用addHandlerResources配置出来的资源映射,是无法替换静态默认映射/** 的,即四个默认的资源位置仍然生效。但是欢迎页不生效了
25、web场景-【源码分析】-静态资源原理
源码的学习是为了更好的调错
- SpringBoot启动默认加载 xxxAutoConfiguration 类(自动配置类)
- SpringMVC功能的自动配置类
WebMvcAutoConfiguration
,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) // 没有这样类的一个组件bean
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
...
}
- 给容器中配置的内容:
- 配置文件的相关属性(加粗标识前缀)的绑定:WebMvcProperties=》spring.mvc;ResourceProperties=》spring.resources
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class }) // 这里有两个配置属性类
@Order(0) //顺序排列
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer { // webmvc自动配置中间处理器
...
}
配置类只有一个有参构造器
// 有参构造器 所有参数的值都会从 容器 中确定
public WebMvcAutoConfigurationAdapter(WebProperties webProperties , WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = webProperties.getResources(); // 由webProperties提供resourceProperties
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
WebMvcAutoConfigurationAdapter参数解析
- ResourceProperties resourceProperties;获取和spring.resources绑定的所有的值的对象 (WebProperties提供,即现在已经没有ResourceProperties 这个类了,而是WebProperties取代了它)
- WebMvcProperties mvcProperties; 获取和spring.mvc绑定的所有的值的对象
- ListableBeanFactory beanFactory; Spring的beanFactory
- HttpMessageConverters; 找到所有的HttpMessageConverters
- ResourceHandlerRegistrationCustomizer; 找到 资源处理器的自定义器。
- DispatcherServletPath;
- ServletRegistrationBean; 给应用注册Servlet、Filter…
资源处理的默认规则
...
public class WebMvcAutoConfiguration {
...
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
... // 注册 资源注册器
@Override
//资源注册器从容器中寻找
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
if (!this.resourceProperties.isAddMappings()) { // 这是里判断 addMappings 是否为true(默认为true,可以在配置文件中修改)
logger.debug("Default resource handling disabled");
return;
}
// 如果addMappings为true,下面的静态资源加载才能执行
ServletContext servletContext = getServletContext();
// pattern访问url:/webjars/** locations:classpath:/META-INF/resources/webjars/
// 这就是为什么:xx8080:webjars/jquery/3.5.1/jquery.js 就能访问到资源
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); // 这里是处理webjars下静态资源 的方法
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations()); // 这个方法就是处理springboot支持默认四个存放静态资源的位置(getStaticLocations是拿到四个位置的方法) 的资源的方法 (如下解释:静态资源规则)
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}
...
}
...
}
根据上述代码,我们可以通过配置禁止所有静态资源规则。
spring:
web:
resources:
add-mappings: false #禁用所有静态资源规则
静态资源规则:
@ConfigurationProperties("spring.web")
public class WebProperties {
// resources 被封装成内部类了
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;
/**
* Whether to enable default resource handling.
*/
private boolean addMappings = true;
private boolean customized = false;
}
}
欢迎页的处理规则
...
public class WebMvcAutoConfiguration {
...
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(WebProperties.class)
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
...
//HandlerMapping 处理器映射。保存了每一个Handler能处理那些请求
@Bean// 欢迎页配置
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping( // 下面解析这个方法
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
WelcomePageHandlerMapping
的构造方法如下:
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
//要用欢迎页功能,静态访问前缀必须是 /**,所以呐,应征了前面失效的实验
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html"); //转发到index视图
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
//上面条件不满足,就走调用Controller /index请求
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
这构造方法内的代码也解释了web场景-welcome与favicon功能中配置static-path-pattern
了,welcome页面和小图标失效的问题。
26、请求处理-【源码分析】-Rest映射及源码解析
请求映射
- @xxxMapping;
- @GetMapping
- @PostMapping
- @PutMapping
- @DeleteMapping
- Rest风格支持(使用HTTP请求方式动词来表示对资源的操作)
- 以前:
- /getUser 获取用户
- /deleteUser 删除用户
- /editUser 修改用户
- /saveUser保存用户
- 现在: /user 作为prefix
- GET-获取用户 /get
- DELETE-删除用户 /delete
- PUT-修改用户 /edit
- POST-保存用户 /save
- 核心Filter;HiddenHttpMethodFilter
- 以前:
- 用法
- 开启页面表单的Rest功能=》 配置文件:
hiddenmethod:filter:enabled: true
- 页面 form的属性method=post,==隐藏域 _method=put、delete==等(如果直接get或post,无需隐藏域)
- 编写请求映射
- 开启页面表单的Rest功能=》 配置文件:
spring:
mvc:
hiddenmethod: # 对应类 HiddenHttpMethodFilter
filter:
enabled: true #开启页面表单的Rest功能
如果不开启hiddenmethod,底层(webMvcAutoConfiguration中,OrderedHiddenHttpMethodFilter
方法matchIfMissing=false
)默认是不装配这个组件的
<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"/> <!--提交delete请求的方法-->
<input value="REST-DELETE 提交" type="submit"/>
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT" /> <!---提交put请求的方法-->
<input value="REST-PUT提交"type="submit" />
<form>
使用put或delete请求的前提,是form表单外层是post请求
拿到是post请求,就尝试拿到里面的 “_method”(methodParam)name为此值的input标签,并拿到其value(put || delete),拼装为对应的请求
@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
的doFilterInternal方法拦截- 请求是否正常,并且是POST
- 获取到
_method
的值。 - 兼容以下请求;PUT.DELETE.PATCH
- 原生request(post),包装模式requesWrapper重写了getMethod方法,返回的是传入的值。
- 过滤器链放行的时候用wrapper。以后的方法调用getMethod是调用requesWrapper的。
- 获取到
- 请求是否正常,并且是POST
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"; // 指定判断name是否为 _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;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
String paramValue = request.getParameter(this.methodParam); // 获取 _method的值
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH); // 转为大写,规范你写请求类型不为大写
if (ALLOWED_METHODS.contains(method)) { //兼容的请求:ALLOWED_METHODS =三个 put,delete,patch
requestToUse = new HttpMethodRequestWrapper(request, method); //HttpMethodRequestWrapper 继承的 HttpServletRequestWrapper 如下
}
}
}
filterChain.doFilter(requestToUse, response);
}
/**
* HttpServletRequestWrapper :最基本的servlet请求封装
* {@link HttpServletRequest#getMethod()}.
*/
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method; // 继承是因为,接收一个新的method传参
public HttpMethodRequestWrapper(HttpServletRequest request, String method) {
super(request);
this.method = method;
}
// 重写getMethod方法,返回的是前端传入的值(即put等三个)
@Override
public String getMethod() {
return this.method;
}
}
}
- Rest使用客户端工具。
- 如PostMan可直接发送put、delete等方式请求。
27、请求处理-【源码分析】-怎么改变默认的_method
@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 {
...
// 隐藏域http请求方法过滤器
@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
。例如:
@Configuration(proxyBeanMethods = false)// 配置类注解
public class WebConfig{
//自定义filter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
}
将\_method
改成_m
。
<form action="/user" method="post">
<input name="_m" type="hidden" value="DELETE"/>
<input value="REST-DELETE 提交" type="submit"/>
</form>
28、请求处理-【源码分析】-请求映射原理
上面的黑体字代表调用者这个类中的主要方法
SpringMVC功能分析都从 org.springframework.web.servlet.DispatcherServlet
-> doDispatch()
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { //ctrl+f12 显示某个类的方法
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()
**方法如下:
// 保存所有类型的handlerMapping
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) { // 在遍历第一个handlerMapping即RequestMappingHandlerMapping就能处理当前请求,所以就由这个handlerMapping处理了
HandlerExecutionChain handler = mapping.getHandler(request); // 将当前请求和当前遍历的handlerMapping匹配,是否能对应上
if (handler != null) {
return handler;
}
}
}
return null;
}
this.handlerMappings
在Debug模式下展现的内容:
其中,RequestMappingHandlerMapping
保存了所有@RequestMapping
和handler
的映射规则。
这里中间还会判断当前请求到底是什么类型
// 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;
}
}
以及(老师分析到了,自己没有debug过去,可能源码升级了),该方法位于AbstractHandlerMethodMapping类中
@Override
@Nullable
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
String lookupPath = initLookupPath(request);
this.mappingRegistry.acquireReadLock();
try {
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}
所有的请求映射都在HandlerMapping中:
-
SpringBoot自动配置欢迎页的
WelcomePageHandlerMapping
。访问 /能访问到index.html; -
SpringBoot自动配置了默认 的
RequestMappingHandlerMapping
(webMvcAutoConfiguration中配置) -
请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
- 如果有就找到这个请求对应的handler
- 如果没有就是下一个 HandlerMapping
-
我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping,用来处理一系列特殊的映射请求(/api/vip1)
IDEA快捷键:
- Ctrl + Alt + U : 以UML的类图展现类有哪些继承类,派生类以及实现哪些接口。
- Crtl + Alt + Shift + U : 同上,区别在于上条快捷键结果在新页展现,而本条快捷键结果在弹窗展现。
- Ctrl + H : 以树形方式展现类层次结构图。
29、请求处理-常用参数注解使用
注解:
@PathVariable
路径变量 (urlPattern中url/1,注解mapping(url/{id}),参数加@PathVariable(“id”) 获取id)@RequestHeader
获取请求头信息 (f12中的request信息)@RequestParam
获取请求参数(指问号后的参数,url?a=1&b=2)@CookieValue
获取Cookie值@RequestBody
获取请求体[POST]@MatrixVariable
矩阵变量@RequestAttribute
获取request域属性@ModelAttribute
使用用例:
@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, // PathVariable对应的Map只能是<String,String> 封装出: {"id":xx,"username":xxx}
@RequestHeader("User-Agent") String userAgent,
@RequestHeader Map<String,String> header,
@RequestParam("age") Integer age,
@RequestParam("inters") List<String> inters,
//直接封装成map
@RequestParam Map<String,String> params,
// cokkie是servlet下的
@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")
// 获取post方式提交的数据 本身的url和只是数据封装位置不同
public Map postMethod(@RequestBody String content){ // content: "name=lisi&age=13"
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
}
30、请求处理-@RequestAttribute
用例:
@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
-
语法: 请求路径:
/cars/sell;low=34;brand=byd,audi,yd
以分号**;**分隔条件, 以逗号 **,**分隔一个key的多个value,第一个分号前面才是 访问路径 -
SpringBoot默认是禁用了以矩阵变量提交数据的功能
@Override //映射路径匹配源代码 public void configurePathMatch(PathMatchConfigurer configurer) { if (this.mvcProperties.getPathmatch() .getMatchingStrategy() == WebMvcProperties.MatchingStrategy.PATH_PATTERN_PARSER) { configurer.setPatternParser(pathPatternParser); } configurer.setUseSuffixPatternMatch(this.mvcProperties.getPathmatch().isUseSuffixPattern()); configurer.setUseRegisteredSuffixPatternMatch( this.mvcProperties.getPathmatch().isUseRegisteredSuffixPattern()); this.dispatcherServletPath.ifAvailable((dispatcherPath) -> { String servletUrlMapping = dispatcherPath.getServletUrlMapping(); if (servletUrlMapping.equals("/") && singleDispatcherServlet()) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setAlwaysUseFullPath(true); configurer.setUrlPathHelper(urlPathHelper); } }); }
- 手动开启:原理。对于路径的处理。UrlPathHelper的removeSemicolonContent设置为false,让其支持矩阵变量的。
-
矩阵变量必须有url路径变量才能被解析
- 意思是: /car/sell;price=100 中 sell路径必须和 ;price=100 看成一个整体,这样矩阵变量才有效
- 后端的mapping注解接收sell路径和矩阵变量需要用 {xx}一起接收,xx用@pathVariable取出来是 sell
手动开启矩阵变量:
- 实现
WebMvcConfigurer
接口:
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
...
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false); // 这里源码默认为true
configurer.setUrlPathHelper(urlPathHelper);
}
}
- 创建返回
WebMvcConfigurer
Bean,自定义bean:
@Configuration(proxyBeanMethods = false)
public class WebConfig{
@Bean // 直接创建一个webMvcConfigurer组件,重写方法
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
// 不移除;后面的内容。矩阵变量功能就可以生效
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
}
}
@MatrixVariable
的用例
@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}") // 获取两个age,但是不同人的age,用路径形参pathVar指定区分
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
开始说起:
todo: 待回看 ,先明白流程,再看设计
public class DispatcherServlet extends FrameworkServlet {
// doDispatch
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。(用的最多的也是对应的RequestMappingHandler)protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { for (HandlerAdapter adapter : this.handlerAdapters) { // 检查当前handler支不支持adapter if (adapter.supports(handler)) { // 如果匹配成功,就返回当前adapter适配器 return adapter; } } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); }
@Override public final boolean supports(Object handler) { // 判断handler的类型和handlerMethod的类型 return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)); }
-
适配器执行目标方法并确定方法参数的每一个值。
HandlerAdapter
默认会加载所有HandlerAdapter
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
:
- 支持方法上标注
@RequestMapping
- 支持函数式编程的
执行目标方法
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());
...
//本节重点 执行handler方法
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
HandlerAdapter
接口实现类RequestMappingHandlerAdapter
(主要用来处理@RequestMapping
)
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
//AbstractHandlerMethodAdapter类的方法,RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return 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目标方法能写多少种参数类型。取决于参数解析器 HandlerMethodArgumentResolver。
@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.argumentResolvers
**在afterPropertiesSet()
方法内初始化
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的包装类)。
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
的源码:
public interface HandlerMethodArgumentResolver {
//当前解析器是否支持解析这种参数
boolean supportsParameter(MethodParameter parameter);
@Nullable//如果支持,就调用 resolveArgument
Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}
返回值处理器
ValueHandler
@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.returnValueHandlers
在afterPropertiesSet()
方法内初始化
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
类如下:
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
接口:
public interface HandlerMethodReturnValueHandler {
boolean supportsReturnType(MethodParameter returnType);
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}
回顾执行目标方法
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mv = null;
...
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
RequestMappingHandlerAdapter
的handle()
方法:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
//AbstractHandlerMethodAdapter类的方法,RequestMappingHandlerAdapter继承AbstractHandlerMethodAdapter
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return 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;
}
}
**RequestMappingHandlerAdapter
**的invokeHandlerMethod()
方法
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
...
ServletInvocableHandlerMethod invocableMethod = 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);
}
finally {
webRequest.requestCompleted();
}
}
invokeAndHandle()
方法如下:
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
...
try {
//returnValue存储起来
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
...
}
}
@Nullable//InvocableHandlerMethod类的,ServletInvocableHandlerMethod类继承InvocableHandlerMethod类
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();//@RequestMapping的方法
ReflectionUtils.makeAccessible(method);
try {
if (KotlinDetector.isSuspendingFunction(method)) {
return CoroutinesUtils.invokeSuspendingFunction(method, getBean(), args);
}
//通过反射调用
return method.invoke(getBean(), args);//getBean()指@RequestMapping的方法所在类的对象。
}
catch (IllegalArgumentException ex) {
...
}
catch (InvocationTargetException ex) {
...
}
}
}
如何确定目标方法每一个参数的值
重点分析ServletInvocableHandlerMethod
的**getMethodArgumentValues
**方法
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
...
@Nullable//InvocableHandlerMethod类的,ServletInvocableHandlerMethod类继承InvocableHandlerMethod类
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
获取方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
....
return doInvoke(args);
}
//本节重点,获取方法的参数值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) { // 依次拿到controller方法上的参数信息(注解中)
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
//查看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 ex) {
....
}
}
return args;
}
}
this.resolvers
的类型为**HandlerMethodArgumentResolverComposite
**(在参数解析器章节提及)
public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}
@Nullable
private 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);//找到了,resolver就缓存起来,方便稍后resolveArgument()方法使用
break;
}
}
}
return result;
}
}
@Override // 以PathVariableMethodArgumentResolver 解析器为例,其中如何判断是否匹配 当前的解析器
public boolean supportsParameter(MethodParameter parameter) {
if (!parameter.hasParameterAnnotation(PathVariable.class)) {
return false;
}
if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) {
PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class);
return (pathVariable != null && StringUtils.hasText(pathVariable.value()));
}
return true;
}
小结
本节描述,一个请求发送到DispatcherServlet后的具体处理流程,也就是SpringMVC的主要原理。
本节内容较多且硬核,对日后编程很有帮助,需耐心对待。
可以运行一个示例,打断点,在Debug模式下,查看程序流程。
33、请求处理-【源码分析】-Servlet API参数解析原理
- WebRequest
- ServletRequest
- MultipartRequest 文件上传请求
- HttpSession
- javax.servlet.http.PushBuilder
- Principal 首要的
- InputStream
- Reader
- HttpMethod
- Locale 地址
- TimeZone
- ZoneId
ServletRequestMethodArgumentResolver用来处理以上的参数
它是怎么找到这个MethodArgumentResolver的,跟上一个类型参数解析一样,底层采用foreach循环遍历所有HandlerMethodArgumentResolver
接口对应的所有实现类, 判断是否能处理servlet参数请求。
@Nullable
private 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;
}
}
}
return result;
}
public class ServletRequestMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Nullable
private static Class<?> pushBuilder;
static {
try {
pushBuilder = ClassUtils.forName("javax.servlet.http.PushBuilder",
ServletRequestMethodArgumentResolver.class.getClassLoader());
}
catch (ClassNotFoundException ex) {
// Servlet 4.0 PushBuilder not found - not supported for injection
pushBuilder = null;
}
}
// 当前这个解析器支持参数的条件
@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 != null && pushBuilder.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
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 != null && pushBuilder.isAssignableFrom(paramType)) {
return PushBuilderDelegate.resolvePushBuilder(request, paramType);
}
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.resolve(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());
}
/**
* Inner class to avoid a hard dependency on Servlet API 4.0 at runtime.
*/
private static class PushBuilderDelegate {
@Nullable
public static Object resolvePushBuilder(HttpServletRequest request, Class<?> 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;
}
}
}
用例:
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了..."); //以此开始debug
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
用例:
@GetMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
//下面三位都是可以给request域中放数据
map.put("hello","world666");
model.addAttribute("world","hello666");
request.setAttribute("message","HelloWorld");
Cookie cookie = new Cookie("c1","v1"); // cookie还可以设置生存区 setDemion()
response.addCookie(cookie);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map success(@RequestAttribute(value = "msg",required = false) String msg, // required=false 表示当前参数不是必须的
@RequestAttribute(value = "code",required = false)Integer code,
HttpServletRequest request){
Object msg1 = request.getAttribute("msg");
Map<String,Object> map = new HashMap<>();
// 直接从request域中拿到map,model设置的值
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> map
与Model model
用什么参数处理器。
Map<String,Object> map
参数用**MapMethodProcessor
**处理: HandlerMethodArgumentResolverComposite
工具类,同样是在 HandlerMethodArgumentResolver
实现类中遍历看谁能处理这个map参数
public class MapMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {
// 处理器
@Override
public boolean supportsParameter(MethodParameter parameter) {
return (Map.class.isAssignableFrom(parameter.getParameterType()) && //Map.class直接指定,只处理Map类型的参数
parameter.getParameterAnnotations().length == 0);
}
@Override
@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()
如下: mavContainer
public class ModelAndViewContainer {
...
private final ModelMap defaultModel = new BindingAwareModelMap();
@Nullable
private ModelMap redirectModel;
...
// 该类的主要方法
public ModelMap getModel() { // 返回一个 ModelMap类型的对象,实际是BindingAwareModelMap类型
if (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 model
用**ModelMethodProcessor
**处理:
public class ModelMethodProcessor implements HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler {....}
return mavContainer.getModel();
这跟MapMethodProcessor
的一致
Model
也是另一种意义的Map
。
接下来看看Map<String,Object> map
与Model model
值是如何做到用request.getAttribute()
获取的。
众所周知,所有的数据都放在 ModelAndView包含要去的页面地址View,还包含Model数据。
先看ModelAndView接下来是如何处理的?
public class DispatcherServlet extends FrameworkServlet {
...
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
...
try {
ModelAndView mv = null;
...
// Actually invoke the handler. 最重要的适配器处理方法,返回视图和数据 MV
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
//处理分发结果
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 {
...
View view;
String viewName = mv.getViewName();
if (viewName != null) {
// We need 解析视图名字
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() + "'");
}
}
view.render(mv.getModelInternal(), request, response); // 页面渲染 方法来自AbstractView,子类InternalResourceView 实现
...
}
}
在Debug模式下,view
属为**InternalResourceView
**类。
public class InternalResourceView extends AbstractUrlBasedView {
@Override//该方法在AbstractView,AbstractUrlBasedView继承了AbstractView
public void render(@Nullable Map<String, ?> model, HttpServletRequest request,
HttpServletResponse response) throws Exception {
...
// 创建一个合并输出model
Map<String, Object> mergedModel = createMergedOutputModel(model, request, response);
prepareResponse(request, response);
//看下一个方法实现, <!---重要!!!---->
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.
// 暴露model作为请求域属性 (下有方法内容)
exposeModelAsRequestAttributes(model, request);//<---重点 (内部将model的属性进行遍历,放入request)==》 为什么model和map的值放在request
// Expose helpers as request attributes, if any.
exposeHelpers(request);
// Determine the path for the request dispatcher.
String dispatcherPath = prepareForRendering(request, response);
// Obtain a RequestDispatcher for the target resource (typically a JSP).
RequestDispatcher rd = 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> map
,Model model
这两种类型数据可以给request域中放数据,用request.getAttribute()
获取。
35、请求处理-【源码分析】-自定义参数绑定原理
@RestController
public class ParameterTestController {
/**
* 数据绑定:页面提交的请求数据(GET、POST)都可以和对象属性进行绑定
* @param person
* @return
*/
@PostMapping("/saveuser")
public Person saveuser(Person person){ // 一直想明白 springmvc是如何将表单一条条数据封装成 对象(DataBinder,反射)
return person;
}
}
/**
* 姓名: <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;
}
BUG:注意,如果前端输入日期设定了input类型为date,后端不做接收处理的话,会出现BindException异常导致400错误。显示的异常提示信息: new BindException(binder.getBindingResult())
封装过程用到**ServletModelAttributeMethodProcessor
。这个过程debug,需要现在dispatchSerlvet每个请求必经之路(6.0.2版本的HandlerMethodArgumentResolverComposite
**
)打断点(跟上一节一样)
有两个ServletModelAttributeMethodProcessor, 因为requestMethodHandlerAdapter
注册了俩
当前情况,后一个才能处理这个时候的参数注入
public class ServletModelAttributeMethodProcessor extends ModelAttributeMethodProcessor {
//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 = parameter.getParameterAnnotation(ModelAttribute.class);
if (ann != null) {
mavContainer.setBinding(name, ann.binding());
}
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
...
}
}
if (bindingResult == null) {
//web数据绑定器 内含待封装的Person空对象名:objectName,根据反射创建
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); //<-- 重点
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
//web数据绑定器用来 将请求参数的值绑定到指定的JavaBean里面**
bindRequestParameters(binder, webRequest); //底层在此方法中分步拿值存值(反射来做)
}
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
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 将请求数据 转成 指定的数据类型(String-> Integer)。再次封装到JavaBean中
-
在过程当中,用到GenericConversionService:在设置每一个值的时候,找它里面的所有converter转换器 哪个可以尝试将这个数据类型(request带来参数的字符串)转换到指定的类型
-
converter的总接口:
@FunctionalInterface public interface Converter<S,T>
36、请求处理-【源码分析】-自定义Converter原理
未来我们可以给WebDataBinder里面放自己的Converter,重写添加addFormatters
; 设置自己想要的数据类型转换
下面演示将字符串“啊猫,3”
转换成Pet
对象。
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean // 直接注入一webmvcConfigurer 组件,添加新的converter
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void addFormatters(FormatterRegistry registry) { // 除了定义converter 还可以定义formatter
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) { // 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数据,需要引入相关的依赖
<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>
spring-boot-starter-json
底层引入了json可能需要的多个依赖
控制层代码如下:
@Controller
public class ResponseTestController {
@ResponseBody //利用 返回值处理器--->消息转换器 进行处理为json数据
//使用jackson 将对象转换为json数据
@GetMapping(value = "/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(28);
person.setBirth(new Date());
person.setUserName("zhangsan");
return person;
}
}
[32、请求处理-【源码分析】-各种类型参数解析原理 - 返回值处理器](#32、请求处理-【源码分析】-各种类型参数解析原理 - 返回值处理器) 除了argumentResolvers也有讨论ReturnValueHandler。现在直接看看重点:
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
...
ServletInvocableHandlerMethod invocableMethod = 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);
}
finally {
webRequest.requestCompleted();
}
}
ServletInvocableHandlerMethod处理返回值的地方
public class ServletInvocableHandlerMethod extends InvocableHandlerMethod {
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); // 执行目标方法(controller方法) ,并得到返回值
...
try {
//看下块代码,处理返回值
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);//getReturnValueType 获取返回值类型
}
catch (Exception ex) {
...
}
}
ALT+F8 计算当前表达式的信息。
HandlerMethodReturnValueHandlerComposite 中的handleReturnValue方法实现
public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler {
...
@Override // 这个方法,仅做一些表面的数值处理
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//selectHandler()实现在下面,也是在这个类中
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
//开始处理,核心处理
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
/**
根据value和Type 获取其适配的处理器
*/
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType); //用所有的返回处理器(因为不知道谁是当前返回的【可使用处理器】)判断是不是 异步返回值
// 遍历哪一个处理器(15个)可以处理当前返回值 ----> make by RequestResponseBodyMethodProcessor
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { //判断handler是不是异步返回handler
continue;
}
if (handler.supportsReturnType(returnType)) { // 返回值处理器判断是否支持这种类型返回值
return handler;
}
}
return null;
}
分析上面源码的 返回值处理器们,SpringMVC支持的返回值有,
ModelAndview
Model
View
ResponseEntity
ResponseBodyEmitter
StreamingResponseBody
HttpEntity
Callable
DeferredResult
ListenableFuture
CompletionStage
WebAsyncTask
.. 各种不常见的类型
@ResponseBody
注解,即RequestResponseBodyMethodProcessor
,它实现HandlerMethodReturnValueHandler
接口
public class RequestResponseBodyMethodProcessor extends AbstractMessageConverterMethodProcessor {
// 这个类中的 supportsReturnType方法,表示,支持非单一类型的返回值,支持标注了@ResponseBody注解
...
<!--HandlerMethodReturnValueHandler 的核心方法在这里实现-->
@Override
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);
// 使用 消息转换器MessageConverter 进行写出操作,本方法(AbstractMessageConverterMethodProcessor类)下一章节介绍:
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
}
38、响应处理-【源码分析】-HTTPMessageConverter原理
HTTPMessageConverter
:json转换器的最高父接口
返回值处理器ReturnValueHandler
分析原理步骤:
- 返回值处理器判断是否支持这种类型返回值
supportsReturnType
- 如果支持,返回值处理器调用
handleReturnValue
进行处理 RequestResponseBodyMethodProcessor
可以处理返回值标了@ResponseBody
注解的- 利用
MessageConverters
进行处理 将数据写为json- 内容协商(浏览器默认会以请求头的方式告诉服务器他能接受什么样的内容类型)
- 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据,
- SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter
,看谁能处理?如何筛选?- 得到
MappingJackson2HttpMessageConverter
可以将对象写为jsonboolean canRead(Class<?> clazz, @Nullable MediaType mediaType);
请求 - 利用
MappingJackson2HttpMessageConverter
将对象转为json再写出去boolean canWrite(Class<?> clazz, @Nullable MediaType mediaType);
响应
- 得到
- 利用
现在升级版本的源码和下面这个有一点点不一样
//RequestResponseBodyMethodProcessor 继承这个类,使用当前这个类的converters
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
...
//承接上一节内容
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
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());
}
...
//内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType(); //获取输出response的contentType
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();
///acceptableTypes 浏览器声明自己能接受的数据类型
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) { //内容类型协商匹配 遍历
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
// 如果mediaTypesToUse为空
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
//mediaTypesToUse中选择一个MediaType 媒体类型 ,赋给selectedMediaType
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) {
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
// 判断selectedMediaType是否为空
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//!!!@@##
// /SpringMVC会挨个遍历所有容器底层的 HttpMessageConverter看谁能处理? ---> MappingJackson2HttpMessageConverter
//本节主角:HttpMessageConverter (共10个MessageConverter)
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
//判断是否可写
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
// body 待转为json数据的信息
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) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
//开始写入 wirte方法
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
...
}
HTTPMessageConverter
接口:
/**
* 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
,这将用到MappingJackson2HttpMessageConverter
public class MappingJackson2HttpMessageConverter extends AbstractJackson2HttpMessageConverter {
... // 利用底层的jackson的 objectMapper (write操作)将对象转为json (这个类是和最底层jackson接触的类)
}
关于MappingJackson2HttpMessageConverter
的实例化请看下节。 TODO:
CTRL+H 查看一个类或接口的子实现类
关于HttpMessageConverters的初始化
DispatcherServlet
的初始化时 会调用initHandlerAdapters(ApplicationContext context)
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);
}
}
...
上述代码会加载ApplicationContext
的所有HandlerAdapter
,用来处理@RequestMapping
的RequestMappingHandlerAdapter
实现HandlerAdapter
接口,RequestMappingHandlerAdapter
也被实例化。
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
private List<HttpMessageConverter<?>> messageConverters;
... //初始化处理器,并且添加一堆的消息转换器
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter()); //添加字节数组消息转换器
this.messageConverters.add(new StringHttpMessageConverter()); // 添加字符串消息转换器
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>()); // 添加资源xml信息转换器
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
在构造器中看到一堆HttpMessageConverter
。接着,重点查看AllEncompassingFormHttpMessageConverter
类:
public class AllEncompassingFormHttpMessageConverter extends FormHttpMessageConverter {
/**
* Boolean flag controlled by a {@code spring.xml.ignore} system property that instructs Spring to
* ignore XML, i.e. to not initialize the XML-related infrastructure.
由{@code Spring.xml.NORE}系统属性控制的布尔标志,该属性指示Spring忽略XML,即不初始化与XML相关的基础结构。
* <p>The default is "false".
*/
private static final boolean shouldIgnoreXml = SpringProperties.getFlag("spring.xml.ignore");
private static final boolean jaxb2Present;
// 各种关于jackson2的出席标志
private static final boolean jackson2Present;
private static final boolean jackson2XmlPresent;
private static final boolean jackson2SmilePresent;
private static final boolean gsonPresent;
private static final boolean jsonbPresent;
private static final boolean kotlinSerializationJsonPresent;
// 静态代码块,判断是否有xxx依赖存在,就引入对应的converter
static {
ClassLoader classLoader = AllEncompassingFormHttpMessageConverter.class.getClassLoader();//获取本类的类加载器
jaxb2Present = ClassUtils.isPresent("javax.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);
gsonPresent = ClassUtils.isPresent("com.google.gson.Gson", classLoader);
jsonbPresent = ClassUtils.isPresent("javax.json.bind.Jsonb", classLoader);
kotlinSerializationJsonPresent = ClassUtils.isPresent("kotlinx.serialization.json.Json", classLoader);
}
//根据出席标志,添加对应的转换器
public AllEncompassingFormHttpMessageConverter() {
if (!shouldIgnoreXml) {
try {
addPartConverter(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
if (jaxb2Present && !jackson2XmlPresent) {
addPartConverter(new Jaxb2RootElementHttpMessageConverter());
}
}
if (jackson2Present) {
addPartConverter(new MappingJackson2HttpMessageConverter()); //<----重点看这里,实例化
}
else if (gsonPresent) {
addPartConverter(new GsonHttpMessageConverter());
}
else if (jsonbPresent) {
addPartConverter(new JsonbHttpMessageConverter());
}
else if (kotlinSerializationJsonPresent) {
addPartConverter(new KotlinSerializationJsonHttpMessageConverter());
}
if (jackson2XmlPresent && !shouldIgnoreXml) {
addPartConverter(new MappingJackson2XmlHttpMessageConverter());
}
if (jackson2SmilePresent) {
addPartConverter(new MappingJackson2SmileHttpMessageConverter());
}
}
}
public class FormHttpMessageConverter implements HttpMessageConverter<MultiValueMap<String, ?>> {
...
private List<HttpMessageConverter<?>> partConverters = new ArrayList<>();
...
public void addPartConverter(HttpMessageConverter<?> partConverter) {
Assert.notNull(partConverter, "'partConverter' must not be null");
this.partConverters.add(partConverter); //添加转换器
}
...
}
在AllEncompassingFormHttpMessageConverter
类构造器看到MappingJackson2HttpMessageConverter
类的实例化,AllEncompassingFormHttpMessageConverter
包含MappingJackson2HttpMessageConverter
。
ReturnValueHandler
是怎么与MappingJackson2HttpMessageConverter
关联起来?请看下节。
ReturnValueHandler与MappingJackson2HttpMessageConverter关联
再次回顾RequestMappingHandlerAdapter
public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter
implements BeanFactoryAware, InitializingBean {
...
@Nullable
private HandlerMethodReturnValueHandlerComposite returnValueHandlers;//我们关注的returnValueHandlers
@Override
@Nullable//本方法在AbstractHandlerMethodAdapter
1. public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
@Override
2. protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
...
mav = invokeHandlerMethod(request, response, handlerMethod);
...
return mav;
}
//!!!
@Nullable
3. 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) {//<---我们关注的returnValueHandlers
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
...
invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}
return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}
@Override
public void afterPropertiesSet() {
// Do this first, it may add ResponseBody advice beans
...
if (this.returnValueHandlers == null) { //赋值returnValueHandlers
List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
}
}
// 获取默认的返回值处理器
private List<HandlerMethodReturnValueHandler> getDefaultReturnValueHandlers() {
List<HandlerMethodReturnValueHandler> handlers = new ArrayList<>(20);
...
// Annotation-based return value types
//这里就是 ReturnValueHandler与 MappingJackson2HttpMessageConverter关联 的关键点
handlers.add(new RequestResponseBodyMethodProcessor(getMessageConverters(),//<---MessageConverters也就传参传进来的
this.contentNegotiationManager, this.requestResponseBodyAdvice));//
...
return handlers;
}
//------
public List<HttpMessageConverter<?>> getMessageConverters() {
return this.messageConverters;//返回所有的信息转换器
}
//RequestMappingHandlerAdapter构造器已初始化部分messageConverters
public RequestMappingHandlerAdapter() {
this.messageConverters = new ArrayList<>(4);
this.messageConverters.add(new ByteArrayHttpMessageConverter());
this.messageConverters.add(new StringHttpMessageConverter());
if (!shouldIgnoreXml) {
try {
this.messageConverters.add(new SourceHttpMessageConverter<>());
}
catch (Error err) {
// Ignore when no TransformerFactory implementation is available
}
}
this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
}
...
}
应用中==WebMvcAutoConfiguration
==(底层是WebMvcConfigurationSupport
实现)传入一大堆messageConverters
,其中就包含MappingJackson2HttpMessageConverter
。
39、响应处理-【源码分析】-内容协商原理
根据客户端接收能力不同,服务端返回不同 媒体类型的数据(media)。
引入XML依赖:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
<!--之前导入的spring-boot-start-json 并不包含xml转换的依赖-->
可用Postman软件分别测试返回json和xml:只需要改变请求头中Accept字段(application/json、application/xml)。
H t t p 协议中规定的, A c c e p t 字段告诉服务器本客户端可以接收的数据类型 Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型 Http协议中规定的,Accept字段告诉服务器本客户端可以接收的数据类型,但是,如果客户端指定了接收数据类型,而服务端无法提供,就会出现警告 :No acceptable representation
内容协商原理流程:
- 判断当前响应头中是否已经有确定的媒体类型
MediaType
。 - 获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端
Accept
请求头字段application/xml)(这一步在下一节有详细介绍)contentNegotiationManager
内容协商管理器 默认使用基于请求头的策略HeaderContentNegotiationStrategy
确定客户端可以接收的内容类型
- 遍历循环所有当前系统的
MessageConverter
,看谁支持操作这个对象(Person)
找出两个支持json两个支持xm的MappingJackson2HttpMessageConverter
- 找到支持操作Person的converter,把converter支持的媒体类型统计出来。
- 客户端需要application/xml,服务端有10种MediaType 处理能力
- 进行内容协商的最佳匹配媒体类型(遍历浏览器和服务端的类型,匹配最佳)
- 用 支持 将对象转为 最佳匹配媒体类型 的converter。调用它进行转化 。
//RequestResponseBodyMethodProcessor继承这类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
...
//跟上一节的代码一致
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
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());
}
...
//本节重点
//内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
MediaType selectedMediaType = null; //选中的媒体数据类型
MediaType contentType = outputMessage.getHeaders().getContentType();//返回流指定数据响应类型
boolean isContentTypePreset = contentType != null && contentType.isConcrete(); //判断响应头是否有contentType
if (isContentTypePreset) {
if (logger.isDebugEnabled()) {
logger.debug("Found 'Content-Type:" + contentType + "' in response");
}
selectedMediaType = contentType;
}
else {
HttpServletRequest request = inputMessage.getServletRequest();
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
if (body != null && producibleTypes.isEmpty()) {
throw new HttpMessageNotWritableException(
"No converter found for return value of type: " + valueType);
}
List<MediaType> mediaTypesToUse = new ArrayList<>();
for (MediaType requestedType : acceptableTypes) {
for (MediaType producibleType : producibleTypes) {
if (requestedType.isCompatibleWith(producibleType)) { // 根据浏览器接收类型,匹配能返回的类型
mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType));
}
}
}
if (mediaTypesToUse.isEmpty()) {
if (body != null) {
throw new HttpMediaTypeNotAcceptableException(producibleTypes);
}
if (logger.isDebugEnabled()) {
logger.debug("No match for " + acceptableTypes + ", supported: " + producibleTypes);
}
return;
}
MediaType.sortBySpecificityAndQuality(mediaTypesToUse);
//选择一个 MediaType 媒体数据类型
for (MediaType mediaType : mediaTypesToUse) {
if (mediaType.isConcrete()) { // 判断媒体数据类型是否满足条件,添加其为被选择
selectedMediaType = mediaType;
break;
}
else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) {
//APPLICATION_OCTET_STREAM = new MediaType("application", "octet-stream");
selectedMediaType = MediaType.APPLICATION_OCTET_STREAM;
break;
}
}
if (logger.isDebugEnabled()) {
logger.debug("Using '" + selectedMediaType + "', given " +
acceptableTypes + " and supported " + producibleTypes);
}
}
// 找到 可用的converter
if (selectedMediaType != null) {
selectedMediaType = selectedMediaType.removeQualityValue();
//本节主角:HttpMessageConverter
for (HttpMessageConverter<?> converter : this.messageConverters) {
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?
(GenericHttpMessageConverter<?>) converter : null);
//判断是否可写
if (genericConverter != null ?
((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) :
converter.canWrite(valueType, selectedMediaType)) {
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) + "]");
addContentDispositionHeader(inputMessage, outputMessage);
//开始对 body写入
if (genericConverter != null) {
// 调用genericConverter的write实现方法 (也是自定义converter中的重写点)
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}
else {
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
else {
if (logger.isDebugEnabled()) {
logger.debug("Nothing to write: null body");
}
}
return;
}
}
}
...
}
40、响应处理-【源码分析】-基于请求参数的内容协商原理
上一节内容协商原理的第二步:
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段application/xml)
contentNegotiationManager类
内容协商管理器 默认使用基于请求头的策略HeaderContentNegotiationStrategy类
确定客户端可以接收的内容类型
//RequestResponseBodyMethodProcessor继承这类
public abstract class AbstractMessageConverterMethodProcessor extends AbstractMessageConverterMethodArgumentResolver
implements HandlerMethodReturnValueHandler {
...
//跟上一节的代码一致
protected <T> void writeWithMessageConverters(@Nullable T value, MethodParameter returnType,
ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
Object body;
Class<?> valueType;
Type targetType;
...
//本节重点
//内容协商(浏览器默认会以请求头(参数Accept)的方式告诉服务器他能接受什么样的内容类型)
MediaType selectedMediaType = null;
MediaType contentType = outputMessage.getHeaders().getContentType();
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();
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
...
}
//获取浏览器支持的媒体数据类型(在AbstractMessageConverterMethodArgumentResolver类内)
private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request)
throws HttpMediaTypeNotAcceptableException {
//内容协商管理器 默认使用基于请求头的策略
return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request));
}
}
ContentNegotiationManager内容协商管理器类
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); // 这里获取 基于参数指定的媒体类型-->底层调用 getMediaType
if (mediaTypes.equals(MEDIA_TYPE_ALL_LIST)) { //如果是媒体数据类型是 */* ,就跳过当前继续下一个遍历
continue;
}
return mediaTypes;
}
return MEDIA_TYPE_ALL_LIST;
}
...
}
HeaderContentNegotiationStrategy类
//基于请求头的策略
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);
MediaType.sortBySpecificityAndQuality(mediaTypes);
return !CollectionUtils.isEmpty(mediaTypes) ? mediaTypes : MEDIA_TYPE_ALL_LIST;
}
catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotAcceptableException(
"Could not parse 'Accept' header " + headerValues + ": " + ex.getMessage());
}
}
}
开启浏览器参数方式内容协商功能
postMan可以很方便修改请求头的Accept信息,但浏览器不可以
为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启请求参数内容协商模式 (WebMvcProperties属性类)
/**
* Whether a request parameter ("format" by default) should be used to determine
* the requested media type(媒体数据类型). 在请求参数上使用format指定接收的结果类型
*/
private boolean favorParameter = false;
http://localhost:8080/test/person?format=xml
如何生效的?
ContentNegotiationManager
内容协商管理器,以前只根据请求头Accpet
的内容进行媒体数据匹配;现在开启了,请求参数内容协商模式,协商管理器也可以根据基于参数的内容协商策略。而且基于参数协商的策略排在第一位
我们可以根据这种方式写几种类型呢?
查看 mediaTypes 可以知道只能有两种类型,一个是 json,一个是 xml,后面可以自定义messageConverter,处理自己想要类型
内容协商管理器,就会多了一个**
ParameterContentNegotiationStrategy
**( 由 S p r i n g 容器注入 由Spring容器注入 由Spring容器注入)
public class ParameterContentNegotiationStrategy extends AbstractMappingContentNegotiationStrategy {
private String parameterName = "format"; //<---从请求调用getParameterName中获取这个参数的名字,得到它的值
public ParameterContentNegotiationStrategy(Map<String, MediaType> mediaTypes) {
super(mediaTypes);
}
/**
* Set the name of the parameter to use to determine requested media types.
* <p>By default this is set to {@code "format"}. <扫描format关键字,得到其后的媒体数据类型>
*/
public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "'parameterName' is required");
this.parameterName = parameterName;}
public String getParameterName() {
return this.parameterName;}
@Override
@Nullable
protected String getMediaTypeKey(NativeWebRequest request) {
return request.getParameter(getParameterName());}
//---以下方法在AbstractMappingContentNegotiationStrategy类
@Override
public List<MediaType> resolveMediaTypes(NativeWebRequest webRequest)
throws HttpMediaTypeNotAcceptableException {
return resolveMediaTypeKey(webRequest, getMediaTypeKey(webRequest));}
/**
* An alternative to {@link #resolveMediaTypes(NativeWebRequest)} that accepts
* an already extracted key.
* @since 3.2.16
*/
public List<MediaType> resolveMediaTypeKey(NativeWebRequest webRequest, @Nullable String key)
throws HttpMediaTypeNotAcceptableException {
if (StringUtils.hasText(key)) {
MediaType mediaType = lookupMediaType(key);
if (mediaType != null) {
handleMatch(key, mediaType);
return Collections.singletonList(mediaType);}
mediaType = handleNoMatch(webRequest, key);
if (mediaType != null) {
addMapping(key, mediaType);
return Collections.singletonList(mediaType);}
}
return MEDIA_TYPE_ALL_LIST;
}
}
浏览器地址输入带format参数的URL:
http://localhost:8080/test/person?format=json
或
http://localhost:8080/test/person?format=xml
这样,后端会根据参数format的值,返回对应json或xml格式的数据。
41、响应处理-【源码分析】-自定义MessageConverter
实现多协议数据兼容。json、xml、x-morsun(这个是自创的)
-
@ResponseBody
响应数据出去 调用RequestResponseBodyMethodProcessor
处理 <–提醒,@ResponseBody不是只为json服务–> -
Processor 处理方法返回值。通过
MessageConverter
处理 -
所有
MessageConverter
合起来可以支持各种媒体类型数据的操作(读、写) -
内容协商找到最终的
messageConverter
所以想要自定义处理一种数据,需要指定一个xxxMessageConverter
和application/x-morsun
SpringMVC的什么功能,一个入口给容器中添加一个 WebMvcConfigurer
添加自己的messageConverter
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
// 扩展自己的converter
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MorSunConverter());
}
}
}
}
自定义内容协议的Converter,write方法写出自定义格式的内容
public class MorSunConverter implements HttpMessageConverter<Person> {
/**
* 暂时不关心读逻辑,直接返回false (即前端传来json数据,需要@RequestBody接收)
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
// 只要clazz的值是想要的Person类型,就返回true
return clazz.isAssignableFrom(Person.class);
}
/**
* 服务器要统计,所有的messageConverter都能写出那些内容,当前操作,就是让服务器能扫描到
* 当前converter,可以操作我们的自定义类型
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-morsun");
}
// 暂不处理
@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() + ";morSun quit;" + person.getAge() + ";morSun adapter;" + person.getBirth();
//写出去
OutputStream stream = outputMessage.getBody();
stream.write(data.getBytes());
}
}
- 添加自定义的MessageConverter进系统底层
- 系统底层就会统计出所有MessageConverter能操作哪些类型
- 客户端内容协商 [morsun—>morsun]
测试:用Postman发送/test/person
(请求头Accept:application/x-morsun
),将返回自定义协议数据的写出。
todo: 使用请求参数的方式,随意切换不同的响应数据类型! 下一章实现
42、响应处理-【源码分析】-浏览器与PostMan内容协商完全适配
假设你想基于自定义请求参数的自定义内容协商功能。
换句话,在地址栏输入http://localhost:8080/test/person?format=morsun
返回数据,跟http://localhost:8080/test/person
且请求头参数Accept:application/x-morsun
的返回自定义协议数据的一致。
请求参数format请求的类型,需添加自定义的这一种类型(本身只有两种)
向contentNegotiationManager
中添加基于参数协商管理的 新策略 ;万事归一,要修改还是要从webMvcConfigurer
中重写
@Configuration(proxyBeanMethods = false)
public class WebConfig /*implements WebMvcConfigurer*/ {
//1、WebMvcConfigurer定制化SpringMVC的功能
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
/**
重写configureContentNegotiation
* 自定义内容协商策略 基于参数
基于参数&基于请求头 两种方法都在这里注册,而且不是覆盖,要自己加载声明一下
* @param configurer
*/
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
//Map<String, MediaType> mediaTypes (AbstractMessageConverterMethodProcessor中使用)
Map<String, MediaType> mediaTypes = new HashMap<>();
mediaTypes.put("json",MediaType.APPLICATION_JSON);
mediaTypes.put("xml",MediaType.APPLICATION_XML);
//自定义新媒体类型 绑定accept
mediaTypes.put("morsun",MediaType.parseMediaType("application/x-morsun"));
//基于参数策略(Strategy)指定:支持解析哪些参数对应的哪些媒体类型
ParameterContentNegotiationStrategy parameterStrategy = new ParameterContentNegotiationStrategy(mediaTypes);
// 修改基于参数解析器的名字format,改为自己想要的(set方法)
parameterStrategy.setParameterName("ff");
//还需添加基于请求头处理策略,否则accept:application/json、application/xml则会失效
HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy();
// 为内容协商配置器 添加策略:基于参数和基于请求头
configurer.strategies(Arrays.asList(parameterStrategy, headeStrategy));
// !!! <!--如果这里忘记添加基于请求头的策略,那么请求头的请求一律返回json-->
}
}
}
}
- 由上述配置,可以看出,我们启动基于参数的内容协商策略,底层只是将 format的值
morsun
跟 Accept请求的appplication/x-morsun
进行绑定 - 日后开发要注意,有可能我们添加的自定义的功能会覆盖默认很多功能,可能导致一些默认的功能失效。
43、视图解析-Thymeleaf初体验
Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
Thymeleaf’s main goal is to bring elegant natural templates to your development workflow — HTML that can be correctly displayed in browsers and also work as static prototypes, allowing for stronger collaboration in development teams.
With modules for Spring Framework, a host of integrations with your favourite tools, and the ability to plug in your own functionality, Thymeleaf is ideal for modern-day HTML5 JVM web development — although there is much more it can do.——Link
thymeleaf使用👀
感觉和 j s p 的 j s t l 标签库相似,不仅是使用还是标签 感觉和jsp的jstl标签库相似,不仅是使用还是标签 感觉和jsp的jstl标签库相似,不仅是使用还是标签
引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自动配置好了thymeleaf
thymeleafAutoConfiguration类
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
...
}
自动配好的策略
-
所有thymeleaf的配置值都在
ThymeleafProperties
类 -
配置好了 SpringTemplateEngine
-
配好了 ThymeleafViewResolver
-
我们只需要直接开发页面
-
thmeleafProperties类中指定页面默认的存放位置
public static final String DEFAULT_PREFIX = "classpath:/templates/";//模板放置处
public static final String DEFAULT_SUFFIX = ".html";//文件的后缀名
编写一个控制层:
@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
:
<!DOCTYPE html>
<!---引入themleaf模板库--->
<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>
server:
servlet:
context-path: /app #设置应用名 (前缀路径)
这个设置后,URL要插入/app
, 如http://localhost:8080/app/hello.html
。
基本语法
表达式😉
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等值 |
选择变量 | *{…} | 获取🤔上下文对象值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成链接,默认在前面添加前缀路径 context-path |
片段表达式 | ~{…} | 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
- 设置单个值
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
</fieldset>
</form>
- 设置多个值
<img src="../../images/gtvglogo.png"
th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
官方文档 - 5 Setting Attribute Values
迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
条件运算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<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
登录页
<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内联写法:
<p>Hello, [[${session.loginUser.userName}]]!</p>
登录控制层
@Controller
public class IndexController {
/**
* 来登录页 ,如果不写的话,templates下的页面是无法访问的
这个操作应该用拦截器来做
* @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";
}
}
}
模型
@AllArgsConstructor
@NoArgsConstructor
@Data
public class User {
private String userName;
private String password;
}