笔记环境:
- SpringBoot 2.4.2
- maven 3.6.3
- mysql 5.7
- redis windows-3.0.504
文章目录
-
- 第一季:SpringBoot2核心技术
- 1.SpringBoot2基础入门
-
- 2.SpringBoot2核心功能
- 2.1 配置文件
-
- 2.2 web开发 [^WebMvcAutoConfiguration]
-
- 2.3 数据访问
-
- 2.4 单元测试
-
- 2.5 指标监控
-
- 2.6 原理解析
-
文章目录
- 第一季:SpringBoot2核心技术
- 1.SpringBoot2基础入门
- 2.SpringBoot2核心功能
- 2.1 配置文件
- 2.2 web开发 [^WebMvcAutoConfiguration]
- 2.3 数据访问
- 2.4 单元测试
- 2.5 指标监控
- 2.6 原理解析
第一季:SpringBoot2核心技术
1.SpringBoot2基础入门
1.1 什么是SpringBoot?
1.1.1 Spring的生态圈
- Spring能做什么?What Spring Can Do?Spring 基于自己的生态圈能做许多事情
- 微服务 Microservices
- 响应式编程 Reactive
- 微服务平台 Cloud
- Web应用程序 Web apps
- 无服务 serverless
- 事件驱动 Event Driven
- 批处理 Batch
- …
- 覆盖了
- Web开发
- 数据访问
- 安全控制
- 分布式
- 消息服务
- 移动开发
- 批处理
- …
1.1.2 SpringBoot存在的意义
- 可以快速的开发基于SpringBoot的一应用程序。Spring中有许多的技术栈,这些技术栈组合在一起就需要大量的配置和搭建,非常繁琐。SpringBoot诞生了,秉着去繁从简的原则,简化了大量的配置(内部自动配置了默认配置),使我们的开发只需要关注代码业务本身,而不需要过多的关注环境的搭建。
- SpringBoot是整合Spring技术栈的一站式框架
- SpringBoot是简化Spring技术栈的快速开发脚手架
1.1.3 SpringBoot的优点
- 创建独立Spring应用。Create stand-alone Spring applications。
- 内嵌web服务器。Embed Tomcat,Jetty or Undertow directly (no need to deploy WAR files)
- 自动Starter依赖,简化构建配置。Provide Opinionated “starter” dependencies to simplify your build configuration。
- 提供生产级别的监控、健康检查及外部化配置。Provide production-ready features such as metrics, health checks, and extermalized configuration
- 无代码生成、无需编写XML。Absolutely no code generation and no requirement for XML Configuration
1.1.4 SpringBoot的缺点
- 版本帝,版本迭代更新快。需要经常关注版本更新内容。版本迭代更新快,表示社区活跃度高
1.1.5 时代背景
- 微服务
- James Lewis and Martin Fowler(2014) 提出微服务完整概念。
- 微服务是一种架构风格
- 一个应用拆分为一组小型服务
- 服务围绕业务功能拆分
- 可以由全自动部署机制独立部署
- 去中心化,服务自治。服务可以使用不同的语言,不同的存储技术
- 分布式(How Microservices)
- 分布式的困难
- 远程调用,服务发现,负载均衡,服务容错,配置管理,服务监控,链路追踪,日志管理,任务调度
- …
- 分布式的解决
- SpringBoot + SpringCloud
- 分布式的困难
- 云原生 原生应用如何上云。Cloud Native
- 上云的困难
- 服务自愈,弹性伸缩,服务隔离,自动化部署,灰度发布,流量治理
- …
- 上云的解决
- 上云的困难
1.2 SpringBoot2入门
1.2.1 Maven配置
1.2.1.1 Mirror国内镜像仓库
<mirror>
<id>aliyunmaven</id>
<mirrorOf>*</mirrorOf>
<name>阿里云公共仓库</name>
<url>https://maven.aliyun.com/repository/public</url>
</mirror>
1.2.1.2 profile个性化默认配置
<profile>
<id>jdk-15</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>15</jdk>
</activation>
<properties>
<maven.compiler.source>15</maven.compiler.source>
<maven.compiler.target>15</maven.compiler.target> <maven.compiler.compilerVersion>15</maven.compiler.compilerVersion>
</properties>
</profile>
1.2.2 引入依赖
-
引入SpringBoot父工程
-
添加starter起步依赖 org.springframework.boot.spirng-boot-starter-web
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.4.2</version> </dependency>
-
1.3 创建主程序 主启动类
/**
* 主程序类
* @SpringBootApplication:这是一个SpringBoot应用程序
*/
@SpringBootApplication
public class SpringBootApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(SpringBootApplicationStarter.class,args);
}
}
1.4 简化部署(可选)
添加SpringBoot Maven Plugin
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
1.5 修改Server端口(可选)
- application配置文件
- application.properties
- application.yml
- application.yaml
server.port=8888
1.3 SpringBoot的特点
1.3.1 依赖管理
依靠Maven等构建工具的依赖管理
<!--SpringBoot父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.2</version>
</parent>
<!--SpringBoot父工程中的父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.4.2</version>
</parent>
-
开发导入starter场景起步依赖。场景启动器
-
见到很多
spring-boot-starter-*
; *就某种场景 -
只要引入starter,这个场景所有常规需要的依赖我们都会自动导入。
-
SpringBoot所有的Starters。
-
还可以自定义starter。但是官方不推荐自定义的starter使用
spring-boot-starter-*
, 推荐使用*-spring-boot-starter
-
所有的场景启动器最底层的依赖都是
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.4.2</version> </dependency>
-
-
-
无需关注版本号,父工程集成依赖中已经指定了版本号。版本仲裁。
- 引入依赖默认都可以不写版本号,但引入非SpringBoot父工程中的集成的依赖,需要手动添加版本号。
-
可以修改版本号。Maven子工程可以覆盖version或properties中的version属性
1.3.2 自动配置
-
内嵌Tomcat、Jetty等服务器,默认使用Tomcat服务器。
-
引入Tomcat依赖
-
<!--spring-boot-starter-web场景起步依赖中集成了tomcat的起步依赖。--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> <version>2.4.2</version> <scope>compile</scope> </dependency>
-
-
-
自动配置SpringMVC
- 引入SpringMVC全套组件
- 自动配置SpringMVC常用组件(功能)
- InternalResourceViewResolver
- CharacterEncodingFilter
- mvc:default-hanlder
-
自动配置Web常见功能,如:字符编码问题 (SpringMVC中的CharacterEncodingFileter)
- multipartResolver
- 注解驱动、Jackson等
-
默认统一的包结构
-
主程序所在包,及其子包下的组件都会被扫描到。无需包扫描配置
-
默认
componet-scan base-package="主程序所在包"
-
指定组件扫描的包:
-
第一种方式:
@SpringBootApplication(scanBasePackages = "com.yyl.application")
-
//第二种扫描包的方式 @SpringBootApplication() //是一个合成注解,等同于下面的三个注解。 @SpringBootConfiguration @EnableAutoConfiguration @ComponentScan() //指定组件扫描的包
-
-
-
各种配置拥有默认值
- 默认配置最终都是映射到
Multipart|Server|* + Properties
类中 - 配置文件的值最终会绑定到每个类上,这个类在容器中创建对象。
- 默认配置最终都是映射到
-
自动配置项按需加载
- 非常多的Starter,引入哪一个starter,这个starter的默认配置才会开启。
org.springframewor.boot.auutoconfigure.*.*
中都是实现自动配置功能的类。
-
…
1.4 容器功能
1.4.1 组件添加
1. @Configuration
-
基本使用
-
Full模式与Lite模式
-
示例
@Configuration(proxyBeanMethods = true)
@Configuration(proxyBeanMethods = false)
-
最佳实战
-
配置 类组件之间无依赖关系用Lite模式加速容器的启动过程,减少判断
-
配置类组件之间有依赖关系,方法被调用会得到容器中的存在的单实例组件,用Full模式
-
/** * 1、@Configuration注解修饰的类在IOC中也是一个Bean,一个组件* * * 2、配置类里面使用@Bean标注在方法上给容器注册组件,默认也是单实例。 * * 3. Spring5.2 之后新增的ProxyBeanMethod(代理Bean方法)属性, 默认为true * true : 手动调用该类类中的方法返回的对象保持单例singleton * false : 手动调用该类中的方法返回的对象原型模式,prototype、 * 从容器中getBean()获得的对象仍旧是singleton, 保持单例。 * proxyBeanMethods=true,该bean会被代理模式代理 MyConfig$$EnhancerBySpringCGLIB$$4da09b2a@1e1e9ef3 * * Full(proxyBeanMethods = true) 全模式 每次执行方法都会去IOC中这个Bean,使Bean保持单例 * Lite(proxyBeanMethods = false) 轻量模式 每次执行方法直接返回一个新的对象 * * */
-
-
2. @Bean、@Component、@Controller、@Service、@Repository
3. @Import
导入,导入指定类型的组件
@Import({*.class, *.class})
//可以在任意类上使用
//导入, 自动创建出该类型的组件、默认组件的名字就是全类名
4. @Conditional
条件装配:满足Conditional指定的条件,则被标记的组件进行注入。
可以标注在 类 和方法上。
@Conditional注解下有很多子注解,分别表示不同的条件
例如**@ConditionalOnBean(name = "user01")
: 容器中存在某个Bean时,才将被标记的组件初始化进容器中。**
1.4.2 原生配置文件引入
1. @ImportResource
@ImportResource("classpath:beans.xml")
导入资源, 使用Spring原生方式,将XML配置文件进行导入,并解析。
1.4.3 配置绑定 @ConfigurationProperties
-
通过配置绑定,可以将SpringBoot配置文件中自定义的**
<k,v>
**属性注入进类的属性中。 -
只有容器中的Bean,才能拥有SpringBoot各种注解的功能。所以被**
@ConfigurationProperties
**标注的类必须存在容器中。
1. @EnableConfigurationProperties
+ @ConfigurationProperties
-
@EnableConfigurationProperties(Car.class)
用于将Car类注册进容器中;用于开启Car类的配置绑定功能 -
该注解标注的类也必须在容器中。
2. @Component
+ @ConfigurationProperties
@Component
将该类注册进容器中, 容器中的Bean可以使用 @ConfigurationProperties
的功能。(必须开启组件扫描功能。)
3. @ConfigurationPropertiesScan
+ @ConfigurationProperties
@ConfigurationPropertiesScan
内部封装了@EnableConfigurationProperties
。扫描使用了配置绑定的类,并将其注册进容器中,并开启配置绑定。使用@ConfigurationPropertiesScan
的类必须在容器中存在才能使用此功能。
1.5 自动配置原理入门
1.5.1 引导加载自动配置类
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {}
1.5.1.1 @SpringBootConfiguration
@Configuration //当前类是一个配置类。
public @interface SpringBootConfiguration {}
1.5.1.2 @EnableAutoConfiguration
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
-
@AutoConfigurationPackage
自动配置包?指定了默认的包规则。
@Import(AutoConfigurationPackages.Registrar.class)//给容器导入一个组件 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 : 被标注的类(启动类)的注解元信息。其中包含了该类的包名,将报名转换成数组,然后使用register方法批量向容器中注册该包下的组件。
-
@Import(AutoConfigurationImportSelector.class)
@Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata); return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations()); } 1、利用getAutoConfigurationEntry(annotationMetadata); 给容器中批量导入一些组件。 2、List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes); 获取到所有需要导入到容器中的配置类。(下图130个配置类,不是所有的配置类都需要注册进容器中。而是根据实际需要。) 3、利用工厂加载Map<String, List<String>> loadSpringFactories(ClassLoader classLoader);从资源文件中得到所有组件。 4、从"META-INF/spring.factories"位置来加载一个文件,默认扫描当前系统所有该位置的文件。(不同的jar中可有这个文件。) 核心:"spring-boot-autoconfigure-2.4.2.jar/META-INF/spring.factories" ,包含了 Auto Configure
"spring-boot-autoconfigure-2.4.2.jar/META-INF/spring.factories"配置文件中写死了,SpringBoot一启动就要加载的所有配置类(只加载其中所需要的)。
1.5.1.3 @ComponentSCan
组件扫描器。
1.5.2 按需开启自动配置项
虽然130个场景的所有自动配置启动的时候默认全部加载。
按照**@Conditional
**条件装配规则,最终会按需配置。
1.5.3 修改默认配置
如果容器中的指定类型的组件不是某个名字,则将这个组件以新的名字注册一遍。
@Bean
@ConditionalOnBean(MultipartResolver.class)
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)//容器中没有这个名字 multipartResolver
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// 给@Bean标注的方法传入了对象参数,这个参数的值就会从容器中找。
// SpringMVC multipartResolver,防止组件名称不规范的。 multipartResolver必须是这个id,
// 而容器中有着类型的组件,但是名字不是multipartResolver,就会自动从容器中找到这个类传入方法参 数中,然后返回出去,以新的名字命名。
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
给容器中加入了文件上传解析器;
CharacterEncodingFilter字符编码过滤器会自动被配置。
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
// matchIfMissing=true , 如果没有配置,默认为true,即,开启状态。 开启状态就会自动配置下面的Bean。
public class HttpEncodingAutoConfiguration {
@Bean //注册一个Bean
@ConditionalOnMissingBean //当容器中没有CharacterEncodingFilter类时,创建该Bean。
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
return filter;
}
给容器中加入CharacterEncodingFilter字符编码过滤器。
SpringBoot默认会在底层默认配置好所有组件,但是用户自己配置了以用户的优先。
@Bean
public CharacterEncodingFilter characterEncodingFilter() {}
总结:
- SpringBoot先加载所有的自动配置类
- 每个自动配置,配置类按照条件进行生效,默认都会绑定配置文件中设置的值。(都会从xxxxProperties类中拿,xxxxProperties类中的属性从配置文件中读取。)
- 生效的配置类就会给容器中装配很多组件
- 只要容器中有这些组件,相当于这些功能就有了
- 定制化配置
- 用户有自己定义配置的,就以用户的优先。
- 用户自己直接@Bean替换底层的组件
- 用户去看这个组件是获取的配置文件什么值就去修改。
- 用户可以在方法参数列表中填入组件类型,SpringBoot会自动从容器中找到该类型的组件,并传入方法,可以直接修改组件中的属性内容。
1.5.4 最佳实践
- 引入场景依赖
- 查看自动配置了哪些(可选)
- 结合底层配置类,手动分析,引入场景对应的自动配置一般都生效了。(配置类中所需类都导入了即配置类生效了。)
- 配置文件中
debug=true
开启自动配置报告,默认关闭。- Positive matches(生效自动配置类),Negative matches(不生效自动配置类),Unconditional classes(无条件装配的类)。
- 是否需要定制化
- 参照文档修改配置项
- Properties官方文档
- 手动分析。xxxProperties绑定了配置文件的哪些。
- 自定义加入或者替换组件
- @Bean @Component…
- 自定义器 XXXXCustomizer
- …
- 参照文档修改配置项
1.6 开发小技巧
1.6.1 Lombok
简化JavaBean开发
1. 引入依赖
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version><!--SpringBoot已继承--></version>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--Lombok只在编译器使用,运行期就不需要使用了,所以排除掉,不参与编译。-->
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
-
安装Lombok插件
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NmS9t7Ut-1612143666389)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210120142359217.png)]
1.6.2 SpringInitializr (SpringBoot初始化向导)
-
创建好了项目结构
-
POM自动引入了依赖
1.6.3 dev-tools
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
</dependency>
-
导入后,默认启用。(构建项目就可以更新代码效果)Ctrl + F9 。
- 不导入devtools,只构建项目后,应用不会自动重启,只会改变静态资源和方法体内容。
- 导入devtools后,构建项目后会自动重启应用。所以就能更新代码效果。
-
严格意义上说,devtools只是一个热重启,而不是一个热部署。
-
可以使用
JRebel
插件实现热部署功能。- IDEA 安装 JRebel
- 网址:在线GUID地址 如果失效刷新GUID替换就可以!
- 服务器地址:https://jrebel.qekang.com/{GUID}
-
或者 手动Ctrl + F9
2.SpringBoot2核心功能
2.1 配置文件
- 三种配置文件
- application.properties
- application.yml
- application.yaml
- 配置文件优先级
- properties > yml > yaml
2.1.1 properties
同以前的properties语法
2.1.2 yaml
非常适合用来做以数据为中心的配置文件。
##### 2.1.2.1 yaml 基本语法
- key: value;kv之间有空格
- 大小写敏感
- 使用缩进表示层级关系
- 缩进不允许使用tab,只允许空格
- 缩进的空格数不重要,只要相同层级的元素左对齐即可。
- “#” 表示注释
- 字符串无需加引号,‘ ’ 与 ” “ 表示字符串内容 会被 转义/不转义
2.1.2.2 数据类型
-
字面量: 单个的、不可再分的值。date、boolean、string、number、null
k: v
-
对象:键值对的集合。map、hash、set、object
行内写法:k: {k1:v1,k2:v2} # 或 k: k1: v1 k2: v2
-
数组:一组按次序排列的值。array、list、queue
行内写法: k: [v1,v2,v3] # 或者 k: - v1 - v2 - v3
2.1.2.3 示例
@Data
@AllArgsConstructor
@NoArgsConstructor
@ConfigurationProperties("com.yyl.springbootstudy.springinitializr.bean.person")
public class Person {
private String name;
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> salary;
private Map<String, List<Pet>> allPets;
}
com:
yyl:
springbootstudy:
springinitializr:
bean:
person:
name: "\n张三" # 双引号\n会转义成换行符, 单引号会作为字符串。
boss: true
birth: 2001/06/03
age: 19
pet:
name: 二哈
type: 哈士奇
interests: 打代码,听音乐,看电影
animal: 阿毛,阿狗
score: {english: 60分,math: 80分,chinese: 100分}
salary:
- 7000.0
- 10000.0
all-pets:
cat:
- {name: 小白,type: 银渐层}
- name: 小银
type: 土猫
daog:
- {name: 二哈,type: 哈士奇}
- name: 毛毛
type: 金毛
2.1.2.4 自定义类绑定的配置提示
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eBvM88T0-1612143666393)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210120191746442.png)]
-
当使用了配置绑定后
- 上方出现红色警告,没有找到SpringBoot配置绑定注解处理器
- 配置文件中没有 配置绑定的属性输入提示
-
解决方案,加入下面的依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
<scope>provided</scope>
</dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--configuration-processor 只在编译器使用,运行期就不需要使用了,所以排除掉,不参与编译。-->
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
2.2 web开发 1
2.2.1 SpringMVC自动配置预览
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(SpringBoot为SpringMVC提供了自动配置,在大多数应用程序中都能很好地工作。)
The auto-configuration adds the following features on top of Spring’s defaults:(自动配置在Spring的默认配置之上添加了以下特性)
- Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 包括“内容协商视图解析器”和“Bean名称视图解析器”Bean。
- Support for serving static resources, including support for WebJars (covered later in this document)).
- 静态资源(包裹webjar)的支持
- Automatic registration of
Converter
,GenericConverter
, andFormatter
beans.- 自动注册
Converter
转换器,GenericConverter
通用转换器,Formatter
格式器
- 自动注册
- Support for
HttpMessageConverters
(covered later in this document).- 支持
HttpMessageConverters
http消息转换器(配合内容协商理解原理)
- 支持
- Automatic registration of
MessageCodesResolver
(covered later in this document).- 自动注册
MessageCodesResolver
(i18n国际化用)
- 自动注册
- Static
index.html
support.- 静态index.html页支持
- Custom
Favicon
support- 自定义
Favicon
- 自定义
- Automatic use of a
ConfigurableWebBindingInitializer
bean (covered later in this document).- 自动使用
ConfigurableWebBindingIntiializer
,(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 typeWebMvcConfigurer
but without@EnableWebMvc
.使用
@Configuration
+WebMvcConfigurer
自定义(拦截器, 格式化器, 视图控制器, 和其他特性)规则。但不使用@EnableWebMVC
注解
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.提供自定义的
RequestMappingHandlerMapping
,RequestMappingHandlerAdapter
, orExceptionHandlerExceptionResolver
,可以用WebMvcRegistrations
声明自定义这些组件。声明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
2.2.2 简单功能分析
2.2.2.1 静态资源访问
-
静态资源目录
只要静态资源放在类路径下:
/static
or/public
or/resources
or/META-INF/resources
(静态资源目录有不同的优先级)访问:当前项目根路径/ + 静态资源名
原理:静态映射,默认映射的Url:/** 2
请求进来,处理器映射器会先去找处理器(Controller),如果映射器找不到处理器,不能处理的请求会交给静态资源处理器,静态资源也找不到就会404
-
静态资源访问前缀
spring: mvc: static-path-pattern: /res/**
项目名 + static-path-pattern + 静态资源名 = 按照优先级3在不同的静态资源文件夹下寻找静态资源。
-
指定静态资源目录
web: resources: # 替换默认的静态资源目录 static-locations: [classpath:/haha/]
-
WebJars
前端资源的jar包形式
<dependency> <groupId>org.webjars</groupId> <artifactId>jquery</artifactId> <version>3.5.1</version> </dependency>
可以看到jquery静态资源在resources/webjars下存放,resources目录是默认的静态资源的目录之一,所以可以通过url直接进行静态资源访问。URL:主机/static-path-pattern/webjars/jquery/3.5.1/jquery.js
2.2.2.2 欢迎页支持
-
SpringBoot 访问项目根路径,默认访问静态资源下的index.html
- 可以配置指定静态资源路径
- 不可以配置静态资源的访问前缀,否则导致访问静态资源下的index.html不能被放问
-
controller能处理/index
-
并且只支持**.html** 3
-
private Resource getIndexHtml(Resource location) { try { // WebMvcAutoConfiguration 自动配置时,只定义了扫描了index.html文件。 Resource resource = location.createRelative("index.html"); if (resource.exists() && (resource.getURL() != null)) { return resource; } } catch (Exception ex) { } return null; }
-
spring:
# mvc:
# static-path-pattern: /res/** # 会导致默认访问index.html页面失效。
web:
resources:
# 替换默认的静态资源目录
static-locations: [classpath:/haha/]
2.2.2.3 favicon
支持
- favicon 放在静态资源目录下即可。
- 静态资源目录下的 favicon.ioc 文件默认作为网站的小图标。
- /favicon.ico 是浏览器请求的默认规则。会自动从服务器中寻找这个路径下的资源文件。
spring:
mvc:
static-path-pattern: /res/** # 会导致默认访问index.html页面 和 favicon.ico图标失效。
2.2.2.4 静态资源配置原理
-
SpringBoot启动默认加载 xxxAutoConfiguration 类[^xxxAutoConfiguration ]
-
SpringMVC 功能的自动配置类 :
WebMvcAutoConfiguration
1@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 { // ...... }
-
给容器中配了什么?
// 内部类, @Configuration:也是一个配置类。 @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 {..}
- 配置文件的相关属性和xxxProperties4进行了配置绑定。WebMvcProperties、ResourceProperties、WebProperties.class
- WebMvcProperties 和 spring.mvc前缀 进行配置绑定。[^WebMvcProperties ]
- ResourcesProperties 和 spring.resources前缀 进行配置绑定。[^ResourcesProperties ]
- WebProperties 和 spring.web前缀 进行配置绑定。[^WebProperties ]
- 配置文件的相关属性和xxxProperties4进行了配置绑定。WebMvcProperties、ResourceProperties、WebProperties.class
-
拓展知识点:一个配置类中只有一个有参构造器
- 有参构造器所有参数的值都会从容器中确定(容器会自动向方法注入需要的参数)
- 前提是使用@Conditional注解确保容器中有这些对象
//该类只有一个有参构造方法。 // ResourceProperties resourcesProperties 获取和 spring.resources绑定的配置类对象 // WebMvcProperties mvcProperties 获取和 spring.mvc绑定的配置类对象。 // WebProperties WebProperties 获取和 spring.web绑定的配置类对象 // ListableBeanFactory beanFactory : Spring的BeanFactory Bean工厂,IOC容器。 // ObjectProvider<HttpMessageConverters> messageConvertersProvider : 找到所有的HttpMessageConverters // resourceHandlerRegistrationCustomizerProvider : 找到资源处理器的自定义器 // dispatcherServletPath : DispatcherServlet的映射路径 // servletRegistrations : 给应用注册原生Servlet public WebMvcAutoConfigurationAdapter(WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory, //Spring的BeanFactory Bean工厂,IOC容器。 ObjectProvider<HttpMessageConverters> messageConvertersProvider, //找到所有的HttpMessageConverters ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider, //资源处理器自定义器 ObjectProvider<DispatcherServletPath> dispatcherServletPath, //DispatcherServlet映射路径 ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) { //Servlet注册Bean, 可以用来注册原生Servlet 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); if (!this.resourceProperties.isAddMappings()) { logger.debug("Default resource handling disabled"); return; } ServletContext servletContext = getServletContext(); addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/"); // classpath:/META-INF/resources/webjars/下的资源 映射到 /webjars/** 请求规则中 addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> { registration.addResourceLocations(this.resourceProperties.getStaticLocations()); if (servletContext != null) { registration.addResourceLocations(new ServletContextResource(servletContext, SERVLET_LOCATION)); } });
# 从上面的规则来看,可以发现 addMappings 可以控制开关所有静态资源的默认配置规则。 spring: web: resources: add-mappings: true # false 禁用所有静态资源默认规则 cache: period: 1100 # 浏览器中请求缓存的存活时间,默认单位/s
// resourceProperties.getStaticLocations() 返回了静态资源默认的4个位置 public static class Resources { // 静态资源默认的4个位置 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;
-
index欢迎页面的处理规则 5
HandlerMapping6 :处理器映射。保存了每一个Handler能处理哪些请求(HandlerMapping通过url找到相应的处理方法)
@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) {
// 这里写死了,静态资源目录/index.html存在 并且 是默认(/)的静态资源前缀(spring.web.static-path-pattern),才会请求转发到这个index.html中。
if (welcomePage != null && "/".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName(“forward:index.html”);
}
//这里判断了 templates模板目录下是否有index,如果有,跳转到模板引擎中的index,如果有index模板,就转发到模板index中。
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info(“Adding welcome page template: index”);
setRootViewName(“index”);
}
}
// =========================================
private boolean welcomeTemplateExists(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext) {
return templateAvailabilityProviders.getProvider(“index”, applicationContext) != null;
}
[^模板引擎]:SpringBoot支持的所有模板引擎---> 下图所示↓
模板引擎:[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4bbR4YgW-1612143666395)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210121133753729.png)](使用模板引擎的前提是导入了模板引擎的jar包。)
不同的模板引擎的后缀可能不同,所以根据**index**来寻找模板文件。
#### 2.2.3 请求参数处理原理
##### 2.2.3.1 请求映射
###### 2.2.3.1.1RESTFul的使用和原理
- @xxxMapping;
- RESTFul风格支持*(使用HTTP请求方式动词(method=GET\DELETE\PUT\POST)来表示对资源的操作*
- 传统风格:/getUser、/deleteUser、/updateUser、 /saveUser
- ResetFul风格:/user GET-获取、 DELETE-删除、 PUT-修改、 POST-保存
- from表单的method值支持GET和POST,设置成其他的请求方式,将会默认为GET。
- 核心Filter; **`HiddenHttpMethodFilter`** [^HiddenHttpMethodFilter]
- 用法:表单**method=post**,附带参数:**_mehod=put**
- RESTFul风格原理
- 原理概述:
- 表单提交会带上**_mehod=put**
- 请求过来被HiddenHttpMethodFilter拦截
- 请求是否正常,并且是POST
- 获取到**_mehod**参数的值
- 兼容以下请求:PUT。DELETE。PATCH
- 原生`Request(Post)`,包装模式`RequestWrapper`重写了`getMethod`方法和`method`属性,返回的`_method`的值
- 过滤器链放行的时候用`RequestWrapper`放行。以后的方法调用`getMethod`是调用`RequestWrapper`中重写过的方法。
- `spring.mvc.hiddenmethod.filter` 如果没有配置enabled, 默认为关闭。
- ~~~java
@Bean
// 容器中没有这个类,就注册这个类
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
// spring.mvc.hiddenmethod.filter 如果没有配置enabled, 默认为关闭。
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
~~~
- ~~~java
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()
));
/** 默认请求参数名. */
public static final String DEFAULT_METHOD_PARAM = "_method";
private String methodParam = DEFAULT_METHOD_PARAM;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
//原生Request
HttpServletRequest requestToUse = request;
//请求必须是POST方式
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 获取到_mehtod参数值
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
// 转换大写
String method = paramValue.toUpperCase(Locale.ENGLISH);
// ALLOWED_METHODS支持的method中有该请求的method
if (ALLOWED_METHODS.contains(method)) {
// 对原生Request进行包装,重写method属性和getMethod()方法
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
filterChain.doFilter(requestToUse, response);
}
}
~~~
- 开启RESTFul风格HTTP请求
- ~~~properties
# 开启RESTFul风格过滤器 HiddenHTTPMethodFilter
spring.mvc.hiddenmethod.filter.enabled=true
~~~
- ~~~html
<!-- 添加一个请求参数 -->
<input type="hidden" name="_method" value="put">
~~~
- Rest使用客户端工具
- PostMan、PostAPI等
- 直接发送PUT,DELETE,等方式请求,无序Filter。(因为HTML仅支持Post和Get)
- 自定义methodParam
- ~~~java
@Bean
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
~~~
###### 2.2.3.1.2 请求映射原理
<img src="C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210121170657522.png" alt="image-20210121170657522" style="margin-left:0" />
SpringMVC的原理分析都从**org.springframework.web.servlet.DispatcherServlet#doDispatch**开始
~~~~java
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.
// 找到当前请求使用哪个Handler(Controller的方法)处理。
mappedHandler = getHandler(processedRequest);
HandlerMapping:处理器映射。/uri ->> 某个处理程序
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jBQwGTkA-1612143666397)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210121194918339.png)]
所有的请求映射都在HandlerMapping中。
-
SpringBoot自动配置欢迎页的HandlerMapping 。访问“/”就能访问index.html 5
-
SpringBoot自动配置了默认的 7
-
请求进来,遍历所有的HandlerMapping看看是否有请求信息。
- 如果有,取出请求对应的handler处理器
-
需要自定义映射处理,可以在容器中注册一个自己的6,实现HandlerMapping接口即可。
2.2.3.2 普通参数与基本注解
@PathVariable(路径变量)
@RequestHeader(获取请求头)
@RequestParam(获取请求参数)
@CookieValue(获取Cookie值)
@RequestAttribute(获取request域属性)
@RequestBody(获取请求体)
@MatrixVariable(矩阵变量)
@PathVariable
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>
参数,则将使用所有路径变量名称和值来填充映射。
@RequestHeader
If the method parameter is {@link java.util.Map Map<String, String>}, * {@link org.springframework.util.MultiValueMap MultiValueMap<String, String>}, * or {@link org.springframework.http.HttpHeaders HttpHeaders} then the map is * populated with all header names and values.
如果方法有@RequestHeader Map<String,String>
或者 @RequestHeader MultiValueMap<String,String>
或者 @RequestHeader HttpHeaders
参数,则将使用所有请求头名称的值来填充映射。
@RequestParam
If the method parameter is {@link java.util.Map Map<String, String>} or * {@link org.springframework.util.MultiValueMap MultiValueMap<String, String>} * and a parameter name is not specified, then the map parameter is populated * with all request parameter names and values.
如果方法有 @RequestParam Map<String,String> 或 @RequestParam MultiValueMap<String,String> 则使用所有请求参数名称和值填充map参数。
@CookieValue
The method parameter may be declared as type {@link javax.servlet.http.Cookie} * or as cookie value type (String, int, etc.).
方法参数可以声明为类型@CookieValue Cookie cookie 或 cookie值类型(字符串,整数等)。
以上的注解使用:
@GetMapping("/car/{id}/owner/{name}")
public Map<String,Object> getCar(@PathVariable Integer id, @PathVariable("name") String username,
@PathVariable Map<String,String> pathVariableMap,
@RequestHeader("User-Agent") String userAgent,
@RequestHeader String cookie,
@RequestHeader Map<String,String> requestHeaderMap,
@RequestHeader MultiValueMap<String,String> requestHeaderMultiValueMap,
@RequestHeader HttpHeaders httpHeaders,
Integer age,
@RequestParam("inters") List<String> interests,
@RequestParam MultiValueMap<String,String> requestParamMultiValueMap,
@CookieValue("Idea-ae83a0fc") String ideaCookie,
@CookieValue("Webstorm-1a43536c") Cookie webStormCookie){
HashMap<String, Object> map = new HashMap<>();
/*@PathVariable*/
map.put("id",id);
map.put("name",username);
// 如果方法有@PathVariable Map<String,String> 参数,则将使用所有**路径变量名称和值**来填充映射。
map.put("pathVariableMap",pathVariableMap);
/*@RequestHeader*/
map.put("userAgent",userAgent);
map.put("cookie",cookie);
//如果方法有@RequestHeader Map<String,String> 或者 @RequestHeader MultiValueMap<String,String> 或者 @RequestHeader HttpHeaders 参数,则将使用所**有请求头名称的值**来填充映射。
map.put("requestHeaderMap",requestHeaderMap);
map.put("httpHeaders",httpHeaders);
map.put("requestHeaderMultiValueMap",requestHeaderMultiValueMap);
/*@RequestParam*/
map.put("age",age);
map.put("inters", interests);
//如果方法有@RequestParam Map<String,String> 或者 @RequestParam MultiValueMap<String,String>,则将使用所**有请求参数名的值**来填充映射。
map.put("requestParamMultiValueMap",requestParamMultiValueMap);
/*@CookieValue*/
// 方法参数可以声明为类型@CookieValue Cookie cookie 或 cookie值类型(字符串,整数等)
map.put("ideaCookie",ideaCookie);
map.put("webStormCookie",webStormCookie);
return map;
}
@RequestBody
@PostMapping("/save")
public Map<String,Object> postMethod(@RequestBody String postContent){
HashMap<String, Object> map = new HashMap<>();
map.put("postContent",postContent);
return map;
}
{"postContent": "userName=lisi&email=y31367208351111%40gmail.com"}
@RequestAttribute
- @RequestAttribute 与 @RequestParam
- @RequestAttribute 获取请求域中的域属性。
- @RequestParam 获取请求中的参数。
@RequestMapping("/goto")
public String gotoPage(HttpServletRequest request){
request.setAttribute("msg","成功了");
request.setAttribute("code",200);
return "forward:/success";
}
@RequestMapping("/success")
@ResponseBody
public Map<String,Object> success(@RequestAttribute("msg") String message,
@RequestAttribute Integer code,
HttpServletRequest request){
HashMap<String, Object> map = new HashMap<>();
map.put("annotation_msg",message);
map.put("annotation_code",code);
map.put("reqObject_msg",request.getAttribute("msg"));
map.put("reqObject_code",request.getAttribute("code"));
return map;
}
@MatrixVariable
11
- 矩阵变量需要在SpringBoot中手动开启
- 根据RFC3986的规范,矩阵变量应当绑定在路径变量中!UrlPageHelper会对路径变量进行解析。否则404
- 若是有多个矩阵变量,应当使用英文符号“;”进行分割
- 若是一个矩阵变量有多个值,应当使用英文,进行分割,或之命名多个重复的key即可。
/cars/{path}?xxx=xxx&aaa=ccc queryString 查询字符转。@RequestParam;
/cars/path;low=34;brand=byd,audi,yd : 矩阵变量
页面开发,cookie禁用了,session里面的内容怎么使用:
session.set(a,b) ---> JSessionId ---> cookie ----> 每次发请求携带。
url重写: /abc;jesssionid=xxxx 把cookie的值使用矩阵变量的方式进行传递。
/boss/1/2 : 1号boss下面的第2个员工 /boss/1;age=20/2;age=20 : 1号且age=20的boss下面的第2个且age=20的员工
1、语法 cars/sell;low=34;brand=byd,audi,yd
2、直接访问,报错没有找到矩阵变量。原因:SpringBoot默认是禁用了矩阵变量的功能。
手动开启:原理:对于路径的处理,都是由UrlPathHelper进行解析。
-UrlPathHelper.removeSemicolonContent(删除分号内容) 支持矩阵变量,默认为true。
-UrlPathHelper的创建在WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#configurePathMatch中
3、矩阵变量需要放在路径变量中,UrlPageHelper会对路径变量进行解析。否则报404
Controller:
@GetMapping("/cars/{path}")
@ResponseBody
public Map<String,Object> carsSell(@MatrixVariable(value = "low") Integer low,
@MatrixVariable List<String> brand,
//可以不获取这个path
@PathVariable 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}/{employeeId}")
@ResponseBody
public Map<String,Object> boss(@MatrixVariable(value = "age", pathVar = "bossId") Integer bossAge,
@MatrixVariable(value = "age", pathVar = "employeeId") Integer employeeAge,
@PathVariable String bossId,
@PathVariable String employeeId){
Map<String,Object> map = new HashMap<>();
map.put("bossAge",bossAge);
map.put("bossId",bossId);
map.put("employeeId",employeeId);
map.put("employeeAge",employeeAge);
return map;
}
开启矩阵变量功能:
-
方式1:
@Configuration public class OpenMatrixVariableConfig implements WebMvcConfigurer { /** * 开启@MatrixVariable 矩阵变量功能, 须实现WebMvcConfigurer接口 * WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter implements WebMvcConfigurer * */ @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); //默认底层中设置了true。该值默认是false urlPathHelper.setAlwaysUseFullPath(true); configurer.setUrlPathHelper(urlPathHelper); } }
-
方式2:
/** * 开启@MatrixVariable 矩阵变量功能。 * 重新注册了一个WebMvcConfigurer类。 * @return 一个自定义个WebMvcConfigurer(WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter) */ @Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void configurePathMatch(PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper(); urlPathHelper.setRemoveSemicolonContent(false); urlPathHelper.setAlwaysUseFullPath(true); configurer.setUrlPathHelper(urlPathHelper); } }; }
-
方式3(推荐):
/** * 开启@MatrixVariable 矩阵变量功能。 * 从容器中拿到已经存在UrlPathHelper,然后更改removeSemicolonContent属性 * @return 再默认的基础上修改后的UrlPathHelper */ @Bean public UrlPathHelper urlPathHelper(UrlPathHelper urlPathHelper){ urlPathHelper.setRemoveSemicolonContent(false); return urlPathHelper; }
Servlet API
WebRequest、ServletRequest、MultipartRequest、HttpSession、javax.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、ZoneId
ServletRequest方法参数解析器,用来解析目标方法中Servlet原生API,
//支持以下xxx.class所有API
@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);
}
复杂参数:
Map、Model(map、model、里面的数据会被放在request的请求域 request.setAttribute)、Errors/BindingResult、RedirectAttributes(重定向携带数据)、ServletResponse(原生response)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map;
Model model;
HttpServletRequest request;
// 都可以在request域中放数据。 request.getAttribute()
自定义对象参数
可以自动类型转换与格式化,可以级联封装。
/**
* 姓名:<input type="text" name="username">
* 年龄:<input type="text" name="age">
* 生日:<input type="text" name="birth">
* 宠物姓名:<input type="text" name="pet.name">
* 宠物年靓:<input type="text" name="pet.age">
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
class Pet {
private String name;
private String age;
}
WebDataBinder
binder = binderFactory.createBinder(webRequest, attribute, name);
** 14 :web数据绑定器,将请求参数的值绑定到指定的JavaBean里面**
WebDataBinder 利用它里面的 Converters 将请求数据转成指定的数据类型。再次封装到JavaBean中
GenericConversionService:在设置每一个值的时候,找它里面的所有converter那个可以将这个数据类型(request带来参数的字符串)转换到指定的类型(JavaBean – Integer)
可以在WebDataBinder中放自定义的Converter;
@FunctionalInterface
//S : source 原类型
//T : target 目标类型
public interface Converter<S, T>
class StringToNumber implements Converter<String, T>
/**
* Add {@link Converter Converters} and {@link Formatter Formatters} in addition to the ones
* registered by default.
* 除了默认注册的之外,还添加Converter 和 Formatters,供数据绑定器使用。
*/
default void WebMvcConfigurer#addFormatters(FormatterRegistry registry) {
}
自定义Converter
-
Spring中如果有自定义的组件,会使用自定义的组件。
-
/* * 自定义Converter注册方式1: * 在容器中注册一个Converter的实现类,底层初始化时,会自动扫描到所有的Converter,并添加进Formatters。 * */ @Bean public Converter<String,Date> MyStringToDateConverterBean(){ return new Converter<String, Date>() { @SneakyThrows({java.text.ParseException.class}) @Override public Date convert(String source) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd"); return dateFormat.parse(source); } }; } /* * 自定义Converter注册方式2: * 从容器中直接获取FormatterRegistry;通过FormatterRegistry向格式化器中注册自定义Converter * */ //@Bean public FormatterRegistry formatterRegistry(FormatterRegistry formatterRegistry){ formatterRegistry.addConverter(new Converter<String, Date>() { @SneakyThrows({java.text.ParseException.class}) @Override public Date convert(String source) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd"); return dateFormat.parse(source); } }); return formatterRegistry; } /* * 自定义Converter注册方式3: * 间接获取FormatterRegistry;通过实现WebMvcConfigurer接口的方法, * 底层调用该实现类的addFormatters时,通过FormatterRegistry向格式化器中注册自定义Converter * */ //@Bean public WebMvcConfigurer webMvcConfigurer(){ return new WebMvcConfigurer() { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Date>() { @SneakyThrows({java.text.ParseException.class}) @Override public Date convert(String source) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd"); return dateFormat.parse(source); } }); } }; } } /* * 自定义Converter注册方式4: * 间接获取FormatterRegistry;通过实现WebMvcConfigurer接口的方法, * 底层调用该实现类的addFormatters时,通过FormatterRegistry向格式化器中注册自定义Converter * */ //@Component class MyWebMvcConfigurer implements WebMvcConfigurer { @Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new Converter<String, Date>() { @SneakyThrows({java.text.ParseException.class}) @Override public Date convert(String source) { SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy.MM.dd"); return dateFormat.parse(source); } }); } }
四种方式比较:
- 宏观上看,方式2,3,4都是同一种原理,获取15,然后通过格式化注册中心注册Converter。
- 微观上看,3,4是同一种,因为都是通过16接口中的addFormatters()方法,该方法需要15参数,方法被调用时容器会自动传入15,然后通过格式化注册中心注册Converter
- 推荐第一种方式,@Bean的定义简洁明确。
自定义Json数据转换Formatter
-
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss:ssss", timezone = "GMT+8") private Date birth;
2.2.3.3 参数封装原理
1. HandlerAdapter17
[^@RequestMappingHanderAdapter]
[^@HandlerFunctionAdapter]
2. 执行目标方法
// Actually invoke the handler.
//DispatcherServlet#doDispatcher
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
//DispatcherServlet#doDispatcher --> AbstractHandlerMethodAdapter#handle --> RequestMappingHandlerAdapter#handleInternal
// 执行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
//RequestMappingHandlerAdapter#invokeHandlerMethod
invocableMethod.invokeAndHandle(webRequest, mavContainer);
//真正执行目标方法
//ServletInvocableHandlerMethod#invokeAndHandle
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
//获取方法参数的值
//InvocableHandlerMethod#invokeForRequest
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
如何获取目标方法的参数
===============================getMethodArgumentValues=====================================
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;
}
3. 参数解析器18
确定将要执行的目标方法的每一个参数值是什么
SpringMVC目标方法能写多少种参数类型,取决于参数解析器。
参数解析器会将请求中的数据注入到目标方法相应的参数中
解析出目标方法中用到的参数注解和相应的解析器。
处理方法参数解析器18
supportsParameter判断当前解析器是否支持处理当前参数
如果支持,就调用resolveArgument进行解析。
4. 处理方法返回值处理器
5.
6.
7. 处理派发结果
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
--->
render(mv, request, response);
--->
view.render(mv.getModelInternal(), request, response);
--->
renderMergedOutputModel(mergedModel, getRequestToExpose(request), response);
--->
// Expose the model object as request attributes.
// 暴露model 作为 请求属性
exposeModelAsRequestAttributes(model, request);
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);
}
});
}
2.2.4 数据响应与内容协商
graph LR
A(数据响应) ---> B[响应页面]
A --> C[响应数据]
C --> D[JSON]
C --> E[XML]
C --> F[xls]
C --> G[图片音视频]
C --> H[自定义协议数据]
2.2.4.1 响应JSON / XML
web起步依赖中集成了Jackson依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.4.2</version>
<scope>compile</scope>
</dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z7FPtOlE-1612143666399)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123173842535.png)]
jackson 支持xml功能模块的依赖需要手动导入
<!--jackson 支持xml功能模块-->
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
源码分析流程21
protected <T> void AbstractMessageConverterMethodProcessor#writeWithMessageConverters(T, org.springframework.core.MethodParameter, org.springframework.http.server.ServletServerHttpRequest, org.springframework.http.server.ServletServerHttpResponse)(@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());
}
if (isResourceType(value, returnType)) {/*返回值类型是资源类型,进行的一些操作。*/}
/*内容协商部分*/
//媒体类型,内容协商。
MediaType selectedMediaType = null;
//从前面包装res的outputMessage中获取前面已经决定的响应内容类型
MediaType contentType = outputMessage.getHeaders().getContentType();
//判断当前响应头中是否已经有确定可用的媒体类型。MediaType
boolean isContentTypePreset = contentType != null && contentType.isConcrete();
if (isContentTypePreset) {
//内容类型有效就把媒体类型设置成内容类型。
selectedMediaType = contentType;
}else {
//内容类型无效,开始确定相应内容类型。
//从包装req的对象中取出原req
HttpServletRequest request = inputMessage.getServletRequest();
//从req中获取浏览器能接受的数据类型(获取req请求头中Accept字段)
List<MediaType> acceptableTypes = getAcceptableMediaTypes(request);
//根据返回值类型获取服务器能产出的媒体类型。(所有HttpMessageConverter中找到能处理的,然后用这个能处理的converter调用getSupportedMediaTypes(),返回所有自身能处理的mediaTypes)
List<MediaType> producibleTypes = getProducibleMediaTypes(request, valueType, targetType);
//最终决定返回值内容类型的集合
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;
}
}
}
if (selectedMediaType != null) {
//移除选中媒体类型中的 “q”权重标志符值。
selectedMediaType = selectedMediaType.removeQualityValue();
/*MessageConverter 核心部分*/
//遍历容器中所有HttpMessageConverter,看谁支持操作这个返回值(Person,将Person转为selectedMediaType:application/xml类型)
for (HttpMessageConverter<?> converter : this.messageConverters) {
// 判断是否能强转成 通用消息转换器,不能就赋值null
GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ?(GenericHttpMessageConverter<?>) converter : null);
// 当前converter是否能canWrite进行写操作
if (genericConverter != null ? ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : converter.canWrite(valueType, selectedMediaType)) {
//body还是Person对象
body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType,
(Class<? extends HttpMessageConverter<?>>) converter.getClass(),
inputMessage, outputMessage);
if (body != null) {
Object theBody = body;
//在响应头部添加 Content-Disposition(下载文件时需要)。内部自动判断。
addContentDispositionHeader(inputMessage, outputMessage);
//调用HttpMessageConverter接口的write方法,进行写操作
if (genericConverter != null) {
genericConverter.write(body, targetType, selectedMediaType, outputMessage);
}else {
//写操作完毕后,res中出现了Content-Type=application:json
//write方法中最终调用writeInternal(......objectWriter.writeValue(generator, value);调用Jackson将对象写成JSON......),
((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage);
}
}
return;
}
}
}
}
1. 返回值解析器
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-S50c4mAw-1612143666402)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123190527721.png)]
2. SpringMVC支持哪些返回值?
根据上面跟踪 this.returnValueHandlers 可以发现一共有15种返回值处理器。
-
ModelAndView
-
Model
-
View
-
ResposeBody
-
StreamingResponseBody
-
HttpEntity
- 自定义对象
-
HttpHeaders
-
Callable
-
DefreedResult
-
AsyncTask
-
ModelAttribute
- 参数被标注 @ModelAttribute
-
RequestResponseBody
- 返回值被标注了
@ResponseBody
- 使用
RequestResponseBodyMethodProcessor
处理器,进行处理。
- 返回值被标注了
-
ViewName
-
Map
-
ModelAttribute
3. 返回值解析器原理 22
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dUlBePMJ-1612143666403)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123194319585.png)]
-
返回值处理器先判断是否支持当前返回值,支持就返回,不支持就继续用下一个返回值。
-
如果支持,就调用
handleReturnValue
进行处理 -
使用
RequestResponseBodyMethodProcessor
处理器处理标注@ResponseBody的返回值- 利用 23进行处理,将数据写为JSON
例:返回值处理器之一:ModelAndViewMethodReturnValueHandler
的两个实现方法
@Override
public boolean supportsReturnType(MethodParameter returnType) {
//返回目标方法返回值 是否ModelAndView类型
return ModelAndView.class.isAssignableFrom(returnType.getParameterType());
}
@Override
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {
//设置请求已经处理
mavContainer.setRequestHandled(true);
//对req,res进行包装
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);
// Try even with null return value. ResponseBodyAdvice could get involved.
//使用MessageConverter消息转换器进行写出。
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}
处理返回值内部实现原理
@Override
public void HandlerMethodReturnValueHandlerComposite#handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
//选择返回值的处理器
HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
//返回值处理器处理返回值
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}
private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) {
boolean isAsyncValue = isAsyncReturnValue(value, returnType);
for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) {
if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) {
continue;
}
//返回值处理器先判断是否支持当前返回值,支持就返回,不支持就继续用下一个返回值处理器进行判断。
if (handler.supportsReturnType(returnType)) {
return handler;
}
}
return null;
}
4. HttpMessageConverter
运行原理 23
-
从req中获取浏览器能接受的数据类型(获取req请求头中Accept字段);根据返回值类型和能接受的类型,获取能产出的媒体类型。
-
SpringMVC会挨个遍历容器中所有
HttpMessageConverter
,看谁支持操作这个返回值(Person,将Person转为selectedMediaType类型) -
找到[^ MappingJackson2HttpMessageConverter] 可以处理@RepsonseBody返回值,将对象写为JSON
-
最终将Person对象转换为JSON字符串。放入res响应体中,并设置
Content-Type
1. HttpMessageConverter
规范
23 : 看是否支持将此Class类型的对象,转为MediaType类型的数据。
例子:Person转Json。或者 JSON转Person
-
系统中默认的
HttpMessageConverter
小例子:24
-
查看该类的supports方法可以看到支持返回值为Resource接口的类型。
-
@Override protected boolean supports(Class<?> clazz) { return Resource.class.isAssignableFrom(clazz); }
-
-
-
25 返回值类型可以用来做文件下载。
-
-
HttpMessageConverter
支持的媒体类型@Override public List<MediaType> getSupportedMediaTypes() { return Collections.unmodifiableList(this.supportedMediaTypes); }
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aw3P432y-1612143666404)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123211417943.png)]
2.2.4.2 内容协商
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMmVscU1-1612143666405)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210123201727227.png)]
根据客户端接收能力,服务器的生产能力不同,返回不同媒体类型。
1. postman分别测试返回Json和Xml
只需要改变请求头中Accept字段。Http协议中规定的,告诉服务器当前客户端可以接收的数据类型。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3yibH1Wf-1612143666405)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124114151933.png)]
2. 内容协商原理
-
判断当前响应头中是否已经有确定的媒体类型。MediaType
-
获取客户端(PostMan、Browser)支持接收的内容类型。(获取客户端Accept请求字段)【application/xml】【application/json】
- contentNegotiationManager 内容协商管理器,默认使用基于请求头的协商策略。
-
- HeaderContentNegotiationStrategy 确定客户端可以接收的内容类型。
-
-
遍历循环容器中所有的MessageConverter,看谁支持操作这个对象(Person,将Person转为MediaType媒体类型)
-
找到支持操作这个对象的Converter,吧Converter支持的媒体类型统计出来。
-
客户端要【application/xml】。服务端能生产【json、xml】
-
进行内容协商的最佳匹配媒体类型
-
用 支持 将对象转为 最佳匹配媒体类型 的Converter。调用它进行转化。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-svQOqrqS-1612143666406)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124134118524.png)]
导入了jackson处理xml的包,xml的Converter就会自动添加进Converters
WebMvcConfigurationSupport#jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
//如果系统中存在处理xml的包,就会将Converter添加进去。
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
3. 开启浏览器参数方式内容协商功能
为了方便内容协商,开启基于请求参数的内容协商功能。
# 开启浏览器参数方式内容协商功能
spring.mvc.contentnegotiation.favor-parameter=true
# 指定内容协商参数名, 默认 format
spring.mvc.contentnegotiation.parameter-name=f
发起请求:
http://localhost:8080/test/person?format=xml
http://localhost:8080/test/person?format=json
27
spring:
mvc:
contentnegotiation:
favor-parameter: true #开启浏览器参数方式内容协商功能
parameter-name: f # 指定内容协商参数名, 默认 format
media-types: {app: application/x-app, x-app: application/x-app} # 参数内容协商策略中添加自定义的媒体类型映射。
或
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
MediaType mediaType = MediaType.parseMediaType("application/x-app");
configurer.mediaType("app",mediaType);
configurer.mediaType("x-app", mediaType);
/*
// 这种方式导致默认ContentHeaderNegotiationStrategy请求头内容协商策略不可用,
// 因为configurer.strategies(Arrays.asList(pcns)); 直接将传入的list覆盖掉了内容协商策略的list,而传入的list中只有一个自定义的。
HashMap<String, MediaType> map = new HashMap<>();
ParameterContentNegotiationStrategy pcns = new ParameterContentNegotiationStrategy(map);
configurer.strategies(Arrays.asList(pcns));
*/
}
};
}
添加之后,可以看到ParameterContentNegotiationStrategy
中出现了我们自定义的类型映射。
2.2.4.3 自定义HttpMessageConverter
内容协商 + HttpMessageConverter
实现协议数据兼容。Json、Xml、。。。
- @ResponseBody 响应数据出去 调用RequestResponseBodyMethodProcessor 处理
- 所有MessageConverter 合起来可以支持各种媒体类型数据的操作(读、写)
- 内容协商找到最终的 HttpMessageConverter
SpringMVC中想自定义配置或修改一些东西,都只有一个入口:WebMmvConfiguer接口。只需要在配置类中给容器添加一个它的实现类,就可以在容器中添加自定义的组件。
/*
注册自定义HttpMessageConverter方式1:
通过SpringMVC自定义配置的入口进行配置。
*/
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer(){
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new AppHttpMessageConverter());
}
};
}
/*
注册自定义HttpMessageConverter方式2:
直接返回自定义的Http消息转换器,容器底层会自动扫描HttpMessageConverter的实现类。
小细节:该种方式注册HttpMessageConverter会导致该converter出现在所有converters的第一个位置,
底层第一次调用canWrite(clazz,null)遍历converters找到服务器(根据能否处理clazz数据)能生产的media,
第二次调用canWrite(clazz,selectedMediaType)遍历converters确定哪个可以将clazz数据转为selectedMediaType媒体类型,
所以第二次调用canWrite时,要判断好selectedMediaType类型(客户端想要的类型之一)。
如果不判断,就会导致客户端本没有发送自定义类型的accept,却收到了自定义类型的数据。
因为位于第一个位置的自定义HttpMessageConverter只判断了可以处理这个对象,而没有判断是否是客户端想要的类型,直接将其转换。
*/
@Bean
public AppHttpMessageConverter appHttpMessageConverter(){return new AppHttpMessageConverter();}
/*
* 自定义Http消息转换器
* 经过内容协商,Person类型数据转换成 自定义x-app类型数据。
* */
public class AppHttpMessageConverter implements HttpMessageConverter<Person> {
@Override
public boolean canRead(Class<?> clazz, MediaType mediaType) {return false;}
// 底层会调用canWrite(clazz, null) 判断是否可以写,
// 如果可以重写则,调用getSupportedMediaType获取当前HttpMessageConverter支持的所有Media。
//然后再调用canWrite(clazz, selectedMediaType)判断是否可以clazz数据转换为selectedMediaType数据类型。
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
boolean assignableFrom = Person.class.isAssignableFrom(clazz);
if (mediaType == null){
return assignableFrom;
}else {
List<MediaType> supportedMediaTypes = getSupportedMediaTypes();
for (MediaType s : supportedMediaTypes) {
return s.toString().equals(mediaType.toString()) && assignableFrom;
}
}
return false;
}
@Override
public List<MediaType> getSupportedMediaTypes() {
return MediaType.parseMediaTypes("application/x-app");
}
@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() + ";" + person.getPet().getName() + ";" + person.getPet().getAge();
OutputStream body = outputMessage.getBody();
body.write(data.getBytes());
body.flush();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Mhlacu9C-1612143666408)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210124203339766.png)]
图中可以看到内容协商策略的27中,默认支持的mediaTypes只有两个xml和json,不支持其他的参数协商策略。
2.2.5 视图解析与模板引擎
视图解析:SpringBot默认不支持JSP,需要引入第三方模板引擎技术实现页面渲染。
视图解析
[^模板引擎]
1. Thymeleaf简介
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of processing HTML, XML, JavaScript, CSS and even plain text.
现代化、服务端Java模板引擎
因为是服务端的Java模板引擎,所有有以下优缺点:
- 优点:
- 语法简单
- 缺点:
- 不是一个高性能模板引擎,不适合高并发的应用场景
- 高并发的情况下最好使用前后端分离,前端让前端开发。简单的单体式应用可以使用Thymeleaf。
2. 基本语法
1. 简单表达式
表达式名字 | 语法 | 用途 |
---|---|---|
变量取值 | ${…} | 获取请求域、session域、对象等中的属性值 |
选择变量 | *{…} | 获取上下文对象值,前面有一个定义好的对象,使用*{…}获取对象中的值 |
消息 | #{…} | 获取国际化等值 |
链接 | @{…} | 生成超链接 |
片段表达式 | ~{…} | jsp:include 作用,引入公共页面 |
2. 字面量
字面量 | |
---|---|
文本值 | ’one text’,‘Another one!’, … |
数字 | 0,34,3.0,12.3,… |
布尔值 | true,false |
空值 | null |
变量 | one,two,…变量不能有空格 |
3. 文本操作
文本操作 | |
---|---|
字符串拼接 | + |
变量替换 | |The Name is $(name)| |
4. 数学运算
运算符 | |
---|---|
运算符 | +,-,*,/,% |
5. 布尔运算
布尔运算 | |
---|---|
运算符 | and,or |
一元运算 | !,not |
6. 比较运算
比较运算 | |
---|---|
比较 | >,<,>=,<=,(gt,lt,ge,le) |
等式 | ==,!=(eq,ne) |
7. 条件运算
条件运算 | |
---|---|
if-then | (if) ? (then) |
if-then-else | (if) ? (then) ? (else) |
Default | (value) ?: (defaultValue) |
8. 特殊操作
特殊操作 | |
---|---|
无操作 | _ |
9. 设置属性值-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}"/> </fieldset> </form>
-
设置多个值
<img scr="../../img/logo.png" th:attr="scr=@{/images/logo.png},title=#{logo},alt=#{alt}">
-
th:attr的代替写法 th:xxxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/> <form action="subscribe.html" th:action="@{/subscribe}">
10. 迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<!--或者 *{name}-->
<td th:text="${prod.price}">2.41</td>
<!--或者 *{price}-->
<td th:text="${prod.inStock ? #{true} : #{false}}">yes</td>
<!--或者 *{inStock ? #{true} : #{false}}-->
</tr>
3. thymeleaf使用
1. 引入Starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
2. ThymeleafAutoConfiguration自动配置类
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {}
自动配好的策略:
- 所有Thymeleaf的配置值都早 ThymeleafProperties
- 配置好了 SpringTemplateEngine
- 配置好了 ThemeleafViewResolver
- 开发者只需要直接开发页面
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
3. Thymeleaf 命名空间
<html lang="en" xmlns:th="http://www.thymeleaf.org">
4. 视图解析原理流程
-
目标方法处理的过程中,所有数据都会被放在ModelAndViewContainer里面。包裹数据和视图地址
-
方法的参数是一个自定义类型对象(从请求参数中确定的),把他重新放在 ModelAndViewContainer
-
任何目标方法执行完成以后都会返回 ModelAndView(数据和视图地址)
-
processDispatcherResult 处理派发结果(页面该如何响应)
-
1、render(mv, request, response); 进行页面渲染逻辑
- 1、根据方法的String返回值得到 View 对象【定义了页面的渲染逻辑】
-
-
- 1、所有的视图解析器尝试是否能根据当前返回值得到View对象
-
- 2、得到了 redirect:/main.html --> Thymeleaf new RedirectView()
- 3、ContentNegotiationViewResolver 里面包含了下面所有的视图解析器,内部还是利用下面所有视图解析器得到视图对象。
- 4、view.render(mv.getModelInternal(), request, response); 视图对象调用自定义的render进行页面渲染工作
-
-
-
-
- RedirectView 如何渲染【重定向到一个页面】
- 1、获取目标url地址
- **2、**response.sendRedirect(encodedURL);
-
-
-
视图解析:
-
- 返回值以 forward: 开始: new InternalResourceView(forwardUrl); --> 转发****request.getRequestDispatcher(path).forward(request, response);
- 返回值以 redirect: 开始: new RedirectView() --》 render就是重定向
- 返回值是普通字符串: new ThymeleafView()—>
2.2.6 拦截器
1. HandlerInterceptor接口
/**
* 登录检查
* 1、配置好拦截器要拦截哪些请求
* 2、把这些配置放在容器中
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法执行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle拦截的请求路径是{}",requestURI);
//登录检查逻辑
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if(loginUser != null){
//放行
return true;
}
//拦截住。未登录。跳转到登录页
request.setAttribute("msg","请先登录");
// re.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法执行完成以后
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("postHandle执行{}",modelAndView);
}
/**
* 页面渲染以后
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("afterCompletion执行异常{}",ex);
}
}
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/**","/js/**"); //放行的请求
}
}
3. 拦截器原理
1、根据当前请求,找到**HandlerExecutionChain【**可以处理请求的handler以及handler的所有 拦截器】
2、先来顺序执行 所有拦截器的 preHandle方法
- 1、如果当前拦截器prehandler返回为true。则执行下一个拦截器的preHandle
- 2、如果当前拦截器返回为false。直接 倒序执行所有已经执行了的拦截器的 afterCompletion;
3、如果任何一个拦截器返回false。直接跳出不执行目标方法
4、所有拦截器都返回True。执行目标方法
5、倒序执行所有拦截器的postHandle方法。
6、前面的步骤有任何异常都会直接倒序触发 afterCompletion
7、页面成功渲染完成以后,也会倒序触发 afterCompletion
2.2.7 文件上传
1. 表单
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
2.上传Controller
/**
* MultipartFile 自动封装上传过来的文件
* @param email
* @param username
* @param headerImg
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上传的信息:email={},username={},headerImg={},photos={}",
email,username,headerImg.getSize(),photos.length);
if(!headerImg.isEmpty()){
//保存到文件服务器,OSS服务器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("H:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("H:\\cache\\"+originalFilename));
}
}
}
return "main";
}
3.自动配置原理
文件上传自动配置类-MultipartAutoConfiguration-MultipartProperties
-
自动配置好了 StandardServletMultipartResolver【文件上传解析器】
-
默认解析器名称multipartResolver
-
public static final String MULTIPART_RESOLVER_BEAN_NAME = "multipartResolver"; @Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) @ConditionalOnMissingBean(MultipartResolver.class) public StandardServletMultipartResolver multipartResolver() { StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver(); multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily()); return multipartResolver; }
-
-
原理步骤
-
-
请求进来使用文件上传解析器判断(isMultipart)并封装(resolveMultipart,返回MUltipartHttpServletRequest)文件上传请求
-
@Override public boolean isMultipart(HttpServletRequest request) { return StringUtils.startsWithIgnoreCase(request.getContentType(), "multipart/"); } @Override public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { return new StandardMultipartHttpServletRequest(request, this.resolveLazily); }
-
-
- 参数解析器RequestPartMethodArgumentResolver来解析请求中的文件内容封装成MultipartFile
-
- 将request中文件信息封装为一个Map;MultiValueMap<String,MultipartFIle>(Key:目标方法参数名,value:上传的文件)
-
- 然后根据当前遍历到的方法的参数名,底层会返回一个List< MultipartFile >
-
最后底层会使用FileCopyUtils实现文件流的拷贝。
2.2.8 异常处理
1. 错误处理
1. 默认规则
-
默认情况下,SpringBoot提供
/error
处理所有错误的映射 -
对于机器客户端,它将生成JSON响应,其中包含错误,HTTP状态和异常消息的详细信息。对于浏览器客户端,响应一个 “whitelabel“ 错误视图,以HTML格式呈现相同的数据
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DeEWoWyT-1612143666414)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210127113124494.png)]
-
要对其进行自定义,添加
View
解析为error
- 默认规则会自动将不同状态的异常解析到error目录下对应的异常页面。优先精确匹配
- 5xx.html表示所有500的服务器异常都解析到5xx.html视图中。
- 404.html只单独将404的异常解析到404.html视图中
- 默认规则会自动将不同状态的异常解析到error目录下对应的异常页面。优先精确匹配
-
要完全替换默认行为,可以实现
ErrorController
并注册该类型的Bean定义,或添加ErrorAttributes类型的组件
以使用现有机制替换其内容。
2. 定制错误处理逻辑
- 自定义错误页
error/404.html
error/5xx.html
;精确匹配优先,没有匹配成功触发白页- @ControllerAdvice + @ExceptionHandler处理异常;底层是 ExceptionHandlerExceptionResolver支持的,优先级低于局部@ExceptionHandler(AOP切面机制)
- @ResponseStatus + 自定义异常;底层是 ResponseStatusExceptionResolver,把responsestatus注解的信息组装成ModelAndView返回,底层调用response.sendError(statusCode,resolvedReason);tomcat发送/error请求,如果有能处理自定义异常的异常处理器,会使用异常处理器进行处理该异常,没有则使用ResponseStatusExceptionResolver。
- Spring底层异常,如 参数类型转换异常;28 处理框架底层的异常。
- ErrorViewResolver 实现自定义异常处理
- response.sendError。 error请求就会转给Controller
- 异常没有任何异常解析器能处理,tomcat底层response.senError。error请求就会传给controller
- basicErrorController 要去的页面地址是ErrorViewResolver解析的。;
- 实现 HandlerExceptionResolver接口 处理异常
- 自定义HandlerExceptionResolver异常解析器在
# 错误页包含异常对象
server.error.include-exception=true
# 错误页包含异常信息。 Exception.getMessage()
server.error.include-message=always
# 错误页不显示堆栈信息。
server.error.include-stacktrace=never
3. 异常处理原理
-
ErrorMVCAutoConfiguration29
-
绑定了ServerProperties配置文件【prefix = “server”】
-
注册组件 类型:DefaultErrorAttributes —> id:errorAttributes
-
定义错误页面中可以包含哪些数据。
-
public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
-
-
注册组件 类型:BasicErrorController—> id:basicErrorController
-
@Controller //动态取值--》${配置属性值:默认值} @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController
-
处理默认/error 路径的请求;页面响应 new ModelAndView(“error”, model);
-
容器中组件 View -> id是error(响应默认错误页:whitelabel白页)
-
容器中放组件 BeanNameViewResolver(Bean名称视图解析器);按照返回的视图名和组件的id进行映射,去容器中找View对象。
-
-
注册组件 类型:DefaultErrorResolver —> id:conventionErrorViewResolver
- 如果发生错误,会以HTTP的状态码作为视图页ViewName,找到真正的页面
- error/404、5xx.html
-
4. 异常处理步骤流程
-
执行目标方法,目标方法运行期间有任何异常都会被catch,而且标识当前请求结束,并且用 dispatchException接收
-
进入视图解析流程(页面渲染?)
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
-
mv = mv = processHandlerException(request, response, handler, exception); 处理handler发生的异常,处理完成返回ModelAndView
-
遍历所有的HandlerExceptionResolvers,看谁能处理当前异常【HandlerExceptionResolver处理器异常解析器】
-
系统默认的:
3028[^ExceptionHandlerExceptionResovler]
-
DefaultErrorAttributes先来处理异常。把异常信息保存到req域,并且返回null;
-
ExceptionHandlerExceptionResovler 是用来处理
@ExceptionHandler标注的方法
31 -
没有任何HandlerExceptionResolver能处理当前目标方法异常,异常再次被抛出
(触发拦截器Completion方法)
- 如果没有任何人能处理,最终底层就会发送/error请求。会被底层的BasicErrorController处理
- 解析错误视图;遍历所有的 ErrorViewResolver,看谁能解析[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nSkuEqsc-1612143666415)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210127154210272.png)]
- 默认的DefaultErrorViewResolver,作用是把响应状态码/状态码系列作为错误页的地址,error/404.html,error/5xx.html
- 最终响应这个页面
-
-
- BasicErrorController中有两个@RequestMapping,一个返回html视图,一个返回JSON视图
- 响应HTML视图:从req中拿到之前底层已经存放的status状态码,再拿到错误信息的model。然后遍历找到可以处理的视图解析器,将req,rep,status,model解析。status拼接“.html”,生成ViewName,判断各个目录是否有可用的View模板,如果有,封装返回ModelAndView,如果没有则通过错误系列到4xx或5xx拼接’.html’,判断各个目录是否有可用的View模板,如果有,封装返回ModelAndView,否则返回null,ModelAndView为NUll则new ModelAndView(
“error”,model),使用自动配置类中注册的 类型:View ,id:error 的组件【白页】 和 BeanNameViewResolverBean名称视图解析器进行解析。ModelAndView不为null返回模型视图
- 响应HTML视图:从req中拿到之前底层已经存放的status状态码,再拿到错误信息的model。然后遍历找到可以处理的视图解析器,将req,rep,status,model解析。status拼接“.html”,生成ViewName,判断各个目录是否有可用的View模板,如果有,封装返回ModelAndView,如果没有则通过错误系列到4xx或5xx拼接’.html’,判断各个目录是否有可用的View模板,如果有,封装返回ModelAndView,否则返回null,ModelAndView为NUll则new ModelAndView(
graph TD
A(BasicErrorController) --> B[RequestMapping响应HTML]
A(BasicErrorController) --> C[RequestMapping响应JSON]
B --> D[req中拿到之前底层已经存放的status]
D --> E[req中拿到之前底层已经存放的错误信息model]
E --> F[遍历找到可以处理的视图解析器]
F --> G[找到视图解析器:req rep status model解析]
G --> H[status拼接'.html' 生成ViewName]
H --> I[判断各个目录是否有可用的View模板]
I ---> J[有:封装返回ModelAndView]
I --> P[没有:通过错误系列4xx或5xx拼接'.html',生成ViewName]
P --> I
J --> Q[ModelAndView != null:返回]
J --> K[ModelAndView == null:new ModelAndView 'error', model,使用自动配置类中注册的类型:View,id:error 的组件,'白页']
K --> O[配置类中注册的BeanNameViewResolver视图名称解析器进行解析id为'error'的视图]
C --> L[req中拿到之前底层已经存放的status]
L --> M[req中拿到之前底层已经存放的错误信息model]
M --> N[封装ResponseEntity返回]
5. 自定义异常解析器
1. @ExceptionHandler异常处理器注解
/**
* 定义在Controller中,只处理当前Controller中发生的异常。
* 定义在@ControllerAdvice中,处理全局Controller中发生的异常
* 局部ExceptionHandler优先级高于Advice中的ExceptionHandler。(AOP切面机制)
*
*
* @ @ExceptionHandler(Class[] value) 定义自定义异常处理器
* value值定义可以处理目标方法发生的指定异常,留空默认为全部异常。
* @param req 原生req
* @param rep 原生 rep
* @param ex 目标方法发生的异常
* @param model Model参数方式转发携带数据
* @return 可以是ModelAndView String Model Json等。
*/
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String exceptionHandle(HttpServletRequest req, HttpServletResponse rep, Exception ex, Model model){
System.out.println(req);
System.out.println(rep);
model.addAttribute("ex",ex);
return "error/myerrorpage";
}
2. @ControllerAdvice + @ExceptionHandler
/*
* @ControllerAdvice() Controller切面
* 继承@Component注解,当前类是一个组件。
* 作用:注解参数决定当前类作为哪些Controller的切面,
* 底层使用AOP机制将当前类中的方法织入指定的Controller中。
* */
@ControllerAdvice(basePackages = {"com.yyl.springbootstudy.errorhandler.controller"})
public class ExceptionController {
@ExceptionHandler({ArithmeticException.class, NullPointerException.class})
public String exceptionHandle(HttpServletRequest req, HttpServletResponse rep, Exception ex, Model model){}
}
3. ResponseStatus
/**
* 验证@ResponseStatus注解的异常
* 没有异常解析器时,会使用@ReponseStatus中设置的statusCode和reason信息跳转error页(whiteLabel)
*/
@ResponseStatus(code = HttpStatus.FORBIDDEN, reason = "用户异常的帅")
4. implements HandlerExceptionResolver
/*
* @Order()提升当前异常解析器的优先级:
* 默认会将自定义HandlerExceptionResolver加入resolvers最后一位,它前面有
* 1.DefaultErrorAttribute。 将ex放入req,返回null
* 2.ExceptionHandlerExceptionResolver。 处理标有@ExceptionHandler注解的
* 3.ResponseStatus。 处理标有@ResponseStatus注解的异常类的
* 4.DefaultHandlerExceptionResolver。 SpringMVC底层解析器的
* 5.自定义HandlerExceptionResolver
* 前面的如果能处理掉发生的异常,后面的异常解析器就不会进行判断了,所以自定义的异常解析器需要添加到list的第一个,
* 如果自定义的解决不了这个异常,就会继续向后寻找能处理的异常解析器。
* */
@Order(value = Ordered.HIGHEST_PRECEDENCE)
@Component
public class CustomExceptionResolver implements HandlerExceptionResolver
2.2.9 Web原生组件注入(Servlet、Filter、Listener)
1. 使用注解式ServletAPI
可以使用@WebServlet
,@WebFilter
,@WebListener
注册三大组件,但是必须开启@ServletComponentScan
, Servlet组件扫描器注解。
**@ServletComponentScan(basePackages=“xxx.xxx”):**默认扫描主启动类下的包及其子包,basePackages手动指定要扫描的包。标注在主启动类上(容器中的Bean才能使用Spring提供的强大注解功能)
WebServlet("/myservlet")
public class MyServlet extends HttpServlet
@WebListener
public class MyListener implements ServletContextListener
@WebFilter(urlPatterns = "/*")
public class MyFilter extends HttpFilter
@ServletComponentScan
@SpringBootApplication
public class ServletComponentsApplication
2.使用RegistrationBean
//保证当前配置类中的组件都是单实例
//Servlet都是单实例,多线程
@Configuration(proxyBeanMethods = true)
public class ServletComponentConfig {
@Bean
public ServletRegistrationBean<HttpServlet> servletRegistrationBean(){
ServletRegistrationBean<HttpServlet> httpServletServletRegistrationBean = new ServletRegistrationBean<>(new HttpServlet() {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("@Bean + ServletRegistrationBean 注册Servlet原生组件");
resp.setContentType("text/html;charset=utf-8");
resp.getWriter().append("@Bean + ServletRegistrationBean 注册Servlet原生组件");
}
});
httpServletServletRegistrationBean.addUrlMappings("/myservlet2");
return httpServletServletRegistrationBean;
}
@Bean
public FilterRegistrationBean<Filter> filterFilterRegistrationBean(){
FilterRegistrationBean<Filter> filterFilterRegistrationBean = new FilterRegistrationBean<>((request, response, chain) -> {
System.out.println("@Bean + FilterRegistrationBean 注册Servlet原生组件");
chain.doFilter(request, response);
});
filterFilterRegistrationBean.addUrlPatterns("/*");
return filterFilterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean<ServletContextListener> servletContextListenerServletListenerRegistrationBean(){
return new ServletListenerRegistrationBean<ServletContextListener>(new ServletContextListener() {
@Override
public void contextInitialized(ServletContextEvent sce) {
System.out.println("@Bean + ServletListenerRegistrationBean 注册Servlet原生组件");
}
});
}
}
3. 思考:Servlet原生组件不能使用mvcInterceptor
-
HandlerInterceptor:属于Dispatcher中的拦截器,在请求没有进入Dispatcher时,是不能够使用拦截器的。
-
Tomcat服务器对多个Servlet可处理同一层的Url的规则是 精确匹配优先
- Dispatcher的UrlPattern:/
- 原生Servlet:/my
- 精确匹配优先
2.2.10 嵌入式Servlet容器
1. 嵌入式Servlet容器
-
默认支持的webServer
Tomcat
,Jetty
,UndertowServletWebServerApplicationContext
容器启动寻找ServletWebServerFactory 并引导创建服务器
-
切换服务器
-
-
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <exclusions> <exclusion> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-tomcat</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-undertow</artifactId> </dependency>
-
-
原理
-
SpringBoot应用启动发现当前是Web应用。web场景包-导入tomcat
-
web应用会创建一个web版的ioc容器
ServletWebServerApplicationContext
-
ServletWebServerApplicationContext
启动的时候寻找ServletWebServerFactory
(ServletWeb服务器工厂 --》 Servlet的web服务器) -
SpringBoot底层默认有很多的WebServer工厂;
-
TomcatServerWebServerFactory
,JettyServletWebServerFactory
,UndertowServletWebServerFactory
-
底层直接会有一个自动配置类。
ServletWebServerFactoryAutoConfiguration
- 该配置类导入了
ServletWebServerFactoryConfiguration(配置类)
ServletWebServerFactoryConfiguration
根据@Conditional动态判断系统中到底导入了哪个Web服务器的包。(默认是web-stater-tomcat包,tomcat的jar包),容器中就有TomcatServletWebServerFactory TomcatServlet服务器工厂
TomcatServletWebServerFactory
创建出Tomcat服务器并启动;TomcatWebServer的构造器有一个初始化方法initialize- 内嵌服务器,就是手动把启动服务器的代码调用(tomcat核心Jar包存在)
- 该配置类导入了
-
-
2. 定制Servlet容器
- 实现 WebServerFactoryCustomizer
- 把配置文件的值和
ServletWebServerFactory
进行绑定
- 把配置文件的值和
- 修改配置文件server.xxx
- 直接自定义 ConfigurableServletWebServerFactory
xxxxCustomizer:定制化器,可以改变xxxx的默认规则
2.2.11 定制化原理
1. 定制化的常见方式
- @Bean替换、增加容器中默认组件;视图解析器
- 修改配置文件
- xxxxCustomizer
- 编写自定义的配置类 xxxConfiguration;+ @Bean
- web应用 实现 WebMVCCOnfigurer 即可定制化Web功能
@EnableWebMvc
32 + WebMvcConfigurer – @Bean 可以全面接管 SpringMVC,所有规则全部自己重新配置;实现定制和拓展功能- WebMVCAutoConfiguration 默认的SpringMVC的自动配置类。静态资源,欢迎页…
- 一旦使用**@EnableWebMVcConfigurer,会@import**(DelegatingWebMvcConfiguration.class)
- DelegatingWebMvcConfigurer 的作用,只保证SpringMVC最基本的使用
- 把系统中所有DelegatingWebMvcConfigurer拿过来,所有功能的定制都是这些WebMvcConfigurer合起来一起声响
- 自动配置了一些非常底层的组件。RequestMappingHandlerMapping、这些组件依赖的组件都是从容器中获取
- WebMvcAutoConfiguration里面的配置要能生效必须**@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)**
- DelegatingWebMvcConfigurer extends WebMvcConfigurationSupport
- @EnableWebMvc 导致了 WebMVCAutoConfiguration 没有生效
- …
2. 原理分析套路
场景Starter - xxxAutoConfiguration - 导入xxx组件 - 绑定xxxProperties(绑定配置文件项)
2.3 数据访问
2.3.1 SQL
1. 数据源的自动配置 - HikariDataSource
1. 导入JDBC场景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
数据库驱动?
为什么导入JDBC场景,官方不导入驱动?官方不知道我们接下来要操作什么数据库。
默认版本:<mysql.version>8.0.22</mysql.version>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--<version>5.1.49</version>-->
</dependency>
想要修改版本
1、直接依赖引入具体版本(Maven的就近依赖原则)
2、重新声明版本(Maven的属性就近原则)
<properties>
<mysql.version>5.1.49</mysql.version>
</properties>
2. 分析自动配置
-
自动配置的类
-
修改数据源相关的配置:spring.datasource
-
数据库连接池的配置,是自己容器中没有DataSource才自动配置的。
@Configuration(proxyBeanMethods = false) @Conditional(PooledDataSourceCondition.class) @ConditionalOnMissingBean({ DataSource.class, XADataSource.class }) @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class }) protected static class PooledDataSourceConfiguration { }
-
底层配置好的默认连接池是:HikariDataSource
@ConditionalOnProperty(name = "spring.datasource.type", havingValue ="com.zaxxer.hikari.HikariDataSource",matchIfMissing = true)
-
修改配置项
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///bootstudy?useSSL=false username: root password: root # type: com.zaxxer.hikari.HikariDataSource 默认HikariDataSource数据源 jdbc: template: query-timeout: 10 #配置JDBCTemplate的请求超时时间。
-
测试
@Autowired JdbcTemplate jdbcTemplate; @Test void testQuery(){ int select_eno_from_emp = jdbcTemplate.queryForList("select eno from emp").size(); log.info("查询出记录数:{}",select_eno_from_emp); }
2. 使用Druid数据源
1. druid官方GitHub地址
https://github.com/alibaba/druid
(Hikari在市面上可能是性能最好的一款产品,但实际开发中,企业更喜欢使用阿里的Druid数据源,因为Druid有着自己的一套解决方案,比如全方位监控,防止sql注入攻击…)
整合第三方技术的两种方式
- 自定义
- 找starter
2. 自定义方式
依赖:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.4</version>
</dependency>
-
配置文件声明式:
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///bootstudy?useSSL=false username: root password: root # type: com.zaxxer.hikari.HikariDataSource 默认HikariDataSource数据源 type: com.alibaba.druid.pool.DruidDataSource
-
@Configuration 编码式
spring: datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql:///bootstudy?useSSL=false username: root password: root
@Bean //将标注方法返回的对象与配置文件进行配置绑定。 @ConfigurationProperties("spring.datasource") /* * HikariDataSource的配置中有注解@ConditionalOnMissingBean(DataSource.class), * 表示容器中没由有DataSource时,才创建HikariDataSource对象 * */ public DataSource druidDatasource(){ return new DruidDataSource(); }
3. 使用starter方式
-
引入Druid-starter
<dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.4</version> </dependency>
-
分析自动配置类
-
拓展配置项:spring.datasource.druid
-
DruidSpringAopConfiguration 监控SpringBean的,配置项:spring.datasource.druid.aop-patterns
-
DruidStatViewServletConfiguration;监控页的配置:spring.datasource.druid.stat-view-servlet
-
DruidWebStatFilterConfiguration;web监控配置:spring.datasource.druid.web-stat-filter
-
DruidFilterConfiguration;所有Druid自己filter的配置。
-
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat"; private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config"; private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding"; private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j"; private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j"; private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2"; private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log"; private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall"; private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
-
-
-
配置实例
druid: filters: stat,wall,log4j # 底层开启功能 stat(SQL监控) wall(防火墙),log4j(日志输出) stat-view-servlet: # 配置监控页功能 enabled: true url-pattern: /druid/* login-username: admin login-password: admin reset-enable: false # 是否允许重置已经监控到的数据 web-stat-filter: # 监控web enabled: true url-pattern: /* exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' filter: # 对filters中每个filter的详细设置 stat: # 对filters中stat的详细设置 enabled: true slow-sql-millis: 1000 log-slow-sql: true wall: enabled: true config: drop-table-allow: false
3. 整合MyBatis
https://github.com/mybatis
spring官方的starter:spring-boot-starter-*
第三方:*-spring-boot-starter
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ccYwy9OE-1612143666418)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210129171744858.png)]
1. 配置模式
- 全局配置文件
- SqlSessionFactory:自动配置好了
- SqlSession自动配置了 SqlSessionTemplate 组合了 SqlSession
- @Import(AutoConfiguredMapperScannerRegistrar.class)
- Mapper:只要Mapper接口上标注@Mapper,接口就会被自动扫描进来。
- @MapperScan(“Mapper所在的包”),Mapper接口可以不添加@Mapper注解。
@org.springframework.context.annotation.Configuration
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
@ConditionalOnSingleCandidate(DataSource.class)
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {
@ConfigurationProperties(prefix = MybatisProperties.MYBATIS_PREFIX)
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
可以修改配置文件中mybatis开始的所有;
mybatis:
config-location: classpath:mybatis/mybatis.xml # 全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml # Mapper配置文件位置。
# config-location 不能和 configuration同时存在。否则报错。因为两个功能都是mybatis功能的配置。
# mybatis配置文件中的配置都可以在boot配置文件的mybatis.configuration.xxx中配置
# configuration:
# use-column-label: true # 开启驼峰命名
# 可以不写mybatis的全局配置文件,所有配置都可以在mybatis.configuration.xxx中配置。
配置 private Configuration configuration; myabtis.configuration下面的所有,相当于修改mybatis全局配置文件中的值。
- 导入mybatis官方starter
- 编写mapper接口。标注@mapper注解 或 主启动类(组件)类标注 @MapperScannerRegister
- 编写sql映射文件并绑定mapper接口
- 在application.yaml中指定mapper配置文件的位置,以及指定全局配置文件中的信息mybatis.configuration.xxx(建议:配置mybatis-config.xml)
2. 注解模式
@Mapper
public interface EmpMapper{
@Select("select * from emp")
@Options(Options可以设置在xml标签中的一些功能,比如 useGeneratedKey = true, keyProperty = "id")
Emp selectEmp();
@insert [+ @Optinos]
@Delete [+ @Options]
@Update [+ @Options]
}
4. 整合 MyBatis-Plus完成CRUD
1. 什么是MyBatis-Plus
MyBatis-Plus(简称MP)是一个MyBatis的增强工具,在MyBatis的基础上制作增强,不做改变,为简化开发,提高效率而生。
建议安装MyBatisX插件
2. 整合MyBatis-Plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qbKbW0ax-1612143666418)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210129185550200.png)]
3. 自动配置
-
MyBatisPlusAutoConfiguration
配置类,MyBatisPlusProperties配置项绑定。mybatis-plus.xxxx -
自动配置好了哪些东西?
SqlSessionFactory
,底层是容器中的数据源mapperLocations
,默认值:classpath*:/mapper/**/*.xml
;classpath任意路径下的mapper文件夹中,任意路径下的任意xml文件(建议所有的mapper文件都放在mapper路径下)SqlSessionTemplate
@Mapper
标注的接口自动被扫描 @MapperScan // 使用后Mapper接口可以不添@Mapper注解。 MyBatisPlus中不用再次声明,否则就会有两个MapperScan,造成invalid bound not found
-
仅需要XXXMapper entends BaseMapper 就可以拥有crud能力。
4. CURD
-
Mapper接口 extends BaseMapper
-
只需要继承BaseMapper就可以拥有很多基本的CURD方法
-
/** MyBatis-Plus BaseMapper */ @Mapper public interface PlusEmpMapper extends BaseMapper<Emp> {}
-
-
Service接口 extends IService
-
只需要继承Iservice就可以拥有很多基本的CURD方法
-
public interface PlusEmpService extends IService<Emp> {}
-
-
Service实现类 extends ServiceImpl<<M extends BaseMapper, T> implements PlusEmpService
-
只需要继承 ServiceImpl<<M extends BaseMapper, T> 就可以使用很多CURD方法
-
@Service public class PlusEmpServiceImp extends ServiceImpl<PlusEmpMapper, Emp> implements PlusEmpService { }
-
-
MyBatisPlus分页插件
-
/** * MyBatisPlus 分页插件 */ @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor(); // 设置请求的页面大于最大页后操作, true调回到首页,false 继续请求 默认false // paginationInterceptor.setOverflow(false); // 设置最大单页限制数量,默认 500 条,-1 不受限制 // paginationInterceptor.setLimit(500); // 开启 count 的 join 优化,只针对部分 left join PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(); paginationInnerInterceptor.setOverflow(true); paginationInnerInterceptor.setMaxLimit(100L); paginationInnerInterceptor.setOptimizeJoin(true); mybatisPlusInterceptor.addInnerInterceptor(paginationInnerInterceptor); return mybatisPlusInterceptor; }
-
MybatisPlusInterceptor
: @since 3.4.0 新更新的插件 -
PaginationInterceptor
:@deprecated 3.4.0 please use {@link MybatisPlusInterceptor} {@link PaginationInnerInterceptor}
-
-
Controller
-
@Autowired PlusEmpService plusEmpService; /** * 查询所有数据 * */ @GetMapping("plus/t_emp") public String t_emp(Model model){ List<Emp> emps = plusEmpService.list(); model.addAttribute("emps",emps); return "emp"; } /** * 分页查询数据 * */ @GetMapping("plus/t_emp_page") public String t_emp_page(Model model, @RequestParam(defaultValue = "1") Integer page){ Page<Emp> empPage = new Page<>(page, 2); Page<Emp> emps = plusEmpService.page(empPage); System.out.println("emps.getRecords() = " + emps.getRecords()); System.out.println("emps.getCountId() = " + emps.getCountId()); System.out.println("emps.getCurrent() = " + emps.getCurrent()); System.out.println("emps.getMaxLimit() = " + emps.getMaxLimit()); System.out.println("emps.getOrders() = " + emps.getOrders()); System.out.println("emps.getTotal() = " + emps.getTotal()); System.out.println("emps.getSize() = " + emps.getSize()); System.out.println("emps.getPages() = " + emps.getPages()); model.addAttribute("emps",emps); return "emp_page"; }
-
2.3.2 NoSql - Redis
Redis 是一个开源(BSD许可)的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息中间件。 它支持多种类型的数据结构,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 与范围查询, bitmaps, hyperloglogs 和 地理空间(geospatial) 索引半径查询。 Redis 内置了 复制(replication),LUA脚本(Lua scripting), LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动 分区(Cluster)提供高可用性(high availability)。
1. Redis自动配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 自动配置:
RedisAutoConfiguration
自动配置类。**RedisProperties
**属性类 -->spring.redis
LettuceConnectionFactory
lettuce连接工厂是准备好的。如果使用的是Jedis客户端,那么底层就会创建Jedis连接工厂。RedisTemplate<Object, Object>
:自动注入了RedisTemplate,支持操作Redis的**<key,value>**StringTemplate
:自动注入了StringTemplate。k,v都是String
- 底层只要使用StringRedisTemplate、RedisTemplate就可以操作Redis 。
2. RedisTemplate 与 Lettuce
Lettuce: 支持Java操作Redis的一款客户端。(还有Jedis,Redisson)。
- 通过自动配置类中可以发现:
- 无论是使用Lettuce还是Jedis,都会在容器中注册一个Lettuce或Jedis的LettuceConnectionFactory或JedisConnectionFactory。
- 这两个ConnectionFactory都是RedisConnectionFactory的子类。
- RedisTemplate通过RedisConnectionFactory生成。
spring:
redis:
host: localhost
port: 6379
@GetMapping("/redis/set/{key}/{value}")
@ResponseBody
public String redisSetKey(@PathVariable String key, @PathVariable String value){
redisTemplate.opsForValue().set(key,value);
return Objects.requireNonNull(redisTemplate.getConnectionFactory()).getClass().toString();
}
@GetMapping("/redis/get/{key}")
public String redisGetKey(@PathVariable String key){
return redisTemplate.opsForValue().get(key);
}
3. 切换Jedis客户端
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<!--不排除Lettuce则默认使用Lettuce,
默认使用Lettuce可以手动指定
redis.client.type: jedis
来切换Redis的客户端。
-->
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
spring:
redis:
host: localhost
port: 6379
# 切换Redis客户端
client-type: jedis
4. 统计访问次数小案例
@WebFilter(urlPatterns = "/*")
public class RedisLookCountFilter implements Filter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
this.redisTemplate.opsForValue().increment(((HttpServletRequest) request).getRequestURI());
chain.doFilter(request,response);
}
}
@SpringBootApplication
@ServletComponentScan
public class DatasourceAutoConfigurationApplication
2.4 单元测试
2.4.1 JUnit 5 的变化
SpringBoot 2.2.0 版本开始引入 JUnit 5 作为单元测试默认库。
作为最新版本的JUnit框架,JUnit5与之前版本的JUnit框架有很大的不同。由三个不同子项目的几个不同模块组成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:是在JVM上启动测试框架的基础,不仅支持Junit自制的测试引擎,其他测试引擎也都可以接入。
JUnit Jupiter:提供了JUnit5的新的编程模型,是JUnit5新特性的核心。内部包含了一个测试引擎,用于JUnit Platform上运行。
JUnit Vintage:由于JUnit已经发展多年,为了照顾老的项目,JUnit Vintage提供了兼容JUnit4.x,JUnit4.x的测试引擎。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DfIzGClc-1612143666420)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130150616217.png)]
SpringBoot 2.4 以上版本移除了默认对Vintage的依赖。如需兼容JUnit4在JUnit Platform上运行,自行引入Vintage
- SpringBoot整合JUnit后
- 编写测试方法:@Test标注(需要使用JUnit5版本的注解)
- junit类具有Spring的功能,@Autowired,@Transactional… 标注测试方法,测试完成后自动回滚。
2.4.2 JUnit 5常用注解
JUnit5的注解于JUnit4的注解有所变化
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotaions
注解 | 作用 |
---|---|
@Test | 表述方法是测试方法。但是与JUnit4的@Test不同,他的职责非常单一不能声明任何属性,拓展的测试将有Jupiter提供额外测试。 |
@ParameterizedTest | 表示方法是参数化测试 |
@RepeatedTest | 重复执行测试方法 |
@DisplayName | 为测试类或者测试方法设置展示名称(为测试方法设置测试名称) |
@BeforeEach | 在每个单元测试之前执行 |
@AfterEach | 在每个单元测试只有执行 |
@BeforeAll | 在所有单元测试之前执行 |
@AfterAll | 在所有测试单元测试之后执行 |
@Tag | 单元测试类别,类似JUnit4中的@Categories |
@Disable | 测试类或测试方法不执行,类似于JUnit4中的@Ignore |
@Timeout | 测试方法运行如果超过了指定事件将会返回错误 |
@ExtendWith | 为测试类或测试方法提供拓展类引用,类似于JUnit4中的@RunWith |
2.4.3 断言(Assertions)
断言(爱色色如同时)是测试方法中的核心部分,用来对测试需要满足的条件进行验证。这些断言方法都是org.junit.jupiter.api.Assertions的静态方法。JUnit5内置的断言可以分成如下几个类别:
检查业务逻辑返回的数据是否合理。
所有测试运行结束后,会有一个详细的测试报告。
测试时,import static org.junit.jupiter.api.Assertions.* ,可以直接使用方法。不需要通过类名调用。
1. 简单断言
用单个值进行简单的验证。
方法 | 说明 |
---|---|
assertEquals | 判断两个对象或两个原始类型是否相等 |
assertNotEquals | 判断两个对象或两个原始类型是否不相等 |
assertSame | 判断两个对象引用是否指向同一个对象 |
assertNotSame | 判断两个对象引用是否指向不同的对象 |
assertTrue | 判断给定的布尔值应用是否为true |
assertFalse | 判断给定的布尔值应用是否为false |
assertNull | 判断给定的对象是否为null |
assertNotNull | 判断给定的对象是否不为NUll |
2. 数组断言 assertArrayEquals()
通过assertArrayEquals方法来判断两个对象或原始类型的数组是否相等。
assertArrayEquals(new int[]{1,2}, new int[]{1,2}) ;
3. 组合断言 assertAll()
assertAll()方法接受多个org.junit.jupiter.api.Executable函数式接口的实例作为要验证的锻压你,可以通过lambda表达式很容易提供这些断言。
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0));
4. 异常断言 asserThrows()
在JUnit4时期,想要测试方法的异常情况时,需要用@Rule注解的ExpectedException变量还是比较麻烦的。而JUnit5提供了一种新的断言方式Assertions.asserThrows(),配合函数式编程就可以进行使用。
断定一定会出现异常
ArithmeticException exception = Assertions.assertThrows(
ArithmeticException.class,
() -> {int i = 10 / 0;},
"没有输出这段话表示业务逻辑成功抛出了一个数学异常。"
)
5. 超时断言 assertTimeout()
**Assertions.assertTimeout()**测试方法执行时间是否超时。
如果测试方法执行时间超过1s将会异常
assertTimeOut(Duration.ofMillis(1000), () -> Thread.sleep(500))
6. 快速失败 fail
通过 fail方法直接使得测试失败
fail("this should fail")
2.4.4 前置条件(assumptions)
JUnit5中的前置条件(assumptions【假设】)类似于断言,不同之处在于不满足的断言会使得测试方法失败,而不满足的前置条件只会使得测试方法的执行跳过。前置条件可以看成是测试方法执行的前提,当该前提不满足时,就没有继续执行的必要。
assumeTrue(false, 结果不是true);
sout("11111");
当assumeTrue断言表达式结果不为true时,会自动中止并跳过该测试方法。
2.2.5 嵌套测试 @Nested
Junit5可以通过java中的内部类和@Nested注解实现嵌套测试,从而可以更好的把相关的测试方法组织在一起。在内部类中可以使用@BeforeEach 和 @AfterEach 注解,而且嵌套的层次没有限制。
内层类中的测试方法可以驱动外层类中的@BeforeEach等,外层无法驱动内层
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
2.2.6 参数化测试
参数化测试时JUnit5很重要的新特性,它使得用不同的参数多次运行测试成为了可能,也为单元测试带来了许多遍历。
利用@ValueSource等注解,指定入参,可以使用不同的参数进行多次单元测试,而不需要每新增一个参数就为新增一个单元测试,省去了很多冗余代码。
注解 | 作用 |
---|---|
@ValueSource | 为参数化测试指定入参来源,支持8大基础类以及String类型,Class类型 |
@NullSource | 为参数测试提供一个null值入参 |
@EnumSource | 为参数化测试提供一个枚举入参 |
@CSVFileSource | 读取指定CSV文件内容作为参数化测试入参 |
@MethodSource | 读取指定方法的返回值违参数化测试入参(注意方法返回需要是一个流Stream.of) |
当然如果参数化测试仅仅只能做到指定普通的入参还达不到让人惊艳的地步。让人整整感受到它的强大之处的地方在于他可以支持外部的各类入参。如:CSV,YML,JSON文件甚至方法的返回值也可以入参。只需要去时间ArgumentsProvider接口,任何外部文件都可以作为它的参数入参。
@ParameterizedTest
@ValueSource(ints = { 1, 2, 3 })
void testWithValueSource(int argument) {
assertTrue(argument > 0 && argument < 4);
}
@ParameterizedTest
@MethodSource("stringProvider")
void testWithExplicitLocalMethodSource(String argument) {
assertNotNull(argument);
}
static Stream<String> stringProvider() {
return Stream.of("apple", "banana");
}
@ParameterizedTest
@NullSource
@EmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
@ParameterizedTest
@NullAndEmptySource
@ValueSource(strings = { " ", " ", "\t", "\n" })
void nullEmptyAndBlankStrings(String text) {
assertTrue(text == null || text.trim().isEmpty());
}
//...
2.5 指标监控
2.5.1 SpringBootActuator
Actuator【 /'æktjʊeɪtə/】
1. 简介
未来每个微服务在云上部署以后,需要对其进行监控、追中、审计、控制等。SpringBoot就抽取了Actuator长江,是的每个微服务快速引用即可获得生产级别的应用监控、审计等功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Gv5jorIi-1612143666421)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130173149020.png)]
2. 1.x 与 2.x 的不同
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ogHoIokB-1612143666423)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130173355164.png)]
3. 如何使用
- 引入starter-actuator
- 访问http://localhost:8080/actuator/**
- 暴露所有监控信息为HTTP
# management 是所有actuator的配置
# management.endpoint.端点名.xxx 对某个端点的具体配置
management:
endpoints:
enabled-by-default: true #暴露所有端点信息
web:
exposure:
include: '*' # 以Web方式暴露
-
测试访问
- http:///actuator
- http:///actuator/configprops
- http:///actuator/metrics
- http:///actuator/metrics/jvm.gc.pause
- metrics指标: http:///actuator/metrics/endpointName监控端点名
- 。。。。。。。。
4. 可视化
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esCMdEgI-1612143666425)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131122841338.png)]
1. SpringBoot Admin Server
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
server.port=8888 #避免和admin server 端口重复
@EnableAdminServer
@SpringBootApplication
public class SpringBootAdminApplication{....}
2. SpringBoot Admin Client
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-client</artifactId>
<version>2.3.1</version>
</dependency>
<!--security安全。为了保护endpoints,建议引入security,也可以不引入。如果引入需要做一下配置-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
//引入了security后,需要做以下配置,使actuator endpoint 可以被访问到。
//如果没有引入security,则不需要配置。
@Configuration
public static class SecurityPermitAllConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().permitAll()
.and().csrf().disable();
}
}
server.port=8080 #避免和admin server 端口重复
spring.boot.admin.client.url=http://localhost:8888 # adminServer的地址
spring.boot.admin.client.instance.prefer-ip=true # 实例地址优先使用ip,否则为主机名。 默认优先主机名。
management.endpoints.web.exposure.include='*' #以WebHttp方式暴露所有endpoints端点。
2.5.2 Actuator Endpoint
1. 最常使用的端点
最常用的Endpoint
- Health:监控状况
- Metrics:运行时指标
- Loggers:日志记录
Endpoint | 描述 |
---|---|
auditevents | 暴露当前应用程序的审核时间信息。需要一个AuditEventRepository |
beans | 显示应用程序中所有SpringBean的完整列表。 |
caches | 暴露可用的缓存。 |
conditions | 显示自动配置的所有条件信息,包括匹配或不匹配的原因。 |
configprops | 显示所有@ConfigurationProperties |
env | 暴露Spring的属性ConfigurableEnvironment |
flyway | 显示已应用的所有Flyway 数据库迁移。需要一个或多个Flyway 组件 |
health | 显示应用程序运行状况信息 |
httptrace | 显示HTTP跟踪信息(默认情况下,最近100个HTTP请求-响应)。需要一个HTTPTraceRepository 。 |
info | 显示应用程序信息 |
intergrationgraph | 显示Spring integrationgraph 。需要依赖spring-integration-core 。 |
loggers | 显示和修改应用程序中日志的配置 |
liquibase | 显示以应用的所有LiquiBase数据库迁移。需要一个或多个。 |
metrics | 显示所有@RequestMapping 路径列表。 |
mappings | 显示所有@RequestMapping 路径列表 |
scheduledtasks | 显示应用程序中的计划任务。 |
sessions | 允许从Spring Session支持的会话存储中检索和删除用户会话。需要使用SpringSession的基于Servlet的Web应用程序。 |
shutdown | 是应用程序正常关闭,默认禁用 |
startup | 显示由ApplicationStartup 收集的启动步骤数据。需要使用SpringApplication 进行配置BufferingApplicationStartup 。 |
threaddump | 执行线程转储。 |
如果您的应用程序是Web应用程序(Spring MVC,Spring WebFlux或Jersey),则可以使用以下附加端点:
ID | 描述 |
---|---|
heapdump | 返回hprof 堆转储文件。 |
jolokia | 通过HTTP暴露JMX bean(需要引入Jolokia,不适用于WebFlux)。需要引入依赖jolokia-core 。 |
logfile | 返回日志文件的内容(如果已设置logging.file.name 或logging.file.path 属性)。支持使用HTTPRange 标头来检索部分日志文件的内容。 |
prometheus | 以Prometheus服务器可以抓取的格式公开指标。需要依赖micrometer-registry-prometheus 。 |
2. Health Endpoint
健康检查端点,我们一般用于在云平台,平台会定时的检查应用的健康状况,我们就需要Health Endpoint可以为平台返回当前应用的一系列组件健康状况的集合。
重要的几点:
- health endpoint返回的结果,应该是一系列健康检查后的一个汇总报告
- 很多的健康检查默认已经自动配置好了,比如:数据库、redis等
- 可以很容易的添加自定义的健康检查机制
management:
endpoint:
health:
show-details: always
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wTqAfzJC-1612143666426)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130192555788.png)]
3. Metrics Endpoint
提供详细的、层级的、空间指标信息,这些信息可以被pull(主动推送)或者push(被动获取)方式得到;
- 通过Metrics对接多种监控系统
- 简化核心Metrics开发
- 添加自定义Metrics或者扩展已有Metrics
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VmiZIQhK-1612143666427)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210130192736315.png)]
4. 管理Endpoint
1. 开启与禁用 Endpoints
- 默认所有的Endpoint除了shutdown都是开启的。
- 需要开启或者禁用某个Endpoint。配置模式为 management.endpoint..enabled = true
management:
endpoint:
beans:
enabled: true
- 或者禁用所有的Endpoint然后手动开启指定的Endpoint
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
2. 暴露Endpoints
支持的暴露方式
- HTTP:默认只暴露health和info Endpoint
- JMX:默认暴露所有Endpoint
- 除过health和info,剩下的Endpoint都应该进行保护访问。如果引入SpringSecurity,则会默认配置安全访问规则
ID | JMX | Web |
---|---|---|
auditevents | Yes | No |
beans | Yes | No |
caches | Yes | No |
conditions | Yes | No |
configprops | Yes | No |
env | Yes | No |
flyway | Yes | No |
health | Yes | Yes |
heapdump | N/A | No |
httptrace | Yes | No |
info | Yes | Yes |
integrationgraph | Yes | No |
jolokia | N/A | No |
logfile | N/A | No |
loggers | Yes | No |
liquibase | Yes | No |
metrics | Yes | No |
mappings | Yes | No |
prometheus | N/A | No |
scheduledtasks | Yes | No |
sessions | Yes | No |
shutdown | Yes | No |
startup | Yes | No |
threaddump | Yes | No |
2.5.3 定制 Endpoint
1. 定制 Health 信息
定制健康信息需要自定义健康信息检查类实现HealthIndicator
健康指标接口。
public abstract class AbstractHealthIndicator implements HealthIndicator
也可以继承AbstractHealthIndicator
类来实现健康检查功能。
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
构建Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
@Component
public class MyComponentHealthIndicator extends AbstractHealthIndicator {
/**
* 真实的检查健康信息方法
* @param builder
* @throws Exception
*/
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
//mongodb 获取连接测试
HashMap<String, Object> map = new HashMap<>();
if (1 == 1){//真实逻辑判断
// builder.up();//健康
builder.status(Status.UP);
map.put("count", 1);
map.put("ms:",100);
}else {
// builder.down();//不健康
builder.status(Status.OUT_OF_SERVICE);
map.put("err", "连接超时");
}
//返回健康信息的详细信息
builder.withDetail("code",100).withDetails(map);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5icig3uA-1612143666429)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131101024640.png)]
2. 定制 Info 信息
常用两种方式:
1. 配置文件
# 根节点是info, 不是management.enpoints.info
# 根节点info 用于指定application的应用信息。
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #@以获取maven的pom文件中的标签值@
mavenProjectVersion: @project.version@
mavenProjectModelVersion: @project.modelVersion@
2. Component
@Component
public class AppInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("msg", "你好").
withDetail("hello", "info world")
.withDetails(Collections.singletonMap("你好","info 世界"));
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ejug1qnd-1612143666430)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131102849207.png)]
3. 定制 Metrics 信息
1. SpringBoot支持自动适配的Metrics
-
JVM metrics, report utilization of:
-
- Various memory and buffer pools
- Statistics related to garbage collection
- Threads utilization
- Number of classes loaded/unloaded
-
CPU metrics
-
File descriptor metrics
-
Kafka consumer and producer metrics
-
Log4j2 metrics: record the number of events logged to Log4j2 at each level
-
Logback metrics: record the number of events logged to Logback at each level
-
Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
-
Tomcat metrics (
server.tomcat.mbeanregistry.enabled
must be set totrue
for all Tomcat metrics to be registered) -
Spring Integration metrics
2. 增加定制Metrics
/*
* 定制指标信息
* */
Counter counter;
public PlusController(MeterRegistry meterRegistry) {
counter = meterRegistry.counter("plusController.t_emp_page.counter");
}
/**
* 分页查询数据
* */
@GetMapping("plus/t_emp_page")
public String t_emp_page(Model model, @RequestParam(defaultValue = "1") Integer page){
counter.increment(); //指标信息,指标自增一。
Page<Emp> empPage = new Page<>(page, 2);
Page<Emp> emps = plusEmpService.page(empPage);
model.addAttribute("emps",emps);
return "emp_page";
}
或
@Configuration
public class MyMetricsConfig {
@Bean
MeterBinder queueSize(Queue queue){
return (registry) -> Gauge.builder("queueSize",queue::size).register(registry);
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-10S2b58F-1612143666431)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131105136908.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dgZ8uIxc-1612143666433)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131110853369.png)]
4. 定制 Endpoint 信息
@Component
@Endpoint(id = "dockerContainer")
public class DockerEndpoint {
@ReadOperation
public Map<String,Object> getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
public void restartDocker(){
System.out.println("docker restarted...");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yAQanpBa-1612143666434)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131112607927.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TBSW9fYm-1612143666434)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131112645278.png)]
场景:开发ReadinessEndpoint
来管理程序是否就绪,或者LivenessEndpoint
来管理是否存活;
当然,这个也可以直接使用
https://docs.spring.io/spring-boot/docs/current/reference/html/pproduction-ready-features.html#production-ready-kubernetes-probes
2.6 原理解析
2.6.1 Profile功能
为了方便多环境适配,SpringBoot简化了profile功能。
1. application-profile功能
- 默认配置文件 application.yaml 任何时候都会加载
- 指定环境配置文件 application-{env}.yaml
- 激活指定环境
- 配置文件激活
- 命令行激活【java -jar -xxx.jar –spring.profiles.active=prod --person.name=haha】
- 修改配置文件的任意值,或添加任意值。
- 优先级最高。
- 默认配置与环境配置同时生效
- 同名配置项,profile环境配置优先(比如默认环境和prod环境都配有server.port,以profile环境优先。)
# application.properties
server.port=8080
peroson.name=lisi
spring.profiles.active=prod # 指定激活的环境。默认配置文件和指定配置文件都会生效。
#application-prod.properties
server.prot=9999
#application-test.properties
server.prot=7777
#最终效果服务器在9999端口启动。且person.name=lisi
2. @Profile条件装配功能
@Profile(“xxx”):如果当前是xxx环境激活,才会执行执行Spring一系列相关操作。
@Configuration(proxyBeanMethods = false)
//如果是生产环境,才会执行Spring一系列相关操作。注入组件
@Profile("production")
public class ProductionConfiguration {
@Bean
// ...
}
//如果是生产环境,才会执行Spring一些列相关操作。配置绑定
@Profile("production")
@Component
@ConfigurationProperties("person")
public class Person{...}
//如果是测试环境,才会执行Spring一些列相关操作。配置绑定
@Profile("test")
@Component
@ConfigurationProperties("person")
public class Person{...}
3. profile分组
#application.properties
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
使用:--spring.profiles.active=production #激活production,会将application.proddb和application.prodmq两个配置文件同时激活。
2.6.2 外部化配置
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-external-config
- Default properties (specified by setting
SpringApplication.setDefaultProperties
). @PropertySource
annotations on your@Configuration
classes. Please note that such property sources are not added to theEnvironment
until the application context is being refreshed. This is too late to configure certain properties such aslogging.*
andspring.main.*
which are read before refresh begins.- Config data (such as
application.properties
files) - A
RandomValuePropertySource
that has properties only inrandom.*
. - OS environment variables.
- Java System properties (
System.getProperties()
). - JNDI attributes from
java:comp/env
. ServletContext
init parameters.ServletConfig
init parameters.- Properties from
SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property). - Command line arguments.
properties
attribute on your tests. Available on@SpringBootTest
and the test annotations for testing a particular slice of your application.@TestPropertySource
annotations on your tests.- Devtools global settings properties in the
$HOME/.config/spring-boot
directory when devtools is active.
1. 外部配置源
常用:Java属性文件、YAML文件、环境变量、命令行参数
2. 配置文件查找位置
- The classpath root
- The classpath
/config
package- The current directory
- The
/config
subdirectory in the current directory- Immediate child directories of the
/config
subdirectory列表按加载顺序排序,后面的会覆盖前面的配置。
- classpath 根路径
- classpath:/config目录
- jar包当前目录(和jar包文件同级目录)
- jar包当前目录的config目录
- /config的直接子目录
3. 配置文件加载顺序
- 当前jar包内部的application
- 当前jar包内部的application-{profile}
- 引用的外部jar包的application
- 引用的外部jar包的application-{profile}
列表按加载顺序排序,后面的会覆盖前面的配置。
2.6.3 自定义Starter
1. starter启动原理
-
starter-pom引入 autoconfigurer包
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-om41ribj-1612143666436)(C:\Users\hp\AppData\Roaming\Typora\typora-user-images\image-20210131141605601.png)]
-
autoconfigurer 包中配置使用 META-INF/spring.factories 中 EnableAutoConfiguration 的值,使得项目启动加载指定自动配置类
-
编写自动配置类 xxxAutoConfiguration -> xxxxProperties
- @Configuration
- @Conditional
- @EnableConfigurationProperties
- @Bean
- …
引入Starter — xxxAutoConfiguration — 容器中放入组件 — 绑定xxxProperties — 配置项。
2. 自定义Starter
1. 启动器
yyl-hello-spring-boot-starter(启动器)
<dependency>
<groupId>com.yyl.bootstudy</groupId>
<artifactId>hello-spring-boot-starter-autoconfiguration</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
2. 自动配置模块
yyl-hello-spring-boot-starter-autoconfiguration(自动配置模块)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</dependency>
</dependencies>
#spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.yyl.bootstudy.hellospringbootstarterautoconfiguration.HelloSpringBootAutoConfiguration
@EnableConfigurationProperties(HelloProperties.class)
@Configuration
public class HelloSpringBootAutoConfiguration {
@Bean
@ConditionalOnMissingBean(HelloService.class)
public HelloService helloService(){
return new HelloService();
}
}
/*
* 此处不标注@Service,因为不知道是否要将该类注册进容器。
* */
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(){
return helloProperties.getPrefix() + ": 张三666" + helloProperties.getSuffix();
}
}
@ConfigurationProperties("custom.hello")
public class HelloProperties {
private String prefix;
private String suffix;
//getter and setter .....
}
3. 使用自定义Starter
<dependency>
<groupId>com.yyl.bootstudy</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
custom:
hello:
prefix: 老师
suffix: ☺
@Controller
public class HelloController {
@Autowired
HelloService helloService;
@GetMapping("say")
@ResponseBody
public String say(){
return helloService.sayHello();
}
}
2.6.4 SpringBoot 原理
Spring原理【Spring注解】、SpringMVC原理、自动配置原理、SpringBoot原理
1. SpringBoot启动过程
2. Application Events and Listeners
https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-application-events-and-listeners
ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("ApplicationContextInitializer....initialize");
}
}
Applicationlistener
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener....onApplicationEvent");
}
}
SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public MySpringApplicationRunListener(SpringApplication application, String[] args) {
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MyApplicationRunner....starting");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MyApplicationRunner....environmentPrepared");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextPrepared");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextLoaded");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....started");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....running");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MyApplicationRunner....failed");
}
}
3. ApplicationRunner 与 CommandLineRunner
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner....run");
}
}
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run");
}
}
学习产出:
nts-and-listeners
ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("ApplicationContextInitializer....initialize");
}
}
Applicationlistener
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener....onApplicationEvent");
}
}
SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public MySpringApplicationRunListener(SpringApplication application, String[] args) {
}
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MyApplicationRunner....starting");
}
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MyApplicationRunner....environmentPrepared");
}
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextPrepared");
}
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....contextLoaded");
}
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....started");
}
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MyApplicationRunner....running");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MyApplicationRunner....failed");
}
}
3. ApplicationRunner 与 CommandLineRunner
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner....run");
}
}
@Component
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run");
}
}
学习产出:
默认无前缀 ↩︎
与application配置文件进行ConfigurationProperties配置绑定的类 ↩︎
Bean名称路径处理程序映射器 ↩︎
反应式编程是一种编程范例,它是功能性的,基于事件的,非阻塞的,异步的,并且以数据流处理为中心。 术语反应式来自以下事实:我们对诸如鼠标单击或 I / O 事件之类的更改做出反应。 ↩︎ ↩︎
简单路径处理器映射器 ↩︎
路径匹配帮助器。 (包含解析MatrixVariable矩阵变量等功能) ↩︎
ServletRequest方法参数解析器,用来解析目标方法中Servlet原生API ↩︎
处理目标方法的自定义复杂类型参数,支持级联属性。 ↩︎
web数据绑定器;它里面的 Converters 将请求数据转成指定的数据类型,然后将值绑定到JavaBean中。 ↩︎
格式化器注册中心,可以用来注册自定义的Converter等。也可以用ConverterRegistry,ConverterRegistry是FormatterRegistry的父接口。 ↩︎ ↩︎ ↩︎
SpringMVC自动配置类的接口。
[^WebMvcProperties ]:WebMvcProperties 和 spring.mvc前缀 进行配置绑定。
[^ResourcesProperties ]:ResourcesProperties 和 spring.resources前缀 进行配置绑定。
[^WebProperties ]:WebProperties 和 spring.web前缀 进行配置绑定。 ↩︎处理器适配器 ↩︎
处理方法参数解析器;supportsParameter方法判断当前解析器是否支持解析这种参数-支持就调用resolveArgument进行解析。 ↩︎ ↩︎
HttpMessageConverter 和 内容协商底层源码 ↩︎
方法返回值处理器接口 ↩︎
Http消息转换器。write方法将输出数据转换器MediaType内容协商后的媒体(内容)类型。
[^ MappingJackson2HttpMessageConverter]:HttpMessageConverter的实现类,内部调用Jackson,支持将对象转换为JSON对象。 ↩︎ ↩︎ ↩︎浏览器会将希望得到的数据类型存放在请求头中 ↩︎
自动配置异常处理默认规则 ↩︎
处理标注有@ResponseStatus注解的异常类的异常解析器 ↩︎
标注的方法发生异常,将会被ExceptionHandlerExceptionResovler进行异常处理。 ↩︎
全面接管SpringMVC,静态资源,视图解析器,欢迎页…的自动配置全部失效,所有规则需要自己全部重新配置 ↩︎
数据源的自动配置类 ↩︎
事务管理器的自动配置类 ↩︎
JdbcTemplate组件的自动配置类,Spring提供的,可以对数据库进行crud ↩︎
Jndi技术数据源自动配置类;web容器配置数据源,比如Tomcat的Jndi技术。 ↩︎
分布式事务相关的。 ↩︎