笔记
SpringBoot基础入门
1 时代背景
1.2 微服务
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 每个服务运行在自己的进程内,也就是可独立部署和升级
- 服务之间使用轻量级HTTP交互
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言、不同的存储技术
1.3 分布式
分布式的困难
• 远程调用
• 服务发现
• 负载均衡
• 服务容错
• 配置管理
• 服务监控
• 链路追踪
• 日志管理
• 任务调度
分布式的解决
• SpringBoot + SpringCloud
1.4 云原生
原生应用如何上云。 Cloud Native
云原生困难
• 服务自愈
• 弹性伸缩
• 服务隔离
• 自动化部署
• 灰度发布
• 流量治理
• …
2 如何学习 SpringBoot
官网文档架构
官方文档:https://spring.io/projects/spring-boot#learn
3 入门小项目
3.1系统要求
• Java 8 & 兼容java14 .
• Maven 3.3+
• idea 2019.1.2
maven设置
<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>
3.2 编写 HelloWorld
需求:浏览发送/hello请求,响应 Hello,Spring Boot 2
参照官方文档:https://docs.spring.io/spring-boot/docs/2.3.11.BUILD-SNAPSHOT/reference/html/getting-started.html#getting-started-first-application
3.2.1 导入依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
3.2.2 编主程序类
/**
* 主程序类
* @SpringBootApplication:加在主程序上,这是一个SpringBoot应用
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class,args);
}
}
3.2.3 编写逻辑业务
//@RestController = @ResponseBody + @Controller
//代表这个类的每一个方法都是返回数据给浏览器,而不是返回页面
@RestController
public class hello {
@RequestMapping("/hello")
public String handler01(){
return "Hello SpringBoot2!";
}
}
3.2.4 测试
运行主程序,结果如下:
我们可以看到Tomcat已经在8080端口运行了
在浏览器端访问:http://localhost:8080/hello
3.2.5 简化配置
server.port = 8888
3.2.6 简化部署
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
执行maven的package命令把项目打成jar包,直接在目标服务器执行即可。
cmd执行打包好的程序:
java -jar boot-01-helloworld-1.0-SNAPSHOT.jar
注意点:取消掉cmd的快速编辑模式
自动配置讲解
1 依赖管理
1.1 父项目做依赖管理
依赖管理
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
</parent>
他的父项目
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.5</version>
</parent>
几乎声明了所有开发中常用的依赖的版本号==>自动版本仲裁机制
1.2 开发导入starter场景启动器
- 见到很多 spring-boot-starter-* : * 指的是某种场景。
- 只要引入starter,这个场景的所有常规需要的依赖我们都自动引入。
- SpringBoot所有支持的场景
https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter - 见到的 *-spring-boot-starter: 第三方为我们提供的简化开发的场景启动器。
- 所有场景启动器最底层的依赖。
- 也可以创建自己的 starter。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
1.3 无需关注版本号,自动版本仲裁
- 引入依赖默认都可以不写版本
- 引入
非
版本仲裁的jar,要写版本号。
1.4 可以修改默认版本号
- 查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
- 在当前项目里面重写 key 的配置 -
**就近优先原则**
1、查看spring-boot-dependencies里面规定当前依赖的版本 用的 key。
2、在当前项目里面重写 key 的配置
<properties>
<mysql.version>5.1.43</mysql.version>
</properties>
2 入门小项目自动配置分析
2.1 查看自动配置的组件
在主程序类查看配置了哪些组件:
/**
* 主程序类
* @SpringBootApplication:加在主程序上,这是一个SpringBoot应用
*/
@SpringBootApplication
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);
}
}
}
2.2 入门小项目自动配置分析
-
自动配好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常见功能,如:字符编码问题
• SpringBoot帮我们配置好了所有web开发的常见场景 -
主程序所在包及其下面的所有子包里面的组件都会被默认扫描进来, 无需以前的包扫描配置
-
改变扫描路径:
@SpringBootApplication(scanBasePackages="com.atguigu")
或者@ComponentScan
指定扫描路径@SpringBootApplication 等同于 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan("com.atguigu.boot")
-
各种配置都有默认值
• 默认配置最终都是映射到MulyipartProperties
• 配置文件的值最终会绑定到每个类上,这个类会在容器中创建对象 -
按需加载所有自动配置
• 非常多的starter
• 引入那些场景,这个场景的自动配置才会开启
• SpringBoot所有的自动配置功能都在spring-boot-autoconfigure
包里面
3 容器功能
3.1 组件增加
@Configuration
-
Full(全模式) 和 Lite(轻量级模式)
- 配置类组件之间无依赖关系用Lite模式加速容器启动过程,减少判断
- 配置类组件之间有依赖关系,方法会被调用得到之前单实例组件,用Full模式
/** * @Configuration * 1.告诉SpringBoot这是一个配置类 == 配置文件 * 2.配置类里可以使用 @Bean 给容器注册组件,默认也是单实例的 * 3.配置类本身也是一个组件 * 4.proxyBeanMethods:代理 Bean 的方法,默认为 true * Full(全模式)==》true:外部无论对配置类中的这个组件注册方法调用多少次,获 * 取到的都是之前注册到容器中的单实例 * Lite(轻量级模式)==》false:在容器中不会保持代理对象,每次调用都会产生新对象, * SpringBoot不会检查容器中是否有代理对象,会直接创建 * 运行速度更快 * 应用场景:有组件依赖 ==》 true * 无组件依赖 ==》 false */ @Configuration(proxyBeanMethods=true) public class MyConfig { /** * 给容器中添加组件。 * 以方法名作为组件 id ,返回类型就是组件类型。 * 返回的值就是组件在容器中的实例。 * @return */ @Bean public User user01(){ return new User("zhangsan",18); } @Bean("tom") // 给组件重命名 public Pet cat(){ return new Pet("tomcat"); } }
测试代码:
/** * 主程序类 * @SpringBootApplication:加在主程序上,这是一个SpringBoot应用 */ @SpringBootApplication 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 tom = run.getBean("tom", Pet.class); System.out.println(tom); // 4.配置类本身也是一个组件 // @Configuration(proxyBeanMethods=true) // SpringBoot总会检查这个组件是否在容器中,有就调用,没有再创建 // 保持单实例 MyConfig myConfig = run.getBean(MyConfig.class); System.out.println(myConfig); User user1 = myConfig.user01(); User user2 = myConfig.user01(); System.out.println(user1 == user2); } }
@Bean、@Component、@Controller、@Service、@Repository
@ComponentScan、@Import
* 4、@Import({User.class, DBHelper.class})
* 给容器中自动创建出这两个类型的组件、默认组件的名字就是 《全类名》
*/
@Import({User.class, DBHelper.class})
@Configuration(proxyBeanMethods = false) //告诉SpringBoot这是一个配置类 == 配置文件
public class MyConfig {
@Bean
public User user01(){
return new User("zhangsan",18);
}
}
测试代码:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IoC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
String[] beanNamesForType = run.getBeanNamesForType(User.class);
for (String s:beanNamesForType
) {
System.out.println(s);
}
DBHelper bean = run.getBean(DBHelper.class);
System.out.println(bean);
}
}
结果:
@Import 高级用法
@Import 高级用法:https://www.bilibili.com/video/BV1gW411W7wy?p=8
@Conditional
条件装配:满足Conditional指定的条件,则进行组件注入
代码:当容器中有tom组件才能加载user01组件
@Configuration(proxyBeanMethods=true)
public class MyConfig {
/**
* 给容器中添加组件。
* 以方法名作为组件 id ,返回类型就是组件类型。
* 返回的值就是组件在容器中的实例。
* @return
*/
@ConditionalOnBean(name = "tom")
@Bean
public User user01(){
return new User("zhangsan",18);
}
// @Bean("tom") // 给组件重命名
public Pet cat(){
return new Pet("tomcat");
}
}
测试代码:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
// 返回IoC容器
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//查看容器中是否有tom组件 ==》false
boolean tom = run.containsBean("tom");
System.out.println(tom);
//查看容器是否有user01组件 ==》false
boolean user01 = run.containsBean("user01");
System.out.println(user01);
}
}
3.2 原生配置文件引入 @ImportResource
作用:将用xml方式创建的组件导入容器
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="haha" class="com.njupt.boot.bean.User">
<property name="name" value="zhangsan"></property>
<property name="age" value="18"></property>
</bean>
<bean id="hehe" class="com.njupt.boot.bean.User">
<property name="name" value="tomcat"></property>
</bean>
</beans>
@ImportResource("classpath:beans.xml")
public class MyConfig {}
测试:
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args);
//查看容器是否存在 haha
boolean haha = run.containsBean("haha");
System.out.println(haha);
}
}
3.3 配置绑定
3.3.1 读取properties文件
如何使用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。
}
}
}
@ConfigurationProperties + @Component
- @ConfigurationProperties + @Component:开启配置绑定功能
在 application.properties 中给对象的属性赋值/** * 注意:只有在容器中的组件,才会拥有SpringBoot提供的强大功能 */ @Component @ConfigurationProperties(prefix = "mycar") public class Car { private String brand; private Integer price; public Car(String brand, Integer price) { this.brand = brand; this.price = price; } public Car() { } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Integer getPrice() { return price; } public void setPrice(Integer price) { this.price = price; } @Override public String toString() { return "Car{" + "brand='" + brand + '\'' + ", price=" + price + '}'; } }
mycar.brand=BYD mycar.price=100000
- 测试:
- 测试方法一:
@RestController public class hello { @Autowired Car car; @RequestMapping("/car") public Car car(){ return car; } }
- 测试方法二:
@SpringBootApplication public class MainApplication { public static void main(String[] args) { ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class, args); //查看car Car car = run.getBean(Car.class); System.out.println(car); } }
- 测试方法一:
@EnableConfigurationProperties + @ConfigurationProperties
- @EnableConfigurationProperties加在配置类上
功能:- 1.开启组件的自动绑定功能
- 2.把这个组件自动注入到容器中
- 测试
@ConfigurationProperties(prefix = "mycar") public class Car { private String brand; private Integer price; public Car(String brand, Integer price) { this.brand = brand; this.price = price; } public Car() { } public String getBrand() { return brand; } public void setBrand(String brand) { this.brand = brand; } public Integer getPrice() { return price; } public void setPrice(Integer price) { this.price = price; } @Override public String toString() { return "Car{" + "brand='" + brand + '\'' + ", price=" + price + '}'; } }
测试方法同 3.2@Configuration(proxyBeanMethods=true) @EnableConfigurationProperties({Car.class}) //1.开启组件的自动绑定功能 //2.把这个组件自动注入到容器中 public class MyConfig {}
3.4 自动配置原理入门 - 源码分析13-15集
3.4.1 @SpringBootApplication分析
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
源码分析:
@SpringBootApplication
等同于
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
1 @SpringBootConfiguration
代表当前是一个配置类
2 @ComponentScan
指定扫描哪些,参考Spring注解
3 @EnableAutoConfiguration(重点)
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {}
@EnableAutoConfiguration核心注解一:@AutoConfigurationPackage
作用:制定了默认包规则并自动配置包
@Import(AutoConfigurationPackages.Registrar.class) //给容器中导入 Registrar 组件
public @interface AutoConfigurationPackage {}
=====================================================
=====================================================
Registrar源码:
static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
}
@Override
public Set<Object> determineImports(AnnotationMetadata metadata) {
return Collections.singleton(new PackageImports(metadata));
}
}
传入参数:AnnotationMetadata metadata ==》 注解源信息
==》 @AutoConfigurationPackage的源信息,它标注在哪个类上
断点调试:
new PackageImports(metadata).getPackageNames().toArray(new String[0]))
根据 metadata 获取包名,然后将这个包中的所有组件全部注册进来,包名默认为主程序类所在的包
总结:
1.AutoConfigurationPackage 利用 Register 给容器中导入一系列组件
2.将指定的一个包下的所有组件导入进来:默认MainApplication所在包下
@EnableAutoConfiguration核心注解二:@Import(AutoConfigurationImportSelector.class)
1.getAutoConfigurationEntry(annotationMetadata);给容器中导入一些组件
2.调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
获取到所有需要导入到容器中的配置类,一共127个
3.利用工厂加载Map<String, List<String>> loadSpringFactories(ClassLoader classLoader);得到所有组件
4.从META-INF/spring.factories位置加载一个文件
默认扫描我们当前系统所有META-INF/spring.factories位置的文件
spring-boot-autoconfigure-2.3.4.RELEASE.jar包里面也有META-INF/spring.factories
这个文件写死了springboot一启动就要给容器中加载的所有配置类
3.4.2 按需开启自动配置项
虽然我们127个场景的所有自动配置启动的时候默认全部加载,但是会通过条件装配规则(@Conditional)
按需配置。
3.4.3 修改默认配置
@Bean
@ConditionalOnBean(MultipartResolver.class) //容器中有这个类型组件
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) //容器中没有这个名字 multipartResolver 的组件
public MultipartResolver multipartResolver(MultipartResolver resolver) {
//给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
//SpringMVC multipartResolver。防止有些用户配置的文件上传解析器不符合规范
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
给容器中加入了文件上传解析器;
SpringBoot默认会在底层配好所有的组件。但是如果用户自己配置了以用户的优先。
例如:
//用户自己配置CharacterEncodingFilter
@Bean
@ConditionalOnMissingBean
public CharacterEncodingFilter characterEncodingFilter() {
}
总结:
- SpringBoot先加载所有的自动配置类 xxxxxAutoConfiguration
- 每个自动配置类按照条件进行生效,默认都会绑定配置文件指定的值。xxxxProperties里面拿。xxxProperties和配置文件进行了绑定
例如:@EnableConfigurationProperties(WebMvcProperties.class)
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 定制化配置 ==》只要用户有自己配置的,就以用户的优先。
• 用户直接自己@Bean替换底层的组件
• 用户去看这个组件是获取的配置文件什么值就在 application.properties中 修改。
流程:
xxxxxAutoConfiguration自动导入组件 ===》 组件从xxxxProperties里面拿值 ===》 xxxxProperties从application.properties获取属性值
3.4.4 SpringBoot开发步骤
- 引入场景依赖,两种方法
• 查看底层
• 查看文档:https://docs.spring.io/spring-boot/docs/current/reference/html/using-spring-boot.html#using-boot-starter - 查看自动配置了哪些(
选做
)
• 自己分析,引入场景对应的自动配置一般都生效了
• 配置文件application.properties中debug=true开启自动配置报告,查看配置是否生效。Negative(不生效)\Positive(生效) - 是否需要修改
• 参照文档修改配置项:https://docs.spring.io/spring-boot/docs/current/reference/html/appendix-application-properties.html#common-application-properties
• 自己分析修改配置项。xxxxProperties绑定了配置文件的哪些。
• 自定义加入或者替换组件
• @Bean、@Component。。。
• 自定义器 XXXXXCustomizer;
• …
4 开发小技巧
4.1 lombok - 简化 JavaBean开发&日志开发
4.1.1 环境配置
引入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
安装插件
4.1.2 使用方法
==============简化JavaBean开发=================
//添加无参构造器
@NoArgsConstructor
//添加全参构造器
@AllArgsConstructor
//重写toString方法
@ToString
//添加 Getter 和 Setter 方法
@Data
@EuqalsAndHashCode
public class Car {
private String brand;
private Integer price;
}
================简化日志开发===================
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String handler01(){
log.info("请求进来了。。。");
return "Hello SpringBoot2!";
}
}
输出结果:
4.2 dev-tools - Restart
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
项目或者页面修改以后:Ctrl + F9
Reload ==》付费 JRebel
4.3 Spring Initailizr(项目初始化向导)
选择场景
自动搭建好项目架构
自动依赖引入
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.njupt</groupId>
<artifactId>boot-02-helloworld2</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>boot-02-helloworld2</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
SpringBoot核心功能
1 配置文件
1.1 properties
同以前的properties用法
1.2 yaml
1.2.1 简介
YAML 是 “YAML Ain’t Markup Language”(YAML 不是一种标记语言)的递归缩写。在开发的这种语言时,YAML 的意思其实是:“Yet Another Markup Language”(仍是一种标记语言)。 非常适合用来做以数据为中心的配置文件。
1.2.2 基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可
- ’ # '表示注释
- 字符串无需加引号,如果要加,’ ’ 表示字符串内容会被转义," "表示字符串内容不会转义
1.2.3 数据类型
- 字面量:单个的、不可再分的值。date、boolean、string、number、null
k: v
- 对象:键值对的集合。map、hash、set、object
行内写法: 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
1.2.4 示例
配置类
创建示例类:
@Component
@ConfigurationProperties(prefix = "person")
@ToString
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@ToString
@Data
public class Pet {
private String name;
private Double weight;
}
person:
# 注意:":"后面要有空格
userName: zhangsan
boss: false
birth: 2019/12/12 20:12:33
age: 18
pet:
name: tomcat
weight: 23.4
# interests: [篮球,游泳]
interests:
- 篮球
- 足球
animal:
- jerry
- mario
# Map的两种写法
# score:
# english: 80
# math: 90
score: {english:80,math:90}
salarys: [3999,4999.98,5999.99]
allPets:
sick:
# 写法一
- {name: tom}
- {name: jerry,weight: 47}
# 写法二
- name: dog
weight: 50
health: [{name: mario,weight: 47}]
测试:
@SpringBootApplication
public class Boot02HelloworldApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(Boot02HelloworldApplication.class, args);
Person person = run.getBean(Person.class);
System.out.println(person);
}
}
配置Springboot设置
spring:
# 配置 banner
banner:
image:
height: 4
location:
# 配置缓存
cache:
type: redis
redis:
time-to-live: 1000
1.2.5 优先原则
application.properties ==》application.yaml
1.2.6 配置Annotation Processor 绑定 JavaBean 和配置文件
作用:在配置文件中配置JavaBean会有提示,也就是开启配置提示功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
Annotation Processor只是为了方便我们开发,打包的时候应该排除掉
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<!-- 打包时排除 Annotation Processor -->
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
2 Web开发
2.1 学习概览
内容概览
文档位置
文档位置:Spring Boot Features ==》7 Developing Web Applications
2.2 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 and BeanNameViewResolver beans.
• 内容协商视图解析器和BeanName视图解析器 - Support for serving static resources, including support for WebJars (covered later in this document)).
• 静态资源(包括webjars) - Automatic registration of Converter, GenericConverter, and Formatter beans.
• 自动注册 Converter,GenericConverter,Formatter - Support for HttpMessageConverters (covered later in this document).
• 支持 HttpMessageConverters (后来我们配合内容协商理解原理) - Automatic registration of MessageCodesResolver (covered later in this document).
• 自动注册 MessageCodesResolver (国际化用) - Static index.html support.
• 静态index.html 页支持 - Custom Favicon support (covered later in this document).
• 自定义 Favicon - Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
• 自动使用 ConfigurableWebBindingInitializer ,(DataBinder负责将请求数据绑定到JavaBean上)
自定义配置:
- If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc.
不用@EnableWebMvc注解。使用 @Configuration + WebMvcConfigurer 自定义规则 - If you want to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver, and still keep the Spring Boot MVC customizations, you can declare a bean of type WebMvcRegistrations and use it to provide custom instances of those components.
声明 WebMvcRegistrations 改变默认底层组件 - If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
使用 @EnableWebMvc+@Configuration+DelegatingWebMvcConfiguration 全面接管SpringMVC
2.3 简单功能分析
2.3.1 静态资源访问
文档位置:Spring Boot Features ==》7 Developing Web Applications ==》7.1.5 Static Content
静态资源目录
静态资源放在类路径下:/static (or /public or /resources or /META-INF/resources)
访问 : 当前项目根路径/ + 静态资源名,例如:http://localhost:8080/moon.png
原理: 静态映射是 /** ,请求进来,先去找Controller看能不能处理,不能处理的所有请求又都交给静态资源处理器。静态资源处理器会在几个静态资源目录下寻找静态资源。如果静态资源也找不到则响应404页面。
改变默认的静态资源路径:
spring:
resources:
static-locations: classpath:/abc/
静态资源访问前缀
作用:静态资源访问默认无前缀,这样的话我们访问静态资源的时候,请求会被拦截器拦截,因此我们需要增加一个前缀以区分静态资源。
# 当前项目 + static-path-pattern + 静态资源名 = 静态资源文件夹下找
# http://localhost:8080/res/moon.png
spring:
mvc:
static-path-pattern: /res/**
WebJars
WebJars官网:https://www.webjars.org/
自动映射: /webjars/**
访问地址:http://localhost:8080/webjars/jquery/3.5.1/jquery.js
后面地址要按照依赖里面的包路径
2.3.2 欢迎页支持
文档位置:Spring Boot Features ==》7 Developing Web Applications ==》7.1.6 Welcome Page
- 静态资源路径下创建 index.html
注意:
• 可以配置静态资源路径
• 但是不可以配置静态资源的访问前缀。否则导致 index.html不能被默认访问
- controller能处理
/index
请求
2.3.3 自定义 Favicon
注意:失败可能需要禁用缓存
2.3.4 静态资源配置原理 (**)
静态资源处理规则
- SpringBoot启动自动加载 xxxAutoConfiguration 自动配置类
- SpringMVC功能的自动配置类大都集中在 ==》 WebMvcAutoConfiguration ,生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// 没有 WebMvcConfigurationSupport 类生效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
- WebMvcAutoConfiguration 生效给容器中配置了什么?
// WebMvcAutoConfiguration 内部类
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
@EnableConfigurationProperties({ WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {}
- 配置文件的相关属性和xxx进行了绑定。
WebMvcProperties ==》spring.mvc
ResourceProperties ==》spring.resources
WebProperties ==》spring.web
- WebMvcAutoConfigurationAdapter 只有一个有参构造器
配置类只有一个有参构造器
有参构造器所有参数值都会从容器中确定
//ResourceProperties resourceProperties;获取和spring.web绑定的所有的值的对象
WebProperties webProperties 下的 Resource 类取代 ResourceProperties resourceProperties
WebMvcProperties mvcProperties 获取和spring.mvc绑定的所有的值的对象
ListableBeanFactory beanFactory Spring的beanFactory
// HttpMessageConverters 找到所有的HttpMessageConverters
messageConvertersProvider
ResourceHandlerRegistrationCustomizer 找到 资源处理器的自定义器。=========
DispatcherServletPath
ServletRegistrationBean 给应用注册Servlet、Filter....
public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
- 资源处理的默认规则
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
super.addResourceHandlers(registry);
// addMappings 为 false,禁用静态资源
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
ServletContext servletContext = getServletContext();
// 设置 webjars 中静态资源访问规则
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 设置静态资源的路径 this.resourceProperties.getStaticLocations() 获得
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (servletContext != null) {
registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION));
}
});
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern, String... locations) {
addResourceHandler(registry, pattern, (registration) -> registration.addResourceLocations(locations));
}
private void addResourceHandler(ResourceHandlerRegistry registry, String pattern,
Consumer<ResourceHandlerRegistration> customizer) {
if (registry.hasMappingForPattern(pattern)) {
return;
}
ResourceHandlerRegistration registration = registry.addResourceHandler(pattern);
customizer.accept(registration);
// 设置缓存
registration.setCachePeriod(getSeconds(this.resourceProperties.getCache().getPeriod()));
registration.setCacheControl(this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl());
customizeResourceHandlerRegistration(registration);
}
spring:
resources:
# 禁用所有静态资源
add-mappings: false
# 设置缓存
cache:
period: 11000
# 修改静态资源路径
static-locations: classpath:/stat_resources/
欢迎页处理规则
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(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
// 要使用欢迎页必须是 /**
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 静态资源路径不是默认配置,就调用 Controller 处理 /index 请求
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
favicon
浏览器默认会发请求访问 /favicon.ico
当静态资源请求规则是 /** 时,可以访问。
如果自己指定了静态资源请求路径则不能访问。
2.4 请求参数处理
2.4.1 请求映射
rest使用与原理 - 26集
spring:
mvc:
hiddenmethod:
filter:
enabled: true #开启页面表单的Rest功能
//@RequestMapping(value = "/user",method = RequestMethod.GET)
@GetMapping("/user")
public String getUser(){
return "GET-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.POST)
@PostMapping("/user")
public String saveUser(){
return "POST-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.PUT)
@PUTMapping("/user")
public String putUser(){
return "PUT-张三";
}
//@RequestMapping(value = "/user",method = RequestMethod.DELETE)
@DeleteMapping("/user")
public String deleteUser(){
return "DELETE-张三";
}
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
请求映射原理
DispatcherServlet调用FrameworkServlet中的doGet和doPost
doGet和doPost ==> processRequest(request, response) ==> doService(request, response)
doService是一个抽象方法,并未做具体实现
FrameworkServlet的子类DispatcherServlet实现了doService
DispatcherServlet的doService ==> doDispatch(request, response)
请求映射流程:请求进来 ==> HttpServlet的doGet和doPost
==> FrameworkServlet中的doGet和doPost
==> DispatcherServlet的doService
==> doService中调用doDispatch(request, response)
每个请求进来都要调用doDispatch(request, response),这也是最终要分析的方法
//找到当前请求使用哪个Handler
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
handlerMappings:处理器映射,有五个值
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认 的
RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
• 如果有就找到这个请求对应的handler
• 如果没有就是下一个 HandlerMapping - 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
2.4.2 普通参数与基本注解
1 注解
@PathVariable、@RequestHeader、@ModelAttribute、@RequestParam、@MatrixVariable、@CookieValue、@RequestBody
@PathVariable - 获取路径变量
@GetMapping("/car/{id}/owner/{username}")
//可以逐个接受,也可以用Map<String,String>接收
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String username,
//If the method parameter is {@link java.util.Map Map<String, String>}
//then the map is populated with all path variable names and values.
@PathVariable Map<String,String> pv){
Map<String,Object> map = new HashMap<>();
map.put("id",id);
map.put("username",username);
map.put("pv",pv);
return map;
}
测试:
<a href="car/3/owner/zhangsan">car/{id}/owner/{username}</a>
@RequestHeader - 获取请求头
@GetMapping("/car/{id}/owner/{username}")
//可以逐个接受,也可以用Map<String,String>接收
public Map<String,Object> getCar(//获取指定请求头信息
@RequestHeader("User-Agent") String userAgent,
//获取所有请求头信息
@RequestHeader Map<String,String> header){
Map<String,Object> map = new HashMap<>();
map.put("userAgent",userAgent);
map.put("header",header);
return map;
}
@RequestParam - 获取请求参数
@GetMapping("/car/{id}/owner/{username}")
//可以逐个接受,也可以用Map<String,String>接收
public Map<String,Object> getCar(@PathVariable("id") Integer id,
@PathVariable("username") String username,
//If the method parameter is {@link java.util.Map Map<String, String>}
//then the map is populated with all path variable names and values.
@PathVariable Map<String,String> pv,
//获取请求头信息
@RequestHeader("User-Agent") String UserAgent,
@RequestHeader Map<String,String> header,
//获取单个请求参数
@RequestParam("age") Integer age,
//获取多个同类请求参数
@RequestParam("interests")List<String> interests,
//获取所有请求参数
@RequestParam Map<String,String> params
){
Map<String,Object> map = new HashMap<>();
map.put("id",id);
map.put("username",username);
map.put("pv",pv);
map.put("userAgent",username);
map.put("header",header);
map.put("age",age);
map.put("interests",interests);
map.put("params",params);
return map;
}
<a href="car/3/owner/zhangsan?age=18&interests=ball&interests=game">test</a>
@CookieValue - 获取Cookie
@GetMapping("/car/{id}/owner/{username}")
public Map<String,Object> getCar(@CookieValue("_ga") String _ga,
@CookieValue Cookie cookie
){
Map<String,Object> map = new HashMap<>();
map.put("_ga",_ga);
System.out.println(cookie.getName()+"==>"+cookie.getValue());
return map;
}
@RequestBody - 获取请求体【POST】
@PostMapping("/save")
public Map postMethod(@RequestBody String content){
Map<String,Object> map = new HashMap<>();
map.put("content",content);
return map;
}
<form action="/save" method="post">
测试@RequestBody获取数据 <br/>
用户名:<input name="userName"/> <br>
邮箱:<input name="email"/><br/>
<input type="submit" value="提交"/>
</form>
@RequestAttribute - 获取请求域中的数据
@Controller
public class RequestController {
@GetMapping("/goto")
public String goToPage(HttpServletRequest request){
request.setAttribute("msg","成功了...");
request.setAttribute("code",200);
return "forward:/success"; //转发到 /success 请求
}
@ResponseBody
@GetMapping("/success")
// 取出 /geto 请求域中的数据
public Map success(// 方式一
@RequestAttribute("msg") String msg,
// 方式二
HttpServletRequest request){
Object code = request.getAttribute("code");
Map<String,Object> map = new HashMap<>();
map.put("msg",msg);
map.put("code",code);
return map;
}
}
@MatrixVariable - 矩阵变量
1. 语法: /cars/sell;low=24;brand=byd,audi,yd
2. SpringBoot默认禁用矩阵变量的功能
手动开启: 对于路径的处理,都是使用 UrlPathHelper 进行解析
UrlPathHelper 中的 removeSemicolonContent 是用来支持矩阵变量的
矩阵变量需要在SpringBoot中手动开启
根据RFC3986的规范,矩阵变量应当绑定在路径变量中!
若是有多个矩阵变量,应当使用英文符号;进行分隔。
若是一个矩阵变量有多个值,应当使用英文符号,进行分隔,或之命名多个重复的key即可。
如:/cars/sell;low=34;brand=byd,audi,yd
@ResponseBody
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand")List<String> list,
@PathVariable("path") String path
){
Map<String,Object> map = new HashMap<>();
map.put("low",low);
map.put("list",list);
map.put("path",path);
return map;
}
@ResponseBody
// /boss/1;age=20/2;age=18
@GetMapping("/boss/{bossId}/{empId}")
public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age",pathVar = "empId") Integer empAge
){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("empAge",empAge);
return map;
}
开启矩阵变量 - 方式一
@Configuration(proxyBeanMethods = false)
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
开启矩阵变量 - 方式二
@Configuration(proxyBeanMethods = false)
public class WebConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
};
}
}
结果:
我们发现获取的路径是 sell,矩阵变量不算在路径中
2 Servlet API
WebRequest、ServletRequest、MultipartRequest、 HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
原理同上 ServletRequestMethodArgumentResolver 能解析以上部分参数
3 复杂参数 - 34集
Map、Model(map、model里面的数据会被放在request的请求域 request.setAttribute)
、Errors/BindingResult、RedirectAttributes( 重定向携带数据)
、ServletResponse(response)
、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
@RequestMapping("/params")
public String testParam(Map<String,Object> map,
Model model,
HttpServletRequest request,
HttpServletResponse response){
map.put("map","hellomap");
model.addAttribute("model","helloModel");
request.setAttribute("request","hellorequest");
Cookie cookie = new Cookie("cookie","hellocookie");
response.addCookie(cookie);
return "forward:/success";
}
@ResponseBody
@RequestMapping("/success")
public Map success(HttpServletRequest request){
Object msg = request.getAttribute("request");
Object mapMsg = request.getAttribute("map");
Object model = request.getAttribute("model");
Map<String,Object> map = new HashMap<>();
map.put("msg",msg);
map.put("mapMsg",mapMsg);
map.put("model",model);
return map;
}
结论:
结论:Map<String,Object> map, Model model, HttpServletRequest request 都是可以给request域中放数据
4 自定义对象参数 - 35集
测试
可以自动类型转换与格式化,可以级联封装。
<form action="/saveUser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
宠物姓名:<input name="pet.name" value="阿猫"/><br/>
宠物年龄:<input name="pet.age" value="5"/>
<input type="submit" value="保存"/>
</form>
/**
* 姓名: <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;
}
/**
* 数据绑定:页面提交的数据(GET、POST)都可以和对象进行绑定
*/
@RequestMapping("/saveUser")
public Person saveUser(Person person){
return person;
}
结果:
原理 - WebDataBinder
- ServletModelAttributeMethodProcessor参数处理器解析自定义类型参数
- ModelAttributeMethodProcessor ==>
WebDataBinder
: web数据绑定器,将请求参数的值绑定到指定的JavaBean中 WebDataBinder
利用它里面的Converter(类型转换器)
将HTTP中的文本转换为指定的数据类型,然后封装到JavaBean中
自定义converter - 代码实现
<form action="/saveUser" method="post">
姓名: <input name="userName" value="zhangsan"/> <br/>
年龄: <input name="age" value="18"/> <br/>
生日: <input name="birth" value="2019/12/10"/> <br/>
<!-- 自定义JavaBean封装方式 -->
宠物:<input name="pet" value="阿猫,5" />
<input type="submit" value="保存"/>
</form>
自定义 converter:
@Configuration(proxyBeanMethods = false)
public class WebConfig /* implements WebMvcConfigurer */{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
// 自定义 converter
@Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new Converter<String, Pet>() {
@Override
public Pet convert(String source) {
if (!StringUtils.isEmpty(source)){
Pet pet = new Pet();
String[] spilt = source.split(",");
pet.setName(spilt[0]);
pet.setAge(Integer.parseInt(spilt[1]));
return pet;
}
return null;
}
});
}
};
}
}
/**
* 数据绑定:页面提交的数据(GET、POST)都可以和对象进行绑定
*/
@RequestMapping("/saveUser")
public Person saveUser(Person person){
return person;
}
结果:
自定义converter - 原理
2.4.4 参数处理原理
1 打断点:DispatcherServlet ==> doDispatch()
2 HandlerMapping - 找到能处理请求的Handler(Controller.method())
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
====>
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
RequestMappingHandlerMapping:保存了所有@RequestMapping 和handler的映射规则。
所有的请求映射都在HandlerMapping中。
- SpringBoot自动配置欢迎页的 WelcomePageHandlerMapping 。访问 /能访问到index.html;
- SpringBoot自动配置了默认 的
RequestMappingHandlerMapping
- 请求进来,挨个尝试所有的HandlerMapping看是否有请求信息。
• 如果有就找到这个请求对应的handler
• 如果没有就是下一个 HandlerMapping - 我们需要一些自定义的映射处理,我们也可以自己给容器中放HandlerMapping。自定义 HandlerMapping
3 HandlerAdapter - 为当前Handler 找一个适配器
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
====>
/**
* Return the HandlerAdapter for this handler object.
* @param handler the handler object to find an adapter for
* @throws ServletException if no HandlerAdapter can be found for the handler. This is a fatal error.
*/
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
//判断四种 HandlerAdapter 中的哪一种支持当前 handler
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("No adapter for handler [" + handler +
"]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");
}
- 一共有四种HandlerAdapter
0 - 支持方法上标注@RequestMapping
1 - 支持函数式编程
… …
4 适配器执行目标方法并确定方法参数的每一个值
总体流程
// Actually invoke the handler.
// 利用找到的 HandlerAdapter 真正执行 handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
1. 首先 mappedHandler.getHandler() 得到目标 handler
====> HandlerExecutionChain
/**
* Return the handler object to execute.
*/
public Object getHandler() {
return this.handler;
}
2. 进入handle方法
====> AbstractHandlerMethodAdapter
/**
* This implementation expects the handler to be an {@link HandlerMethod}.
*/
@Override
@Nullable
public final ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return handleInternal(request, response, (HandlerMethod) handler);
}
3. 最终的执行在 RequestMappingHandlerAdapter 中的 handleInternal()方法,执行返回 ModelAndView
====> RequestMappingHandlerAdapter
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {
ModelAndView mav;
checkRequest(request);
// Execute invokeHandlerMethod in synchronized block if required.
if (this.synchronizeOnSession) {
HttpSession session = request.getSession(false);
if (session != null) {
Object mutex = WebUtils.getSessionMutex(session);
synchronized (mutex) {
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No HttpSession available -> no mutex necessary
执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
}
}
else {
// No synchronization on session demanded at all...
mav = invokeHandlerMethod(request, response, handlerMethod);
}
if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
}
else {
prepareResponse(response);
}
}
return mav;
}
4. invokeHandlerMethod 执行目标方法
====>
### argumentResolvers 参数解析器 ###
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
### returnValueHandlers 返回值处理器 ###
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
### 执行目标方法 ###
invocableMethod.invokeAndHandle(webRequest, mavContainer);
====> ServletInvocableHandlerMethod
### 执行目标方法 ###
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
====> InvocableHandlerMethod
// 获取执行方法的参数值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
============================ 获取执行方法参数值 ===============================
============================ 获取执行方法参数值 ===============================
============================ 获取执行方法参数值 ===============================
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++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
### 判断解析器是否支持这种数据类型,通过查看参数标注的注解(反射机制)确定 ###
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) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
return args;
}
参数解析器 - HandlerMethodArgumentResolver(接口)
argumentResolvers ==> 26个参数解析器:SprignMVC能写多少种参数类型取决于参数解析器
- 当前解析器支持解析哪种参数
- 支持就调用 resolveArgument
返回值处理器
返回值处理器:14个
getMethodArgumentValues ==> getMethodParameters() 获取参数详细信息
resolvers.supportsParameter - 判断参数解析器是否支持这种类型
============= HandlerMethodArgumentResolverComposite ===============
@Override
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
挨个便利参数解析器,看那个支持
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
// 遍历 26 个参数解析器,看哪个支持
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
resolvers.resolveArgument
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);
}
5 总结
遍历每个参数,找到参数解析器解析参数
2.5 数据响应与内容协商
2.5.1 响应JSON - 38集
1 jackson.jar+@ResponseBody
<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>
测试代码:
@Controller
public class ResponseTestController {
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(18);
person.setUserName("zhangsan");
person.setBirth(new Date());
return person;
}
}
结果:
2 返回值解析器
@ResponseBody: 利用返回值处理器中的消息转换器进行处理
RequestResponseBodyMethodProcessor
==> MessageConverter
RequestMappingHandlerAdapter
==> handleInternal
==> invokeHandlerMethod
### 设置返回值解析器 ###
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.invokeAndHandle(webRequest, mavContainer);
==>
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
### 得到handeler的参数值,这里是得到person对象 ###
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
try {
=====================================================================
################ 利用返回值处理器处理返回值 ################
=====================================================================
this.returnValueHandlers.handleReturnValue(
// 获取返回值类型
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}
catch (Exception ex) {
if (logger.isTraceEnabled()) {
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
==>
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
=====================================================================
### 寻找哪个返回值处理器能够处理返回值 ###
=====================================================================
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
=====================================================================
### 利用 RequestResponseBodyMethodProcessor的handleReturnValue 方法处理返回值 ###
=====================================================================
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
@Nullable
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
### 遍历所有返回值处理器,看是不是异步返回值 ###
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
### 看是不是异步返回值 ###
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
=====================================================================
### 看返回值处理器是不是支持这种返回值 ###
### 最终这种返回值处理器支持@ResponseBody注解:RequestResponseBodyMethodProcessor ###
=====================================================================
result
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
===> 调用 handleReturnValue 处理返回值
@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);
// Try even with null return value. ResponseBodyAdvice could get involved.
### 使用消息转换器进行消息写出操作 ###
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
===> writeWithMessageConverters: 利用MessageConverter(消息转换器)将数据写为json
=====================================================================
### 内容协商: 浏览器会以请求头的方式告诉服务器它能接收什么类型的内容
利用MessageConverter处理数据
=====================================================================
3 默认的MessageConverter
- 0 - 只支持Byte类型的
- 1 - String
- 2 - String
- 3 - Resource
- 4 - ResourceRegion
- 5 - DOMSource.class \ SAXSource.class) \ StAXSource.class \StreamSource.class \Source.class
- 6 - MultiValueMap
- 7 - 直接返回 true,能将任何类型的数据转为JSON
- 8 - 直接返回 true,能将任何类型的数据转为JSON
- 9 - 支持注解方式xml处理的。
4 内容协商抢原理先看
- 浏览器在发起请求时会以请求头的方式告诉服务器它能接收什么类型的内容
- 服务器最终根据自己自身的能力,决定服务器能生产出什么样内容类型的数据
- SpringMVC会挨个遍历所有容器底层的
HttpMessageConverter
,看谁能处理
HttpMessageConverter
: 看是否支持将此Class类型的数据转为MediaType类型的数据
例子:Person对象转为JSON。或者 JSON转为Person
1)得到MappingJackson2HttpMessageConverter可以将对象写为json
2)利用MappingJackson2HttpMessageConverter将对象转为json再写出去。
2.5.2 内容协商
根据客户端接收能力不同,返回不同媒体类型的数据。
1 引入xml依赖
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
2 浏览器测试返回xml
@Controller
public class ResponseTestController {
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(18);
person.setUserName("zhangsan");
person.setBirth(new Date());
return person;
}
}
结果:
为什么浏览器会直接返回xml格式的数据:
因为xml优先级高
3 postman分别测试返回JSON和XML
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器本客户端可以接收的数据类型。
4 内容协商原理
-
判断当前响应头中是否已经有确定的媒体类型。MediaType
MediaType contentType = outputMessage.getHeaders().getContentType(); if (isContentTypePreset) { if (logger.isDebugEnabled()) { logger.debug("Found 'Content-Type:" + contentType + "' in response"); } // 如果有就用确定的 selectedMediaType = contentType; }
-
获取客户端(PostMan、浏览器)支持接收的内容类型。(获取客户端Accept请求头字段)
acceptableTypes = getAcceptableMediaTypes(request);
-
contentNegotiationManager 内容协商管理器 默认使用基于请求头的策略
-
HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型
-
遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person),找到支持操作Person的converter,把converter支持的媒体类型统计出来,通过结果可以看到服务端有10种MessageConverter能处理数据。messageConverters在系统加载是会在WebMvcAutoConfiguration中自动配置底层的MessageConverter
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType); ====> protected List<MediaType> getProducibleMediaTypes( HttpServletRequest request, Class<?> valueClass, @Nullable Type targetType) { Set<MediaType> mediaTypes = (Set<MediaType>) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); if (!CollectionUtils.isEmpty(mediaTypes)) { return new ArrayList<>(mediaTypes); } List<MediaType> result = new ArrayList<>(); for (HttpMessageConverter<?> converter : this.messageConverters) { if (converter instanceof GenericHttpMessageConverter && targetType != null) { if (((GenericHttpMessageConverter<?>) converter).canWrite(targetType, valueClass, null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } } else if (converter.canWrite(valueClass, null)) { result.addAll(converter.getSupportedMediaTypes(valueClass)); } } return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); }
-
进行内容协商的最佳匹配媒体类型,看服务器产出的哪种数据类型能匹配浏览器接收的类型.
List<MediaType> mediaTypesToUse = new ArrayList<>(); for (MediaType requestedType : acceptableTypes) { for (MediaType producibleType : producibleTypes) { if (requestedType.isCompatibleWith(producibleType)) { mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); } } }
-
对匹配完的结果进行排序,并选中第一个媒体类型
MediaType.sortBySpecificityAndQuality(mediaTypesToUse); for (MediaType mediaType : mediaTypesToUse) { if (mediaType.isConcrete()) { selectedMediaType = mediaType; break; } else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; break; } }
-
判断哪个converter能支持将对象转为最佳匹配媒体类型(xml)的converter。调用write()方法进行转化 。
if (selectedMediaType != null) { selectedMediaType = selectedMediaType.removeQualityValue(); 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); 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; } } }
最终找到的MessageConverter是:
MappingJackson2XmlHttpMessageConverter
注意:- MessageConverter一共使用了两次
1.第一次用于判断服务器有哪些MessageConverter能处理目标数据,用于最佳匹配
2.第二次用于寻找能将指定类型转为最佳匹配类型的MessageConverter
- MessageConverter一共使用了两次
5 总结
流程回顾
参数处理流程回顾:
- @ResponseBody 响应数据出去 ==》调用 RequestResponseBodyMethodProcessor 处理
- Processor 处理参数返回值 ==》通过 MessageConverter 处理
- 所有 MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的 messageConverter;
优化建议
- 没有必要每次接受请求都遍历循环所有当前系统的 MessageConverter,看谁支持操作这个对象(Person),可以提前将服务器处理数据的能力提前缓存起来
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
6 开启浏览器基于请求参数方式内容协商功能
测试
浏览器请求头无法修改,所以为了方便内容协商,开启基于请求参数的内容协商功能。
spring:
mvc:
contentnegotiation:
# Whether a request parameter ("format" by default) should be used to determine
# the requested media type.
favor-parameter: true
浏览器想返回xml数据:http://localhost:8080/test/person?format=xml
浏览器想返回json数据:http://localhost:8080/test/person?format=json
原理
-
我们可以看到内容协商管理器策略在原来只有
HeadContentNegotiationStrategy
的基础上多了一种ParameterContentNegotiationStrategy
的策略,它支持两种mediaTypes。private List<MediaType> getAcceptableMediaTypes(HttpServletRequest request) throws HttpMediaTypeNotAcceptableException { return this.contentNegotiationManager.resolveMediaTypes(new ServletWebRequest(request)); }
-
ParameterContentNegotiationStrategy
策略优先,获取请求参数中的format的值,确定要返回什么数据 -
最终进行内容协商返回给客户端json即可
7 自定义MessageConverter - 41-42集
实现多协议数据兼容。json、xml、x-test(自定义)
SpringMVC自定义功能只需要给容器中添加一个WebMvcConfiguration
内容协商 - 基于请求头 Accept
@Controller
public class ResponseTestController {
/**
* 1.浏览器发请求返回xml
* 2.如果是ajax请求,返回json
* 3.如果是 njuptApp 发请求,返回自定义协议数据 x-test
*
* 实现步骤:
* 1.添加自定义的MessageConverter进系统底层
* 2.系统底层会统计出所有MessageConverter能操作哪些类型
* 3.客户端进行内容协商
* @return
*/
@ResponseBody
@GetMapping("/test/person")
public Person getPerson(){
Person person = new Person();
person.setAge(18);
person.setUserName("zhangsan");
person.setBirth(new Date());
return person;
}
}
自定义converter
/**
* 自定义的 Converter
*/
public class TestMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {
return false;
}
/**
* 只要是 Person 类型就能写
* @param clazz
* @param mediaType
* @return
*/
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return clazz.isAssignableFrom(Person.class);
}
/**
* 用于服务器统计所有 MessageConverter 能写出哪些内容类型
* @return
*/
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-test");
}
@Override
public Person read(Class<? extends Person> clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
return null;
}
/**
* 自定义协议数据的写出
*/
@Override
public void write(Person person, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
String data = person.getUserName()+";"+person.getAge()+";"+person.getBirth();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
}
}
将自定义的converter添加到WebConfig
@Configuration(proxyBeanMethods = false)
public class WebConfig /* implements WebMvcConfigurer */{
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new TestMessageConverter());
}
};
}
}
利用postman发送请求,结果如下:
内容协商 - 基于请求参数 format
- 基于请求参数的内容协商默认只支持两种类型 - xml、json
- 代码实现:
@Configuration(proxyBeanMethods = false) public class WebConfig /* implements WebMvcConfigurer */{ @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 传入参数: Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("x-test",MediaType.parseMediaType("application/x-test")); // 指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mediaTypes); configurer.strategies(Arrays.asList(paramStrategy)); } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new TestMessageConverter()); } }; } }
问题
- 当我们自定义了基于请求参数的内容写上策略后,就无法解析基于请求头的内容协商了,如果这时发起基于请求头的内容协商,不管请求头要求返回什么类型的数据,都会默认返回JSON
- 解决方法:自定义基于请求头的策略。
@Configuration(proxyBeanMethods = false) public class WebConfig /* implements WebMvcConfigurer */{ @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { // 传入参数: Map<String, MediaType> mediaTypes Map<String, MediaType> mediaTypes = new HashMap<>(); mediaTypes.put("json",MediaType.APPLICATION_JSON); mediaTypes.put("xml",MediaType.APPLICATION_XML); mediaTypes.put("x-test",MediaType.parseMediaType("application/x-test")); // 指定支持解析哪些参数对应的哪些媒体类型 ParameterContentNegotiationStrategy paramStrategy = new ParameterContentNegotiationStrategy(mediaTypes); // 自己设置参数请求名 // paramStrategy.setParameterName("param"); // 定义基于请求头的策略 HeaderContentNegotiationStrategy headerStrategy = new HeaderContentNegotiationStrategy(); configurer.strategies(Arrays.asList(paramStrategy,headerStrategy)); } @Override public void extendMessageConverters(List<HttpMessageConverter<?>> converters) { converters.add(new TestMessageConverter()); } }; } }
- 有可能我们添加的自定义功能会覆盖默认的很多功能,导致一些默认的功能失效
2.6 视图解析与模板引擎
视图解析:SpringBoot默认不支持 JSP,需要引入第三方模板引擎技术实现页面渲染。
2.6.1 模板引擎 - thymeleaf
1 thymeleaf使用
引入starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
自动配置好了thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
配置策略:
1、所有thymeleaf的配置值都在 ThymeleafProperties
2、配置好了 SpringTemplateEngine
3、配好了 ThymeleafViewResolver
4、我们只需要直接开发页面
//视图解析器
//前缀
public static final String DEFAULT_PREFIX = "classpath:/templates/";
//后缀
public static final String DEFAULT_SUFFIX = ".html"; //xxx.html
开发示例
@Controller
public class ViewTestController {
@GetMapping("/njupt")
public String njupt(Model model){
model.addAttribute("msg","hello thymeleaf");
model.addAttribute("link","http://www.baidu.com");
return "success";
}
}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}">哈哈</h1>
<h2>
<a href="www.baidu.com" th:href="${link}">去百度</a> <br/>
<a href="www.baidu.com" th:href="@{/link}">去百度2</a> <br/>
</h2>
</body>
</html>
2 基本语法
表达式
3 构建后台管理系统
页面跳转 - 登录页面跳转主页面
@Controller
public class LoginController {
/**
* 来登录页
* @return
*/
@GetMapping(value = {"/","/login"})
public String loginPage(){
return "login";
}
@PostMapping("/main")
public String main(User user, HttpSession session, Model model){ //RedirectAttributes
if(StringUtils.hasLength(user.getUserName()) && "111".equals(user.getPassword())){
//把登陆成功的用户保存起来
session.setAttribute("loginUser",user);
//登录成功重定向到main.html; 重定向防止表单重复提交
return "redirect:/main.html";
}else {
model.addAttribute("msg","账号密码错误!");
//回到登录页面
return "login";
}
}
@GetMapping("/main.html")
public String mainPage(HttpSession session,Model model){
// 判断是否登录。最好使用拦截器和过滤器
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null){
return "main";
} else {
model.addAttribute("msg","当前未登录,请重新登录!");
return "login";
}
}
}
数据渲染
- 主页面顶部显示管理员信息
<li>
<a href="#" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
<img src="images/photos/user-avatar.png" alt="" />
<!-- thymeleaf - 行内写法 -->
[[${session.loginUser.userName}]]
<span class="caret"></span>
</a>
<ul class="dropdown-menu dropdown-menu-usermenu pull-right">
<li><a href="#"><i class="fa fa-user"></i> Profile</a></li>
<li><a href="#"><i class="fa fa-cog"></i> Settings</a></li>
<li><a href="#"><i class="fa fa-sign-out"></i> Log Out</a></li>
</ul>
</li>
抽取页面公共部分
遍历取出数据
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>Id</th>
<th>UserName</th>
<th>Password</th>
</tr>
</thead>
<tbody>
<!-- 遍历取出数据 -->
<tr class="gradeX" th:each="user,stats:${users}">
<td th:text="${stats.count}">Id</td>
<td th:text="${user.userName}">Internet</td>
<!-- 也可以使用 thymeleaf 行内写法 -->
<td>[[${user.password}]]</td>
</tr>
</tbody>
</table>
- 目标方法处理的过程中,所有数据都会被放在 ModelAndViewContainer 里面。包括数据和视图地址
- 方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
- 任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)。
- processDispatchResult 处理派发结果(页面改如何响应)
2.6.1 视图解析
1 视图解析原理流程 - 47集
2.7 拦截器
2.7.1 实现步骤
1 编写HandlerInterceptor接口
/**
* 登录检查
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前 ==> 登陆检查
* 1.配置拦截器拦截那些请求
* 2.把这些配置放在容器中
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 打印被拦截的路径
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
log.info("放行...");
return true;
}
//拦截住 ==> 未登录,跳转到登录页
request.setAttribute("msg","当前未登录,请重新登录!");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行");
}
/**
* 页面渲染以后
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行");
}
}
2 配置拦截器
/**
* 1、编写一个拦截器实现HandlerInterceptor接口
* 2、拦截器注册到容器中(实现WebMvcConfigurer的addInterceptors)
* 3、指定拦截规则【如果是拦截所有,静态资源也会被拦截】
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 所有请求都被拦截包括静态资源
.excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**",
"/main", // 有问题
"/js/**","/aa/**"); //放行的请求
}
}
3 拦截器执行顺序
2.7.2 拦截器原理
执行流程图解
永远会触发已执行的 Interceptor 的 afterCompletion
代码分析
-
根据当前请求,找到HandlerExecutionChain【可以处理当前请求的handler以及handler的所有 拦截器】
// Determine handler for the current request. mappedHandler = getHandler(processedRequest);
-
HandlerAdapter - 为当前Handler 找一个适配器
// Determine handler adapter for the current request. HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
-
先来顺序执行所有拦截器的 preHandle方法
- 如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 如果当前拦截器返回为false。直接倒序执行所有已经执行了的拦截器的 afterCompletion;
- 如果任何一个拦截器返回false。直接跳出不执行目标方法
//如果任何一个拦截器返回false。直接跳出不执行目标方法 if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } ====> boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { for (int i = 0; i < this.interceptorList.size(); i++) { HandlerInterceptor interceptor = this.interceptorList.get(i); if (!interceptor.preHandle(request, response, this.handler)) { triggerAfterCompletion(request, response, null); return false; } this.interceptorIndex = i; } return true; }
-
所有拦截器都返回True。执行目标方法
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
-
倒序执行所有拦截器的postHandle方法。
mappedHandler.applyPostHandle(processedRequest, response, mv);
-
渲染页面,并且页面渲染完成后也会触发所有拦截器的 afterCompletion
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); ====> if (mappedHandler != null) { // Exception (if any) is already handled.. mappedHandler.triggerAfterCompletion(request, response, null); }
-
前面的步骤有任何异常都会直接倒序触发所有拦截器的 afterCompletion
catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); }
2.8 文件上传
2.8.1 文件上传实现
<!-- 固定写法 -->
<form role="form" method="post" th:action="@{/upload}" enctype="multipart/form-data">
<div class="form-group">
<label for="exampleInputEmail1">邮箱</label>
<input type="email" name="email" class="form-control" id="exampleInputEmail1" placeholder="Enter email">
</div>
<div class="form-group">
<label for="exampleInputPassword1">姓名</label>
<input type="username" name="username" class="form-control" id="exampleInputPassword1" placeholder="Password">
</div>
<!-- 单文件上传 -->
<div class="form-group">
<label for="exampleInputFile">头像</label>
<input type="file" name="hearerImg" id="exampleInputFile">
</div>
<!-- 多文件上传 -->
<div class="form-group">
<label for="exampleInputFile">生活照</label>
<input type="file" name="photos" multiple>
</div>
<button type="submit" class="btn btn-primary">提交</button>
</form>
2.8.2 文件上传原理 &设置文件上传大小
===================================================================================
文件上传自动配置类 ===> MultipartAutoConfiguration ===> MultipartProperties封装所有属性
===================================================================================
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ Servlet.class, StandardServletMultipartResolver.class, MultipartConfigElement.class })
// 所有属性在SpringBoot配置文件中的前缀
@ConditionalOnProperty(prefix = "spring.servlet.multipart", name = "enabled", matchIfMissing = true)
@ConditionalOnWebApplication(type = Type.SERVLET)
// 引入 MultipartProperties 组件
@EnableConfigurationProperties(MultipartProperties.class)
public class MultipartAutoConfiguration {}
====》所有偶尔ui之
@ConfigurationProperties(prefix = "spring.servlet.multipart", ignoreUnknownFields = false)
public class MultipartProperties {
/**
* Max file size.
*/
private DataSize maxFileSize = DataSize.ofMegabytes(1);
/**
* Max request size.
*/
private DataSize maxRequestSize = DataSize.ofMegabytes(10);
}
使用配置文件设置文件上传大小
spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 100MB
2.8.2 文件上传原理深入分析
=========================================================================
文件上传自动配置类 ===> MultipartAutoConfiguration ===> MultipartProperties
=========================================================================
=========================================================================
自动配置好了 StandardServletMultipartResolver 【文件上传解析器】
====>
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)
//容器中没有 MultipartResolver 就创建一个
@ConditionalOnMissingBean(MultipartResolver.class)
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
=========================================================================
=========================================================================
原理分析
=========================================================================
doDispatch()
// 1.判断文件上传请求是否被解析了
boolean multipartRequestParsed = false;
// 2.判断当前请求是不是文件上传请求
processedRequest = checkMultipart(request);
====> (1)利用 multipartResolver.isMultipart(request) 判断是否为文件上传请求
============================== 判断过程 ===================================
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {}
====> 判断请求类型
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/");
}
=========================================================================
====> (2)判断完成后对请求进行封装
return this.multipartResolver.resolveMultipart(request);
====> 将请求封装为 MultipartHttpServletRequest
public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException {
return new StandardMultipartHttpServletRequest(request, this.resolveLazily);
}
// 3.如果是文件上传请求 multipartRequestParsed 为 true,否则为 false,此时文件解析完成
multipartRequestParsed = (processedRequest != request);
// 4.找到能处理该请求的 controller
mappedHandler = getHandler(processedRequest);
// 5.找到处理器是配置器 ==> RequestMappingHandlerAdapter
// Determine handler adapter for the current request.
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// 6.处理请求
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
===> (1)执行请求
mav = invokeHandlerMethod(request, response, handlerMethod);
===> 参数解析器来解析请求中的文件内容封装成MultipartFile ==》RequestPartMethodArgumentResolver
===> 执行方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
===> 准备执行方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
===> 获取所有参数的值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
===> (1)获取所有参数
MethodParameter[] parameters = getMethodParameters();
===> (2)遍历解析获取到的参数
===> 判断是否有解析器支持该参数,如果没有就抛异常
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
===> 利用 getArgumentResolver() 方法判断
public boolean supportsParameter(MethodParameter parameter) {
return getArgumentResolver(parameter) != null;
}
===> getArgumentResolver()方法遍历循环所有参数解析器看是否支持处理参数,如果有就放到缓存中
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;
}
===> 返回结果:RequestPartMethodArgumentResolver
===> (3) 执行参数解析
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
===> 获取参数解析器
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
===> getArgumentResolver()中从缓存中去出参数解析器 RequestPartMethodArgumentResolver
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
===> 利用获取的参数解析器解析参数
resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
===> 利用参数解析代理来解析参数
Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest);
===> resolveMultipartArgument()判断文件是哪种类型,下面以数组为例
else if (isMultipartFileArray(parameter)) {
if (!isMultipart) {
return null;
}
if (multipartRequest == null) {
multipartRequest = new StandardMultipartHttpServletRequest(request);
}
List<MultipartFile> files = multipartRequest.getFiles(name);
return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null);
}
===>
public List<MultipartFile> getFiles(String name) {
List<MultipartFile> multipartFiles = getMultipartFiles().get(name);
if (multipartFiles != null) {
return multipartFiles;
}
else {
return Collections.emptyList();
}
}
===> getMultipartFiles() 返回一个 Map,通过传入的name获取指定值
将request中文件信息封装为一个Map,这里是将headimg和photos封装为map
===> (4) 返回解析好的参数
return args;
FileCopyUtils:实现文件流的拷贝
2.9 异常处理
Spring Boot Features ===> 7. Developing Web Applications ===> 7.1.10. Error Handling
2.9.1 默认错误处理机制
-
By default, Spring Boot provides an /error mapping that handles all errors in a sensible way, and it is registered as a “global” error page in the servlet container.
-
For machine clients(such as Postman), it produces a JSON response with details of the error, the HTTP status, and the exception message.
-
For browser clients, there is a “whitelabel” error view that renders the same data in HTML format (to customize it, add a View that resolves to error).
2.9.2 自定义默认错误模板页面
- If you want to display a custom HTML error page for a given status code, you can add a file to an /error directory.
- error/下的4xx,5xx页面会被自动解析
5xx.html打印错误信息:<h3 th:text="${message}">Something went wrong.</h3> <p class="nrml-txt" th:text="${trace}">Why not try refreshing you page? Or you can <a href="#">contact our support</a> if the problem persists.</p>
2.9.3 定制错误处理逻辑
1 自定义错误页 - 同2.9.2
- error/404.html error/5xx.html;有精确的错误状态码页面就匹配精确,没有就找 4xx.html;如果都没有就触发白页
页面命名为 4xx.html/** * 不带请求参数或者参数类型不对 400:Bad Request 一般都是浏览器的参数没有传递正确 * @param a * @return */ @GetMapping("/test") public String test(@RequestParam("a") int a){ int i = 10/0; return "main"; }
<div class="container "> <section class="error-wrapper text-center"> <h1><img alt="" src="images/404-error.png"></h1> <!-- 打印错误状态 --> <h2 th:text="${status}">page not found</h2> <!-- 打印错误消息 --> <h3 th:text="${message}">We Couldn’t Find This Page</h3> <a class="back-btn" th:href="@{/main.html}"> Back To Home</a> </section> </div>
2 @ControllerAdvice+@ExceptionHandler处理全局异常
-
它的底层是 ExceptionHandlerExceptionResolver 支持的
-
代码示例:
/** * 不带请求参数或者参数类型不对 400:Bad Request 一般都是浏览器的参数没有传递正确 * @param a * @return */ @GetMapping("/test") public String test(@RequestParam("a") int a){ int i = 10/0; return "main"; } ====================================================== @Slf4j @ControllerAdvice public class GlobalExceptionHandler { /** * 处理 MissingServletRequestParameterException 异常 * @param e * @return */ @ExceptionHandler({MissingServletRequestParameterException.class}) public String handleMissingServletRequestParameterException(Exception e){ // 异常会被自动封装 log.error("异常是:"+e); return "login"; } }
3 @ResponseStatus+自定义异常
-
代码实现:
@GetMapping("/test/customizeEx") public void test(){ throw new CustomizeException(); } ====================================== @ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "这是一个自定义异常") public class CustomizeException extends RuntimeException { public CustomizeException() { super(); } public CustomizeException(String message) { super(message); } }
结果:
-
原理分析:底层是**
ResponseStatusExceptionResolver
**- 1.把ResponseStatus注解的信息封装成 ModelAndView
- 2.调用 response.sendError(statusCode, resolvedReason)发送错误请求403 ==》它代表当前请求结束,并由tomcat发送的/error请求
- 3.最终适配到 4xx.html
-
Spring底层的异常,如 参数类型转换异常 ==》
DefaultHandlerExceptionResolver
处理- 1.response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
- 1.response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
4 自定义异常解析器
-
可以作为默认的全局异常处理规则,代码实现
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 优先级,数字越小,优先级越高 @Component public class CustomizeHandlerExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { try { response.sendError(511,"自定义异常解析器"); } catch (IOException e) { e.printStackTrace(); } return new ModelAndView(); } }
5 ErrorViewResolver 实现自定义处理异常
- response.sendError 。error请求就会转给controller
- 你的异常没有任何人能处理。tomcat底层 response.sendError。error请求就会转给controller
- basicErrorController 要去的页面地址是 ErrorViewResolver ;
2.9.4 异常处理自动配置原理
ErrorMvcAutoConfiguration
自动配置异常处理规则- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
- public class
DefaultErrorAttributes
implementsErrorAttributes, HandlerExceptionResolver
- DefaultErrorAttributes:定义错误页面中可以包含哪些数据。
- public class
- 容器中的组件:类型:BasicErrorController --> id:basicErrorController(
json+白页 适配响应
)- 处理默认 /error 路径的请求;页面响应 new ModelAndView(“error”, model);
- 容器中有组件 View->id是error;(响应默认错误页)
- 容器中的组件:类型:DefaultErrorViewResolver -> id:conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面 ⇒
error/404、5xx.html
如果想要返回页面;就会找error视图【StaticView】。(默认是一个白页)
- 如果发生错误,会以HTTP的状态码 作为视图页地址(viewName),找到真正的页面 ⇒
- 容器中的组件:类型:DefaultErrorAttributes -> id:errorAttributes
2.9.5 异常处理流程
- 执行目标方法,目标方法运行期间有任何异常都会被catch,而且标志当前请求结束;并使用dispatchException封装
- 进入视图解析流程(页面渲染流程),只有当前方法正确执行了mv才会有值,mv当前的值为null
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
- processHandlerException()处理handler中的异常,处理完成返回 ModelAndView
mv = processHandlerException(request, response, handler, exception);
-
processHandlerException()
中遍历所有的 handlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器
】if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } }
-
系统默认的异常解析器
-
处理流程分析
1)DefaultErrorAttributes先来处理异常。把异常信息保存到request域,并且返回null;===> resolver.resolveException(request, response, handler, ex); public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { this.storeErrorAttributes(request, ex); return null; } ===> storeErrorAttributes(request, ex); private void storeErrorAttributes(HttpServletRequest request, Exception ex) { request.setAttribute(ERROR_ATTRIBUTE, ex); }
异常解析器必须解析出结果遍历才会结束,所以还需要继续遍历
if (exMv != null) { break; }
2)HandlerExceptionResolver也无法处理错误
3)默认没有任何人能处理异常,所以异常会被抛出1.如果没有任何人能处理最终底层就会发送 /error 请求。会被底层的BasicErrorController处理片 2.解析错误视图;遍历所有的 ErrorViewResolver 看谁能解析。 3.默认的 DefaultErrorViewResolver ,作用是把响应状态码作为错误页的地址,error/500.html 4.模板引擎最终响应这个页面 error/500.html
- processHandlerException()处理handler中的异常,处理完成返回 ModelAndView
2.10 Web原生组件注入(Servlet、Filter、Listener)
Spring Boot Features ==> 7.Developing Web Applications ==> 7.4 Embedded Servlet Container Support
2.10.1 使用Servlet API
1 @WebServlet + @ServletComponentScan
@ServletComponentScan(basePackages = “com.njupt”) :指定原生Servlet组件都放在那里
@WebServlet(urlPatterns = “/my”):效果:===>直接响应,没有经过Spring的拦截器
@WebServlet(urlPatterns = "/myservlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("MyServlet");
}
}
==========================================================
// 指定原生Servlet组件存放地点
@ServletComponentScan(basePackages = "com.njupt")
@SpringBootApplication
public class Boot05Web03Application {
public static void main(String[] args) {
SpringApplication.run(Boot05Web03Application.class, args);
}
}
2 @WebFilter + @ServletComponentScan
@Slf4j
// 拦截静态资源,单 * 是Servlet写法,** 是SpringBoot写法
@WebFilter(urlPatterns = {"/css/*","/images/*"})
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成!");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter内部逻辑!");
//放行
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁!");
}
}
3 @WebListener + @ServletComponentScan
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁");
}
}
2.10.2 使用RegistrationBean
**ServletRegistrationBean**
**FilterRegistrationBean**
**ServletListenerRegistrationBean**
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("MyServlet");
}
}
=========================================================
@Slf4j
// 拦截静态资源,单 * 是Servlet写法,** 是SpringBoot写法
public class MyFilter implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成!");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter内部逻辑!");
//放行
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter销毁!");
}
}
=========================================================
@Slf4j
//@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目初始化完成!");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener监听到项目销毁!");
}
}
=========================================================
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
return new ServletRegistrationBean(new MyServlet(),"/myservlet","/myservlet02");
}
@Bean
public FilterRegistrationBean myFilter(){
// 拦截servlet的路径
// return new FilterRegistrationBean(new MyFilter(),myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(new MyFilter());
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
return new ServletListenerRegistrationBean(new MyServletContextListener());
}
}
2.10.3 为什么原生Servlet没有被Spring拦截器拦截?
目前容器中有两个Servlet:
- MyServlet ==》/myservlet
- DispatcherServlet ==》/
1 DispatchServlet 如何注册进来 ==》DispatcherServletAutoConfiguration
- 容器中自动配置了 DispatcherServlet 属性绑定到 WebMvcProperties;对应的配置文件配置项是 spring.mvc。
- 通过 ServletRegistrationBean 把 DispatcherServlet 配置进来。
修改DispatcherServlet的拦截路径,默认是 /:@Bean(name = DEFAULT_DISPATCHER_SERVLET_REGISTRATION_BEAN_NAME) @ConditionalOnBean(value = DispatcherServlet.class, name = DEFAULT_DISPATCHER_SERVLET_BEAN_NAME) // 方法中传入的参数从容器中寻找 public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) { DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, // DispatcherServlet 的拦截路径 webMvcProperties.getServlet().getPath()); registration.setName(DEFAULT_DISPATCHER_SERVLET_BEAN_NAME); registration.setLoadOnStartup(webMvcProperties.getServlet().getLoadOnStartup()); multipartConfig.ifAvailable(registration::setMultipartConfig); return registration; }
spring: mvc: servlet: path: /mvc/
2 原因分析
- 多个Servlet都能处理同一层路径,遵循精确优先原则:
2.11 嵌入式Servlet容器(服务器)
2.11.1 嵌入式Servlet容器原理
文档:Spring Boot Features ==> 7.4.3. The ServletWebServerApplicationContext
- 默认支持的webServer
- 1.Tomcat, Jetty, or Undertow
- 2.ServletWebServerApplicationContext 容器启动寻找ServletWebServerFactory 并引导创建服务器
- 原理
- 1.SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- 1.SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
- 2.web应用会创建一个web版的ioc容器 ServletWebServerApplicationContext
- 3.ServletWebServerApplicationContext 启动的时候寻找 ServletWebServerFactory(Servlet 的web服务器工厂 ==> Servlet 的web服务器)
- 4.SpringBoot底层默认有很多的WebServer工厂,常用的有**
TomcatServletWebServerFactory, JettyServletWebServerFactory, or UndertowServletWebServerFactory
** - 5.底层直接会有一个自动配置类。ServletWebServerFactoryAutoConfiguration
- 6.ServletWebServerFactoryAutoConfiguration导入了ServletWebServerFactoryConfiguration(配置类)
- 7.ServletWebServerFactoryConfiguration 配置类 根据动态判断系统中到底导入了那个Web服务器的包。(默认是web-starter导入tomcat包),容器中就有 TomcatServletWebServerFactory
- 8.TomcatServletWebServerFactory 创建出Tomcat服务器并启动;TomcatWebServer 的构造器拥有初始化方法initialize—this.tomcat.start();
- 9.内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心jar包存在)
2.11.2 切换嵌入式Servlet容器
- 切换服务器
- 以使用undertow为例
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <!-- 排除Tomcat --> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <!-- 引入undertow场景 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
- 以使用undertow为例
2.11.3 定制Servlet容器
文档:Spring Boot Features ==> 7.4.4. Customizing Embedded Servlet Containers
-
方法一:实现
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
- 把配置文件的值和
ServletWebServerFactory
进行绑定@Component public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } }
- 推荐使用配置文件进行绑定
server.servlet.session.timeout=60m
- 把配置文件的值和
-
方法二:直接自定义
ConfigurableServletWebServerFactory
-
方法三:使用
WebServerFactoryCustomizer
-
注意:在SpringBoot中见到
xxxxxCustomizer
:定制化器,它可以改变xxxx的默认规则
2.12 定制化原理 - 总结
2.12.1 定制化的常见方式
- 编写自定义的配置类
xxxConfiguration + @Bean
替换或增加容器中默认组件;视图解析器 - Web应用 - 自定义Web功能 ==> 7.1.1. Spring MVC Auto-configuration
- 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns("/**") // 所有请求都被拦截包括静态资源 .excludePathPatterns("/","/login","/css/**","/fonts/**","/images/**", "/main", // 有问题 "/js/**","/aa/**"); //放行的请求 } }
- 修改
RequestMappingHandlerMapping, RequestMappingHandlerAdapter, or ExceptionHandlerExceptionResolver
,在WebMvcConfigurer 实现类中使用WebMvcRegistrations + @Bean
实现@Bean public WebMvcRegistrations webMvcRegistrations(){ return new WebMvcRegistrations() { // 自定义HandlerMapping @Override public RequestMappingHandlerMapping getRequestMappingHandlerMapping() { return null; } }; }
-
全面接管
Spring MVC, you can add your own@Configuration annotated with @EnableWebMvc
, or alternatively add your own @Configuration-annotated DelegatingWebMvcConfiguration as described in the Javadoc of @EnableWebMvc.
所有规则全部自己重新配置- 原理:
- 1.WebMvcAutoConfiguration 默认的SpringMVC的自动配置功能类。静态资源、欢迎页…
- 2.一旦使用 @EnableWebMvc ,会 @Import(DelegatingWebMvcConfiguration.class)导入这个组件
- (1)把所有系统中的 WebMvcConfigurer 拿过来。所有功能的定制都是这些 WebMvcConfigurer 合起来一起生效
- (2)自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- (3)public class DelegatingWebMvcConfiguration extends
WebMvcConfigurationSupport
- 3.WebMvcAutoConfiguration 里面的配置要能生效必须满足: @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
- 4.@EnableWebMvc 引入DelegatingWebMvcConfiguration就相当于引入了WebMvcConfigurationSupport,从而导致了 WebMvcAutoConfiguration 没有生效。
- 编写一个配置类实现 WebMvcConfigurer 即可定制化web功能;+ @Bean给容器中再扩展一些组件
- xxxxxCustomizer ==> 参考 2.11.3
2.12.2 原理分析
某一场景starter - xxxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties – 绑定配置文件项(我们只需要修改配置文件项)